diff --git a/.bldr.toml b/.bldr.toml new file mode 100644 index 00000000..31812b2a --- /dev/null +++ b/.bldr.toml @@ -0,0 +1,7 @@ +[knife] +build_targets = [ + "x86_64-linux", + "aarch64-linux", + "x86_64-windows", + "aarch64-darwin" +] diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command index d86d1e89..f744ec83 100644 --- a/.buildkite/hooks/pre-command +++ b/.buildkite/hooks/pre-command @@ -6,4 +6,16 @@ elif [[ $BUILDKITE_ORGANIZATION_SLUG = 'chef' ]] || [[ $BUILDKITE_ORGANIZATION_S AWS_REGION='us-west-2' fi -export HAB_AUTH_TOKEN=$(aws ssm get-parameter --name 'habitat-prod-auth-token' --with-decryption --query Parameter.Value --output text --region ${AWS_REGION}) +if [[ "$BUILDKITE_LABEL" =~ [Mm]ac[Oo][Ss]|mac_os_x|darwin ]]; then + if ! command -v vault-util-init &>/dev/null; then + echo "ERROR: vault-util-init not found on macOS worker" >&2 + exit 1 + fi + export VAULT_ADDR="https://vault.ps.chef.co" + export VAULT_TOKEN="${VAULT_TOKEN:-$(cat ~/.vault-token 2>/dev/null || true)}" + . vault-util-init + HAB_AUTH_TOKEN=$(vault kv get -field auth_token account/static/habitat/chef-ci) + export HAB_AUTH_TOKEN +else + export HAB_AUTH_TOKEN=$(aws ssm get-parameter --name 'habitat-prod-auth-token' --with-decryption --query Parameter.Value --output text --region ${AWS_REGION}) +fi diff --git a/.github/workflows/habitat-build.yml b/.github/workflows/habitat-build.yml new file mode 100644 index 00000000..b792b5b9 --- /dev/null +++ b/.github/workflows/habitat-build.yml @@ -0,0 +1,68 @@ +name: macos_habitat_build + +"on": + pull_request_target: + branches: [main] + push: + branches: [main] + # allow manual runs + workflow_dispatch: + +permissions: {} + +concurrency: + group: macos-habitat-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + workflow_guard: + uses: ./.github/workflows/workflow-change-guard.yml + permissions: + contents: read + + habitat-plan: + needs: workflow_guard + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [macos-14] + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + env: + HAB_ORIGIN: gha + HAB_BLDR_CHANNEL: base-2025 + HAB_REFRESH_CHANNEL: base-2025 + HAB_AUTH_TOKEN: ${{ secrets.HAB_AUTH_TOKEN }} + HAB_LICENSE: accept-no-persist + HAB_NONINTERACTIVE: "true" + steps: + - uses: actions/checkout@v7 + with: + clean: true + + - name: Install Habitat CLI + run: | + curl https://raw.githubusercontent.com/habitat-sh/habitat/main/components/hab/install.sh | sudo -E bash -s -- -t aarch64-darwin + hab license accept + hab --version + + - name: Generate Habitat Origin Key + run: hab origin key generate "$HAB_ORIGIN" + + - name: Build Habitat Package + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + hab pkg build habitat/aarch64-darwin + + - name: Install Habitat Package + run: | + source ./results/last_build.env + echo "Installing: $pkg_ident ($pkg_artifact)" + sudo -E hab pkg install "./results/$pkg_artifact" + + - name: Run test.darwin.sh + run: | + source ./results/last_build.env + sudo -E ./habitat/tests/test.darwin.sh "$pkg_ident" diff --git a/.github/workflows/workflow-change-guard.yml b/.github/workflows/workflow-change-guard.yml new file mode 100644 index 00000000..395eaa48 --- /dev/null +++ b/.github/workflows/workflow-change-guard.yml @@ -0,0 +1,61 @@ +name: workflow-change-guard + +on: + workflow_call: + outputs: + workflow_changed: + description: "Whether workflow files were modified" + value: ${{ jobs.detect.outputs.workflow_changed }} + +permissions: {} + +jobs: + detect: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + workflow_changed: ${{ steps.diff.outputs.changed }} + steps: + - name: Checkout base branch + uses: actions/checkout@v7 + with: + allow-unsafe-pr-checkout: true + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false + clean: true + + - name: Fetch PR head + run: git fetch origin ${{ github.event.pull_request.head.sha }} + + # NOTE!! + # Do NOT add other paths here. This is not a general "security" + # check, this is a check to make sure new secrets are not added + # to pull_request_target PRs. That is the ONLY purpose. + - name: Detect workflow file changes + id: diff + run: | + set -euo pipefail + CHANGED=$(git diff --name-only \ + ${{ github.event.pull_request.base.sha }} \ + ${{ github.event.pull_request.head.sha }} \ + | grep '^.github/workflows/' || true) + + if [ -n "$CHANGED" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Workflow files changed:" + echo "$CHANGED" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No workflow changes detected." + fi + + approve: + needs: detect + if: (github.event_name == 'pull_request_target' && needs.detect.outputs.workflow_changed == 'true') + runs-on: ubuntu-latest + # Approval happens ONLY here + environment: workflow_guard + permissions: {} + steps: + - run: echo "Workflow changes approved" diff --git a/cspell.json b/cspell.json index bdf32ce6..9bc9abca 100644 --- a/cspell.json +++ b/cspell.json @@ -333,6 +333,7 @@ "dslocal", "DUPEUX", "DWORDLONG", + "DYLD", "DYNALINK", "DYNLINK", "EAGAIN", diff --git a/habitat/aarch64-darwin/plan.sh b/habitat/aarch64-darwin/plan.sh new file mode 100644 index 00000000..92e255b7 --- /dev/null +++ b/habitat/aarch64-darwin/plan.sh @@ -0,0 +1,190 @@ +export HAB_BLDR_CHANNEL="base-2025" +export HAB_REFRESH_CHANNEL="base-2025" + +pkg_name=knife +pkg_origin=chef +ruby_pkg="core/ruby3_4" + +# Ruby version for gem directory structure +# NOTE: Bundler normalizes Ruby versions to major.minor.0 format for gem compatibility. +ruby_gem_version="3.4.0" + +pkg_description="knife is a command-line tool that provides an interface between a local chef-repo and the Chef Infra Server." +pkg_deps=(${ruby_pkg} core/coreutils core/libarchive core/cacerts) +pkg_build_deps=( + core/make + core/sed + core/clang + core/cmake +) +pkg_bin_dirs=(bin) + +do_setup_environment() { + # vendor/ is where gem install places gems; vendor/ruby/$VERSION/ is where bundler places them + push_runtime_env GEM_PATH "${pkg_prefix}/vendor" + push_runtime_env GEM_PATH "${pkg_prefix}/vendor/ruby/${ruby_gem_version}" + + set_runtime_env APPBUNDLER_ALLOW_RVM "true" + set_runtime_env LANG "en_US.UTF-8" + set_runtime_env LC_CTYPE "en_US.UTF-8" +} + +do_prepare() { + build_line "Setting up build environment for native extensions" + export PATH="$(pkg_path_for core/cmake)/bin:$(pkg_path_for core/make)/bin:$(pkg_path_for core/clang)/bin:$PATH" + export CC="$(pkg_path_for core/clang)/bin/clang" + export CXX="$(pkg_path_for core/clang)/bin/clang++" +} + +pkg_version() { + cat "$PLAN_CONTEXT/../../VERSION" +} + +do_before() { + update_pkg_version +} + +do_unpack() { + mkdir -pv "$HAB_CACHE_SRC_PATH/$pkg_dirname" + cp -RT "$PLAN_CONTEXT"/../.. "$HAB_CACHE_SRC_PATH/$pkg_dirname/" +} + +do_build() { + cd "$HAB_CACHE_SRC_PATH/$pkg_dirname" || exit_with "unable to cd to source directory" 1 + + export GEM_HOME="$pkg_prefix/vendor" + export GEM_PATH="$GEM_HOME" + export HOME="$HAB_CACHE_SRC_PATH/$pkg_dirname" + export GEM_SPEC_CACHE="$HAB_CACHE_SRC_PATH/$pkg_dirname/.gem/specs" + mkdir -p "$GEM_SPEC_CACHE" + export PATH="$(pkg_path_for core/cmake)/bin:$(pkg_path_for core/make)/bin:$(pkg_path_for core/clang)/bin:$PATH" + export CC="$(pkg_path_for core/clang)/bin/clang" + export CXX="$(pkg_path_for core/clang)/bin/clang++" + + build_line "Building the Knife gem from the gemspec with GEM_HOME=$GEM_HOME and GEM_PATH=$GEM_PATH" + bundle config set --local path "$GEM_HOME" + bundle config --local without integration deploy maintenance development omnibus_package test + bundle config --local with habitat + bundle config --local jobs 4 + bundle config --local retry 5 + bundle config --local silence_root_warning 1 + bundle install + + gem build knife.gemspec + + # Set GEM_HOME to ruby version directory for cleanup script to find lint_roller gem + export GEM_HOME="$pkg_prefix/vendor/ruby/${ruby_gem_version}" + export GEM_PATH="$pkg_prefix/vendor" + ruby ./scripts/cleanup_lint_roller.rb +} + +do_install() { + cd "$HAB_CACHE_SRC_PATH/$pkg_dirname" || exit_with "unable to cd to source directory" 1 + + # Copy NOTICE to the package directory + if [[ -f "NOTICE" ]]; then + build_line "Copying NOTICE to package directory" + cp "NOTICE" "$pkg_prefix/" + else + build_line "Warning: NOTICE not found in source directory" + fi + + export GEM_HOME="$pkg_prefix/vendor" + export HOME="$HAB_CACHE_SRC_PATH/$pkg_dirname" + export GEM_SPEC_CACHE="$HAB_CACHE_SRC_PATH/$pkg_dirname/.gem/specs" + mkdir -p "$GEM_SPEC_CACHE" + + build_line "Setting GEM_PATH=$GEM_HOME" + export GEM_PATH="$GEM_HOME" + + build_line "Installing the Knife gem" + local knife_version=$(cat VERSION) + # Use --ignore-dependencies: bundler already installed all runtime deps to + # vendor/ruby/${ruby_gem_version}/ at Gemfile.lock-pinned versions. + gem install "knife-${knife_version}.gem" --no-document --ignore-dependencies + + make_pkg_official_distrib + + build_line "** installing appbundler" + gem install appbundler --no-document + + build_line "** generating binstubs for knife with precise version pins" + # Expose both gem stores to appbundler: knife lives in vendor/ (gem install), + # all its deps live in vendor/ruby/${ruby_gem_version}/ (bundler Gemfile.lock). + GEM_HOME="$pkg_prefix/vendor" \ + GEM_PATH="$pkg_prefix/vendor:$pkg_prefix/vendor/ruby/${ruby_gem_version}" \ + "${pkg_prefix}/vendor/bin/appbundler" . "$pkg_prefix/bin" knife + + build_line "** patching binstubs to allow running directly" + local patch_content + patch_content=$(cat "${HAB_CACHE_SRC_PATH}/${pkg_dirname}/binstub_patch.rb") + for binstub in ${pkg_prefix}/bin/*; do + # Use awk to insert patch content after 'require "rubygems"' line + awk -v patch="$patch_content" '/require "rubygems"/{print; print patch; next}1' "$binstub" > "${binstub}.tmp" + mv "${binstub}.tmp" "$binstub" + done + + if ! grep -q 'APPBUNDLER_ALLOW_RVM' "${pkg_prefix}/bin/knife"; then + build_line "ERROR: binstub patch injection failed for ${pkg_prefix}/bin/knife" + return 1 + fi + + build_line "** creating wrapper for runtime environment" + mkdir -p "$pkg_prefix/libexec" + mv "$pkg_prefix/bin/knife" "$pkg_prefix/libexec/knife" + cat < "$pkg_prefix/bin/knife" +#!/bin/bash +set -e +# GEM_HOME points to the bundler-managed gem tree (where 'chef' and Gemfile deps live) +export GEM_HOME="$pkg_prefix/vendor/ruby/${ruby_gem_version}" +# GEM_PATH also includes the flat vendor tree (where gem install puts knife runtime deps) +export GEM_PATH="$pkg_prefix/vendor" +export DYLD_LIBRARY_PATH="$(pkg_path_for core/libarchive)/lib:\$DYLD_LIBRARY_PATH" +# SSL certificate verification - point OpenSSL to CA certificates +export SSL_CERT_FILE="$(pkg_path_for core/cacerts)/ssl/certs/cacert.pem" +export SSL_CERT_DIR="$(pkg_path_for core/cacerts)/ssl/certs" +exec $(pkg_path_for ${ruby_pkg})/bin/ruby $pkg_prefix/libexec/knife "\$@" +EOF + chmod -v 755 "$pkg_prefix/bin/knife" + + build_line "** fixing binstub shebangs" + fix_interpreter "${pkg_prefix}/libexec/*" "$ruby_pkg" bin/ruby + + rm -rf "$GEM_PATH/cache" + rm -rf "$GEM_PATH/bundler" + rm -rf "$GEM_PATH/doc" + # Also clean the bundler-managed gem tree + rm -rf "$pkg_prefix/vendor/ruby/${ruby_gem_version}/cache/" + rm -rf "$pkg_prefix/vendor/ruby/${ruby_gem_version}/bundler/" +} + +make_pkg_official_distrib() { + # Test if artifactory-internal.ps.chef.co is reachable + build_line "Testing connectivity to artifactory-internal.ps.chef.co..." + artifactory_url="https://artifactory-internal.ps.chef.co/artifactory/omnibus-gems-local/" + if curl --head --silent --fail --connect-timeout 30 "$artifactory_url" > /dev/null 2>&1; then + build_line "Artifactory is reachable, proceeding with chef-official-distribution installation" + # Install to the Bundler-created ruby gem directory structure + local install_dir="$GEM_HOME/ruby/${ruby_gem_version}" + gem sources --add "$artifactory_url" + gem install chef-official-distribution --no-document --install-dir "$install_dir" + gem sources -r "$artifactory_url" + + build_line "Verifying chef-official-distribution installation" + if ! GEM_HOME="$install_dir" GEM_PATH="$install_dir" gem list -i chef-official-distribution; then + build_line "Error: chef-official-distribution installation failed" + exit 1 + fi + else + build_line "Artifactory is not reachable, skipping chef-official-distribution installation" + fi +} + +do_after() { + build_line "Removing .github directories from vendored gems..." + find "$pkg_prefix/vendor" -type d -name ".github" -exec rm -rf {} + 2>/dev/null || true +} + +do_strip() { + return 0 +} diff --git a/habitat/tests/test.darwin.sh b/habitat/tests/test.darwin.sh new file mode 100755 index 00000000..f3d31f7b --- /dev/null +++ b/habitat/tests/test.darwin.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +pkg_ident="$1" + +error () { + local message="$1" + echo -e "\nERROR: ${message}\n" >&2 + exit 1 +} + +[[ -n "$pkg_ident" ]] || error 'no hab package identity provided' + +package_version=$(awk -F / '{print $3}' <<<"$pkg_ident") + +# Get the installed package path +pkg_path=$(hab pkg path "$pkg_ident") + +echo "Testing ${pkg_ident} executables at ${pkg_path}" + +# Test knife version +version=$("${pkg_path}/bin/knife" -v) +echo "$version" +actual_version=$(echo "$version" | sed -E 's/.*version: ([0-9]+\.[0-9]+\.[0-9]+).*/\1/') +echo "Detected version: $actual_version" + +if [[ "$actual_version" != *"$package_version"* ]]; then + error "knife version is not the expected version. Expected '$package_version', got '$actual_version'" +fi + +echo "Verifying bundled knife plugins are available" +plugin_checks=( + "ec2:Available ec2 subcommands" + "google:Available google subcommands" + "windows:Available windows subcommands" +) + +for plugin_check in "${plugin_checks[@]}"; do + plugin_name="${plugin_check%%:*}" + expected_output="${plugin_check#*:}" + + output=$("${pkg_path}/bin/knife" "${plugin_name}" 2>&1 || true) + if echo "${output}" | grep -q "${expected_output}"; then + echo "Plugin '${plugin_name}' is available" + else + echo -e "\nERROR: knife plugin '${plugin_name}' is not available in package '${pkg_ident}'\n" >&2 + exit 1 + fi +done + +echo "All bundled plugins verified successfully"