From fc04e2ec2a89b11212515ea35987583a97e2b844 Mon Sep 17 00:00:00 2001 From: Saravanan Gnanaguru Date: Wed, 15 Apr 2026 18:54:50 +0530 Subject: [PATCH 1/3] feat: enhance devcontainer configuration for multi-language support - Updated `.devcontainer/devcontainer.env.json` to enable additional languages: Java, Node.js, Ruby, C#, PHP, Rust, Kotlin, C, C++, JavaScript, and Go. - Modified `.devcontainer/devcontainer.json` to install the newly supported languages and tools, including their respective versions and features. - Added a new `bootstrap-toolbox.sh` script to install language-specific tools and link the Kubernetes generator. - Enhanced the `init` command in `cli/devopsos.py` to preserve existing `.devcontainer/` configurations and write generated output to `.devcontainer.generated/`. - Updated tests in `cli/test_cli.py` to verify the preservation of existing configurations and the correct generation of new files. - Revised documentation to reflect changes in the devcontainer setup and CLI command behavior. --- .devcontainer/Dockerfile | 102 +----- .devcontainer/README.md | 24 +- .devcontainer/bootstrap-toolbox.sh | 73 ++++ .devcontainer/configure.py | 509 ++++++++++++++++++---------- .devcontainer/devcontainer.env.json | 27 +- .devcontainer/devcontainer.json | 99 ++++-- README.md | 13 +- cli/devopsos.py | 18 +- cli/test_cli.py | 132 ++++++++ docs/CLI-COMMANDS-REFERENCE.md | 6 +- docs/CLI-TEST-REPORT.md | 3 + docs/DEVOPS-OS-README.md | 1 + 12 files changed, 687 insertions(+), 320 deletions(-) create mode 100755 .devcontainer/bootstrap-toolbox.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 49dd509..68e7c8a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,34 +1,18 @@ # DevOps OS dev container (Ubuntu LTS optimized) -FROM mcr.microsoft.com/devcontainers/base:ubuntu24.04 +FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 SHELL ["/bin/bash", "-o", "pipefail", "-c"] -# Core language/runtime versions -ARG PYTHON_VERSION=3.12 ARG JAVA_VERSION=21 -ARG NODE_VERSION=22 -ARG NVM_VERSION=0.40.3 -ARG GO_VERSION=1.25.0 ARG TARGETOS=linux ARG TARGETARCH=amd64 -# Compatibility args (generated by scaffold command); currently not implemented in this image -ARG INSTALL_RUBY=false +# Unsupported language/runtime toggles ARG INSTALL_CSHARP=false -ARG INSTALL_PHP=false -ARG INSTALL_RUST=false -ARG INSTALL_TYPESCRIPT=false ARG INSTALL_KOTLIN=false ARG INSTALL_C=false ARG INSTALL_CPP=false -# Primary language toggles -ARG INSTALL_PYTHON=true -ARG INSTALL_JAVA=true -ARG INSTALL_JS=true -ARG INSTALL_JAVASCRIPT=false -ARG INSTALL_GO=true - # CI/CD + platform tools ARG INSTALL_DOCKER=true ARG INSTALL_PODMAN=false @@ -59,9 +43,6 @@ ARG MINIKUBE_VERSION=1.37.0 ARG INSTALL_OPENSHIFT_CLI=false # Build tools -ARG INSTALL_GRADLE=true -ARG INSTALL_MAVEN=true -ARG INSTALL_ANT=true ARG INSTALL_MAKE=true ARG INSTALL_CMAKE=true @@ -72,8 +53,6 @@ ARG INSTALL_CHECKSTYLE=true ARG CHECKSTYLE_VERSION=12.1.2 ARG INSTALL_PMD=true ARG PMD_VERSION=7.18.0 -ARG INSTALL_ESLINT=true -ARG INSTALL_PYLINT=true # DevOps tools ARG INSTALL_NEXUS=true @@ -86,9 +65,8 @@ ARG INSTALL_ELK=true ARG INSTALL_JENKINS=false ENV DEBIAN_FRONTEND=noninteractive -ENV PATH="/usr/local/go/bin:/root/go/bin:/opt/sonar-scanner/bin:/opt/pmd/bin:${PATH}" +ENV PATH="/opt/sonar-scanner/bin:/opt/pmd/bin:${PATH}" ENV SHELL=/bin/bash -ENV NVM_DIR=/usr/local/share/nvm # Base packages shared by most install flows. RUN apt-get update \ @@ -107,68 +85,19 @@ RUN apt-get update \ wget \ xz-utils -# Python -RUN if [ "${INSTALL_PYTHON}" = "true" ]; then \ - if [ "${PYTHON_VERSION}" != "3.12" ]; then \ - add-apt-repository -y ppa:deadsnakes/ppa && apt-get update; \ - fi \ - && apt-get install -y --no-install-recommends \ - python${PYTHON_VERSION} \ - python${PYTHON_VERSION}-dev \ - python${PYTHON_VERSION}-venv \ - python3-pip \ - && ln -sf /usr/bin/python${PYTHON_VERSION} /usr/local/bin/python3 \ - && ln -sf /usr/local/bin/python3 /usr/local/bin/python \ - && python3 -m pip install --no-cache-dir --upgrade pip \ - && pip install --no-cache-dir pytest black flake8 mypy pipenv tox coverage pytest-cov; \ - fi - -# Java and Java build tools -RUN if [ "${INSTALL_JAVA}" = "true" ] || [ "${INSTALL_MAVEN}" = "true" ] || [ "${INSTALL_GRADLE}" = "true" ] || [ "${INSTALL_ANT}" = "true" ]; then \ - apt-get update \ - && apt-get install -y --no-install-recommends openjdk-${JAVA_VERSION}-jdk; \ +# Unsupported language/toolchain installers kept in the repo layer. +RUN if [ "${INSTALL_CSHARP}" = "true" ]; then \ + wget -q https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb -O /tmp/packages-microsoft-prod.deb \ + && dpkg -i /tmp/packages-microsoft-prod.deb \ + && rm -f /tmp/packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y --no-install-recommends dotnet-sdk-8.0; \ fi \ - && if [ "${INSTALL_MAVEN}" = "true" ]; then apt-get update && apt-get install -y --no-install-recommends maven; fi \ - && if [ "${INSTALL_GRADLE}" = "true" ]; then apt-get update && apt-get install -y --no-install-recommends gradle; fi \ - && if [ "${INSTALL_ANT}" = "true" ]; then apt-get update && apt-get install -y --no-install-recommends ant; fi \ - && if [ "${INSTALL_JAVA}" = "true" ]; then \ - echo "export JAVA_HOME=/usr/lib/jvm/java-${JAVA_VERSION}-openjdk-$(dpkg --print-architecture)" > /etc/profile.d/java_home.sh; \ - fi - -# JavaScript / TypeScript -RUN if [ "${INSTALL_JS}" = "true" ] || [ "${INSTALL_JAVASCRIPT}" = "true" ] || [ "${INSTALL_TYPESCRIPT}" = "true" ]; then \ - curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash - \ - && apt-get install -y --no-install-recommends nodejs \ - && mkdir -p "${NVM_DIR}" \ - && curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh" | PROFILE=/dev/null NVM_DIR="${NVM_DIR}" bash \ - && printf 'export NVM_DIR=%s\n[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"\n' "${NVM_DIR}" > /etc/profile.d/nvm.sh \ - && chmod +x /etc/profile.d/nvm.sh \ - && . "${NVM_DIR}/nvm.sh" \ - && nvm alias default system \ - && npm install -g yarn typescript jest prettier; \ + && if [ "${INSTALL_KOTLIN}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends kotlin; \ fi \ - && if [ "${INSTALL_ESLINT}" = "true" ]; then \ - if ! command -v npm >/dev/null 2>&1; then \ - curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash - \ - && apt-get install -y --no-install-recommends nodejs; \ - fi \ - && npm install -g eslint; \ - fi - -# Go -RUN if [ "${INSTALL_GO}" = "true" ]; then \ - GO_DOWNLOAD_VERSION="${GO_VERSION}" \ - && case "${GO_DOWNLOAD_VERSION}" in *.*.*) ;; *.*) GO_DOWNLOAD_VERSION="${GO_DOWNLOAD_VERSION}.0" ;; esac \ - && case "${TARGETARCH}" in \ - amd64|x86_64) GO_ARCH="amd64" ;; \ - arm64|aarch64) GO_ARCH="arm64" ;; \ - *) echo "Unsupported TARGETARCH for Go: ${TARGETARCH}" >&2; exit 1 ;; \ - esac \ - && GO_TARBALL="go${GO_DOWNLOAD_VERSION}.${TARGETOS}-${GO_ARCH}.tar.gz" \ - && curl -fsSLo "/tmp/${GO_TARBALL}" "https://go.dev/dl/${GO_TARBALL}" \ - && tar -C /usr/local -xzf "/tmp/${GO_TARBALL}" \ - && rm -f "/tmp/${GO_TARBALL}" \ - && /usr/local/go/bin/go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest; \ + && if [ "${INSTALL_C}" = "true" ] || [ "${INSTALL_CPP}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends clang gdb; \ fi # Docker CLI / Podman @@ -325,9 +254,6 @@ RUN if [ "${INSTALL_SONARQUBE}" = "true" ]; then \ && unzip -q /tmp/pmd.zip -d /opt \ && mv "/opt/pmd-bin-${PMD_VERSION}" /opt/pmd \ && rm -f /tmp/pmd.zip; \ - fi \ - && if [ "${INSTALL_PYLINT}" = "true" ] && command -v pip >/dev/null 2>&1; then \ - pip install --no-cache-dir pylint; \ fi # DevOps toolchain binaries diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 8ce255a..1f9ee09 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -1,12 +1,13 @@ # Multi-Language Development Container -This development container provides a consistent environment for Java, JavaScript, Go, and Python development, along with CI/CD tools. +This development container provides a toolbox-style environment across the major languages supported by DevOps-OS, along with CI/CD and Kubernetes tooling. ## Features -- **Multiple Languages**: Java, JavaScript/TypeScript, Go, and Python with all necessary build tools +- **Hybrid Runtime Strategy**: official Dev Container Features install mainstream runtimes, while the repo Dockerfile keeps Ubuntu 24.04 plus unsupported language/toolbox extras +- **Multiple Languages**: Python, Java, Node.js/JavaScript/TypeScript, Go, Ruby, PHP, Rust, C#, Kotlin, and C/C++ are available by default - **CI/CD Tools**: Docker, Terraform, Kubernetes (kubectl), Helm, GitHub Actions -- **Customizable**: Configure which languages and tools to include +- **Customizable**: `.devcontainer/devcontainer.env.json` remains the single control plane for languages and tools ## Getting Started @@ -45,6 +46,15 @@ Run `python -m cli.scaffold_devcontainer --help` to see all available options in "languages": { "python": true, "java": true, + "node": true, + "ruby": true, + "csharp": true, + "php": true, + "rust": true, + "typescript": true, + "kotlin": true, + "c": true, + "cpp": true, "javascript": true, "go": true }, @@ -95,9 +105,11 @@ After configuring (via either option), open the project in VS Code and click "Re ### Languages and Tools - **Python**: Python interpreter, pip, pytest, black, flake8, mypy -- **Java**: JDK, Maven, Gradle +- **Java/Kotlin**: JDK, Maven, Gradle, Ant, Kotlin compiler - **JavaScript/TypeScript**: Node.js, npm, yarn, TypeScript, Jest, ESLint, Prettier - **Go**: Go compiler, golangci-lint +- **Ruby/PHP/Rust/C#**: runtime/compiler support installed for toolbox workflows +- **C/C++**: build-essential, clang, gdb, cmake ### CI/CD Tools @@ -122,8 +134,8 @@ For detailed information on using the Kubernetes capabilities, see [kubernetes-c You can: 1. Disable languages or tools you don't need -2. Change versions of languages -3. Add additional tools by editing the Dockerfile +2. Change pinned versions for Python, Java, Node, and Go +3. Add additional repo-local tools in `bootstrap-toolbox.sh` or the Dockerfile ## Troubleshooting diff --git a/.devcontainer/bootstrap-toolbox.sh b/.devcontainer/bootstrap-toolbox.sh new file mode 100755 index 0000000..7baa405 --- /dev/null +++ b/.devcontainer/bootstrap-toolbox.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/devcontainer.env.json" + +json_bool() { + jq -r "$1 // false" "${CONFIG_FILE}" +} + +install_python_tools() { + if ! command -v python3 >/dev/null 2>&1; then + return + fi + + PYTHON_BIN="$(command -v python3)" + sudo "${PYTHON_BIN}" -m pip install --no-cache-dir --upgrade pip + sudo "${PYTHON_BIN}" -m pip install --no-cache-dir \ + pytest black flake8 mypy pipenv tox coverage pytest-cov pylint +} + +install_node_tools() { + if ! command -v npm >/dev/null 2>&1; then + return + fi + + if command -v eslint >/dev/null 2>&1 && command -v prettier >/dev/null 2>&1 && command -v jest >/dev/null 2>&1 && command -v tsc >/dev/null 2>&1; then + return + fi + + NPM_BIN="$(command -v npm)" + sudo "${NPM_BIN}" install -g typescript jest prettier eslint +} + +install_go_tools() { + if ! command -v go >/dev/null 2>&1; then + return + fi + + if command -v golangci-lint >/dev/null 2>&1; then + return + fi + + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest +} + +link_k8s_generator() { + if [[ ! -f "${SCRIPT_DIR}/k8s-config-generator.py" ]]; then + return + fi + + sudo chmod +x "${SCRIPT_DIR}/k8s-config-generator.py" + sudo ln -sf "${SCRIPT_DIR}/k8s-config-generator.py" /usr/local/bin/k8s-config-generator +} + +if [[ "$(json_bool '.languages.python')" == "true" ]]; then + install_python_tools +fi + +if [[ "$(json_bool '.languages.node')" == "true" || "$(json_bool '.languages.javascript')" == "true" || "$(json_bool '.languages.typescript')" == "true" || "$(json_bool '.code_analysis.eslint')" == "true" ]]; then + install_node_tools +fi + +if [[ "$(json_bool '.languages.go')" == "true" ]]; then + install_go_tools +fi + +if [[ "$(jq -r '.kubernetes | any(.[]; . == true)' "${CONFIG_FILE}")" == "true" ]]; then + link_k8s_generator +fi + +printf 'Devcontainer bootstrap complete.\n' diff --git a/.devcontainer/configure.py b/.devcontainer/configure.py index 88c79c1..407ee81 100755 --- a/.devcontainer/configure.py +++ b/.devcontainer/configure.py @@ -1,25 +1,37 @@ #!/usr/bin/env python3 import json import os +from copy import deepcopy -# Path to the configuration file -CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'devcontainer.env.json') -DEVCONTAINER_FILE = os.path.join(os.path.dirname(__file__), 'devcontainer.json') +CONFIG_FILE = os.path.join(os.path.dirname(__file__), "devcontainer.env.json") +DEVCONTAINER_FILE = os.path.join(os.path.dirname(__file__), "devcontainer.json") -# Default configuration -default_config = { +FEATURE_LANGUAGE_KEYS = ["python", "java", "node", "ruby", "php", "rust", "typescript", "javascript", "go"] +FALLBACK_LANGUAGE_KEYS = ["csharp", "kotlin", "c", "cpp"] + +DEFAULT_CONFIG = { "languages": { "python": True, "java": True, + "node": True, + "ruby": True, + "csharp": True, + "php": True, + "rust": True, + "typescript": True, + "kotlin": True, + "c": True, + "cpp": True, "javascript": True, - "go": True + "go": True, }, "cicd": { "docker": True, "terraform": True, "kubectl": True, "helm": True, - "github_actions": True + "github_actions": True, + "podman": False, }, "kubernetes": { "k9s": True, @@ -30,28 +42,28 @@ "flux": True, "kind": True, "minikube": True, - "openshift_cli": False + "openshift_cli": False, }, "build_tools": { "gradle": True, "maven": True, "ant": True, "make": True, - "cmake": True + "cmake": True, }, "code_analysis": { "sonarqube": True, "checkstyle": True, "pmd": True, "eslint": True, - "pylint": True + "pylint": True, }, "devops_tools": { "nexus": True, "prometheus": True, "grafana": True, "elk": True, - "jenkins": False + "jenkins": False, }, "versions": { "python": "3.12", @@ -64,207 +76,342 @@ "k9s": "0.50.16", "argocd": "3.3.6", "flux": "2.8.5", - "kustomize": "5.8.0" - } + "kustomize": "5.8.0", + }, +} + + +BUILD_ARG_FACTORIES = { + "INSTALL_CSHARP": lambda c: c["languages"]["csharp"], + "INSTALL_KOTLIN": lambda c: c["languages"]["kotlin"], + "INSTALL_C": lambda c: c["languages"]["c"], + "INSTALL_CPP": lambda c: c["languages"]["cpp"], + "INSTALL_DOCKER": lambda c: c["cicd"]["docker"], + "INSTALL_PODMAN": lambda c: c["cicd"].get("podman", False), + "INSTALL_TERRAFORM": lambda c: c["cicd"]["terraform"], + "INSTALL_KUBECTL": lambda c: c["cicd"]["kubectl"], + "INSTALL_HELM": lambda c: c["cicd"]["helm"], + "INSTALL_GITHUB_ACTIONS": lambda c: c["cicd"]["github_actions"], + "INSTALL_K9S": lambda c: c["kubernetes"]["k9s"], + "INSTALL_KUSTOMIZE": lambda c: c["kubernetes"]["kustomize"], + "INSTALL_ARGOCD_CLI": lambda c: c["kubernetes"]["argocd_cli"], + "INSTALL_LENS": lambda c: c["kubernetes"]["lens"], + "INSTALL_KUBESEAL": lambda c: c["kubernetes"]["kubeseal"], + "INSTALL_FLUX": lambda c: c["kubernetes"]["flux"], + "INSTALL_KIND": lambda c: c["kubernetes"]["kind"], + "INSTALL_MINIKUBE": lambda c: c["kubernetes"]["minikube"], + "INSTALL_OPENSHIFT_CLI": lambda c: c["kubernetes"]["openshift_cli"], + "INSTALL_MAKE": lambda c: c["build_tools"]["make"], + "INSTALL_CMAKE": lambda c: c["build_tools"]["cmake"], + "INSTALL_SONARQUBE": lambda c: c["code_analysis"]["sonarqube"], + "INSTALL_CHECKSTYLE": lambda c: c["code_analysis"]["checkstyle"], + "INSTALL_PMD": lambda c: c["code_analysis"]["pmd"], + "INSTALL_NEXUS": lambda c: c["devops_tools"]["nexus"], + "INSTALL_PROMETHEUS": lambda c: c["devops_tools"]["prometheus"], + "INSTALL_GRAFANA": lambda c: c["devops_tools"]["grafana"], + "INSTALL_ELK": lambda c: c["devops_tools"]["elk"], + "INSTALL_JENKINS": lambda c: c["devops_tools"]["jenkins"], +} + + +VERSION_ARG_FACTORIES = { + "JAVA_VERSION": lambda c: c["versions"].get("java", "21") if c["devops_tools"]["nexus"] else None, + "TERRAFORM_VERSION": lambda c: "1.14.7" if c["cicd"]["terraform"] else None, + "HELM_VERSION": lambda c: "4.0.1" if c["cicd"]["helm"] else None, + "ACTIONS_RUNNER_VERSION": lambda c: "2.330.0" if c["cicd"]["github_actions"] else None, + "K9S_VERSION": lambda c: c["versions"].get("k9s", "0.50.16") if c["kubernetes"]["k9s"] else None, + "KUSTOMIZE_VERSION": lambda c: c["versions"].get("kustomize", "5.8.0") if c["kubernetes"]["kustomize"] else None, + "ARGOCD_VERSION": lambda c: c["versions"].get("argocd", "3.3.6") if c["kubernetes"]["argocd_cli"] else None, + "FLUX_VERSION": lambda c: c["versions"].get("flux", "2.8.5") if c["kubernetes"]["flux"] else None, + "KUBESEAL_VERSION": lambda c: "0.33.1" if c["kubernetes"]["kubeseal"] else None, + "KIND_VERSION": lambda c: "0.31.0" if c["kubernetes"]["kind"] else None, + "MINIKUBE_VERSION": lambda c: "1.37.0" if c["kubernetes"]["minikube"] else None, + "SONAR_SCANNER_VERSION": lambda c: "8.0.1.6346" if c["code_analysis"]["sonarqube"] else None, + "CHECKSTYLE_VERSION": lambda c: "12.1.2" if c["code_analysis"]["checkstyle"] else None, + "PMD_VERSION": lambda c: "7.18.0" if c["code_analysis"]["pmd"] else None, + "NEXUS_VERSION": lambda c: c["versions"].get("nexus", "3.91.0") if c["devops_tools"]["nexus"] else None, + "PROMETHEUS_VERSION": lambda c: c["versions"].get("prometheus", "3.5.1") if c["devops_tools"]["prometheus"] else None, + "GRAFANA_VERSION": lambda c: c["versions"].get("grafana", "12.4.2") if c["devops_tools"]["grafana"] else None, } -# Read configuration file if it exists, otherwise use defaults + +FEATURE_REFS = { + "python": "ghcr.io/devcontainers/features/python:1", + "java": "ghcr.io/devcontainers/features/java:1", + "node": "ghcr.io/devcontainers/features/node:1", + "go": "ghcr.io/devcontainers/features/go:1", + "ruby": "ghcr.io/devcontainers/features/ruby:1", + "php": "ghcr.io/devcontainers/features/php:1", + "rust": "ghcr.io/devcontainers/features/rust:1", +} + + +def deep_merge(defaults, data): + merged = deepcopy(defaults) + for key, value in data.items(): + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = deep_merge(merged[key], value) + else: + merged[key] = value + return merged + + if os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, 'r') as f: - config = json.load(f) + with open(CONFIG_FILE, "r", encoding="utf-8") as handle: + config = deep_merge(DEFAULT_CONFIG, json.load(handle)) else: - config = default_config - # Write default config - with open(CONFIG_FILE, 'w') as f: - json.dump(default_config, f, indent=2) + config = deepcopy(DEFAULT_CONFIG) + with open(CONFIG_FILE, "w", encoding="utf-8") as handle: + json.dump(config, handle, indent=2) + handle.write("\n") -# Create the devcontainer.json content -devcontainer = { - "name": "DevOps OS - Multi-Language Development Environment", - "build": { - "dockerfile": "Dockerfile", - "args": { - # Language versions - "PYTHON_VERSION": config["versions"]["python"], - "JAVA_VERSION": config["versions"]["java"], - "NODE_VERSION": config["versions"]["node"], - "GO_VERSION": config["versions"]["go"], - - # Languages - "INSTALL_PYTHON": str(config["languages"]["python"]).lower(), - "INSTALL_JAVA": str(config["languages"]["java"]).lower(), - "INSTALL_JS": str(config["languages"]["javascript"]).lower(), - "INSTALL_GO": str(config["languages"]["go"]).lower(), - - # CI/CD tools - "INSTALL_DOCKER": str(config["cicd"]["docker"]).lower(), - "INSTALL_TERRAFORM": str(config["cicd"]["terraform"]).lower(), - "INSTALL_KUBECTL": str(config["cicd"]["kubectl"]).lower(), - "INSTALL_HELM": str(config["cicd"]["helm"]).lower(), - "INSTALL_GITHUB_ACTIONS": str(config["cicd"]["github_actions"]).lower(), - - # Kubernetes tools - "INSTALL_K9S": str(config["kubernetes"]["k9s"]).lower(), - "K9S_VERSION": config["versions"].get("k9s", "0.50.16"), - "INSTALL_KUSTOMIZE": str(config["kubernetes"]["kustomize"]).lower(), - "KUSTOMIZE_VERSION": config["versions"].get("kustomize", "5.8.0"), - "INSTALL_ARGOCD_CLI": str(config["kubernetes"]["argocd_cli"]).lower(), - "ARGOCD_VERSION": config["versions"].get("argocd", "3.3.6"), - "INSTALL_LENS": str(config["kubernetes"]["lens"]).lower(), - "INSTALL_KUBESEAL": str(config["kubernetes"]["kubeseal"]).lower(), - "INSTALL_FLUX": str(config["kubernetes"]["flux"]).lower(), - "FLUX_VERSION": config["versions"].get("flux", "2.8.5"), - "INSTALL_KIND": str(config["kubernetes"]["kind"]).lower(), - "INSTALL_MINIKUBE": str(config["kubernetes"]["minikube"]).lower(), - "INSTALL_OPENSHIFT_CLI": str(config["kubernetes"]["openshift_cli"]).lower(), - - # Build tools - "INSTALL_GRADLE": str(config["build_tools"]["gradle"]).lower(), - "INSTALL_MAVEN": str(config["build_tools"]["maven"]).lower(), - "INSTALL_ANT": str(config["build_tools"]["ant"]).lower(), - "INSTALL_MAKE": str(config["build_tools"]["make"]).lower(), - "INSTALL_CMAKE": str(config["build_tools"]["cmake"]).lower(), - - # Code analysis - "INSTALL_SONARQUBE": str(config["code_analysis"]["sonarqube"]).lower(), - "INSTALL_CHECKSTYLE": str(config["code_analysis"]["checkstyle"]).lower(), - "INSTALL_PMD": str(config["code_analysis"]["pmd"]).lower(), - "INSTALL_ESLINT": str(config["code_analysis"]["eslint"]).lower(), - "INSTALL_PYLINT": str(config["code_analysis"]["pylint"]).lower(), - - # DevOps tools - "INSTALL_NEXUS": str(config["devops_tools"]["nexus"]).lower(), - "NEXUS_VERSION": config["versions"].get("nexus", "3.91.0"), - "INSTALL_PROMETHEUS": str(config["devops_tools"]["prometheus"]).lower(), - "PROMETHEUS_VERSION": config["versions"].get("prometheus", "3.5.1"), - "INSTALL_GRAFANA": str(config["devops_tools"]["grafana"]).lower(), - "GRAFANA_VERSION": config["versions"].get("grafana", "12.4.2"), - "INSTALL_ELK": str(config["devops_tools"]["elk"]).lower(), - "INSTALL_JENKINS": str(config["devops_tools"]["jenkins"]).lower() - } - }, - "mounts": [ - "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" - ], - "customizations": { - "vscode": { - "extensions": [] - } - }, - "forwardPorts": [] -} -# Add language-specific extensions -if config["languages"]["python"]: - devcontainer["customizations"]["vscode"]["extensions"].extend([ - "ms-python.python", - "ms-python.vscode-pylance", - "ms-python.black-formatter" - ]) +def unique(values): + seen = set() + ordered = [] + for value in values: + if value not in seen: + seen.add(value) + ordered.append(value) + return ordered -if config["languages"]["java"]: - devcontainer["customizations"]["vscode"]["extensions"].extend([ - "vscjava.vscode-java-pack", - "redhat.java", - "vscjava.vscode-maven", - "vscjava.vscode-gradle" - ]) -if config["languages"]["javascript"]: - devcontainer["customizations"]["vscode"]["extensions"].extend([ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "ms-vscode.vscode-typescript-next" - ]) +def bool_string(value): + return str(bool(value)).lower() -if config["languages"]["go"]: - devcontainer["customizations"]["vscode"]["extensions"].extend([ - "golang.go" - ]) -# Add CI/CD extensions -if config["cicd"]["docker"]: - devcontainer["customizations"]["vscode"]["extensions"].append("ms-azuretools.vscode-docker") +def normalize_go_version(version): + parts = str(version).split(".") + if len(parts) >= 2: + return ".".join(parts[:2]) + return str(version) -if config["cicd"]["terraform"]: - devcontainer["customizations"]["vscode"]["extensions"].append("hashicorp.terraform") -if config["cicd"]["kubectl"] or config["cicd"]["helm"]: - devcontainer["customizations"]["vscode"]["extensions"].append("ms-kubernetes-tools.vscode-kubernetes-tools") +def java_feature_enabled(cfg): + return ( + cfg["languages"]["java"] + or cfg["build_tools"]["gradle"] + or cfg["build_tools"]["maven"] + or cfg["build_tools"]["ant"] + or cfg["code_analysis"]["checkstyle"] + or cfg["code_analysis"]["pmd"] + ) -# Add Kubernetes extensions -if any(config["kubernetes"].values()): - devcontainer["customizations"]["vscode"]["extensions"].extend([ - "ms-kubernetes-tools.vscode-kubernetes-tools", - "mindaro.mindaro", # Bridge to Kubernetes - ]) - - # Add Kubernetes templates to postCreateCommand - devcontainer["postCreateCommand"] = "chmod +x /workspaces/.devcontainer/k8s-config-generator.py && ln -sf /workspaces/.devcontainer/k8s-config-generator.py /usr/local/bin/k8s-config-generator" - -if config["kubernetes"]["argocd_cli"]: - devcontainer["customizations"]["vscode"]["extensions"].append("argoproj.argocd-vscode-extension") -if config["kubernetes"]["flux"]: - devcontainer["customizations"]["vscode"]["extensions"].append("weaveworks.vscode-gitops-tools") +def node_feature_enabled(cfg): + return ( + cfg["languages"]["node"] + or cfg["languages"]["javascript"] + or cfg["languages"]["typescript"] + or cfg["code_analysis"]["eslint"] + ) + + +def build_features(cfg): + features = {} + versions = cfg["versions"] + + if cfg["languages"]["python"]: + features[FEATURE_REFS["python"]] = { + "version": versions.get("python", "3.12"), + "installTools": False, + } -if config["cicd"]["github_actions"]: - devcontainer["customizations"]["vscode"]["extensions"].append("github.vscode-github-actions") + if java_feature_enabled(cfg): + features[FEATURE_REFS["java"]] = { + "version": versions.get("java", "21"), + "jdkDistro": "ms", + "installGradle": cfg["build_tools"]["gradle"], + "installMaven": cfg["build_tools"]["maven"], + "installAnt": cfg["build_tools"]["ant"], + } -# Add code analysis tools extensions -if config["code_analysis"]["sonarqube"]: - devcontainer["customizations"]["vscode"]["extensions"].append("SonarSource.sonarlint-vscode") + if node_feature_enabled(cfg): + features[FEATURE_REFS["node"]] = { + "version": versions.get("node", "22"), + "nodeGypDependencies": True, + } -if config["code_analysis"]["checkstyle"] and config["languages"]["java"]: - devcontainer["customizations"]["vscode"]["extensions"].append("shengchen.vscode-checkstyle") + if cfg["languages"]["go"]: + features[FEATURE_REFS["go"]] = { + "version": normalize_go_version(versions.get("go", "1.25.0")), + } -if config["code_analysis"]["pmd"] and config["languages"]["java"]: - devcontainer["customizations"]["vscode"]["extensions"].append("vscjava.vscode-java-dependency") + if cfg["languages"]["ruby"]: + features[FEATURE_REFS["ruby"]] = {} -if config["code_analysis"]["eslint"] and config["languages"]["javascript"]: - devcontainer["customizations"]["vscode"]["extensions"].append("dbaeumer.vscode-eslint") + if cfg["languages"]["php"]: + features[FEATURE_REFS["php"]] = {"installComposer": True} -if config["code_analysis"]["pylint"] and config["languages"]["python"]: - devcontainer["customizations"]["vscode"]["extensions"].append("ms-python.pylint") + if cfg["languages"]["rust"]: + features[FEATURE_REFS["rust"]] = { + "profile": "minimal", + "components": "rust-analyzer,rust-src,rustfmt,clippy", + } -# Add DevOps tools extensions -if config["devops_tools"]["jenkins"]: - devcontainer["customizations"]["vscode"]["extensions"].append("secanis.jenkinsfile-support") + return features + + +def build_args(cfg): + args = {} + for name, factory in BUILD_ARG_FACTORIES.items(): + args[name] = bool_string(factory(cfg)) + + for name, factory in VERSION_ARG_FACTORIES.items(): + value = factory(cfg) + if value is not None: + args[name] = str(value) + + return args + + +def build_extensions(cfg): + extensions = [] + langs = cfg["languages"] + cicd = cfg["cicd"] + k8s = cfg["kubernetes"] + build_tools = cfg["build_tools"] + analysis = cfg["code_analysis"] + devops = cfg["devops_tools"] + + if langs["python"]: + extensions.extend([ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + ]) + + if java_feature_enabled(cfg): + extensions.extend([ + "vscjava.vscode-java-pack", + "redhat.java", + "vscjava.vscode-maven", + "vscjava.vscode-gradle", + ]) + + if node_feature_enabled(cfg): + extensions.extend([ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-vscode.vscode-typescript-next", + ]) + + if langs["go"]: + extensions.append("golang.go") + if langs["ruby"]: + extensions.append("Shopify.ruby-lsp") + if langs["php"]: + extensions.extend([ + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug", + ]) + if langs["rust"]: + extensions.extend([ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + ]) + if langs["csharp"]: + extensions.extend([ + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit", + ]) + if langs["c"] or langs["cpp"]: + extensions.append("ms-vscode.cpptools") + if langs["kotlin"]: + extensions.append("fwcd.kotlin") + + if cicd["docker"] or cicd.get("podman", False): + extensions.append("ms-azuretools.vscode-docker") + if cicd["terraform"]: + extensions.append("hashicorp.terraform") + if cicd["kubectl"] or cicd["helm"]: + extensions.append("ms-kubernetes-tools.vscode-kubernetes-tools") + if any(k8s.values()): + extensions.extend([ + "ms-kubernetes-tools.vscode-kubernetes-tools", + "mindaro.mindaro", + ]) + if k8s["argocd_cli"]: + extensions.append("argoproj.argocd-vscode-extension") + if k8s["flux"]: + extensions.append("weaveworks.vscode-gitops-tools") + if cicd["github_actions"]: + extensions.append("github.vscode-github-actions") + if analysis["sonarqube"]: + extensions.append("SonarSource.sonarlint-vscode") + if analysis["checkstyle"] and java_feature_enabled(cfg): + extensions.append("shengchen.vscode-checkstyle") + if analysis["pmd"] and java_feature_enabled(cfg): + extensions.append("vscjava.vscode-java-dependency") + if devops["jenkins"]: + extensions.append("secanis.jenkinsfile-support") + + extensions.extend([ + "github.copilot", + "github.copilot-chat", + "ms-vsliveshare.vsliveshare", + "streetsidesoftware.code-spell-checker", + "eamodio.gitlens", + ]) -# Forward ports for DevOps tools -if config["devops_tools"]["nexus"]: - devcontainer["forwardPorts"].append(8081) # Nexus port + return unique(extensions) -if config["devops_tools"]["prometheus"]: - devcontainer["forwardPorts"].append(9090) # Prometheus port -if config["devops_tools"]["grafana"]: - devcontainer["forwardPorts"].append(3000) # Grafana port +def build_forward_ports(cfg): + ports = [] + if cfg["devops_tools"]["nexus"]: + ports.append(8081) + if cfg["devops_tools"]["prometheus"]: + ports.append(9090) + if cfg["devops_tools"]["grafana"]: + ports.append(3000) + if cfg["devops_tools"]["elk"]: + ports.extend([9200, 9300, 5601]) + if cfg["devops_tools"]["jenkins"]: + ports.append(8080) + return ports -if config["devops_tools"]["elk"]: - devcontainer["forwardPorts"].extend([9200, 9300, 5601]) # Elasticsearch and Kibana ports -if config["devops_tools"]["jenkins"]: - devcontainer["forwardPorts"].append(8080) # Jenkins port +features = build_features(config) +devcontainer = { + "name": "DevOps OS - Multi-Language Development Environment", + "build": { + "dockerfile": "Dockerfile", + "args": build_args(config), + }, + "features": features, + "mounts": [ + "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" + ], + "customizations": { + "vscode": { + "extensions": build_extensions(config), + } + }, + "postCreateCommand": "bash .devcontainer/bootstrap-toolbox.sh", +} -# Add general useful extensions -devcontainer["customizations"]["vscode"]["extensions"].extend([ - "github.copilot", - "github.copilot-chat", - "ms-vsliveshare.vsliveshare", - "streetsidesoftware.code-spell-checker", - "eamodio.gitlens" -]) +forward_ports = build_forward_ports(config) +if forward_ports: + devcontainer["forwardPorts"] = forward_ports -# Write the devcontainer.json file -with open(DEVCONTAINER_FILE, 'w') as f: - json.dump(devcontainer, f, indent=2) +with open(DEVCONTAINER_FILE, "w", encoding="utf-8") as handle: + json.dump(devcontainer, handle, indent=2) + handle.write("\n") -print(f"Created devcontainer.json with configuration for:") +print("Created devcontainer.json with configuration for:") print("\nProgramming Languages:") for lang, enabled in config["languages"].items(): print(f"- {lang.capitalize()}: {'Enabled' if enabled else 'Disabled'}") +print("\nFeature-installed Languages:") +for lang in FEATURE_LANGUAGE_KEYS: + print(f"- {lang}: {'Enabled' if config['languages'][lang] else 'Disabled'}") + +print("\nFallback-installed Languages:") +for lang in FALLBACK_LANGUAGE_KEYS: + print(f"- {lang}: {'Enabled' if config['languages'][lang] else 'Disabled'}") + print("\nCI/CD Tools:") for tool, enabled in config["cicd"].items(): print(f"- {tool.capitalize()}: {'Enabled' if enabled else 'Disabled'}") @@ -286,8 +433,8 @@ print(f"- {tool.capitalize()}: {'Enabled' if enabled else 'Disabled'}") print("\nForwarded Ports:") -if devcontainer["forwardPorts"]: - for port in devcontainer["forwardPorts"]: +if forward_ports: + for port in forward_ports: print(f"- Port {port}") else: print("- No ports forwarded") diff --git a/.devcontainer/devcontainer.env.json b/.devcontainer/devcontainer.env.json index dffaec7..b5828b8 100644 --- a/.devcontainer/devcontainer.env.json +++ b/.devcontainer/devcontainer.env.json @@ -1,25 +1,26 @@ { "languages": { "python": true, - "java": false, - "node": false, - "ruby": false, - "csharp": false, - "php": false, - "rust": false, - "typescript": false, - "kotlin": false, - "c": false, - "cpp": false, - "javascript": false, - "go": false + "java": true, + "node": true, + "ruby": true, + "csharp": true, + "php": true, + "rust": true, + "typescript": true, + "kotlin": true, + "c": true, + "cpp": true, + "javascript": true, + "go": true }, "cicd": { "docker": true, "terraform": true, "kubectl": true, "helm": true, - "github_actions": true + "github_actions": true, + "podman": false }, "kubernetes": { "k9s": true, diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5e67cbd..9226983 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,50 +3,80 @@ "build": { "dockerfile": "Dockerfile", "args": { - "PYTHON_VERSION": "3.12", - "JAVA_VERSION": "21", - "NODE_VERSION": "22", - "GO_VERSION": "1.25.0", - "INSTALL_PYTHON": "true", - "INSTALL_JAVA": "false", - "INSTALL_JS": "false", - "INSTALL_GO": "false", + "INSTALL_CSHARP": "true", + "INSTALL_KOTLIN": "true", + "INSTALL_C": "true", + "INSTALL_CPP": "true", "INSTALL_DOCKER": "true", + "INSTALL_PODMAN": "false", "INSTALL_TERRAFORM": "true", "INSTALL_KUBECTL": "true", "INSTALL_HELM": "true", "INSTALL_GITHUB_ACTIONS": "true", "INSTALL_K9S": "true", - "K9S_VERSION": "0.50.16", "INSTALL_KUSTOMIZE": "true", - "KUSTOMIZE_VERSION": "5.8.0", "INSTALL_ARGOCD_CLI": "true", - "ARGOCD_VERSION": "3.3.6", "INSTALL_LENS": "false", "INSTALL_KUBESEAL": "true", "INSTALL_FLUX": "true", - "FLUX_VERSION": "2.8.5", "INSTALL_KIND": "true", "INSTALL_MINIKUBE": "true", "INSTALL_OPENSHIFT_CLI": "false", - "INSTALL_GRADLE": "true", - "INSTALL_MAVEN": "true", - "INSTALL_ANT": "true", "INSTALL_MAKE": "true", "INSTALL_CMAKE": "true", "INSTALL_SONARQUBE": "true", "INSTALL_CHECKSTYLE": "true", "INSTALL_PMD": "true", - "INSTALL_ESLINT": "true", - "INSTALL_PYLINT": "true", "INSTALL_NEXUS": "true", - "NEXUS_VERSION": "3.91.0", "INSTALL_PROMETHEUS": "true", - "PROMETHEUS_VERSION": "3.5.1", "INSTALL_GRAFANA": "true", - "GRAFANA_VERSION": "12.4.2", "INSTALL_ELK": "true", - "INSTALL_JENKINS": "false" + "INSTALL_JENKINS": "false", + "JAVA_VERSION": "21", + "TERRAFORM_VERSION": "1.14.7", + "HELM_VERSION": "4.0.1", + "ACTIONS_RUNNER_VERSION": "2.330.0", + "K9S_VERSION": "0.50.16", + "KUSTOMIZE_VERSION": "5.8.0", + "ARGOCD_VERSION": "3.3.6", + "FLUX_VERSION": "2.8.5", + "KUBESEAL_VERSION": "0.33.1", + "KIND_VERSION": "0.31.0", + "MINIKUBE_VERSION": "1.37.0", + "SONAR_SCANNER_VERSION": "8.0.1.6346", + "CHECKSTYLE_VERSION": "12.1.2", + "PMD_VERSION": "7.18.0", + "NEXUS_VERSION": "3.91.0", + "PROMETHEUS_VERSION": "3.5.1", + "GRAFANA_VERSION": "12.4.2" + } + }, + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12", + "installTools": false + }, + "ghcr.io/devcontainers/features/java:1": { + "version": "21", + "jdkDistro": "ms", + "installGradle": true, + "installMaven": true, + "installAnt": true + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "22", + "nodeGypDependencies": true + }, + "ghcr.io/devcontainers/features/go:1": { + "version": "1.25" + }, + "ghcr.io/devcontainers/features/ruby:1": {}, + "ghcr.io/devcontainers/features/php:1": { + "installComposer": true + }, + "ghcr.io/devcontainers/features/rust:1": { + "profile": "minimal", + "components": "rust-analyzer,rust-src,rustfmt,clippy" } }, "mounts": [ @@ -58,16 +88,33 @@ "ms-python.python", "ms-python.vscode-pylance", "ms-python.black-formatter", + "vscjava.vscode-java-pack", + "redhat.java", + "vscjava.vscode-maven", + "vscjava.vscode-gradle", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-vscode.vscode-typescript-next", + "golang.go", + "Shopify.ruby-lsp", + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit", + "ms-vscode.cpptools", + "fwcd.kotlin", "ms-azuretools.vscode-docker", "hashicorp.terraform", "ms-kubernetes-tools.vscode-kubernetes-tools", - "ms-kubernetes-tools.vscode-kubernetes-tools", "mindaro.mindaro", "argoproj.argocd-vscode-extension", "weaveworks.vscode-gitops-tools", "github.vscode-github-actions", "SonarSource.sonarlint-vscode", - "ms-python.pylint", + "shengchen.vscode-checkstyle", + "vscjava.vscode-java-dependency", "github.copilot", "github.copilot-chat", "ms-vsliveshare.vsliveshare", @@ -76,6 +123,7 @@ ] } }, + "postCreateCommand": "bash .devcontainer/bootstrap-toolbox.sh", "forwardPorts": [ 8081, 9090, @@ -83,6 +131,5 @@ 9200, 9300, 5601 - ], - "postCreateCommand": "chmod +x /workspaces/.devcontainer/k8s-config-generator.py && ln -sf /workspaces/.devcontainer/k8s-config-generator.py /usr/local/bin/k8s-config-generator" -} \ No newline at end of file + ] +} diff --git a/README.md b/README.md index f9c2dee..c298f92 100644 --- a/README.md +++ b/README.md @@ -349,8 +349,8 @@ The pre-configured dev container gives you a consistent multi-language environme | Category | Tools | |----------|-------| -| **Languages** | Python 3.11 · Java 17 · Node.js 20 · Go 1.21 | -| **Build tools** | pip · Maven · Gradle · npm · yarn | +| **Languages** | Python 3.12 · Java 21 · Node.js 22 · Go 1.25 · Ruby · PHP · Rust · C# · Kotlin · C/C++ | +| **Build tools** | pip · Maven · Gradle · npm · yarn · Composer · dotnet · Kotlin compiler · clang | | **Linting/Testing** | pytest · black · flake8 · mypy · Jest · ESLint · golangci-lint | | **Containers** | Docker CLI · Docker Compose | | **IaC** | Terraform | @@ -362,6 +362,13 @@ The pre-configured dev container gives you a consistent multi-language environme +The repo-local dev container now uses a hybrid model: + +- Dev Container Features install the mainstream runtimes +- The repo Dockerfile keeps Ubuntu 24.04 plus unsupported language/toolbox extras +- `.devcontainer/devcontainer.env.json` remains the single control plane +- Toolbox mode is the repo default, so all languages stay available unless you turn them off + Generate a dev container configuration from the CLI instead of editing JSON by hand: ```bash @@ -373,7 +380,7 @@ python -m cli.devopsos scaffold devcontainer \ --devops-tools prometheus,grafana ``` -You can also customize `.devcontainer/devcontainer.env.json` directly to enable or disable any language or tool, then reopen in VS Code. +You can also customize `.devcontainer/devcontainer.env.json` directly to enable or disable any language or tool, then reopen in VS Code. For this repository, the checked-in file defaults to the full toolbox profile. --- diff --git a/cli/devopsos.py b/cli/devopsos.py index 915a093..c2556a2 100644 --- a/cli/devopsos.py +++ b/cli/devopsos.py @@ -762,8 +762,22 @@ def _sel(group): return selected_by_group.get(group, []) typer.echo("Aborted by user.") raise typer.Exit(1) - # Write to .devcontainer/devcontainer.env.json - devcontainer_dir = Path(directory) / ".devcontainer" + target_root = Path(directory) + primary_devcontainer_dir = target_root / ".devcontainer" + preserve_existing = primary_devcontainer_dir.exists() + if preserve_existing: + devcontainer_dir = target_root / ".devcontainer.generated" + typer.echo( + f"Existing {primary_devcontainer_dir} detected. Preserving it and writing " + f"generated output to {devcontainer_dir}." + ) + typer.echo( + ".devcontainer.generated/ is a review/reference copy and may be overwritten " + "by later init runs." + ) + else: + devcontainer_dir = primary_devcontainer_dir + devcontainer_dir.mkdir(parents=True, exist_ok=True) env_json_path = devcontainer_dir / "devcontainer.env.json" with open(env_json_path, "w") as f: diff --git a/cli/test_cli.py b/cli/test_cli.py index 48884e0..30a81b4 100644 --- a/cli/test_cli.py +++ b/cli/test_cli.py @@ -86,6 +86,138 @@ def test_init_dir_option_creates_devcontainer_in_specified_dir(): "Expected devcontainer.env.json inside .devcontainer" ) +def test_init_preserves_existing_devcontainer_and_writes_generated_copy(): + """Existing .devcontainer must be preserved; generated output goes to .devcontainer.generated.""" + from unittest.mock import MagicMock, patch + from typer.testing import CliRunner + from cli.devopsos import app + + checkbox_mock = MagicMock() + checkbox_mock.execute.return_value = [] + + confirm_proceed = MagicMock() + confirm_proceed.execute.return_value = True + confirm_skip = MagicMock() + confirm_skip.execute.return_value = False + + with tempfile.TemporaryDirectory() as tmp: + existing_dir = Path(tmp) / ".devcontainer" + existing_dir.mkdir(parents=True, exist_ok=True) + existing_env = existing_dir / "devcontainer.env.json" + existing_json = existing_dir / "devcontainer.json" + existing_env.write_text('{"protected": true}\n') + existing_json.write_text('{"protected": true}\n') + + with patch("cli.devopsos.inquirer.checkbox", return_value=checkbox_mock), \ + patch("cli.devopsos.inquirer.confirm", + side_effect=[confirm_proceed, confirm_skip]): + runner = CliRunner() + result = runner.invoke(app, ["init", "--dir", tmp]) + + assert result.exit_code == 0, result.output + assert "Preserving it and writing generated output" in result.output + assert existing_env.read_text() == '{"protected": true}\n' + assert existing_json.read_text() == '{"protected": true}\n' + + generated_dir = Path(tmp) / ".devcontainer.generated" + assert generated_dir.is_dir() + assert (generated_dir / "devcontainer.env.json").exists() + assert not (generated_dir / "devcontainer.json").exists() + +def test_init_preserves_existing_devcontainer_when_only_one_file_exists(): + """Any existing .devcontainer directory is protected even if only one config file exists.""" + from unittest.mock import MagicMock, patch + from typer.testing import CliRunner + from cli.devopsos import app + + checkbox_mock = MagicMock() + checkbox_mock.execute.return_value = [] + + confirm_proceed = MagicMock() + confirm_proceed.execute.return_value = True + confirm_generate = MagicMock() + confirm_generate.execute.return_value = True + + with tempfile.TemporaryDirectory() as tmp: + existing_dir = Path(tmp) / ".devcontainer" + existing_dir.mkdir(parents=True, exist_ok=True) + existing_env = existing_dir / "devcontainer.env.json" + existing_env.write_text('{"protected": true}\n') + + with patch("cli.devopsos.inquirer.checkbox", return_value=checkbox_mock), \ + patch("cli.devopsos.inquirer.confirm", + side_effect=[confirm_proceed, confirm_generate]): + result = CliRunner().invoke(app, ["init", "--dir", tmp]) + + assert result.exit_code == 0, result.output + assert existing_env.read_text() == '{"protected": true}\n' + assert not (existing_dir / "devcontainer.json").exists() + + generated_dir = Path(tmp) / ".devcontainer.generated" + assert (generated_dir / "devcontainer.env.json").exists() + assert (generated_dir / "devcontainer.json").exists() + +def test_init_rerun_refreshes_generated_output_without_touching_devcontainer(): + """Rerunning init refreshes .devcontainer.generated and leaves .devcontainer unchanged.""" + from unittest.mock import MagicMock, patch + from typer.testing import CliRunner + from cli.devopsos import app + + selections = [ + ["python"], + ["docker"], + [], + [], + [], + [], + [], + ] + sel_iter = iter(selections) + + def _checkbox_factory(**kwargs): + mock = MagicMock() + mock.execute.return_value = next(sel_iter) + return mock + + text_mock = MagicMock() + text_mock.execute.return_value = "3.12" + + confirm_proceed = MagicMock() + confirm_proceed.execute.return_value = True + confirm_generate = MagicMock() + confirm_generate.execute.return_value = True + + with tempfile.TemporaryDirectory() as tmp: + existing_dir = Path(tmp) / ".devcontainer" + existing_dir.mkdir(parents=True, exist_ok=True) + existing_env = existing_dir / "devcontainer.env.json" + existing_env.write_text('{"protected": true}\n') + + generated_dir = Path(tmp) / ".devcontainer.generated" + generated_dir.mkdir(parents=True, exist_ok=True) + stale_env = generated_dir / "devcontainer.env.json" + stale_json = generated_dir / "devcontainer.json" + stale_env.write_text('{"stale": true}\n') + stale_json.write_text('{"stale": true}\n') + + with patch("cli.devopsos.inquirer.checkbox", side_effect=_checkbox_factory), \ + patch("cli.devopsos.inquirer.text", return_value=text_mock), \ + patch("cli.devopsos.inquirer.confirm", + side_effect=[confirm_proceed, confirm_generate]): + result = CliRunner().invoke(app, ["init", "--dir", tmp]) + + assert result.exit_code == 0, result.output + assert existing_env.read_text() == '{"protected": true}\n' + + env_cfg = json.loads(stale_env.read_text()) + dc_cfg = json.loads(stale_json.read_text()) + assert env_cfg["languages"]["python"] is True + assert env_cfg["cicd"]["docker"] is True + assert env_cfg["languages"]["java"] is False + assert dc_cfg["build"]["args"]["INSTALL_PYTHON"] == "true" + assert dc_cfg["build"]["args"]["INSTALL_DOCKER"] == "true" + assert dc_cfg["build"]["args"]["INSTALL_JAVA"] == "false" + def test_init_checkbox_includes_space_instruction(): """Each checkbox prompt must include an instruction so users know to press Space.""" from unittest.mock import MagicMock, patch, call diff --git a/docs/CLI-COMMANDS-REFERENCE.md b/docs/CLI-COMMANDS-REFERENCE.md index 469726c..59e9b21 100644 --- a/docs/CLI-COMMANDS-REFERENCE.md +++ b/docs/CLI-COMMANDS-REFERENCE.md @@ -438,7 +438,11 @@ python -m cli.devopsos init [--dir DIRECTORY] ### Output files -`.devcontainer/devcontainer.env.json` and `.devcontainer/devcontainer.json` +- Fresh target with no existing `.devcontainer/`: + `.devcontainer/devcontainer.env.json` and `.devcontainer/devcontainer.json` +- Existing target with `.devcontainer/` already present: + `.devcontainer/` is preserved unchanged and generated output is written to + `.devcontainer.generated/devcontainer.env.json` and `.devcontainer.generated/devcontainer.json` --- diff --git a/docs/CLI-TEST-REPORT.md b/docs/CLI-TEST-REPORT.md index d1f14bb..a08b761 100644 --- a/docs/CLI-TEST-REPORT.md +++ b/docs/CLI-TEST-REPORT.md @@ -39,6 +39,9 @@ |---------------------|------|--------| | `init --help` shows `--dir` option | `test_init_help_shows_dir_option` | ✅ | | `init --dir ` creates `.devcontainer/` in target dir | `test_init_dir_option_creates_devcontainer_in_specified_dir` | ✅ | +| Existing `.devcontainer/` is preserved and generated output goes to `.devcontainer.generated/` | `test_init_preserves_existing_devcontainer_and_writes_generated_copy` | ✅ | +| Existing `.devcontainer/` with partial config is still preserved | `test_init_preserves_existing_devcontainer_when_only_one_file_exists` | ✅ | +| Rerunning `init` refreshes `.devcontainer.generated/` only | `test_init_rerun_refreshes_generated_output_without_touching_devcontainer` | ✅ | | Checkbox prompt includes Space-to-toggle instruction | `test_init_checkbox_includes_space_instruction` | ✅ | | Selected tools are written to `devcontainer.json` | `test_init_selections_written_to_config` | ✅ | diff --git a/docs/DEVOPS-OS-README.md b/docs/DEVOPS-OS-README.md index e485a25..892460d 100644 --- a/docs/DEVOPS-OS-README.md +++ b/docs/DEVOPS-OS-README.md @@ -57,6 +57,7 @@ Before getting started, ensure you have the following installed: ``` This creates `.devcontainer/devcontainer.json` and `.devcontainer/devcontainer.env.json` with the correct build args, VS Code extensions, and forwarded ports for your selected tools. + If the target directory already has a `.devcontainer/`, `devopsos init` preserves it and writes a review copy to `.devcontainer.generated/` instead of overwriting the existing files. Run `python -m cli.scaffold_devcontainer --help` to see all options, including version overrides (e.g. `--python-version 3.12`). From c03d4ad7ca20f238087c052e70af2984708e434b Mon Sep 17 00:00:00 2001 From: Saravanan Gnanaguru Date: Thu, 16 Apr 2026 09:43:09 +0530 Subject: [PATCH 2/3] feat: add template-backed devcontainer generation helpers - Introduced `devcontainer_templates.py` for generating devcontainer configurations using templates. - Added Dockerfile template for a multi-language development environment. - Created JSON templates for `devcontainer.env.json` and `devcontainer.json` to facilitate environment variable and configuration management. - Implemented functions for building environment configurations, features, arguments, extensions, and post-create commands. - Enhanced support for various programming languages, CI/CD tools, Kubernetes utilities, build tools, code analysis tools, and DevOps tools. --- .devcontainer/README.md | 152 +----- .../devcontainer}/Dockerfile | 0 .legacy/devcontainer/README.md | 153 ++++++ .../devcontainer}/bootstrap-toolbox.sh | 0 .../devcontainer}/configure.py | 0 .../devcontainer}/devcontainer.env.json | 0 .../devcontainer}/devcontainer.json | 0 README-USECASE-EXAMPLES.md | 2 +- README.md | 15 +- cli/devcontainer_templates.py | 485 ++++++++++++++++++ cli/devopsos.py | 130 +---- cli/templates/devcontainer/Dockerfile.tpl | 309 +++++++++++ .../devcontainer/devcontainer.env.json.tpl | 9 + .../devcontainer/devcontainer.json.tpl | 17 + cli/test_cli.py | 112 +--- docs/CLI-COMMANDS-REFERENCE.md | 27 +- docs/CLI-TEST-REPORT.md | 3 +- docs/DEVOPS-OS-README.md | 39 +- docs/HowTo-Create-DevOps-Os-GHA-Jenkins.md | 36 +- docs/kubernetes-capabilities.md | 32 +- .../content/docs/dev-container/_index.md | 6 +- mcp_server/server.py | 7 +- 22 files changed, 1092 insertions(+), 442 deletions(-) rename {.devcontainer => .legacy/devcontainer}/Dockerfile (100%) create mode 100644 .legacy/devcontainer/README.md rename {.devcontainer => .legacy/devcontainer}/bootstrap-toolbox.sh (100%) rename {.devcontainer => .legacy/devcontainer}/configure.py (100%) rename {.devcontainer => .legacy/devcontainer}/devcontainer.env.json (100%) rename {.devcontainer => .legacy/devcontainer}/devcontainer.json (100%) create mode 100644 cli/devcontainer_templates.py create mode 100644 cli/templates/devcontainer/Dockerfile.tpl create mode 100644 cli/templates/devcontainer/devcontainer.env.json.tpl create mode 100644 cli/templates/devcontainer/devcontainer.json.tpl diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 1f9ee09..7d2ce10 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -1,148 +1,18 @@ -# Multi-Language Development Container +# Devcontainer Status -This development container provides a toolbox-style environment across the major languages supported by DevOps-OS, along with CI/CD and Kubernetes tooling. +The checked-in repo-local devcontainer stack has been retired. -## Features +Active generation paths: -- **Hybrid Runtime Strategy**: official Dev Container Features install mainstream runtimes, while the repo Dockerfile keeps Ubuntu 24.04 plus unsupported language/toolbox extras -- **Multiple Languages**: Python, Java, Node.js/JavaScript/TypeScript, Go, Ruby, PHP, Rust, C#, Kotlin, and C/C++ are available by default -- **CI/CD Tools**: Docker, Terraform, Kubernetes (kubectl), Helm, GitHub Actions -- **Customizable**: `.devcontainer/devcontainer.env.json` remains the single control plane for languages and tools +- `python -m cli.devopsos init` on a fresh target generates `.devcontainer/Dockerfile`, `.devcontainer/devcontainer.json`, and `.devcontainer/devcontainer.env.json` from templates. +- `python -m cli.devopsos scaffold devcontainer` generates the legacy two-file `.devcontainer/devcontainer.json` and `.devcontainer/devcontainer.env.json`. -## Getting Started +Active source files: -### Prerequisites +- `cli/devcontainer_templates.py` +- `cli/templates/devcontainer/` +- `cli/scaffold_devcontainer.py` -- [Docker](https://www.docker.com/products/docker-desktop) -- [Visual Studio Code](https://code.visualstudio.com/) -- [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) +Archived legacy implementation: -### Configuration - -#### Option A — Use the CLI (recommended) - -Generate `devcontainer.json` and `devcontainer.env.json` with a single command: - -```bash -# From the repository root -python -m cli.scaffold_devcontainer \ - --languages python,java,go \ - --cicd-tools docker,terraform,kubectl,helm \ - --kubernetes-tools k9s,kustomize,argocd_cli,flux \ - --devops-tools prometheus,grafana - -# Or via the unified CLI -python -m cli.devopsos scaffold devcontainer -``` - -Run `python -m cli.scaffold_devcontainer --help` to see all available options including version overrides (`--python-version`, `--go-version`, etc.). - -#### Option B — Edit JSON manually - -1. Customize the environment by editing `.devcontainer/devcontainer.env.json`: - -```json -{ - "languages": { - "python": true, - "java": true, - "node": true, - "ruby": true, - "csharp": true, - "php": true, - "rust": true, - "typescript": true, - "kotlin": true, - "c": true, - "cpp": true, - "javascript": true, - "go": true - }, - "cicd": { - "docker": true, - "terraform": true, - "kubectl": true, - "helm": true, - "github_actions": true - }, - "kubernetes": { - "k9s": true, - "kustomize": true, - "argocd_cli": true, - "lens": false, - "kubeseal": true, - "flux": true, - "kind": true, - "minikube": true, - "openshift_cli": false - }, - "versions": { - "python": "3.12", - "java": "21", - "node": "22", - "go": "1.25.0", - "k9s": "0.50.16", - "argocd": "3.3.6", - "flux": "2.8.5", - "kustomize": "5.8.0" - } -} -``` - -2. Run the configuration script: - -```bash -cd .devcontainer -python3 configure.py -``` - -#### Open in VS Code - -After configuring (via either option), open the project in VS Code and click "Reopen in Container" when prompted. - -## What's Included - -### Languages and Tools - -- **Python**: Python interpreter, pip, pytest, black, flake8, mypy -- **Java/Kotlin**: JDK, Maven, Gradle, Ant, Kotlin compiler -- **JavaScript/TypeScript**: Node.js, npm, yarn, TypeScript, Jest, ESLint, Prettier -- **Go**: Go compiler, golangci-lint -- **Ruby/PHP/Rust/C#**: runtime/compiler support installed for toolbox workflows -- **C/C++**: build-essential, clang, gdb, cmake - -### CI/CD Tools - -- **Docker**: Docker CLI, Docker Compose -- **Infrastructure as Code**: Terraform -- **Kubernetes**: kubectl, Helm -- **Workflows**: GitHub Actions runner - -### Kubernetes Tools - -- **Cluster Management**: K9s terminal UI, KinD, Minikube -- **Application Deployment**: Kustomize, Helm -- **GitOps**: ArgoCD CLI, Flux CD -- **Secret Management**: Kubeseal for Sealed Secrets -- **Observability**: Integrated with Prometheus and Grafana -- **Configuration Generator**: Built-in tool to generate Kubernetes manifests for kubectl, Kustomize, ArgoCD, and Flux - -For detailed information on using the Kubernetes capabilities, see [kubernetes-capabilities.md](kubernetes-capabilities.md). - -## Customization - -You can: - -1. Disable languages or tools you don't need -2. Change pinned versions for Python, Java, Node, and Go -3. Add additional repo-local tools in `bootstrap-toolbox.sh` or the Dockerfile - -## Troubleshooting - -- **Docker Access Issues**: Make sure the Docker socket is properly mounted -- **Performance Issues**: Adjust Docker Desktop resource settings -- **Missing Tools**: Check the logs during container build or modify the Dockerfile - -## Contributing - -Feel free to submit issues or pull requests to improve this development container. +- `.legacy/devcontainer/` diff --git a/.devcontainer/Dockerfile b/.legacy/devcontainer/Dockerfile similarity index 100% rename from .devcontainer/Dockerfile rename to .legacy/devcontainer/Dockerfile diff --git a/.legacy/devcontainer/README.md b/.legacy/devcontainer/README.md new file mode 100644 index 0000000..9660f9d --- /dev/null +++ b/.legacy/devcontainer/README.md @@ -0,0 +1,153 @@ +# Legacy Repo-Local Development Container + +This directory contains the archived repo-local devcontainer implementation that previously lived under `.devcontainer/`. +It is kept for historical reference and is no longer the active generation path. + +# Multi-Language Development Container + +This development container provides a toolbox-style environment across the major languages supported by DevOps-OS, along with CI/CD and Kubernetes tooling. + +## Features + +- **Hybrid Runtime Strategy**: official Dev Container Features install mainstream runtimes, while the repo Dockerfile keeps Ubuntu 24.04 plus unsupported language/toolbox extras +- **Multiple Languages**: Python, Java, Node.js/JavaScript/TypeScript, Go, Ruby, PHP, Rust, C#, Kotlin, and C/C++ are available by default +- **CI/CD Tools**: Docker, Terraform, Kubernetes (kubectl), Helm, GitHub Actions +- **Customizable**: `.devcontainer/devcontainer.env.json` remains the single control plane for languages and tools + +## Getting Started + +### Prerequisites + +- [Docker](https://www.docker.com/products/docker-desktop) +- [Visual Studio Code](https://code.visualstudio.com/) +- [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) + +### Configuration + +#### Option A — Use the CLI (recommended) + +Generate `devcontainer.json` and `devcontainer.env.json` with a single command: + +```bash +# From the repository root +python -m cli.scaffold_devcontainer \ + --languages python,java,go \ + --cicd-tools docker,terraform,kubectl,helm \ + --kubernetes-tools k9s,kustomize,argocd_cli,flux \ + --devops-tools prometheus,grafana + +# Or via the unified CLI +python -m cli.devopsos scaffold devcontainer +``` + +Run `python -m cli.scaffold_devcontainer --help` to see all available options including version overrides (`--python-version`, `--go-version`, etc.). + +#### Option B — Edit JSON manually + +1. Customize the environment by editing `.devcontainer/devcontainer.env.json`: + +```json +{ + "languages": { + "python": true, + "java": true, + "node": true, + "ruby": true, + "csharp": true, + "php": true, + "rust": true, + "typescript": true, + "kotlin": true, + "c": true, + "cpp": true, + "javascript": true, + "go": true + }, + "cicd": { + "docker": true, + "terraform": true, + "kubectl": true, + "helm": true, + "github_actions": true + }, + "kubernetes": { + "k9s": true, + "kustomize": true, + "argocd_cli": true, + "lens": false, + "kubeseal": true, + "flux": true, + "kind": true, + "minikube": true, + "openshift_cli": false + }, + "versions": { + "python": "3.12", + "java": "21", + "node": "22", + "go": "1.25.0", + "k9s": "0.50.16", + "argocd": "3.3.6", + "flux": "2.8.5", + "kustomize": "5.8.0" + } +} +``` + +2. Run the configuration script: + +```bash +cd .devcontainer +python3 configure.py +``` + +#### Open in VS Code + +After configuring (via either option), open the project in VS Code and click "Reopen in Container" when prompted. + +## What's Included + +### Languages and Tools + +- **Python**: Python interpreter, pip, pytest, black, flake8, mypy +- **Java/Kotlin**: JDK, Maven, Gradle, Ant, Kotlin compiler +- **JavaScript/TypeScript**: Node.js, npm, yarn, TypeScript, Jest, ESLint, Prettier +- **Go**: Go compiler, golangci-lint +- **Ruby/PHP/Rust/C#**: runtime/compiler support installed for toolbox workflows +- **C/C++**: build-essential, clang, gdb, cmake + +### CI/CD Tools + +- **Docker**: Docker CLI, Docker Compose +- **Infrastructure as Code**: Terraform +- **Kubernetes**: kubectl, Helm +- **Workflows**: GitHub Actions runner + +### Kubernetes Tools + +- **Cluster Management**: K9s terminal UI, KinD, Minikube +- **Application Deployment**: Kustomize, Helm +- **GitOps**: ArgoCD CLI, Flux CD +- **Secret Management**: Kubeseal for Sealed Secrets +- **Observability**: Integrated with Prometheus and Grafana +- **Configuration Generator**: Built-in tool to generate Kubernetes manifests for kubectl, Kustomize, ArgoCD, and Flux + +For detailed information on using the Kubernetes capabilities, see [kubernetes-capabilities.md](kubernetes-capabilities.md). + +## Customization + +You can: + +1. Disable languages or tools you don't need +2. Change pinned versions for Python, Java, Node, and Go +3. Add additional repo-local tools in `bootstrap-toolbox.sh` or the Dockerfile + +## Troubleshooting + +- **Docker Access Issues**: Make sure the Docker socket is properly mounted +- **Performance Issues**: Adjust Docker Desktop resource settings +- **Missing Tools**: Check the logs during container build or modify the Dockerfile + +## Contributing + +Feel free to submit issues or pull requests to improve this development container. diff --git a/.devcontainer/bootstrap-toolbox.sh b/.legacy/devcontainer/bootstrap-toolbox.sh similarity index 100% rename from .devcontainer/bootstrap-toolbox.sh rename to .legacy/devcontainer/bootstrap-toolbox.sh diff --git a/.devcontainer/configure.py b/.legacy/devcontainer/configure.py similarity index 100% rename from .devcontainer/configure.py rename to .legacy/devcontainer/configure.py diff --git a/.devcontainer/devcontainer.env.json b/.legacy/devcontainer/devcontainer.env.json similarity index 100% rename from .devcontainer/devcontainer.env.json rename to .legacy/devcontainer/devcontainer.env.json diff --git a/.devcontainer/devcontainer.json b/.legacy/devcontainer/devcontainer.json similarity index 100% rename from .devcontainer/devcontainer.json rename to .legacy/devcontainer/devcontainer.json diff --git a/README-USECASE-EXAMPLES.md b/README-USECASE-EXAMPLES.md index 4d856f9..b3bbaed 100644 --- a/README-USECASE-EXAMPLES.md +++ b/README-USECASE-EXAMPLES.md @@ -12,7 +12,7 @@ This document provides practical examples and advanced use cases for each major - **Version Pinning**: Ensure all developers use the same versions of compilers, linters, and CI/CD tools. ### Example: Adding a New Language -Edit `.devcontainer/devcontainer.env.json` to enable Ruby support: +In a generated project devcontainer, edit `.devcontainer/devcontainer.env.json` to enable Ruby support: ```json { "languages": { diff --git a/README.md b/README.md index c298f92..1d40e83 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,8 @@ python -m cli.devopsos scaffold gha --help ```text devops_os/ -├── .devcontainer/ # Dev container config (Dockerfile, devcontainer.json, setup scripts) +├── .devcontainer/ # Redirect note; active devcontainer generation lives in cli/templates/devcontainer/ +├── .legacy/ # Archived repo-local devcontainer implementation ├── .github/workflows/ # CI, Sanity Tests, and GitHub Pages workflows ├── cli/ # CLI scaffold tools (scaffold_gha, gitlab, jenkins, argocd, sre, unittest, devopsos) ├── kubernetes/ # Kubernetes manifest generator @@ -362,12 +363,12 @@ The pre-configured dev container gives you a consistent multi-language environme -The repo-local dev container now uses a hybrid model: +Dev container generation now uses two supported paths: -- Dev Container Features install the mainstream runtimes -- The repo Dockerfile keeps Ubuntu 24.04 plus unsupported language/toolbox extras -- `.devcontainer/devcontainer.env.json` remains the single control plane -- Toolbox mode is the repo default, so all languages stay available unless you turn them off +- `python -m cli.devopsos init` on a fresh target generates `.devcontainer/Dockerfile`, `.devcontainer/devcontainer.json`, and `.devcontainer/devcontainer.env.json` from templates. +- `python -m cli.devopsos scaffold devcontainer` generates the legacy two-file `.devcontainer/devcontainer.json` and `.devcontainer/devcontainer.env.json`. + +The old checked-in repo-local `.devcontainer` stack has been archived under `.legacy/devcontainer/`. Generate a dev container configuration from the CLI instead of editing JSON by hand: @@ -380,7 +381,7 @@ python -m cli.devopsos scaffold devcontainer \ --devops-tools prometheus,grafana ``` -You can also customize `.devcontainer/devcontainer.env.json` directly to enable or disable any language or tool, then reopen in VS Code. For this repository, the checked-in file defaults to the full toolbox profile. +For generated projects, you can customize the generated `.devcontainer/devcontainer.env.json` directly to enable or disable any language or tool, then rebuild or reopen in VS Code. --- diff --git a/cli/devcontainer_templates.py b/cli/devcontainer_templates.py new file mode 100644 index 0000000..a87c76b --- /dev/null +++ b/cli/devcontainer_templates.py @@ -0,0 +1,485 @@ +#!/usr/bin/env python3 +"""Template-backed devcontainer generation helpers used by ``devopsos init`` only. + +This module intentionally supports the `init` flow and should not be used to +change the public contract of `scaffold devcontainer`, which remains on its +legacy generator path for backward compatibility. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +TEMPLATE_DIR = Path(__file__).resolve().parent / "templates" / "devcontainer" + +ALL_LANGUAGES = [ + "python", "java", "node", "ruby", "csharp", "php", + "rust", "typescript", "kotlin", "c", "cpp", "javascript", "go", +] +ALL_CICD = [ + "docker", "podman", "terraform", "kubectl", "helm", "github_actions", "jenkins", +] +ALL_KUBERNETES = [ + "k9s", "kustomize", "argocd_cli", "lens", "kubeseal", "flux", + "kind", "minikube", "openshift_cli", +] +ALL_BUILD_TOOLS = ["gradle", "maven", "ant", "make", "cmake"] +ALL_CODE_ANALYSIS = ["sonarqube", "checkstyle", "pmd", "eslint", "pylint"] +ALL_DEVOPS_TOOLS = ["nexus", "prometheus", "grafana", "elk", "jenkins"] + +DEFAULT_VERSIONS = { + "python": "3.12", + "java": "21", + "node": "22", + "go": "1.25.0", + "nexus": "3.91.0", + "prometheus": "3.5.1", + "grafana": "12.4.2", + "k9s": "0.50.16", + "argocd": "3.3.6", + "flux": "2.8.5", + "kustomize": "5.8.0", + "jenkins": "2.440.1", +} + +FEATURE_REFS = { + "python": "ghcr.io/devcontainers/features/python:1", + "java": "ghcr.io/devcontainers/features/java:1", + "node": "ghcr.io/devcontainers/features/node:1", + "go": "ghcr.io/devcontainers/features/go:1", + "ruby": "ghcr.io/devcontainers/features/ruby:1", + "php": "ghcr.io/devcontainers/features/php:1", + "rust": "ghcr.io/devcontainers/features/rust:1", +} + +BUILD_ARG_FACTORIES = { + "INSTALL_CSHARP": lambda c: c["languages"]["csharp"], + "INSTALL_KOTLIN": lambda c: c["languages"]["kotlin"], + "INSTALL_C": lambda c: c["languages"]["c"], + "INSTALL_CPP": lambda c: c["languages"]["cpp"], + "INSTALL_DOCKER": lambda c: c["cicd"]["docker"], + "INSTALL_PODMAN": lambda c: c["cicd"].get("podman", False), + "INSTALL_TERRAFORM": lambda c: c["cicd"]["terraform"], + "INSTALL_KUBECTL": lambda c: c["cicd"]["kubectl"], + "INSTALL_HELM": lambda c: c["cicd"]["helm"], + "INSTALL_GITHUB_ACTIONS": lambda c: c["cicd"]["github_actions"], + "INSTALL_K9S": lambda c: c["kubernetes"]["k9s"], + "INSTALL_KUSTOMIZE": lambda c: c["kubernetes"]["kustomize"], + "INSTALL_ARGOCD_CLI": lambda c: c["kubernetes"]["argocd_cli"], + "INSTALL_LENS": lambda c: c["kubernetes"]["lens"], + "INSTALL_KUBESEAL": lambda c: c["kubernetes"]["kubeseal"], + "INSTALL_FLUX": lambda c: c["kubernetes"]["flux"], + "INSTALL_KIND": lambda c: c["kubernetes"]["kind"], + "INSTALL_MINIKUBE": lambda c: c["kubernetes"]["minikube"], + "INSTALL_OPENSHIFT_CLI": lambda c: c["kubernetes"]["openshift_cli"], + "INSTALL_MAKE": lambda c: c["build_tools"]["make"], + "INSTALL_CMAKE": lambda c: c["build_tools"]["cmake"], + "INSTALL_SONARQUBE": lambda c: c["code_analysis"]["sonarqube"], + "INSTALL_CHECKSTYLE": lambda c: c["code_analysis"]["checkstyle"], + "INSTALL_PMD": lambda c: c["code_analysis"]["pmd"], + "INSTALL_NEXUS": lambda c: c["devops_tools"]["nexus"], + "INSTALL_PROMETHEUS": lambda c: c["devops_tools"]["prometheus"], + "INSTALL_GRAFANA": lambda c: c["devops_tools"]["grafana"], + "INSTALL_ELK": lambda c: c["devops_tools"]["elk"], + "INSTALL_JENKINS": lambda c: c["devops_tools"]["jenkins"], +} + +VERSION_ARG_FACTORIES = { + "JAVA_VERSION": lambda c: c["versions"].get("java", DEFAULT_VERSIONS["java"]) if java_feature_enabled(c) or c["devops_tools"]["nexus"] else None, + "TERRAFORM_VERSION": lambda c: "1.14.7" if c["cicd"]["terraform"] else None, + "HELM_VERSION": lambda c: "4.0.1" if c["cicd"]["helm"] else None, + "ACTIONS_RUNNER_VERSION": lambda c: "2.330.0" if c["cicd"]["github_actions"] else None, + "K9S_VERSION": lambda c: c["versions"].get("k9s", DEFAULT_VERSIONS["k9s"]) if c["kubernetes"]["k9s"] else None, + "KUSTOMIZE_VERSION": lambda c: c["versions"].get("kustomize", DEFAULT_VERSIONS["kustomize"]) if c["kubernetes"]["kustomize"] else None, + "ARGOCD_VERSION": lambda c: c["versions"].get("argocd", DEFAULT_VERSIONS["argocd"]) if c["kubernetes"]["argocd_cli"] else None, + "FLUX_VERSION": lambda c: c["versions"].get("flux", DEFAULT_VERSIONS["flux"]) if c["kubernetes"]["flux"] else None, + "KUBESEAL_VERSION": lambda c: "0.33.1" if c["kubernetes"]["kubeseal"] else None, + "KIND_VERSION": lambda c: "0.31.0" if c["kubernetes"]["kind"] else None, + "MINIKUBE_VERSION": lambda c: "1.37.0" if c["kubernetes"]["minikube"] else None, + "SONAR_SCANNER_VERSION": lambda c: "8.0.1.6346" if c["code_analysis"]["sonarqube"] else None, + "CHECKSTYLE_VERSION": lambda c: "12.1.2" if c["code_analysis"]["checkstyle"] else None, + "PMD_VERSION": lambda c: "7.18.0" if c["code_analysis"]["pmd"] else None, + "NEXUS_VERSION": lambda c: c["versions"].get("nexus", DEFAULT_VERSIONS["nexus"]) if c["devops_tools"]["nexus"] else None, + "PROMETHEUS_VERSION": lambda c: c["versions"].get("prometheus", DEFAULT_VERSIONS["prometheus"]) if c["devops_tools"]["prometheus"] else None, + "GRAFANA_VERSION": lambda c: c["versions"].get("grafana", DEFAULT_VERSIONS["grafana"]) if c["devops_tools"]["grafana"] else None, +} + + +def split_csv(csv_string: str) -> list[str]: + return [token.strip() for token in csv_string.split(",") if token.strip()] + + +def bool_string(value: Any) -> str: + return str(bool(value)).lower() + + +def normalize_go_version(version: str) -> str: + parts = str(version).split(".") + if len(parts) >= 2: + return ".".join(parts[:2]) + return str(version) + + +def unique(values: list[str]) -> list[str]: + seen = set() + ordered = [] + for value in values: + if value not in seen: + seen.add(value) + ordered.append(value) + return ordered + + +def java_feature_enabled(cfg: dict[str, Any]) -> bool: + return ( + cfg["languages"]["java"] + or cfg["build_tools"]["gradle"] + or cfg["build_tools"]["maven"] + or cfg["build_tools"]["ant"] + or cfg["code_analysis"]["checkstyle"] + or cfg["code_analysis"]["pmd"] + ) + + +def node_feature_enabled(cfg: dict[str, Any]) -> bool: + return ( + cfg["languages"]["node"] + or cfg["languages"]["javascript"] + or cfg["languages"]["typescript"] + or cfg["code_analysis"]["eslint"] + ) + + +def build_env_config( + *, + languages: list[str], + cicd_tools: list[str], + kubernetes_tools: list[str], + build_tools: list[str], + code_analysis: list[str], + devops_tools: list[str], + versions: dict[str, str], +) -> dict[str, Any]: + resolved_versions = {} + for key in ("python", "java", "node", "go"): + resolved_versions[key] = versions.get(key, DEFAULT_VERSIONS[key]) + + optional_version_keys = { + "k9s": kubernetes_tools, + "argocd": kubernetes_tools, + "flux": kubernetes_tools, + "kustomize": kubernetes_tools, + "nexus": devops_tools, + "prometheus": devops_tools, + "grafana": devops_tools, + "jenkins": devops_tools, + } + selection_names = { + "k9s": "k9s", + "argocd": "argocd_cli", + "flux": "flux", + "kustomize": "kustomize", + "nexus": "nexus", + "prometheus": "prometheus", + "grafana": "grafana", + "jenkins": "jenkins", + } + for key, selected_list in optional_version_keys.items(): + if selection_names[key] in selected_list: + resolved_versions[key] = versions.get(key, DEFAULT_VERSIONS[key]) + + return { + "languages": {lang: lang in languages for lang in ALL_LANGUAGES}, + "cicd": {tool: tool in cicd_tools for tool in ALL_CICD}, + "kubernetes": {tool: tool in kubernetes_tools for tool in ALL_KUBERNETES}, + "build_tools": {tool: tool in build_tools for tool in ALL_BUILD_TOOLS}, + "code_analysis": {tool: tool in code_analysis for tool in ALL_CODE_ANALYSIS}, + "devops_tools": {tool: tool in devops_tools for tool in ALL_DEVOPS_TOOLS}, + "versions": resolved_versions, + } + + +def build_features(cfg: dict[str, Any]) -> dict[str, Any]: + versions = cfg["versions"] + features: dict[str, Any] = {} + + if cfg["languages"]["python"]: + features[FEATURE_REFS["python"]] = { + "version": versions.get("python", DEFAULT_VERSIONS["python"]), + "installTools": False, + } + if java_feature_enabled(cfg): + features[FEATURE_REFS["java"]] = { + "version": versions.get("java", DEFAULT_VERSIONS["java"]), + "jdkDistro": "ms", + "installGradle": cfg["build_tools"]["gradle"], + "installMaven": cfg["build_tools"]["maven"], + "installAnt": cfg["build_tools"]["ant"], + } + if node_feature_enabled(cfg): + features[FEATURE_REFS["node"]] = { + "version": versions.get("node", DEFAULT_VERSIONS["node"]), + "nodeGypDependencies": True, + } + if cfg["languages"]["go"]: + features[FEATURE_REFS["go"]] = { + "version": normalize_go_version(versions.get("go", DEFAULT_VERSIONS["go"])), + } + if cfg["languages"]["ruby"]: + features[FEATURE_REFS["ruby"]] = {} + if cfg["languages"]["php"]: + features[FEATURE_REFS["php"]] = {"installComposer": True} + if cfg["languages"]["rust"]: + features[FEATURE_REFS["rust"]] = { + "profile": "minimal", + "components": "rust-analyzer,rust-src,rustfmt,clippy", + } + + return features + + +def build_args(cfg: dict[str, Any]) -> dict[str, str]: + args: dict[str, str] = {} + for name, factory in BUILD_ARG_FACTORIES.items(): + args[name] = bool_string(factory(cfg)) + + for name, factory in VERSION_ARG_FACTORIES.items(): + value = factory(cfg) + if value is not None: + args[name] = str(value) + + return args + + +def build_extensions(cfg: dict[str, Any]) -> list[str]: + langs = cfg["languages"] + cicd = cfg["cicd"] + k8s = cfg["kubernetes"] + build_tools = cfg["build_tools"] + analysis = cfg["code_analysis"] + devops = cfg["devops_tools"] + extensions: list[str] = [] + + if langs["python"]: + extensions.extend([ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + ]) + if analysis["pylint"]: + extensions.append("ms-python.pylint") + + if java_feature_enabled(cfg): + extensions.extend([ + "vscjava.vscode-java-pack", + "redhat.java", + "vscjava.vscode-maven", + "vscjava.vscode-gradle", + ]) + if analysis["checkstyle"]: + extensions.append("shengchen.vscode-checkstyle") + if analysis["pmd"]: + extensions.append("vscjava.vscode-java-dependency") + + if node_feature_enabled(cfg): + extensions.extend([ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-vscode.vscode-typescript-next", + ]) + + if langs["go"]: + extensions.append("golang.go") + if langs["ruby"]: + extensions.append("Shopify.ruby-lsp") + if langs["php"]: + extensions.extend([ + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug", + ]) + if langs["rust"]: + extensions.extend([ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + ]) + if langs["csharp"]: + extensions.extend([ + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit", + ]) + if langs["c"] or langs["cpp"]: + extensions.append("ms-vscode.cpptools") + if langs["kotlin"]: + extensions.append("fwcd.kotlin") + + if cicd["docker"] or cicd.get("podman", False): + extensions.append("ms-azuretools.vscode-docker") + if cicd["terraform"]: + extensions.append("hashicorp.terraform") + if cicd["kubectl"] or cicd["helm"] or any(k8s.values()): + extensions.append("ms-kubernetes-tools.vscode-kubernetes-tools") + if any(k8s.values()): + extensions.append("mindaro.mindaro") + if k8s["argocd_cli"]: + extensions.append("argoproj.argocd-vscode-extension") + if k8s["flux"]: + extensions.append("weaveworks.vscode-gitops-tools") + if cicd["github_actions"]: + extensions.append("github.vscode-github-actions") + if analysis["sonarqube"]: + extensions.append("SonarSource.sonarlint-vscode") + if devops["jenkins"]: + extensions.append("secanis.jenkinsfile-support") + + extensions.extend([ + "github.copilot", + "github.copilot-chat", + "ms-vsliveshare.vsliveshare", + "streetsidesoftware.code-spell-checker", + "eamodio.gitlens", + ]) + + return unique(extensions) + + +def build_forward_ports(cfg: dict[str, Any]) -> list[int]: + ports = [] + if cfg["devops_tools"]["nexus"]: + ports.append(8081) + if cfg["devops_tools"]["prometheus"]: + ports.append(9090) + if cfg["devops_tools"]["grafana"]: + ports.append(3000) + if cfg["devops_tools"]["elk"]: + ports.extend([9200, 9300, 5601]) + if cfg["devops_tools"]["jenkins"]: + ports.append(8080) + return ports + + +def build_post_create_command(cfg: dict[str, Any]) -> str: + commands = ["set -euo pipefail"] + + if cfg["languages"]["python"]: + commands.append( + "if command -v python3 >/dev/null 2>&1; then " + "sudo python3 -m pip install --no-cache-dir --upgrade pip && " + "sudo python3 -m pip install --no-cache-dir pytest black flake8 mypy pipenv tox coverage pytest-cov pylint; " + "fi" + ) + + if node_feature_enabled(cfg): + commands.append( + "if command -v npm >/dev/null 2>&1; then " + "sudo npm install -g typescript jest prettier eslint; " + "fi" + ) + + if cfg["languages"]["go"]: + commands.append( + "if command -v go >/dev/null 2>&1; then " + "GO_BIN=\"$(go env GOPATH 2>/dev/null)/bin/golangci-lint\"; " + "go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && " + "if [ -f \"${GO_BIN}\" ]; then sudo install -m 0755 \"${GO_BIN}\" /usr/local/bin/golangci-lint; fi; " + "fi" + ) + + if cfg["cicd"]["kubectl"] or cfg["cicd"]["helm"] or any(cfg["kubernetes"].values()): + commands.append( + "if [ -f ./kubernetes/k8s-config-generator.py ]; then " + "sudo chmod +x ./kubernetes/k8s-config-generator.py && " + "sudo ln -sf \"$(pwd)/kubernetes/k8s-config-generator.py\" /usr/local/bin/k8s-config-generator; " + "fi" + ) + + commands.append("printf 'Devcontainer bootstrap complete.\\n'") + return "bash -lc " + json.dumps("; ".join(commands)) + + +def build_devcontainer_json(cfg: dict[str, Any]) -> dict[str, Any]: + devcontainer = { + "name": "DevOps OS - Multi-Language Development Environment", + "build": { + "dockerfile": "Dockerfile", + "args": build_args(cfg), + }, + "features": build_features(cfg), + "mounts": [ + "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind", + ], + "customizations": { + "vscode": { + "extensions": build_extensions(cfg), + } + }, + "postCreateCommand": build_post_create_command(cfg), + } + forward_ports = build_forward_ports(cfg) + if forward_ports: + devcontainer["forwardPorts"] = forward_ports + return devcontainer + + +def _load_template(name: str) -> str: + return (TEMPLATE_DIR / name).read_text(encoding="utf-8") + + +def _json_fragment(value: Any, indent_prefix: int) -> str: + text = json.dumps(value, indent=2) + if "\n" not in text: + return text + lines = text.splitlines() + return lines[0] + "\n" + "\n".join((" " * indent_prefix) + line for line in lines[1:]) + + +def render_env_json(cfg: dict[str, Any]) -> str: + template = _load_template("devcontainer.env.json.tpl") + replacements = { + "__LANGUAGES__": _json_fragment(cfg["languages"], 2), + "__CICD__": _json_fragment(cfg["cicd"], 2), + "__KUBERNETES__": _json_fragment(cfg["kubernetes"], 2), + "__BUILD_TOOLS__": _json_fragment(cfg["build_tools"], 2), + "__CODE_ANALYSIS__": _json_fragment(cfg["code_analysis"], 2), + "__DEVOPS_TOOLS__": _json_fragment(cfg["devops_tools"], 2), + "__VERSIONS__": _json_fragment(cfg["versions"], 2), + } + for placeholder, value in replacements.items(): + template = template.replace(placeholder, value) + return template + + +def render_devcontainer_json(cfg: dict[str, Any]) -> str: + devcontainer = build_devcontainer_json(cfg) + template = _load_template("devcontainer.json.tpl") + forward_ports_block = "" + if "forwardPorts" in devcontainer: + forward_ports_block = ",\n \"forwardPorts\": " + _json_fragment(devcontainer["forwardPorts"], 2) + replacements = { + "__BUILD_ARGS__": _json_fragment(devcontainer["build"]["args"], 4), + "__FEATURES__": _json_fragment(devcontainer["features"], 2), + "__EXTENSIONS__": _json_fragment(devcontainer["customizations"]["vscode"]["extensions"], 6), + "__FORWARD_PORTS_BLOCK__": forward_ports_block, + "__POST_CREATE_COMMAND__": json.dumps(devcontainer["postCreateCommand"]), + } + for placeholder, value in replacements.items(): + template = template.replace(placeholder, value) + return template + + +def render_dockerfile() -> str: + return _load_template("Dockerfile.tpl") + + +def write_generated_devcontainer(output_root: Path, cfg: dict[str, Any]) -> dict[str, Path]: + output_root.mkdir(parents=True, exist_ok=True) + files = { + "env": output_root / "devcontainer.env.json", + "json": output_root / "devcontainer.json", + "dockerfile": output_root / "Dockerfile", + } + files["env"].write_text(render_env_json(cfg), encoding="utf-8") + files["json"].write_text(render_devcontainer_json(cfg), encoding="utf-8") + files["dockerfile"].write_text(render_dockerfile(), encoding="utf-8") + return files diff --git a/cli/devopsos.py b/cli/devopsos.py index c2556a2..125b540 100644 --- a/cli/devopsos.py +++ b/cli/devopsos.py @@ -18,6 +18,16 @@ import cli.scaffold_unittest as scaffold_unittest import cli.process_first as process_first from cli import __version__ +from cli.devcontainer_templates import ( + ALL_BUILD_TOOLS, + ALL_CICD, + ALL_CODE_ANALYSIS, + ALL_DEVOPS_TOOLS, + ALL_KUBERNETES, + ALL_LANGUAGES, + DEFAULT_VERSIONS, + write_generated_devcontainer, +) class ProcessFirstSection(str, enum.Enum): """Valid sections for the process-first command.""" @@ -636,22 +646,6 @@ def init( typer.echo("Welcome to DevOps-OS Init Wizard!") typer.echo("Tools are grouped by Process-First DevOps principles (Systems Thinking).\n") - # ── Canonical tool lists ────────────────────────────────────────────── - ALL_LANGUAGES = ["python", "java", "node", "ruby", "csharp", "php", "rust", - "typescript", "kotlin", "c", "cpp", "javascript", "go"] - ALL_CICD = ["docker", "podman", "terraform", "kubectl", "helm", "github_actions", "jenkins"] - ALL_KUBERNETES = ["k9s", "kustomize", "argocd_cli", "lens", "kubeseal", - "flux", "kind", "minikube", "openshift_cli"] - ALL_BUILD_TOOLS = ["gradle", "maven", "ant", "make", "cmake"] - ALL_CODE_ANALYSIS = ["sonarqube", "checkstyle", "pmd", "eslint", "pylint"] - ALL_DEVOPS_TOOLS = ["nexus", "prometheus", "grafana", "elk", "jenkins"] - - versions_defaults = { - "python": "3.12", "java": "21", "node": "22", "go": "1.25.0", "nexus": "3.91.0", - "prometheus": "3.5.1", "grafana": "12.4.2", "k9s": "0.50.16", "argocd": "3.3.6", - "flux": "2.8.5", "kustomize": "5.8.0", "jenkins": "2.440.1" - } - # ── Wizard groups aligned with Process-First DevOps principles ──────── # Each group maps to a DevOps stage in the Systems Thinking value stream. wizard_groups = { @@ -702,10 +696,10 @@ def init( all_selected = {tool for tools in selected_by_group.values() for tool in tools} for tool in all_selected: vkey = "argocd" if tool == "argocd_cli" else tool - if vkey in versions_defaults: + if vkey in DEFAULT_VERSIONS: selected_versions[vkey] = inquirer.text( message=f"{tool.title()} version:", - default=versions_defaults[vkey], + default=DEFAULT_VERSIONS[vkey], ).execute() # ── Map wizard selections back to legacy JSON structure ─────────────── @@ -763,98 +757,18 @@ def _sel(group): return selected_by_group.get(group, []) raise typer.Exit(1) target_root = Path(directory) - primary_devcontainer_dir = target_root / ".devcontainer" - preserve_existing = primary_devcontainer_dir.exists() - if preserve_existing: - devcontainer_dir = target_root / ".devcontainer.generated" + devcontainer_dir = target_root / ".devcontainer" + if devcontainer_dir.exists(): typer.echo( - f"Existing {primary_devcontainer_dir} detected. Preserving it and writing " - f"generated output to {devcontainer_dir}." + f"Existing {devcontainer_dir} detected. Preserving it and skipping devcontainer generation." ) - typer.echo( - ".devcontainer.generated/ is a review/reference copy and may be overwritten " - "by later init runs." - ) - else: - devcontainer_dir = primary_devcontainer_dir - - devcontainer_dir.mkdir(parents=True, exist_ok=True) - env_json_path = devcontainer_dir / "devcontainer.env.json" - with open(env_json_path, "w") as f: - json.dump(config, f, indent=2) - typer.echo(f"Wrote configuration to {env_json_path}") - - # Offer to generate Dockerfile/devcontainer.json - if inquirer.confirm(message="Generate Dockerfile and devcontainer.json now?", default=True).execute(): - # Map config to build args for devcontainer.json - build_args = {} - # Languages - lang_map = { - "python": "INSTALL_PYTHON", "java": "INSTALL_JAVA", "node": "INSTALL_JS", - "ruby": "INSTALL_RUBY", "csharp": "INSTALL_CSHARP", "php": "INSTALL_PHP", - "rust": "INSTALL_RUST", "typescript": "INSTALL_TYPESCRIPT", - "kotlin": "INSTALL_KOTLIN", "c": "INSTALL_C", "cpp": "INSTALL_CPP", - "javascript": "INSTALL_JS", "go": "INSTALL_GO" - } - for lang, arg in lang_map.items(): - build_args[arg] = str(config["languages"].get(lang, False)).lower() - # CICD (includes container runtimes docker/podman) - cicd_map = { - "docker": "INSTALL_DOCKER", "podman": "INSTALL_PODMAN", - "terraform": "INSTALL_TERRAFORM", - "kubectl": "INSTALL_KUBECTL", "helm": "INSTALL_HELM", - "github_actions": "INSTALL_GITHUB_ACTIONS", "jenkins": "INSTALL_JENKINS" - } - for tool, arg in cicd_map.items(): - build_args[arg] = str(config["cicd"].get(tool, False)).lower() - # Kubernetes - k8s_map = { - "k9s": "INSTALL_K9S", "kustomize": "INSTALL_KUSTOMIZE", - "argocd_cli": "INSTALL_ARGOCD_CLI", "lens": "INSTALL_LENS", - "kubeseal": "INSTALL_KUBESEAL", "flux": "INSTALL_FLUX", - "kind": "INSTALL_KIND", "minikube": "INSTALL_MINIKUBE", - "openshift_cli": "INSTALL_OPENSHIFT_CLI" - } - for tool, arg in k8s_map.items(): - build_args[arg] = str(config["kubernetes"].get(tool, False)).lower() - # Build tools - build_map = { - "gradle": "INSTALL_GRADLE", "maven": "INSTALL_MAVEN", "ant": "INSTALL_ANT", - "make": "INSTALL_MAKE", "cmake": "INSTALL_CMAKE" - } - for tool, arg in build_map.items(): - build_args[arg] = str(config["build_tools"].get(tool, False)).lower() - # Code analysis - analysis_map = { - "sonarqube": "INSTALL_SONARQUBE", "checkstyle": "INSTALL_CHECKSTYLE", - "pmd": "INSTALL_PMD", "eslint": "INSTALL_ESLINT", "pylint": "INSTALL_PYLINT" - } - for tool, arg in analysis_map.items(): - build_args[arg] = str(config["code_analysis"].get(tool, False)).lower() - # DevOps tools - devops_map = { - "nexus": "INSTALL_NEXUS", "prometheus": "INSTALL_PROMETHEUS", - "grafana": "INSTALL_GRAFANA", "elk": "INSTALL_ELK", - "jenkins": "INSTALL_JENKINS" - } - for tool, arg in devops_map.items(): - build_args[arg] = str(config["devops_tools"].get(tool, False)).lower() - # Versions (only for selected) - for k, v in config["versions"].items(): - build_args[k.upper() + ("_VERSION" if k not in ["k9s", "argocd", "flux", "kustomize"] else "")] = v - # Update devcontainer.json - devcontainer_json_path = devcontainer_dir / "devcontainer.json" - if devcontainer_json_path.exists(): - with open(devcontainer_json_path) as f: - devcontainer_json = json.load(f) - else: - devcontainer_json = {"build": {"dockerfile": "Dockerfile", "args": {}}} - devcontainer_json.setdefault("build", {})["args"] = build_args - with open(devcontainer_json_path, "w") as f: - json.dump(devcontainer_json, f, indent=2) - typer.echo(f"Updated {devcontainer_json_path} with build args.") - # Optionally, update Dockerfile (not strictly needed if it uses build args) - typer.echo("Dockerfile uses build args; ensure it references the correct ARGs.") + typer.echo("Remove or rename the existing .devcontainer/ directory if you want init to generate a new one.") + raise typer.Exit(0) + + written = write_generated_devcontainer(devcontainer_dir, config) + typer.echo(f"Wrote configuration to {written['env']}") + typer.echo(f"Wrote Dockerfile to {written['dockerfile']}") + typer.echo(f"Wrote devcontainer.json to {written['json']}") @app.command("process-first") diff --git a/cli/templates/devcontainer/Dockerfile.tpl b/cli/templates/devcontainer/Dockerfile.tpl new file mode 100644 index 0000000..68e7c8a --- /dev/null +++ b/cli/templates/devcontainer/Dockerfile.tpl @@ -0,0 +1,309 @@ +# DevOps OS dev container (Ubuntu LTS optimized) +FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ARG JAVA_VERSION=21 +ARG TARGETOS=linux +ARG TARGETARCH=amd64 + +# Unsupported language/runtime toggles +ARG INSTALL_CSHARP=false +ARG INSTALL_KOTLIN=false +ARG INSTALL_C=false +ARG INSTALL_CPP=false + +# CI/CD + platform tools +ARG INSTALL_DOCKER=true +ARG INSTALL_PODMAN=false +ARG INSTALL_TERRAFORM=true +ARG TERRAFORM_VERSION=1.14.7 +ARG INSTALL_KUBECTL=true +ARG INSTALL_HELM=true +ARG HELM_VERSION=4.0.1 +ARG INSTALL_GITHUB_ACTIONS=true +ARG ACTIONS_RUNNER_VERSION=2.330.0 + +# Kubernetes tools +ARG INSTALL_K9S=true +ARG K9S_VERSION=0.50.16 +ARG INSTALL_KUSTOMIZE=true +ARG KUSTOMIZE_VERSION=5.8.0 +ARG INSTALL_ARGOCD_CLI=true +ARG ARGOCD_VERSION=3.3.6 +ARG INSTALL_LENS=false +ARG INSTALL_KUBESEAL=true +ARG KUBESEAL_VERSION=0.33.1 +ARG INSTALL_FLUX=true +ARG FLUX_VERSION=2.8.5 +ARG INSTALL_KIND=true +ARG KIND_VERSION=0.31.0 +ARG INSTALL_MINIKUBE=true +ARG MINIKUBE_VERSION=1.37.0 +ARG INSTALL_OPENSHIFT_CLI=false + +# Build tools +ARG INSTALL_MAKE=true +ARG INSTALL_CMAKE=true + +# Code analysis tools +ARG INSTALL_SONARQUBE=true +ARG SONAR_SCANNER_VERSION=8.0.1.6346 +ARG INSTALL_CHECKSTYLE=true +ARG CHECKSTYLE_VERSION=12.1.2 +ARG INSTALL_PMD=true +ARG PMD_VERSION=7.18.0 + +# DevOps tools +ARG INSTALL_NEXUS=true +ARG NEXUS_VERSION=3.91.0 +ARG INSTALL_PROMETHEUS=true +ARG PROMETHEUS_VERSION=3.5.1 +ARG INSTALL_GRAFANA=true +ARG GRAFANA_VERSION=12.4.2 +ARG INSTALL_ELK=true +ARG INSTALL_JENKINS=false + +ENV DEBIAN_FRONTEND=noninteractive +ENV PATH="/opt/sonar-scanner/bin:/opt/pmd/bin:${PATH}" +ENV SHELL=/bin/bash + +# Base packages shared by most install flows. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + apt-transport-https \ + build-essential \ + ca-certificates \ + curl \ + git \ + gnupg \ + jq \ + lsb-release \ + software-properties-common \ + tar \ + unzip \ + wget \ + xz-utils + +# Unsupported language/toolchain installers kept in the repo layer. +RUN if [ "${INSTALL_CSHARP}" = "true" ]; then \ + wget -q https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb -O /tmp/packages-microsoft-prod.deb \ + && dpkg -i /tmp/packages-microsoft-prod.deb \ + && rm -f /tmp/packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y --no-install-recommends dotnet-sdk-8.0; \ + fi \ + && if [ "${INSTALL_KOTLIN}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends kotlin; \ + fi \ + && if [ "${INSTALL_C}" = "true" ] || [ "${INSTALL_CPP}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends clang gdb; \ + fi + +# Docker CLI / Podman +RUN if [ "${INSTALL_DOCKER}" = "true" ]; then \ + install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && chmod a+r /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends docker-ce-cli docker-buildx-plugin docker-compose-plugin; \ + fi \ + && if [ "${INSTALL_PODMAN}" = "true" ]; then \ + apt-get update \ + && apt-get install -y --no-install-recommends podman; \ + fi + +# Terraform (vendor-pinned binary + checksum) +RUN if [ "${INSTALL_TERRAFORM}" = "true" ]; then \ + case "${TARGETARCH}" in \ + amd64|x86_64) TF_ARCH="amd64" ;; \ + arm64|aarch64) TF_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH for Terraform: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && TF_BASE="terraform_${TERRAFORM_VERSION}_linux_${TF_ARCH}" \ + && curl -fsSLo "/tmp/${TF_BASE}.zip" "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/${TF_BASE}.zip" \ + && curl -fsSLo /tmp/terraform_SHA256SUMS "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_SHA256SUMS" \ + && grep " ${TF_BASE}.zip$" /tmp/terraform_SHA256SUMS | sha256sum -c - \ + && unzip -q "/tmp/${TF_BASE}.zip" -d /usr/local/bin \ + && rm -f "/tmp/${TF_BASE}.zip" /tmp/terraform_SHA256SUMS; \ + fi + +# kubectl +RUN if [ "${INSTALL_KUBECTL}" = "true" ]; then \ + case "${TARGETARCH}" in \ + amd64|x86_64) K_ARCH="amd64" ;; \ + arm64|aarch64) K_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH for kubectl: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && KUBECTL_VERSION="$(curl -fsSL https://dl.k8s.io/release/stable.txt)" \ + && curl -fsSLo /tmp/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/${TARGETOS}/${K_ARCH}/kubectl" \ + && curl -fsSLo /tmp/kubectl.sha256 "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/${TARGETOS}/${K_ARCH}/kubectl.sha256" \ + && echo "$(cat /tmp/kubectl.sha256) /tmp/kubectl" | sha256sum --check --status \ + && install -o root -g root -m 0755 /tmp/kubectl /usr/local/bin/kubectl \ + && rm -f /tmp/kubectl /tmp/kubectl.sha256; \ + fi + +# Helm (vendor-pinned binary + checksum) +RUN if [ "${INSTALL_HELM}" = "true" ]; then \ + case "${TARGETARCH}" in \ + amd64|x86_64) H_ARCH="amd64" ;; \ + arm64|aarch64) H_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH for Helm: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && HELM_TGZ="helm-v${HELM_VERSION}-${TARGETOS}-${H_ARCH}.tar.gz" \ + && curl -fsSLo "/tmp/${HELM_TGZ}" "https://get.helm.sh/${HELM_TGZ}" \ + && curl -fsSLo "/tmp/${HELM_TGZ}.sha256sum" "https://get.helm.sh/${HELM_TGZ}.sha256sum" \ + && (cd /tmp && sha256sum -c "${HELM_TGZ}.sha256sum") \ + && tar -xzf "/tmp/${HELM_TGZ}" -C /tmp \ + && install -m 0755 "/tmp/${TARGETOS}-${H_ARCH}/helm" /usr/local/bin/helm \ + && rm -rf "/tmp/${HELM_TGZ}" "/tmp/${HELM_TGZ}.sha256sum" "/tmp/${TARGETOS}-${H_ARCH}"; \ + fi + +# Kubernetes utility CLIs +RUN case "${TARGETARCH}" in \ + amd64|x86_64) BIN_ARCH="amd64" ;; \ + arm64|aarch64) BIN_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH for Kubernetes CLIs: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && if [ "${INSTALL_K9S}" = "true" ]; then \ + mkdir -p /tmp/k9s && cd /tmp/k9s \ + && curl -fsSLo k9s.tar.gz "https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_${BIN_ARCH}.tar.gz" \ + && tar -xzf k9s.tar.gz \ + && install -m 0755 k9s /usr/local/bin/k9s \ + && rm -rf /tmp/k9s; \ + fi \ + && if [ "${INSTALL_KUSTOMIZE}" = "true" ]; then \ + curl -fsSLo /tmp/kustomize.tar.gz "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_${BIN_ARCH}.tar.gz" \ + && mkdir -p /tmp/kustomize \ + && tar -xzf /tmp/kustomize.tar.gz -C /tmp/kustomize \ + && install -m 0755 /tmp/kustomize/kustomize /usr/local/bin/kustomize \ + && rm -rf /tmp/kustomize /tmp/kustomize.tar.gz; \ + fi \ + && if [ "${INSTALL_ARGOCD_CLI}" = "true" ]; then \ + curl -fsSLo /usr/local/bin/argocd "https://github.com/argoproj/argo-cd/releases/download/v${ARGOCD_VERSION}/argocd-linux-${BIN_ARCH}" \ + && chmod +x /usr/local/bin/argocd; \ + fi \ + && if [ "${INSTALL_KUBESEAL}" = "true" ]; then \ + curl -fsSLo /tmp/kubeseal.tar.gz "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-${BIN_ARCH}.tar.gz" \ + && tar -xzf /tmp/kubeseal.tar.gz -C /tmp kubeseal \ + && install -m 0755 /tmp/kubeseal /usr/local/bin/kubeseal \ + && rm -f /tmp/kubeseal /tmp/kubeseal.tar.gz; \ + fi \ + && if [ "${INSTALL_FLUX}" = "true" ]; then \ + curl -fsSL https://fluxcd.io/install.sh | FLUX_VERSION="v${FLUX_VERSION}" bash; \ + fi \ + && if [ "${INSTALL_KIND}" = "true" ]; then \ + curl -fsSLo /usr/local/bin/kind "https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-${TARGETOS}-${BIN_ARCH}" \ + && chmod +x /usr/local/bin/kind; \ + fi \ + && if [ "${INSTALL_MINIKUBE}" = "true" ]; then \ + curl -fsSLo /usr/local/bin/minikube "https://storage.googleapis.com/minikube/releases/v${MINIKUBE_VERSION}/minikube-${TARGETOS}-${BIN_ARCH}" \ + && chmod +x /usr/local/bin/minikube; \ + fi \ + && if [ "${INSTALL_OPENSHIFT_CLI}" = "true" ]; then \ + curl -fsSLo /tmp/oc.tar.gz "https://mirror.openshift.com/pub/openshift-v4/clients/ocp/latest/openshift-client-linux.tar.gz" \ + && mkdir -p /tmp/oc \ + && tar -xzf /tmp/oc.tar.gz -C /tmp/oc \ + && install -m 0755 /tmp/oc/oc /usr/local/bin/oc \ + && rm -rf /tmp/oc /tmp/oc.tar.gz; \ + fi + +# Lens desktop app is not practical in headless dev containers. +RUN if [ "${INSTALL_LENS}" = "true" ]; then \ + echo "INSTALL_LENS=true requested. Skipping: Lens desktop app is not supported in headless dev containers." >&2; \ + fi + +# GitHub Actions runner +RUN if [ "${INSTALL_GITHUB_ACTIONS}" = "true" ]; then \ + case "${TARGETARCH}" in \ + amd64|x86_64) RUNNER_ARCH="x64" ;; \ + arm64|aarch64) RUNNER_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH for actions-runner: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && mkdir -p /opt/actions-runner \ + && cd /opt/actions-runner \ + && curl -fsSLo "actions-runner-linux-${RUNNER_ARCH}-${ACTIONS_RUNNER_VERSION}.tar.gz" "https://github.com/actions/runner/releases/download/v${ACTIONS_RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${ACTIONS_RUNNER_VERSION}.tar.gz" \ + && tar xzf "actions-runner-linux-${RUNNER_ARCH}-${ACTIONS_RUNNER_VERSION}.tar.gz" \ + && rm -f "actions-runner-linux-${RUNNER_ARCH}-${ACTIONS_RUNNER_VERSION}.tar.gz" \ + && ./bin/installdependencies.sh; \ + fi + +# Build tool toggles +RUN if [ "${INSTALL_MAKE}" = "true" ]; then apt-get update && apt-get install -y --no-install-recommends make; fi \ + && if [ "${INSTALL_CMAKE}" = "true" ]; then apt-get update && apt-get install -y --no-install-recommends cmake; fi + +# Code analysis tools +RUN if [ "${INSTALL_SONARQUBE}" = "true" ]; then \ + curl -fsSLo /tmp/sonar-scanner-cli.zip "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux-x64.zip" \ + && rm -rf /opt/sonar-scanner \ + && mkdir -p /tmp/sonar-scanner \ + && unzip -q /tmp/sonar-scanner-cli.zip -d /tmp/sonar-scanner \ + && mv "/tmp/sonar-scanner/sonar-scanner-${SONAR_SCANNER_VERSION}-linux-x64" /opt/sonar-scanner \ + && rm -rf /tmp/sonar-scanner /tmp/sonar-scanner-cli.zip; \ + fi \ + && if [ "${INSTALL_CHECKSTYLE}" = "true" ]; then \ + mkdir -p /opt/checkstyle \ + && curl -fsSLo /opt/checkstyle/checkstyle.jar "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-${CHECKSTYLE_VERSION}/checkstyle-${CHECKSTYLE_VERSION}-all.jar" \ + && printf '#!/usr/bin/env bash\nexec java -jar /opt/checkstyle/checkstyle.jar "$@"\n' > /usr/local/bin/checkstyle \ + && chmod +x /usr/local/bin/checkstyle; \ + fi \ + && if [ "${INSTALL_PMD}" = "true" ]; then \ + mkdir -p /opt/pmd \ + && curl -fsSLo /tmp/pmd.zip "https://github.com/pmd/pmd/releases/download/pmd_releases/${PMD_VERSION}/pmd-dist-${PMD_VERSION}-bin.zip" \ + && unzip -q /tmp/pmd.zip -d /opt \ + && mv "/opt/pmd-bin-${PMD_VERSION}" /opt/pmd \ + && rm -f /tmp/pmd.zip; \ + fi + +# DevOps toolchain binaries +RUN case "${TARGETARCH}" in \ + amd64|x86_64) BIN_ARCH="amd64"; NEXUS_ARCH="x86-64" ;; \ + arm64|aarch64) BIN_ARCH="arm64"; NEXUS_ARCH="aarch64" ;; \ + *) echo "Unsupported TARGETARCH for DevOps binaries: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && if [ "${INSTALL_NEXUS}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends openjdk-${JAVA_VERSION}-jre-headless \ + && mkdir -p /opt/nexus \ + && curl -fsSLo /tmp/nexus.tar.gz "https://download.sonatype.com/nexus/3/nexus-${NEXUS_VERSION}-linux-${NEXUS_ARCH}.tar.gz" \ + && tar -xzf /tmp/nexus.tar.gz -C /opt \ + && rm -rf /opt/nexus \ + && mv /opt/nexus-* /opt/nexus \ + && rm -f /tmp/nexus.tar.gz \ + && printf '#!/usr/bin/env bash\nexec /opt/nexus/bin/nexus "$@"\n' > /usr/local/bin/nexus \ + && chmod +x /usr/local/bin/nexus \ + && echo 'export NEXUS_HOME=/opt/nexus' > /etc/profile.d/nexus_home.sh; \ + fi \ + && if [ "${INSTALL_PROMETHEUS}" = "true" ]; then \ + mkdir -p /opt/prometheus \ + && curl -fsSLo /tmp/prometheus.tar.gz "https://github.com/prometheus/prometheus/releases/download/v${PROMETHEUS_VERSION}/prometheus-${PROMETHEUS_VERSION}.linux-${BIN_ARCH}.tar.gz" \ + && tar -xzf /tmp/prometheus.tar.gz -C /opt \ + && rm -rf /opt/prometheus \ + && mv /opt/prometheus-* /opt/prometheus \ + && rm -f /tmp/prometheus.tar.gz \ + && printf '#!/usr/bin/env bash\nexec /opt/prometheus/prometheus "$@"\n' > /usr/local/bin/prometheus \ + && chmod +x /usr/local/bin/prometheus; \ + fi \ + && if [ "${INSTALL_GRAFANA}" = "true" ]; then \ + apt-get update && apt-get install -y --no-install-recommends adduser libfontconfig1 \ + && curl -fsSLo /tmp/grafana.deb "https://dl.grafana.com/oss/release/grafana_${GRAFANA_VERSION}_${BIN_ARCH}.deb" \ + && apt-get install -y --no-install-recommends /tmp/grafana.deb \ + && rm -f /tmp/grafana.deb; \ + fi \ + && if [ "${INSTALL_ELK}" = "true" ]; then \ + curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" > /etc/apt/sources.list.d/elastic-8.x.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends elasticsearch kibana logstash; \ + fi \ + && if [ "${INSTALL_JENKINS}" = "true" ]; then \ + curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | tee /usr/share/keyrings/jenkins-keyring.asc >/dev/null \ + && echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/" > /etc/apt/sources.list.d/jenkins.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends jenkins; \ + fi + +# Final cleanup +RUN apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* diff --git a/cli/templates/devcontainer/devcontainer.env.json.tpl b/cli/templates/devcontainer/devcontainer.env.json.tpl new file mode 100644 index 0000000..17961b0 --- /dev/null +++ b/cli/templates/devcontainer/devcontainer.env.json.tpl @@ -0,0 +1,9 @@ +{ + "languages": __LANGUAGES__, + "cicd": __CICD__, + "kubernetes": __KUBERNETES__, + "build_tools": __BUILD_TOOLS__, + "code_analysis": __CODE_ANALYSIS__, + "devops_tools": __DEVOPS_TOOLS__, + "versions": __VERSIONS__ +} diff --git a/cli/templates/devcontainer/devcontainer.json.tpl b/cli/templates/devcontainer/devcontainer.json.tpl new file mode 100644 index 0000000..42ac898 --- /dev/null +++ b/cli/templates/devcontainer/devcontainer.json.tpl @@ -0,0 +1,17 @@ +{ + "name": "DevOps OS - Multi-Language Development Environment", + "build": { + "dockerfile": "Dockerfile", + "args": __BUILD_ARGS__ + }, + "features": __FEATURES__, + "mounts": [ + "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" + ], + "customizations": { + "vscode": { + "extensions": __EXTENSIONS__ + } + }__FORWARD_PORTS_BLOCK__, + "postCreateCommand": __POST_CREATE_COMMAND__ +} diff --git a/cli/test_cli.py b/cli/test_cli.py index 30a81b4..5e2065d 100644 --- a/cli/test_cli.py +++ b/cli/test_cli.py @@ -65,17 +65,13 @@ def test_init_dir_option_creates_devcontainer_in_specified_dir(): checkbox_mock = MagicMock() checkbox_mock.execute.return_value = [] # nothing selected - # First confirm → Proceed = True - # Second confirm → Generate Dockerfile = False (skip, we only need env.json) + # Confirm proceed = True confirm_proceed = MagicMock() confirm_proceed.execute.return_value = True - confirm_skip = MagicMock() - confirm_skip.execute.return_value = False with tempfile.TemporaryDirectory() as tmp: with patch("cli.devopsos.inquirer.checkbox", return_value=checkbox_mock), \ - patch("cli.devopsos.inquirer.confirm", - side_effect=[confirm_proceed, confirm_skip]): + patch("cli.devopsos.inquirer.confirm", side_effect=[confirm_proceed]): runner = CliRunner() result = runner.invoke(app, ["init", "--dir", tmp]) @@ -85,9 +81,11 @@ def test_init_dir_option_creates_devcontainer_in_specified_dir(): assert (dc_dir / "devcontainer.env.json").exists(), ( "Expected devcontainer.env.json inside .devcontainer" ) + assert (dc_dir / "devcontainer.json").exists() + assert (dc_dir / "Dockerfile").exists() -def test_init_preserves_existing_devcontainer_and_writes_generated_copy(): - """Existing .devcontainer must be preserved; generated output goes to .devcontainer.generated.""" +def test_init_preserves_existing_devcontainer_and_stops(): + """Existing .devcontainer must be preserved and init must stop without alternate output.""" from unittest.mock import MagicMock, patch from typer.testing import CliRunner from cli.devopsos import app @@ -97,9 +95,6 @@ def test_init_preserves_existing_devcontainer_and_writes_generated_copy(): confirm_proceed = MagicMock() confirm_proceed.execute.return_value = True - confirm_skip = MagicMock() - confirm_skip.execute.return_value = False - with tempfile.TemporaryDirectory() as tmp: existing_dir = Path(tmp) / ".devcontainer" existing_dir.mkdir(parents=True, exist_ok=True) @@ -109,20 +104,15 @@ def test_init_preserves_existing_devcontainer_and_writes_generated_copy(): existing_json.write_text('{"protected": true}\n') with patch("cli.devopsos.inquirer.checkbox", return_value=checkbox_mock), \ - patch("cli.devopsos.inquirer.confirm", - side_effect=[confirm_proceed, confirm_skip]): + patch("cli.devopsos.inquirer.confirm", side_effect=[confirm_proceed]): runner = CliRunner() result = runner.invoke(app, ["init", "--dir", tmp]) assert result.exit_code == 0, result.output - assert "Preserving it and writing generated output" in result.output + assert "Preserving it and skipping devcontainer generation" in result.output assert existing_env.read_text() == '{"protected": true}\n' assert existing_json.read_text() == '{"protected": true}\n' - - generated_dir = Path(tmp) / ".devcontainer.generated" - assert generated_dir.is_dir() - assert (generated_dir / "devcontainer.env.json").exists() - assert not (generated_dir / "devcontainer.json").exists() + assert not (Path(tmp) / ".devcontainer.generated").exists() def test_init_preserves_existing_devcontainer_when_only_one_file_exists(): """Any existing .devcontainer directory is protected even if only one config file exists.""" @@ -135,9 +125,6 @@ def test_init_preserves_existing_devcontainer_when_only_one_file_exists(): confirm_proceed = MagicMock() confirm_proceed.execute.return_value = True - confirm_generate = MagicMock() - confirm_generate.execute.return_value = True - with tempfile.TemporaryDirectory() as tmp: existing_dir = Path(tmp) / ".devcontainer" existing_dir.mkdir(parents=True, exist_ok=True) @@ -145,78 +132,13 @@ def test_init_preserves_existing_devcontainer_when_only_one_file_exists(): existing_env.write_text('{"protected": true}\n') with patch("cli.devopsos.inquirer.checkbox", return_value=checkbox_mock), \ - patch("cli.devopsos.inquirer.confirm", - side_effect=[confirm_proceed, confirm_generate]): + patch("cli.devopsos.inquirer.confirm", side_effect=[confirm_proceed]): result = CliRunner().invoke(app, ["init", "--dir", tmp]) assert result.exit_code == 0, result.output assert existing_env.read_text() == '{"protected": true}\n' assert not (existing_dir / "devcontainer.json").exists() - - generated_dir = Path(tmp) / ".devcontainer.generated" - assert (generated_dir / "devcontainer.env.json").exists() - assert (generated_dir / "devcontainer.json").exists() - -def test_init_rerun_refreshes_generated_output_without_touching_devcontainer(): - """Rerunning init refreshes .devcontainer.generated and leaves .devcontainer unchanged.""" - from unittest.mock import MagicMock, patch - from typer.testing import CliRunner - from cli.devopsos import app - - selections = [ - ["python"], - ["docker"], - [], - [], - [], - [], - [], - ] - sel_iter = iter(selections) - - def _checkbox_factory(**kwargs): - mock = MagicMock() - mock.execute.return_value = next(sel_iter) - return mock - - text_mock = MagicMock() - text_mock.execute.return_value = "3.12" - - confirm_proceed = MagicMock() - confirm_proceed.execute.return_value = True - confirm_generate = MagicMock() - confirm_generate.execute.return_value = True - - with tempfile.TemporaryDirectory() as tmp: - existing_dir = Path(tmp) / ".devcontainer" - existing_dir.mkdir(parents=True, exist_ok=True) - existing_env = existing_dir / "devcontainer.env.json" - existing_env.write_text('{"protected": true}\n') - - generated_dir = Path(tmp) / ".devcontainer.generated" - generated_dir.mkdir(parents=True, exist_ok=True) - stale_env = generated_dir / "devcontainer.env.json" - stale_json = generated_dir / "devcontainer.json" - stale_env.write_text('{"stale": true}\n') - stale_json.write_text('{"stale": true}\n') - - with patch("cli.devopsos.inquirer.checkbox", side_effect=_checkbox_factory), \ - patch("cli.devopsos.inquirer.text", return_value=text_mock), \ - patch("cli.devopsos.inquirer.confirm", - side_effect=[confirm_proceed, confirm_generate]): - result = CliRunner().invoke(app, ["init", "--dir", tmp]) - - assert result.exit_code == 0, result.output - assert existing_env.read_text() == '{"protected": true}\n' - - env_cfg = json.loads(stale_env.read_text()) - dc_cfg = json.loads(stale_json.read_text()) - assert env_cfg["languages"]["python"] is True - assert env_cfg["cicd"]["docker"] is True - assert env_cfg["languages"]["java"] is False - assert dc_cfg["build"]["args"]["INSTALL_PYTHON"] == "true" - assert dc_cfg["build"]["args"]["INSTALL_DOCKER"] == "true" - assert dc_cfg["build"]["args"]["INSTALL_JAVA"] == "false" + assert not (Path(tmp) / ".devcontainer.generated").exists() def test_init_checkbox_includes_space_instruction(): """Each checkbox prompt must include an instruction so users know to press Space.""" @@ -229,13 +151,9 @@ def test_init_checkbox_includes_space_instruction(): confirm_proceed = MagicMock() confirm_proceed.execute.return_value = True - confirm_skip = MagicMock() - confirm_skip.execute.return_value = False - with tempfile.TemporaryDirectory() as tmp: with patch("cli.devopsos.inquirer.checkbox", return_value=checkbox_mock) as mock_cb, \ - patch("cli.devopsos.inquirer.confirm", - side_effect=[confirm_proceed, confirm_skip]): + patch("cli.devopsos.inquirer.confirm", side_effect=[confirm_proceed]): CliRunner().invoke(app, ["init", "--dir", tmp]) # Every checkbox call must pass a non-empty 'instruction' keyword argument @@ -278,14 +196,10 @@ def _checkbox_factory(**kwargs): confirm_proceed = MagicMock() confirm_proceed.execute.return_value = True - confirm_skip = MagicMock() - confirm_skip.execute.return_value = False - with tempfile.TemporaryDirectory() as tmp: with patch("cli.devopsos.inquirer.checkbox", side_effect=_checkbox_factory), \ patch("cli.devopsos.inquirer.text", return_value=text_mock), \ - patch("cli.devopsos.inquirer.confirm", - side_effect=[confirm_proceed, confirm_skip]): + patch("cli.devopsos.inquirer.confirm", side_effect=[confirm_proceed]): result = CliRunner().invoke(app, ["init", "--dir", tmp]) assert result.exit_code == 0, result.output diff --git a/docs/CLI-COMMANDS-REFERENCE.md b/docs/CLI-COMMANDS-REFERENCE.md index 59e9b21..f81d861 100644 --- a/docs/CLI-COMMANDS-REFERENCE.md +++ b/docs/CLI-COMMANDS-REFERENCE.md @@ -285,17 +285,17 @@ python -m cli.devopsos scaffold devcontainer [options] | `--build-tools TOOLS` | `DEVOPS_OS_DEVCONTAINER_BUILD_TOOLS` | _(none)_ | Comma-separated build tools | | `--code-analysis TOOLS` | `DEVOPS_OS_DEVCONTAINER_CODE_ANALYSIS` | _(none)_ | Comma-separated analysis tools | | `--devops-tools TOOLS` | `DEVOPS_OS_DEVCONTAINER_DEVOPS_TOOLS` | _(none)_ | Comma-separated DevOps tools | -| `--python-version VER` | `DEVOPS_OS_DEVCONTAINER_PYTHON_VERSION` | `3.11` | Python version | -| `--java-version VER` | `DEVOPS_OS_DEVCONTAINER_JAVA_VERSION` | `17` | Java JDK version | -| `--node-version VER` | `DEVOPS_OS_DEVCONTAINER_NODE_VERSION` | `20` | Node.js version | -| `--go-version VER` | `DEVOPS_OS_DEVCONTAINER_GO_VERSION` | `1.21` | Go version | -| `--k9s-version VER` | `DEVOPS_OS_DEVCONTAINER_K9S_VERSION` | `0.29.1` | K9s version | -| `--argocd-version VER` | `DEVOPS_OS_DEVCONTAINER_ARGOCD_VERSION` | `2.8.4` | ArgoCD CLI version | -| `--flux-version VER` | `DEVOPS_OS_DEVCONTAINER_FLUX_VERSION` | `2.1.2` | Flux version | -| `--kustomize-version VER` | `DEVOPS_OS_DEVCONTAINER_KUSTOMIZE_VERSION` | `5.2.1` | Kustomize version | -| `--nexus-version VER` | `DEVOPS_OS_DEVCONTAINER_NEXUS_VERSION` | `3.50.0` | Nexus version | -| `--prometheus-version VER` | `DEVOPS_OS_DEVCONTAINER_PROMETHEUS_VERSION` | `2.45.0` | Prometheus version | -| `--grafana-version VER` | `DEVOPS_OS_DEVCONTAINER_GRAFANA_VERSION` | `10.0.0` | Grafana version | +| `--python-version VER` | `DEVOPS_OS_DEVCONTAINER_PYTHON_VERSION` | `3.12` | Python version | +| `--java-version VER` | `DEVOPS_OS_DEVCONTAINER_JAVA_VERSION` | `21` | Java JDK version | +| `--node-version VER` | `DEVOPS_OS_DEVCONTAINER_NODE_VERSION` | `22` | Node.js version | +| `--go-version VER` | `DEVOPS_OS_DEVCONTAINER_GO_VERSION` | `1.25.0` | Go version | +| `--k9s-version VER` | `DEVOPS_OS_DEVCONTAINER_K9S_VERSION` | `0.50.16` | K9s version | +| `--argocd-version VER` | `DEVOPS_OS_DEVCONTAINER_ARGOCD_VERSION` | `3.3.6` | ArgoCD CLI version | +| `--flux-version VER` | `DEVOPS_OS_DEVCONTAINER_FLUX_VERSION` | `2.8.5` | Flux version | +| `--kustomize-version VER` | `DEVOPS_OS_DEVCONTAINER_KUSTOMIZE_VERSION` | `5.8.0` | Kustomize version | +| `--nexus-version VER` | `DEVOPS_OS_DEVCONTAINER_NEXUS_VERSION` | `3.91.0` | Nexus version | +| `--prometheus-version VER` | `DEVOPS_OS_DEVCONTAINER_PROMETHEUS_VERSION` | `3.5.1` | Prometheus version | +| `--grafana-version VER` | `DEVOPS_OS_DEVCONTAINER_GRAFANA_VERSION` | `12.4.2` | Grafana version | | `--output-dir DIR` | `DEVOPS_OS_DEVCONTAINER_OUTPUT_DIR` | `.` | Root output directory | ### Output files @@ -439,10 +439,9 @@ python -m cli.devopsos init [--dir DIRECTORY] ### Output files - Fresh target with no existing `.devcontainer/`: - `.devcontainer/devcontainer.env.json` and `.devcontainer/devcontainer.json` + `.devcontainer/Dockerfile`, `.devcontainer/devcontainer.env.json`, and `.devcontainer/devcontainer.json` - Existing target with `.devcontainer/` already present: - `.devcontainer/` is preserved unchanged and generated output is written to - `.devcontainer.generated/devcontainer.env.json` and `.devcontainer.generated/devcontainer.json` + `.devcontainer/` is preserved unchanged and `init` stops without generating alternate output --- diff --git a/docs/CLI-TEST-REPORT.md b/docs/CLI-TEST-REPORT.md index a08b761..7be1831 100644 --- a/docs/CLI-TEST-REPORT.md +++ b/docs/CLI-TEST-REPORT.md @@ -39,9 +39,8 @@ |---------------------|------|--------| | `init --help` shows `--dir` option | `test_init_help_shows_dir_option` | ✅ | | `init --dir ` creates `.devcontainer/` in target dir | `test_init_dir_option_creates_devcontainer_in_specified_dir` | ✅ | -| Existing `.devcontainer/` is preserved and generated output goes to `.devcontainer.generated/` | `test_init_preserves_existing_devcontainer_and_writes_generated_copy` | ✅ | +| Existing `.devcontainer/` is preserved and init stops without alternate output | `test_init_preserves_existing_devcontainer_and_stops` | ✅ | | Existing `.devcontainer/` with partial config is still preserved | `test_init_preserves_existing_devcontainer_when_only_one_file_exists` | ✅ | -| Rerunning `init` refreshes `.devcontainer.generated/` only | `test_init_rerun_refreshes_generated_output_without_touching_devcontainer` | ✅ | | Checkbox prompt includes Space-to-toggle instruction | `test_init_checkbox_includes_space_instruction` | ✅ | | Selected tools are written to `devcontainer.json` | `test_init_selections_written_to_config` | ✅ | diff --git a/docs/DEVOPS-OS-README.md b/docs/DEVOPS-OS-README.md index 892460d..d3a57b7 100644 --- a/docs/DEVOPS-OS-README.md +++ b/docs/DEVOPS-OS-README.md @@ -30,12 +30,13 @@ Before getting started, ensure you have the following installed: cd your-project ``` -2. Copy the DevOps-OS .devcontainer files to your project: +2. Generate a dev container in the project root: ```bash - mkdir -p .devcontainer - cp -r /path/to/devops-os/.devcontainer/* ./.devcontainer/ + python -m cli.devopsos init --dir . ``` + On a fresh target, this creates `.devcontainer/Dockerfile`, `.devcontainer/devcontainer.json`, and `.devcontainer/devcontainer.env.json` from the template-backed generator. + 3. Open the project in VS Code and click "Reopen in Container" when prompted, or run the "Dev Containers: Reopen in Container" command from the Command Palette. ### Option 2: Create a New DevOps-OS Container from Scratch @@ -48,18 +49,13 @@ Before getting started, ensure you have the following installed: 2. Generate the dev container configuration using the CLI: ```bash - # Generate devcontainer.json and devcontainer.env.json - python -m cli.scaffold_devcontainer \ - --languages python,java,go \ - --cicd-tools docker,terraform,kubectl,helm \ - --kubernetes-tools k9s,kustomize,argocd_cli,flux \ - --output-dir . + python -m cli.devopsos init --dir . ``` - This creates `.devcontainer/devcontainer.json` and `.devcontainer/devcontainer.env.json` with the correct build args, VS Code extensions, and forwarded ports for your selected tools. - If the target directory already has a `.devcontainer/`, `devopsos init` preserves it and writes a review copy to `.devcontainer.generated/` instead of overwriting the existing files. + This creates `.devcontainer/Dockerfile`, `.devcontainer/devcontainer.json`, and `.devcontainer/devcontainer.env.json`. + If you want the legacy two-file output instead, use `python -m cli.scaffold_devcontainer --output-dir .`. - Run `python -m cli.scaffold_devcontainer --help` to see all options, including version overrides (e.g. `--python-version 3.12`). + Run `python -m cli.devopsos init --help` for the interactive initializer, or `python -m cli.scaffold_devcontainer --help` to see the legacy scaffold options and version overrides.
Alternative: create the files manually @@ -75,10 +71,6 @@ Before getting started, ensure you have the following installed: "context": ".", "args": {} }, - "runArgs": [ - "--init", - "--privileged" - ], "overrideCommand": false, "customizations": { "vscode": { @@ -97,8 +89,7 @@ Before getting started, ensure you have the following installed: }, "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" - ], - "postCreateCommand": "python3 .devcontainer/configure.py" + ] } ``` @@ -140,9 +131,7 @@ Before getting started, ensure you have the following installed:
-3. Copy the Dockerfile from this repository to your `.devcontainer` directory. - -4. Open the project in VS Code and click "Reopen in Container" when prompted, or run the "Dev Containers: Reopen in Container" command from the Command Palette. +3. Open the project in VS Code and click "Reopen in Container" when prompted, or run the "Dev Containers: Reopen in Container" command from the Command Palette. ## Customizing DevOps-OS @@ -156,9 +145,9 @@ python -m cli.scaffold_devcontainer \ --devops-tools prometheus,grafana ``` -This regenerates both `devcontainer.json` and `devcontainer.env.json`. +This regenerates `devcontainer.json` and `devcontainer.env.json`. If you need a generated `Dockerfile` as well, use `python -m cli.devopsos init --dir .` on a fresh target. -You can also edit `devcontainer.env.json` by hand. This file lets you specify which languages, tools, and features to include in your environment. +You can also edit the generated `devcontainer.env.json` by hand. This file lets you specify which languages, tools, and features to include in your environment. ### Language Configuration @@ -252,8 +241,8 @@ If the container fails to build: If a specific tool isn't working properly: 1. Check the tool is enabled in `devcontainer.env.json` -2. Run the configuration script again: `python3 .devcontainer/configure.py` -3. Check if the tool requires additional configuration +2. Regenerate the dev container files with `python -m cli.scaffold_devcontainer ...` using the desired tool flags +3. Rebuild the container and check if the tool requires additional configuration ## Next Steps diff --git a/docs/HowTo-Create-DevOps-Os-GHA-Jenkins.md b/docs/HowTo-Create-DevOps-Os-GHA-Jenkins.md index 4fc6027..38c2b46 100644 --- a/docs/HowTo-Create-DevOps-Os-GHA-Jenkins.md +++ b/docs/HowTo-Create-DevOps-Os-GHA-Jenkins.md @@ -10,12 +10,12 @@ First, let's create a GitHub repository for your DevOps-OS: 2. Structure it like this: ``` devops-os/ - ├── .devcontainer/ - │ ├── Dockerfile - │ ├── devcontainer.json - │ ├── devcontainer.env.json - │ ├── configure.py - │ └── README.md + ├── .legacy/ + │ └── devcontainer/ + │ ├── Dockerfile + │ ├── devcontainer.json + │ ├── devcontainer.env.json + │ └── README.md ├── .github/ │ └── workflows/ │ └── example-pipeline.yml @@ -120,7 +120,9 @@ on: push: branches: [ main ] paths: - - '.devcontainer/**' + - 'cli/devcontainer_templates.py' + - 'cli/templates/devcontainer/**' + - 'cli/scaffold_devcontainer.py' workflow_dispatch: jobs: @@ -134,10 +136,11 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Generate devcontainer.json + - name: Generate devcontainer files run: | - cd .devcontainer - python configure.py + python -m cli.devopsos scaffold devcontainer \ + --languages python,java,javascript,go \ + --cicd-tools docker,terraform,kubectl,helm,github_actions - name: Login to GitHub Container Registry uses: docker/login-action@v2 @@ -447,7 +450,9 @@ You can dynamically generate the dev container configuration during the CI/CD pr } } EOF - python configure.py + python -m cli.devopsos scaffold devcontainer \ + --languages python,java,javascript,go \ + --cicd-tools docker,terraform,kubectl,helm,github_actions ``` ### 2. Matrix Testing with Different Tools @@ -475,10 +480,11 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Configure for ${{ matrix.language }} + - name: Generate language-specific devcontainer files run: | - cd .devcontainer - python configure.py --enable-only-language ${{ matrix.language }} + python -m cli.devopsos scaffold devcontainer \ + --languages ${{ matrix.language }} \ + --cicd-tools docker - name: Run tests run: ${{ matrix.test_command }} ``` @@ -491,4 +497,4 @@ jobs: 4. **Version Control**: Tool configurations are version controlled with your code 5. **Reusable Workflows**: Create standardized pipelines that can be reused across projects -By creating and sharing your DevOps-OS container, you provide a standardized environment that can be used across the entire software development lifecycle, from local development to CI/CD pipelines, ensuring consistency and reliability throughout. \ No newline at end of file +By creating and sharing your DevOps-OS container, you provide a standardized environment that can be used across the entire software development lifecycle, from local development to CI/CD pipelines, ensuring consistency and reliability throughout. diff --git a/docs/kubernetes-capabilities.md b/docs/kubernetes-capabilities.md index 47a10f5..186f56a 100644 --- a/docs/kubernetes-capabilities.md +++ b/docs/kubernetes-capabilities.md @@ -201,35 +201,13 @@ When integrating Kubernetes with your CI/CD pipelines: ## Configuring the DevOps-OS Container for Kubernetes -You can customize which Kubernetes tools are included in your container by editing the `.devcontainer/devcontainer.env.json` file: - -```json -{ - "kubernetes": { - "k9s": true, - "kustomize": true, - "argocd_cli": true, - "lens": false, - "kubeseal": true, - "flux": true, - "kind": true, - "minikube": true, - "openshift_cli": false - }, - "versions": { - "k9s": "0.29.1", - "argocd": "2.8.4", - "flux": "2.1.2", - "kustomize": "5.2.1" - } -} -``` - -After modifying the configuration, run the configure script to update your container: +You can customize which Kubernetes tools are included in your container by regenerating the devcontainer configuration with the desired Kubernetes tool flags: ```bash -cd .devcontainer -python configure.py +python -m cli.devopsos scaffold devcontainer \ + --languages python,go \ + --cicd-tools docker,kubectl,helm \ + --kubernetes-tools k9s,kustomize,argocd_cli,flux,kind,minikube ``` Then rebuild your container to apply the changes. diff --git a/hugo-docs/content/docs/dev-container/_index.md b/hugo-docs/content/docs/dev-container/_index.md index 8aa78a3..16d9a7b 100644 --- a/hugo-docs/content/docs/dev-container/_index.md +++ b/hugo-docs/content/docs/dev-container/_index.md @@ -7,6 +7,8 @@ weight: 60 DevOps-OS provides a pre-configured VS Code Dev Container that gives you a consistent, multi-language development environment with all CI/CD tools included. +The repository's old checked-in `.devcontainer` stack has been archived under `.legacy/devcontainer/`. Generate project-local `.devcontainer/` files with the CLI instead. + --- ## Quick Start @@ -81,7 +83,7 @@ This generates a dev container with: ## Manual Configuration -Edit `.devcontainer/devcontainer.env.json` directly: +Edit the generated `.devcontainer/devcontainer.env.json` directly: ```json { @@ -161,4 +163,4 @@ code . 1. Check the tool is `true` in `devcontainer.env.json` 2. Rebuild the container: **"Dev Containers: Rebuild Container"** -3. Run `python3 .devcontainer/configure.py` manually inside the container +3. Regenerate the devcontainer files with `python -m cli.devopsos scaffold devcontainer ...` if you changed the selected tools diff --git a/mcp_server/server.py b/mcp_server/server.py index 328def9..515ed01 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -405,8 +405,13 @@ def scaffold_devcontainer( "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], - "postCreateCommand": "python3 .devcontainer/configure.py", } + if k8s_list: + devcontainer_json["postCreateCommand"] = ( + "chmod +x /workspaces/.devcontainer/k8s-config-generator.py " + "&& ln -sf /workspaces/.devcontainer/k8s-config-generator.py " + "/usr/local/bin/k8s-config-generator" + ) return json.dumps( { From ce7211c291f288420511da16e9ac6fbb8b44d5ba Mon Sep 17 00:00:00 2001 From: Saravanan Gnanaguru Date: Thu, 16 Apr 2026 20:42:06 +0530 Subject: [PATCH 3/3] feat: add manual smoke testing script for DevOps-OS --- docs/MANUAL-TESTING.md | 111 ++++++++++ scripts/manual_test_devops_os.py | 360 +++++++++++++++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 docs/MANUAL-TESTING.md create mode 100644 scripts/manual_test_devops_os.py diff --git a/docs/MANUAL-TESTING.md b/docs/MANUAL-TESTING.md new file mode 100644 index 0000000..36fb74d --- /dev/null +++ b/docs/MANUAL-TESTING.md @@ -0,0 +1,111 @@ +# DevOps-OS Manual Smoke Testing + +This guide is for fast local validation of the main DevOps-OS feature surface without running the full pytest suite. + +Use it when you want to verify: + +- CLI scaffold generators still run end-to-end +- `devopsos init` still generates the template-backed devcontainer set +- MCP tool functions still return valid-looking output + +This is a smoke check, not exhaustive coverage. + +## What It Covers + +The smoke script validates: + +- `scaffold gha` +- `scaffold jenkins` +- `scaffold gitlab` +- `scaffold argocd` +- `scaffold sre` +- `scaffold devcontainer` +- `scaffold cicd` +- `scaffold unittest` +- `process-first` +- `init` +- MCP tool functions in `mcp_server/server.py` + +It runs entirely in temporary directories and does not modify repo-tracked files. + +## Prerequisites + +Install the same local dependencies used by the CLI and MCP server: + +```bash +pip install -r cli/requirements.txt -r mcp_server/requirements.txt +``` + +If you also want the full automated suite: + +```bash +pip install pytest pytest-html +``` + +## Run the Smoke Script + +From the repository root: + +```bash +python scripts/manual_test_devops_os.py +``` + +If your local environment does not have MCP dependencies installed yet, you can still run the CLI and `init` smoke checks: + +```bash +python scripts/manual_test_devops_os.py --skip-mcp +``` + +If you want to inspect the generated files afterwards: + +```bash +python scripts/manual_test_devops_os.py --keep-temp +``` + +The script prints a `PASS` line for each validated surface and ends with: + +```text +All manual smoke tests passed. +``` + +## What the Script Actually Verifies + +- CLI commands exit successfully +- expected output files are created +- key generated content looks structurally correct +- `init` still creates: + - `.devcontainer/Dockerfile` + - `.devcontainer/devcontainer.json` + - `.devcontainer/devcontainer.env.json` +- `scaffold devcontainer` still creates only: + - `.devcontainer/devcontainer.json` + - `.devcontainer/devcontainer.env.json` +- MCP functions return parseable JSON or expected YAML/text fragments + +If MCP dependencies are missing and you did not pass `--skip-mcp`, the script fails fast with an install hint. + +## Manual Checks Still Worth Doing + +The script does not replace these higher-confidence checks: + +1. Rebuild a generated devcontainer in VS Code or Cursor. +2. Run the full pytest suite: + +```bash +python -m pytest cli/test_cli.py mcp_server/test_server.py tests/test_comprehensive.py -v +``` + +3. If you changed MCP setup or editor integration, validate one real client config: + - `.cursor/mcp.json` + - `.claude/settings.local.json` + - VS Code MCP config + +## When to Use This + +Use this script before a commit when you changed: + +- CLI command wiring +- scaffold generators +- devcontainer generation +- MCP tool wrappers +- docs that describe command behavior and expected outputs diff --git a/scripts/manual_test_devops_os.py b/scripts/manual_test_devops_os.py new file mode 100644 index 0000000..c0f12f4 --- /dev/null +++ b/scripts/manual_test_devops_os.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 +"""Manual smoke test runner for DevOps-OS. + +This script exercises the public CLI generators plus the MCP tool functions +without touching repo-tracked files. It is intended as a fast local confidence +check, not a replacement for the full pytest suite. +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +REPO_ROOT = Path(__file__).resolve().parent.parent +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from typer.testing import CliRunner + +from cli.devopsos import app + + + +def _ok(message: str) -> None: + print(f"PASS {message}") + + +def _fail(message: str) -> None: + print(f"FAIL {message}", file=sys.stderr) + raise SystemExit(1) + + +def _assert(condition: bool, message: str) -> None: + if not condition: + _fail(message) + + +def _run_cli(*args: str, cwd: Path) -> subprocess.CompletedProcess[str]: + env = dict(os.environ) + existing_pythonpath = env.get("PYTHONPATH") + env["PYTHONPATH"] = ( + f"{REPO_ROOT}{os.pathsep}{existing_pythonpath}" + if existing_pythonpath + else str(REPO_ROOT) + ) + result = subprocess.run( + [sys.executable, "-m", "cli.devopsos", *args], + cwd=cwd, + capture_output=True, + text=True, + env=env, + ) + if result.returncode != 0: + details = result.stdout + "\n" + result.stderr + _fail(f"`python -m cli.devopsos {' '.join(args)}` failed\n{details}") + return result + + +def _assert_exists(path: Path) -> None: + _assert(path.exists(), f"Expected path to exist: {path}") + + +def run_cli_smoke(workdir: Path) -> None: + _run_cli( + "scaffold", + "gha", + "--name", + "manual-app", + "--type", + "complete", + "--languages", + "python,go", + "--kubernetes", + "--k8s-method", + "argocd", + cwd=workdir, + ) + workflow = workdir / ".github" / "workflows" / "manual-app-complete.yml" + _assert_exists(workflow) + _assert("name:" in workflow.read_text(encoding="utf-8"), "Generated GHA workflow looks invalid") + _ok("CLI scaffold gha") + + _run_cli( + "scaffold", + "jenkins", + "--name", + "manual-app", + "--type", + "complete", + "--languages", + "java", + "--output", + str(workdir / "jenkins" / "Jenkinsfile"), + cwd=workdir, + ) + jenkinsfile = workdir / "jenkins" / "Jenkinsfile" + _assert_exists(jenkinsfile) + _assert("pipeline" in jenkinsfile.read_text(encoding="utf-8").lower(), "Generated Jenkinsfile looks invalid") + _ok("CLI scaffold jenkins") + + _run_cli( + "scaffold", + "gitlab", + "--name", + "manual-app", + "--type", + "complete", + "--languages", + "python,go", + "--kubernetes", + "--k8s-method", + "flux", + cwd=workdir, + ) + gitlab = workdir / ".gitlab-ci.yml" + _assert_exists(gitlab) + _assert("stages:" in gitlab.read_text(encoding="utf-8"), "Generated GitLab CI file looks invalid") + _ok("CLI scaffold gitlab") + + _run_cli( + "scaffold", + "argocd", + "--name", + "manual-app", + "--repo", + "https://github.com/example/manual-app.git", + "--rollouts", + cwd=workdir, + ) + app_yaml = workdir / "argocd" / "application.yaml" + project_yaml = workdir / "argocd" / "appproject.yaml" + rollout_yaml = workdir / "argocd" / "rollout.yaml" + _assert_exists(app_yaml) + _assert_exists(project_yaml) + _assert_exists(rollout_yaml) + _ok("CLI scaffold argocd") + + _run_cli( + "scaffold", + "sre", + "--name", + "manual-app", + "--team", + "platform", + cwd=workdir, + ) + _assert_exists(workdir / "sre" / "alert-rules.yaml") + _assert_exists(workdir / "sre" / "grafana-dashboard.json") + _assert_exists(workdir / "sre" / "slo.yaml") + _ok("CLI scaffold sre") + + _run_cli( + "scaffold", + "devcontainer", + "--languages", + "python,go", + "--cicd-tools", + "docker,terraform,kubectl,helm", + "--kubernetes-tools", + "k9s,flux", + cwd=workdir, + ) + devcontainer_dir = workdir / ".devcontainer" + _assert_exists(devcontainer_dir / "devcontainer.json") + _assert_exists(devcontainer_dir / "devcontainer.env.json") + _assert(not (devcontainer_dir / "Dockerfile").exists(), "Legacy scaffold should not generate Dockerfile") + _ok("CLI scaffold devcontainer") + + _run_cli( + "scaffold", + "unittest", + "--name", + "manual-app", + "--languages", + "python,javascript,go", + cwd=workdir, + ) + unittest_dir = workdir / "unittest" + _assert_exists(unittest_dir / "pytest.ini") + _assert_exists(unittest_dir / "tests" / "test_sample.py") + _assert_exists(unittest_dir / "tests" / "sample.test.js") + _assert_exists(unittest_dir / "manual_app_test.go") + _ok("CLI scaffold unittest") + + cicd_dir = workdir / "combined-cicd" + cicd_dir.mkdir() + _run_cli( + "scaffold", + "cicd", + "--name", + "manual-app", + "--type", + "build", + "--languages", + "python", + "--github", + "--jenkins", + "--output-dir", + str(cicd_dir), + cwd=workdir, + ) + _assert_exists(cicd_dir / ".github" / "workflows" / "manual-app-build.yml") + _assert_exists(cicd_dir / "Jenkinsfile") + _ok("CLI scaffold cicd") + + result = _run_cli("process-first", "--section", "what", cwd=workdir) + _assert("Process-First" in result.stdout, "process-first output looks incomplete") + _ok("CLI process-first") + + +def run_init_smoke() -> None: + checkbox_selections = iter( + [ + ["python", "go"], + ["docker"], + ["gradle"], + ["sonarqube"], + ["kubectl", "k9s"], + ["github_actions", "terraform"], + ["prometheus"], + ] + ) + + def checkbox_factory(**_: object) -> MagicMock: + mock = MagicMock() + mock.execute.return_value = next(checkbox_selections) + return mock + + text_mock = MagicMock() + text_mock.execute.return_value = "3.12" + confirm_mock = MagicMock() + confirm_mock.execute.return_value = True + + with tempfile.TemporaryDirectory(prefix="devopsos-init-") as tmpdir: + with patch("cli.devopsos.inquirer.checkbox", side_effect=checkbox_factory), patch( + "cli.devopsos.inquirer.text", return_value=text_mock + ), patch("cli.devopsos.inquirer.confirm", return_value=confirm_mock): + result = CliRunner().invoke(app, ["init", "--dir", tmpdir]) + + if result.exit_code != 0: + _fail(f"`devopsos init` smoke test failed\n{result.output}") + + devcontainer_dir = Path(tmpdir) / ".devcontainer" + _assert_exists(devcontainer_dir / "Dockerfile") + _assert_exists(devcontainer_dir / "devcontainer.json") + _assert_exists(devcontainer_dir / "devcontainer.env.json") + + generated = json.loads((devcontainer_dir / "devcontainer.json").read_text(encoding="utf-8")) + _assert("features" in generated, "`init` should generate hybrid devcontainer.json with features") + _assert("build" in generated, "`init` should generate build config") + _ok("CLI init") + + +def run_mcp_smoke() -> None: + try: + from mcp_server.server import ( + generate_argocd_config, + generate_github_actions_workflow, + generate_gitlab_ci_pipeline, + generate_jenkins_pipeline, + generate_k8s_config, + generate_sre_configs, + generate_unittest_config, + scaffold_devcontainer, + ) + except ModuleNotFoundError as exc: + _fail( + "MCP smoke tests require mcp_server dependencies. " + "Install `pip install -r mcp_server/requirements.txt` " + f"or rerun with --skip-mcp. Missing module: {exc.name}" + ) + + gha = generate_github_actions_workflow(name="manual-app", workflow_type="complete", languages="python,go") + _assert("manual-app" in gha, "MCP GHA output missing app name") + _ok("MCP generate_github_actions_workflow") + + jenkins = generate_jenkins_pipeline(name="manual-app", pipeline_type="complete", languages="java") + _assert("pipeline" in jenkins.lower(), "MCP Jenkins output looks invalid") + _ok("MCP generate_jenkins_pipeline") + + k8s = generate_k8s_config(app_name="manual-app", image="ghcr.io/example/manual-app:latest") + _assert("Deployment" in k8s, "MCP Kubernetes output missing Deployment") + _ok("MCP generate_k8s_config") + + devcontainer = json.loads(scaffold_devcontainer(languages="python,go", cicd_tools="docker,github_actions")) + _assert("devcontainer_json" in devcontainer, "MCP scaffold_devcontainer missing devcontainer_json") + _assert("devcontainer_env_json" in devcontainer, "MCP scaffold_devcontainer missing devcontainer_env_json") + _ok("MCP scaffold_devcontainer") + + gitlab = generate_gitlab_ci_pipeline(name="manual-app", pipeline_type="complete", languages="python") + _assert("stages:" in gitlab, "MCP GitLab output looks invalid") + _ok("MCP generate_gitlab_ci_pipeline") + + argocd = json.loads( + generate_argocd_config( + name="manual-app", + repo="https://github.com/example/manual-app.git", + auto_sync=True, + rollouts=True, + allow_any_source_repo=True, + ) + ) + _assert("argocd/application.yaml" in argocd, "MCP ArgoCD output missing application") + _assert("argocd/appproject.yaml" in argocd, "MCP ArgoCD output missing AppProject") + _ok("MCP generate_argocd_config") + + sre = json.loads(generate_sre_configs(name="manual-app", team="platform")) + _assert("alert_rules_yaml" in sre, "MCP SRE output missing alert rules") + _assert("grafana_dashboard_json" in sre, "MCP SRE output missing dashboard") + _ok("MCP generate_sre_configs") + + unittest_cfg = json.loads(generate_unittest_config(name="manual-app", languages="python,javascript,go")) + _assert("pytest.ini" in unittest_cfg, "MCP unittest output missing pytest.ini") + _assert("tests/sample.test.js" in unittest_cfg, "MCP unittest output missing JS sample") + _ok("MCP generate_unittest_config") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run DevOps-OS manual smoke tests.") + parser.add_argument( + "--keep-temp", + action="store_true", + help="Keep the temporary CLI output directory and print its path.", + ) + parser.add_argument( + "--skip-mcp", + action="store_true", + help="Skip MCP smoke tests when mcp_server dependencies are not installed locally.", + ) + args = parser.parse_args() + + tempdir = Path(tempfile.mkdtemp(prefix="devopsos-manual-smoke-")) + print(f"Using temp directory: {tempdir}") + + try: + run_cli_smoke(tempdir) + run_init_smoke() + if args.skip_mcp: + print("SKIP MCP smoke tests") + else: + run_mcp_smoke() + finally: + if args.keep_temp: + print(f"Kept temp directory: {tempdir}") + else: + shutil.rmtree(tempdir, ignore_errors=True) + + print("All manual smoke tests passed.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())