mirror of
https://github.com/hashicorp/packer.git
synced 2026-05-21 17:38:02 -04:00
feat: enforce required auto_generate field and refactor Packer release download logic
This commit is contained in:
parent
3ec8bb8811
commit
fc038d32e3
3 changed files with 311 additions and 179 deletions
|
|
@ -10,19 +10,15 @@ import (
|
|||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
|
||||
|
|
@ -31,7 +27,6 @@ import (
|
|||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
||||
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||
packerversion "github.com/hashicorp/packer/version"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
|
@ -60,7 +55,7 @@ type Config struct {
|
|||
// the remote host. When enabled, the provisioner uploads the running Packer
|
||||
// binary (which embeds the Syft SDK) to the remote VM and executes it there
|
||||
// to generate an SBOM. Mutually exclusive with `source`.
|
||||
AutoGenerate bool `mapstructure:"auto_generate" required:"false"`
|
||||
AutoGenerate bool `mapstructure:"auto_generate" required:"true"`
|
||||
|
||||
// Arguments to pass to `packer sbom-generate`. Default:
|
||||
// `["-o", "cyclonedx-json"]`.
|
||||
|
|
@ -471,175 +466,6 @@ func (p *Provisioner) getUserDestination() (string, error) {
|
|||
return dst, nil
|
||||
}
|
||||
|
||||
func downloadText(ctx context.Context, client *http.Client, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build request for %s: %w", url, err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download %s: %w", url, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download failed: HTTP %d for %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed reading response body for %s: %w", url, err)
|
||||
}
|
||||
if len(strings.TrimSpace(string(body))) == 0 {
|
||||
return "", fmt.Errorf("empty response body for %s", url)
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func isValidSHA256Hex(s string) bool {
|
||||
if len(s) != 64 {
|
||||
return false
|
||||
}
|
||||
_, err := hex.DecodeString(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func expectedZipSHA256FromSums(sumsContent, fileName string) (string, error) {
|
||||
for _, line := range strings.Split(sumsContent, "\n") {
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
candidateFileName := strings.TrimPrefix(fields[len(fields)-1], "*")
|
||||
if candidateFileName == fileName {
|
||||
hash := strings.ToLower(fields[0])
|
||||
if !isValidSHA256Hex(hash) {
|
||||
return "", fmt.Errorf("invalid SHA256 checksum format for %s in SHA256SUMS", fileName)
|
||||
}
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("checksum for %s not found in SHA256SUMS", fileName)
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open %s for hashing: %w", path, err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", fmt.Errorf("failed hashing %s: %w", path, err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// releaseBaseURL is the base URL for downloading Packer release artifacts.
|
||||
// Override this to point at a local release server (e.g. for air-gapped testing):
|
||||
//
|
||||
// PACKER_RELEASE_SERVER=http://127.0.0.1:3231
|
||||
const defaultReleaseBaseURL = "https://releases.hashicorp.com"
|
||||
|
||||
func getReleaseBaseURL() string {
|
||||
if v := os.Getenv("PACKER_RELEASE_SERVER"); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
}
|
||||
return defaultReleaseBaseURL
|
||||
}
|
||||
|
||||
// downloadPackerRelease downloads the Packer release zip for the given
|
||||
// GOOS/GOARCH and verifies its checksum. It returns the local temp zip path.
|
||||
// Set PACKER_RELEASE_SERVER=http://127.0.0.1:3231 to use the local release
|
||||
// server instead of releases.hashicorp.com.
|
||||
func downloadPackerRelease(ctx context.Context, goos, goarch, version string) (string, error) {
|
||||
base := getReleaseBaseURL()
|
||||
fileName := fmt.Sprintf("packer_%s_%s_%s.zip", version, goos, goarch)
|
||||
url := fmt.Sprintf("%s/packer/%s/%s", base, version, fileName)
|
||||
shaSumsURL := fmt.Sprintf("%s/packer/%s/packer_%s_SHA256SUMS", base, version, version)
|
||||
|
||||
log.Printf("[INFO] Downloading Packer %s for %s/%s from %s...", version, goos, goarch, url)
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Minute}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build download request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download Packer release: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download failed: HTTP %d for %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
zipFile, err := os.CreateTemp("", "packer-release-*.zip")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp zip file: %w", err)
|
||||
}
|
||||
zipPath := zipFile.Name()
|
||||
|
||||
if _, err := io.Copy(zipFile, resp.Body); err != nil {
|
||||
_ = zipFile.Close()
|
||||
return "", fmt.Errorf("failed to write zip file: %w", err)
|
||||
}
|
||||
_ = zipFile.Close()
|
||||
|
||||
sumsContent, err := downloadText(ctx, client, shaSumsURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve release checksums: %w", err)
|
||||
}
|
||||
|
||||
expectedSHA, err := expectedZipSHA256FromSums(sumsContent, fileName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve expected checksum: %w", err)
|
||||
}
|
||||
|
||||
actualSHA, err := fileSHA256(zipPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !strings.EqualFold(expectedSHA, actualSHA) {
|
||||
return "", fmt.Errorf("release checksum verification failed for %s: expected %s, got %s", fileName, expectedSHA, actualSHA)
|
||||
}
|
||||
|
||||
binaryName := "packer"
|
||||
if goos == "windows" {
|
||||
binaryName = "packer.exe"
|
||||
}
|
||||
|
||||
// Validate expected binary exists in the archive before uploading it remotely.
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", fmt.Errorf("failed to open downloaded zip: %w", err)
|
||||
}
|
||||
defer func() { _ = zr.Close() }()
|
||||
|
||||
foundBinary := false
|
||||
for _, f := range zr.File {
|
||||
if f.Name == binaryName {
|
||||
foundBinary = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBinary {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", fmt.Errorf("packer binary not found in release zip %s", url)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Downloaded Packer release zip to: %s", zipPath)
|
||||
return zipPath, nil
|
||||
}
|
||||
|
||||
// uploadScanner uploads the scanner binary to the remote host.
|
||||
// For Windows: uploads zip file and extracts remotely (optimization for slow WinRM uploads).
|
||||
// For Unix: uploads binary directly.
|
||||
|
|
@ -799,9 +625,8 @@ func (p *Provisioner) provisionWithNativeGeneration(
|
|||
if mapped, ok := archMap[targetGOARCH]; ok {
|
||||
targetGOARCH = mapped
|
||||
}
|
||||
version := packerversion.Version
|
||||
ui.Say(fmt.Sprintf("Downloading Packer %s for %s/%s from %s...", version, targetGOOS, targetGOARCH, getReleaseBaseURL()))
|
||||
scannerZipPath, err := downloadPackerRelease(ctx, targetGOOS, targetGOARCH, version)
|
||||
ui.Say(fmt.Sprintf("Downloading latest Packer release for %s/%s from %s...", targetGOOS, targetGOARCH, getReleaseBaseURL()))
|
||||
scannerZipPath, err := downloadPackerRelease(ctx, targetGOOS, targetGOARCH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download Packer release for %s/%s: %w", targetGOOS, targetGOARCH, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ type FlatConfig struct {
|
|||
Source *string `mapstructure:"source" required:"true" cty:"source" hcl:"source"`
|
||||
Destination *string `mapstructure:"destination" required:"false" cty:"destination" hcl:"destination"`
|
||||
SbomName *string `mapstructure:"sbom_name" required:"false" cty:"sbom_name" hcl:"sbom_name"`
|
||||
AutoGenerate *bool `mapstructure:"auto_generate" required:"false" cty:"auto_generate" hcl:"auto_generate"`
|
||||
AutoGenerate *bool `mapstructure:"auto_generate" required:"true" cty:"auto_generate" hcl:"auto_generate"`
|
||||
ScannerArgs []string `mapstructure:"scanner_args" required:"false" cty:"scanner_args" hcl:"scanner_args"`
|
||||
ScannerURL *string `mapstructure:"scanner_url" required:"false" cty:"scanner_url" hcl:"scanner_url"`
|
||||
ScannerChecksum *string `mapstructure:"scanner_checksum" required:"false" cty:"scanner_checksum" hcl:"scanner_checksum"`
|
||||
|
|
|
|||
307
provisioner/hcp-sbom/release_download.go
Normal file
307
provisioner/hcp-sbom/release_download.go
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcp_sbom
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
semver "github.com/Masterminds/semver/v3"
|
||||
"github.com/hashicorp/packer-plugin-sdk/retry"
|
||||
)
|
||||
|
||||
// releaseBaseURL is the base URL for downloading Packer release artifacts.
|
||||
// Override this to point at a local release server (e.g. for air-gapped testing):
|
||||
//
|
||||
// PACKER_RELEASE_SERVER=http://127.0.0.1:3231
|
||||
const defaultReleaseBaseURL = "https://releases.hashicorp.com"
|
||||
|
||||
func getReleaseBaseURL() string {
|
||||
if v := os.Getenv("PACKER_RELEASE_SERVER"); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
}
|
||||
return defaultReleaseBaseURL
|
||||
}
|
||||
|
||||
// releaseIndex is the top-level structure of https://releases.hashicorp.com/packer/index.json.
|
||||
type releaseIndex struct {
|
||||
Versions map[string]releaseVersion `json:"versions"`
|
||||
}
|
||||
|
||||
// releaseVersion represents one version entry in the release index.
|
||||
type releaseVersion struct {
|
||||
Version string `json:"version"`
|
||||
Shasums string `json:"shasums"`
|
||||
Builds []releaseBuild `json:"builds"`
|
||||
}
|
||||
|
||||
// releaseBuild represents one platform build inside a release version.
|
||||
type releaseBuild struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Filename string `json:"filename"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// fetchLatestPackerVersion queries the HashiCorp releases index, sorts all
|
||||
// stable (non-prerelease) versions with semver, and returns the highest one.
|
||||
func fetchLatestPackerVersion(ctx context.Context, client *http.Client) (string, error) {
|
||||
indexURL := defaultReleaseBaseURL + "/packer/index.json"
|
||||
var indexData releaseIndex
|
||||
|
||||
err := retry.Config{
|
||||
Tries: 3,
|
||||
RetryDelay: func() time.Duration { return 5 * time.Second },
|
||||
}.Run(ctx, func(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, indexURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build index request: %w", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch release index: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP %d for %s", resp.StatusCode, indexURL)
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(&indexData)
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve packer release index from %s: %w", indexURL, err)
|
||||
}
|
||||
|
||||
var semverList []*semver.Version
|
||||
for vStr := range indexData.Versions {
|
||||
v, parseErr := semver.NewVersion(vStr)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
if v.Prerelease() != "" {
|
||||
continue // skip alpha/beta/rc
|
||||
}
|
||||
semverList = append(semverList, v)
|
||||
}
|
||||
|
||||
if len(semverList) == 0 {
|
||||
return "", fmt.Errorf("no stable Packer releases found in index at %s", indexURL)
|
||||
}
|
||||
|
||||
sort.Sort(semver.Collection(semverList))
|
||||
latest := semverList[len(semverList)-1]
|
||||
log.Printf("[INFO] Latest stable Packer version from releases index: %s", latest.Original())
|
||||
return latest.Original(), nil
|
||||
}
|
||||
|
||||
// downloadURLToTempFile downloads url into a new temp file and returns its path.
|
||||
// On any error the temp file is removed. The caller owns the returned file on success.
|
||||
func downloadURLToTempFile(ctx context.Context, client *http.Client, url, suffix string) (string, error) {
|
||||
f, err := os.CreateTemp("", "packer-dl-*"+suffix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
tmpPath := f.Name()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("HTTP request failed: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
_, copyErr := io.Copy(f, resp.Body)
|
||||
closeErr := f.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("failed to write download: %w", copyErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("failed to close temp file: %w", closeErr)
|
||||
}
|
||||
|
||||
return tmpPath, nil
|
||||
}
|
||||
|
||||
// downloadChecksumFile fetches the SHA256SUMS text file at url.
|
||||
func downloadChecksumFile(ctx context.Context, client *http.Client, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build request for %s: %w", url, err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download %s: %w", url, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download failed: HTTP %d for %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed reading response body for %s: %w", url, err)
|
||||
}
|
||||
if len(strings.TrimSpace(string(body))) == 0 {
|
||||
return "", fmt.Errorf("empty response body for %s", url)
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func isValidSHA256Hex(s string) bool {
|
||||
if len(s) != 64 {
|
||||
return false
|
||||
}
|
||||
_, err := hex.DecodeString(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func expectedZipSHA256FromSums(sumsContent, fileName string) (string, error) {
|
||||
for _, line := range strings.Split(sumsContent, "\n") {
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
candidateFileName := strings.TrimPrefix(fields[len(fields)-1], "*")
|
||||
if candidateFileName == fileName {
|
||||
hash := strings.ToLower(fields[0])
|
||||
if !isValidSHA256Hex(hash) {
|
||||
return "", fmt.Errorf("invalid SHA256 checksum format for %s in SHA256SUMS", fileName)
|
||||
}
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("checksum for %s not found in SHA256SUMS", fileName)
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open %s for hashing: %w", path, err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", fmt.Errorf("failed hashing %s: %w", path, err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// downloadPackerRelease fetches the latest stable Packer version from the
|
||||
// HashiCorp releases index (releases.hashicorp.com/packer/index.json), then
|
||||
// downloads and checksum-verifies the zip for the given GOOS/GOARCH.
|
||||
// All HTTP operations are retried up to three times.
|
||||
// Set PACKER_RELEASE_SERVER=http://127.0.0.1:3231 to use a local release
|
||||
// server instead of releases.hashicorp.com.
|
||||
func downloadPackerRelease(ctx context.Context, goos, goarch string) (string, error) {
|
||||
base := getReleaseBaseURL()
|
||||
client := &http.Client{Timeout: 5 * time.Minute}
|
||||
|
||||
// Resolve the latest stable version from the releases index.
|
||||
version, err := fetchLatestPackerVersion(ctx, client)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to determine latest Packer version: %w", err)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("packer_%s_%s_%s.zip", version, goos, goarch)
|
||||
zipURL := fmt.Sprintf("%s/packer/%s/%s", base, version, fileName)
|
||||
shaSumsURL := fmt.Sprintf("%s/packer/%s/packer_%s_SHA256SUMS", base, version, version)
|
||||
|
||||
log.Printf("[INFO] Downloading Packer %s for %s/%s...", version, goos, goarch)
|
||||
|
||||
// Download the release zip.
|
||||
zipPath, err := downloadURLToTempFile(ctx, client, zipURL, ".zip")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download Packer release zip: %w", err)
|
||||
}
|
||||
|
||||
// Download the SHA256SUMS file with retry.
|
||||
var sumsContent string
|
||||
err = retry.Config{
|
||||
Tries: 3,
|
||||
RetryDelay: func() time.Duration { return 5 * time.Second },
|
||||
}.Run(ctx, func(ctx context.Context) error {
|
||||
var e error
|
||||
sumsContent, e = downloadChecksumFile(ctx, client, shaSumsURL)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", fmt.Errorf("failed to download release checksums: %w", err)
|
||||
}
|
||||
|
||||
// Verify checksum.
|
||||
expectedSHA, err := expectedZipSHA256FromSums(sumsContent, fileName)
|
||||
if err != nil {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", fmt.Errorf("failed to resolve expected checksum: %w", err)
|
||||
}
|
||||
actualSHA, err := fileSHA256(zipPath)
|
||||
if err != nil {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", err
|
||||
}
|
||||
if !strings.EqualFold(expectedSHA, actualSHA) {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", fmt.Errorf("checksum mismatch for %s: expected %s, got %s", fileName, expectedSHA, actualSHA)
|
||||
}
|
||||
|
||||
// Validate the expected binary exists inside the archive.
|
||||
binaryName := "packer"
|
||||
if goos == "windows" {
|
||||
binaryName = "packer.exe"
|
||||
}
|
||||
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", fmt.Errorf("failed to open downloaded zip: %w", err)
|
||||
}
|
||||
defer func() { _ = zr.Close() }()
|
||||
|
||||
foundBinary := false
|
||||
for _, f := range zr.File {
|
||||
if f.Name == binaryName {
|
||||
foundBinary = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBinary {
|
||||
_ = os.Remove(zipPath)
|
||||
return "", fmt.Errorf("packer binary %q not found in release zip %s", binaryName, zipURL)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Downloaded and verified Packer release zip: %s", zipPath)
|
||||
return zipPath, nil
|
||||
}
|
||||
Loading…
Reference in a new issue