diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 618e5381..0f617c33 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -75,15 +75,18 @@ jobs: for f in dist/helm-diff-*.tgz; do echo "== $f ==" tar tzf "$f" - for member in diff/plugin.yaml diff/install-binary.sh diff/install-binary.ps1; do + # Archives are wrapped in a directory named after the archive + # itself (required by helm 4 installs from a tarball, see #1071) + base="$(basename "$f" .tgz)" + for member in "$base/plugin.yaml" "$base/install-binary.sh" "$base/install-binary.ps1"; do if ! tar tzf "$f" | grep -q "^${member}$"; then echo "ERROR: ${member} missing from ${f}" missing=1 fi done # the binary has a .exe suffix on windows archives - if ! tar tzf "$f" | grep -qE '^diff/bin/diff(\.exe)?$'; then - echo "ERROR: diff/bin/diff missing from ${f}" + if ! tar tzf "$f" | grep -qE "^${base}/bin/diff(\\.exe)?$"; then + echo "ERROR: ${base}/bin/diff missing from ${f}" missing=1 fi done @@ -107,9 +110,9 @@ jobs: mkdir -p /tmp/archive-test tar xzf dist/helm-diff-linux-amd64.tgz -C /tmp/archive-test echo "Extracted archive layout:" - find /tmp/archive-test/diff -maxdepth 2 -type f | sort + find /tmp/archive-test/helm-diff-linux-amd64 -maxdepth 2 -type f | sort - out="$(helm plugin install /tmp/archive-test/diff 2>&1)" + out="$(helm plugin install /tmp/archive-test/helm-diff-linux-amd64 2>&1)" echo "$out" # The install hook must find the bundled binary already staged in # HELM_PLUGIN_DIR and skip the network download. @@ -120,6 +123,43 @@ jobs: } helm diff version echo "End-to-end archive install test passed: installed from archive without downloading" + - + name: Set up Helm 4 + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + uses: azure/setup-helm@v5 + with: + version: v4.3.0 + - + # Regression test for issue #1071: helm 4 must be able to install + # directly from a local tarball (which it verifies by default), so + # archives must be wrapped in a directory named after the tarball. + name: Helm 4 offline tarball install test (snapshot only) + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + env: + HELM_DATA_HOME: /tmp/helm4-data + run: | + set -e + archive="dist/helm-diff-linux-amd64.tgz" + + export GNUPGHOME="$(mktemp -d)" + chmod 700 "$GNUPGHOME" + gpg --batch --pinentry-mode loopback --passphrase '' \ + --quick-generate-key "helm-diff-test" rsa3072 sign 0 + GPG_FINGERPRINT=$(gpg --batch --with-colons --list-secret-keys "helm-diff-test" \ + | grep '^fpr:' | head -1 | cut -d: -f10) + export GPG_FINGERPRINT GPG_PASSPHRASE="" + + ./scripts/sign-provenance.sh "$archive" "$archive.prov" + gpg --export --armor "helm-diff-test" > /tmp/keyring.gpg + + out="$(helm plugin install "$archive" --keyring /tmp/keyring.gpg 2>&1)" + echo "$out" + echo "$out" | grep -q "Plugin Hash Verified" || { + echo "ERROR: helm 4 did not verify the signed archive." + exit 1 + } + helm diff version + echo "Helm 4 offline tarball install test passed: installed and verified from a local tarball" - name: Export and upload public key if: ${{ startsWith(github.ref, 'refs/tags/v') }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 2c91cfd5..573a83fe 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -47,7 +47,12 @@ archives: - linux-additional format: tgz name_template: '{{ .ProjectName }}-{{ if eq .Os "darwin" }}macos{{ else }}{{ .Os }}{{ end }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - wrap_in_directory: diff + # Wrap archives in a directory named after the archive itself + # (e.g. helm-diff-linux-amd64/). Helm 4's plugin installer requires this + # when installing from a local tarball: it derives the expected directory + # from the tarball filename and fails with "plugin.yaml not found in + # expected directory" otherwise (see issue #1071). + wrap_in_directory: true files: - README.md - plugin.yaml diff --git a/Makefile b/Makefile index 941d5318..125cd254 100644 --- a/Makefile +++ b/Makefile @@ -68,34 +68,40 @@ docker-run-release: -v $(shell pwd)/docker-run-release-cache:/.cache \ -w $(pkg) helm-diff-release make release +# dist-package builds the plugin for a single platform and packs it into +# release/$(1).tgz. The archive content is wrapped in a directory named +# after the archive itself (e.g. helm-diff-linux-amd64/). Helm 4's plugin +# installer requires this when installing directly from a tarball: it +# derives the expected directory from the tarball filename and fails with +# "plugin.yaml not found in expected directory" otherwise (see issue #1071). +# Usage: $(call dist-package,,,[,]) +DIST_PACKAGE_FILES := README.md LICENSE plugin.yaml install-binary.sh install-binary.ps1 +define dist-package +mkdir -p build/helm-diff-$(1)/bin +cp $(DIST_PACKAGE_FILES) build/helm-diff-$(1)/ +goarch=$(3); bin=diff; [ "$(2)" = "windows" ] && bin=$$bin.exe; \ + $(4) GOOS=$(2) GOARCH=$$goarch $(GO) build -o build/helm-diff-$(1)/bin/$$bin -trimpath -ldflags="$(LDFLAGS)" +tar -C build/ -zcvf $(CURDIR)/release/helm-diff-$(1).tgz helm-diff-$(1)/ +rm -rf build/helm-diff-$(1) + +endef + .PHONY: dist dist: export COPYFILE_DISABLE=1 #teach OSX tar to not put ._* files in tar archive dist: export CGO_ENABLED=0 dist: - rm -rf build/diff/* release/* - mkdir -p build/diff/bin release/ - cp README.md LICENSE plugin.yaml build/diff - GOOS=linux GOARCH=amd64 $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-linux-amd64.tgz diff/ - GOOS=linux GOARCH=arm64 $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-linux-arm64.tgz diff/ - GOOS=linux GOARCH=arm GOARM=6 $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-linux-armv6.tgz diff/ - GOOS=linux GOARCH=arm GOARM=7 $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-linux-armv7.tgz diff/ - GOOS=linux GOARCH=ppc64le $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-linux-ppc64le.tgz diff/ - GOOS=linux GOARCH=s390x $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-linux-s390x.tgz diff/ - GOOS=freebsd GOARCH=amd64 $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-freebsd-amd64.tgz diff/ - GOOS=darwin GOARCH=amd64 $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-macos-amd64.tgz diff/ - GOOS=darwin GOARCH=arm64 $(GO) build -o build/diff/bin/diff -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-macos-arm64.tgz diff/ - rm build/diff/bin/diff - GOOS=windows GOARCH=amd64 $(GO) build -o build/diff/bin/diff.exe -trimpath -ldflags="$(LDFLAGS)" - tar -C build/ -zcvf $(CURDIR)/release/helm-diff-windows-amd64.tgz diff/ + rm -rf build/ release/* + mkdir -p release/ + $(call dist-package,linux-amd64,linux,amd64) + $(call dist-package,linux-arm64,linux,arm64) + $(call dist-package,linux-armv6,linux,arm,GOARM=6) + $(call dist-package,linux-armv7,linux,arm,GOARM=7) + $(call dist-package,linux-ppc64le,linux,ppc64le) + $(call dist-package,linux-s390x,linux,s390x) + $(call dist-package,freebsd-amd64,freebsd,amd64) + $(call dist-package,macos-amd64,darwin,amd64) + $(call dist-package,macos-arm64,darwin,arm64) + $(call dist-package,windows-amd64,windows,amd64) .PHONY: release release: lint dist diff --git a/README.md b/README.md index 91be5298..23250075 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,13 @@ helm plugin install https://github.com/databus23/helm-diff If installing this in an offline/airgapped environment, download the platform-specific binary archive (e.g., `helm-diff-linux-amd64.tgz` or `helm-diff-windows-amd64.tgz`) from [releases](https://github.com/databus23/helm-diff/releases). Make sure to select the correct `.tgz` file for your operating system and architecture. -The release archives include everything needed to install the plugin (binary, `plugin.yaml`, and the install scripts). The simplest way to install offline is to extract the archive and point `helm plugin install` at the extracted directory: +The release archives include everything needed to install the plugin (binary, `plugin.yaml`, and the install scripts). Each archive wraps its content in a directory named after the archive itself (e.g. `helm-diff-linux-amd64/`), which is what Helm 4 expects when installing from a tarball. + +The simplest way to install offline is to extract the archive and point `helm plugin install` at the extracted directory: ``` -tar xzf helm-diff-linux-amd64.tgz # extracts into a ./diff directory -helm plugin install ./diff +tar xzf helm-diff-linux-amd64.tgz # extracts into a ./helm-diff-linux-amd64 directory +helm plugin install ./helm-diff-linux-amd64 ``` The install script detects that the binary is already bundled and skips the GitHub download. @@ -63,6 +65,16 @@ gpg --list-keys --with-fingerprint EA17A2A206AFF8CD helm plugin install https://github.com/databus23/helm-diff/releases/latest/download/helm-diff-linux-amd64.tgz ``` +For offline/airgapped environments with Helm 4, transfer the release tarball **together with its `.prov` file** (and keep the original tarball file name — provenance verification matches the file name against the checksums recorded in the `.prov` file) and install directly from the tarball: + +```shell +curl -LO https://github.com/databus23/helm-diff/releases/download//helm-diff-linux-amd64.tgz +curl -LO https://github.com/databus23/helm-diff/releases/download//helm-diff-linux-amd64.tgz.prov +helm plugin install helm-diff-linux-amd64.tgz --keyring +``` + +(Replace `` with the release you are installing. The direct tarball install requires Helm 4. On Helm 3, extract the archive and install from the extracted directory as described above.) + For offline/airgapped environments, download the public key from the maintainer's GitHub profile on a connected machine, transfer it, and import it locally: ```shell @@ -82,11 +94,12 @@ For more information about Helm 4's plugin verification, see: ### Pre Helm 2.3.0 Installation Pick a release tarball from the [releases](https://github.com/databus23/helm-diff/releases) page. -Unpack the tarball in your helm plugins directory (`$(helm home)/plugins`). +Unpack the tarball in your helm plugins directory (`$(helm home)/plugins`) into a `diff` directory. E.g. ``` -curl -L $TARBALL_URL | tar -C $(helm home)/plugins -xzv +mkdir -p $(helm home)/plugins/diff +curl -L $TARBALL_URL | tar -C $(helm home)/plugins/diff --strip-components=1 -xzv ``` ### From Source diff --git a/install-binary.ps1 b/install-binary.ps1 index 56e33874..6b5c563a 100644 --- a/install-binary.ps1 +++ b/install-binary.ps1 @@ -64,14 +64,28 @@ function Install-Plugin { tar -xzf $ArchiveName -C . Pop-Location New-Item -ItemType Directory -Path $Destination -Force - Copy-Item -Path (Join-Path $ArchiveDirectory "diff" "bin" "diff.exe") -Destination $Destination -Force + # Release archives wrap their content in a directory named after the + # archive itself (e.g. helm-diff-windows-amd64/bin/diff.exe), as required + # by helm 4 when installing directly from a tarball (issue #1071). + # Archives from earlier releases wrap the content in a + # directory named "diff" instead. + $wrapDir = [System.IO.Path]::GetFileNameWithoutExtension($ArchiveName) + $binary = Join-Path $ArchiveDirectory $wrapDir "bin" "diff.exe" + if (-not (Test-Path $binary -PathType Leaf)) { + $binary = Join-Path $ArchiveDirectory "diff" "bin" "diff.exe" + } + Copy-Item -Path $binary -Destination $Destination -Force } $ErrorActionPreference = "Stop" -$archiveName = "helm-diff.tgz" $arch = Get-Architecture +# Archives wrap their content in a directory named after the archive +# itself (see Install-Plugin below), so the temporary copy must keep the +# original file name. +$archiveName = "helm-diff-windows-${arch}.tgz" + # If installing (not updating) and the binary is already staged in the # plugin dir (e.g. installing from a release archive that bundles the # correct platform binary), skip the redundant download. Update mode @@ -84,7 +98,6 @@ if (-not $Update -and (Test-Path $pluginBin -PathType Leaf)) { $tmpDir = New-TemporaryDirectory trap { Remove-Item -path $tmpDir -Recurse -Force } -$output = Join-Path $tmpDir $archiveName # Check for offline installation via environment variable if ($env:HELM_DIFF_BIN_TGZ) { @@ -92,10 +105,13 @@ if ($env:HELM_DIFF_BIN_TGZ) { if (-not (Test-Path $env:HELM_DIFF_BIN_TGZ -PathType Leaf)) { throw "Offline installation failed: File not found at '$($env:HELM_DIFF_BIN_TGZ)'" } + $archiveName = [System.IO.Path]::GetFileName($env:HELM_DIFF_BIN_TGZ) + $output = Join-Path $tmpDir $archiveName Copy-Item -Path $env:HELM_DIFF_BIN_TGZ -Destination $output } else { # Proceed with online installation + $output = Join-Path $tmpDir $archiveName $version = Get-Version -Update $Update $url = Get-Url -Version $version -Architecture $arch Download-Plugin -Url $url -Output $output diff --git a/install-binary.sh b/install-binary.sh index 7af7612e..7f0df84c 100755 --- a/install-binary.sh +++ b/install-binary.sh @@ -109,19 +109,21 @@ rmTempDir() { # downloadFile downloads the latest binary package and also the checksum # for that binary. downloadFile() { - PLUGIN_TMP_FILE="${HELM_TMP}/${PROJECT_NAME}.tgz" - - # If HELM_DIFF_BIN_TGZ is set, copy the local file instead of downloading + # If HELM_DIFF_BIN_TGZ is set, copy the local file instead of downloading. + # Keep its original file name: release archives wrap their content in a + # directory named after the archive (see installFile below). if [ -n "$HELM_DIFF_BIN_TGZ" ]; then echo "Using local package at $HELM_DIFF_BIN_TGZ" if [ ! -f "$HELM_DIFF_BIN_TGZ" ]; then echo "Error: file not found at $HELM_DIFF_BIN_TGZ" exit 1 fi + PLUGIN_TMP_FILE="${HELM_TMP}/$(basename "$HELM_DIFF_BIN_TGZ")" cp "$HELM_DIFF_BIN_TGZ" "$PLUGIN_TMP_FILE" return fi + PLUGIN_TMP_FILE="${HELM_TMP}/helm-diff-${OS}-${ARCH}.tgz" echo "Downloading $DOWNLOAD_URL" # Retry with backoff to absorb transient failures, e.g. a release window # where the "latest" asset is already published but not fully uploaded yet. @@ -155,11 +157,20 @@ downloadFile() { # Unpack the archive file, then install it into the helm directory. installFile() { - PLUGIN_TMP_FILE="${HELM_TMP}/${PROJECT_NAME}.tgz" tar xzf "$PLUGIN_TMP_FILE" -C "$HELM_TMP" - HELM_TMP_BIN="$HELM_TMP/diff/bin/diff" + bin="diff" if [ "${OS}" = "windows" ]; then - HELM_TMP_BIN="$HELM_TMP_BIN.exe" + bin="$bin.exe" + fi + # Release archives wrap their content in a directory named after the + # archive itself (e.g. helm-diff-linux-amd64/bin/diff), as required by + # helm 4 when installing directly from a tarball (issue #1071). + # Archives from earlier releases wrap the content in a directory named + # "diff" instead. + wrap_dir="$(basename "$PLUGIN_TMP_FILE" .tgz)" + HELM_TMP_BIN="$HELM_TMP/$wrap_dir/bin/$bin" + if [ ! -f "$HELM_TMP_BIN" ] && [ -f "$HELM_TMP/diff/bin/$bin" ]; then + HELM_TMP_BIN="$HELM_TMP/diff/bin/$bin" fi echo "Preparing to install into ${HELM_PLUGIN_DIR}" mkdir -p "$HELM_PLUGIN_DIR/bin"