feat: add local release server script and update download logic for Packer binaries

remove_syft_bin
Hari Om 1 week ago
parent 9633ab7fc8
commit 00d8e27bef

@ -108,7 +108,6 @@ func (cmd *SBOMGenerateCommand) ParseArgs(args []string) (*sbom.Config, int) {
}
if strings.HasPrefix(arg, "-") {
cmd.Ui.Say(fmt.Sprintf("Warning: unsupported sbom-generate argument ignored: %s", arg))
continue
}
@ -154,8 +153,8 @@ Usage: packer sbom-generate [options] <path>
This command is typically invoked internally by the hcp-sbom provisioner.
Options:
-o <format> Output format: cyclonedx-json, spdx-json (default: cyclonedx-json)
--exclude <glob> Optional: exclude path glob from scanning (repeatable)
--scope <scope> Optional: scan scope: squashed, all-layers (default: squashed)
--exclude <glob> Optional: exclude path glob from scanning (repeatable)
--scope <scope> Optional: scan scope: squashed, all-layers (default: squashed)
Arguments:
<path> Path to scan (default: /)
Examples:

@ -539,13 +539,28 @@ func fileSHA256(path string) (string, error) {
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 from releases.hashicorp.com, verifies its checksum, and
// extracts the packer binary to a local temp file.
// GOOS/GOARCH, verifies its checksum, and extracts the packer binary to a
// local temp file. 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("https://releases.hashicorp.com/packer/%s/%s", version, fileName)
shaSumsURL := fmt.Sprintf("https://releases.hashicorp.com/packer/%s/packer_%s_SHA256SUMS", version, version)
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)
@ -708,7 +723,7 @@ func (p *Provisioner) provisionWithNativeGeneration(
targetGOARCH = mapped
}
version := packerversion.Version
ui.Say(fmt.Sprintf("Downloading Packer %s for %s/%s from releases.hashicorp.com...", version, targetGOOS, targetGOARCH))
ui.Say(fmt.Sprintf("Downloading Packer %s for %s/%s from %s...", version, targetGOOS, targetGOARCH, getReleaseBaseURL()))
scannerLocalPath, err := downloadPackerRelease(ctx, targetGOOS, targetGOARCH, version)
if err != nil {
return fmt.Errorf("failed to download Packer release for %s/%s: %w", targetGOOS, targetGOARCH, err)
@ -779,7 +794,7 @@ func (p *Provisioner) runScanner(ctx context.Context, ui packersdk.Ui,
// Use Windows-specific default if on Windows and user hasn't customized
executeCommand := p.config.ExecuteCommand
if isWindows && executeCommand == "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}" {
// User didn't customize, use Windows default (no sudo, uses sbom-generate subcommand)
// User didn't customize, use Windows default (no sudo)
executeCommand = "{{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}"
}

@ -109,6 +109,70 @@ func TestConfigPrepare(t *testing.T) {
false,
"",
},
{
"auto_generate with scanner_args including -q",
map[string]interface{}{
"auto_generate": true,
"scanner_args": []string{"-o", "cyclonedx-json", "-q"},
},
interpolate.Context{},
&Config{
AutoGenerate: true,
ScanPath: "/",
ScannerArgs: []string{"-o", "cyclonedx-json", "-q"},
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
},
false,
"",
},
{
"auto_generate with scanner_args excluding -q",
map[string]interface{}{
"auto_generate": true,
"scanner_args": []string{"-o", "cyclonedx-json"},
},
interpolate.Context{},
&Config{
AutoGenerate: true,
ScanPath: "/",
ScannerArgs: []string{"-o", "cyclonedx-json"},
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
},
false,
"",
},
{
"auto_generate with scanner_args using long scope and exclude flags",
map[string]interface{}{
"auto_generate": true,
"scanner_args": []string{"-o", "cyclonedx-json", "--scope", "all-layers", "--exclude", "/tmp/**"},
},
interpolate.Context{},
&Config{
AutoGenerate: true,
ScanPath: "/",
ScannerArgs: []string{"-o", "cyclonedx-json", "--scope", "all-layers", "--exclude", "/tmp/**"},
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
},
false,
"",
},
{
"auto_generate with scanner_args using equals scope and exclude flags",
map[string]interface{}{
"auto_generate": true,
"scanner_args": []string{"-o", "cyclonedx-json", "--scope=all-layers", "--exclude=/var/cache/**"},
},
interpolate.Context{},
&Config{
AutoGenerate: true,
ScanPath: "/",
ScannerArgs: []string{"-o", "cyclonedx-json", "--scope=all-layers", "--exclude=/var/cache/**"},
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
},
false,
"",
},
{
"auto_generate with deprecated scanner_url (should warn but not fail)",
map[string]interface{}{

@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
local-release-server.py
=======================
A local HTTP server that mimics the HashiCorp releases.hashicorp.com structure
for Packer binaries. It reads binaries from the bin/ directory, zips them
on demand, computes SHA256 checksums, and serves them so the hcp-sbom
provisioner can download them without hitting the real release server.
URL layout served:
GET /packer/<version>/packer_<version>_<os>_<arch>.zip -> zip of the binary
GET /packer/<version>/packer_<version>_SHA256SUMS -> checksum file
Binary resolution order (bin/ directory, relative to repo root):
1. bin/packer-<os>-<arch> (e.g. bin/packer-linux-amd64)
2. bin/packer-<os>-<arch>.exe (Windows)
3. bin/packer (fallback whatever single binary is present)
Usage:
python3 scripts/local-release-server.py [--port 3231] [--bin-dir bin/]
"""
import argparse
import hashlib
import http.server
import io
import os
import re
import sys
import zipfile
from pathlib import Path
from typing import Optional
REPO_ROOT = Path(__file__).resolve().parent.parent
def find_binary(bin_dir, goos, goarch):
# type: (Path, str, str) -> Optional[Path]
"""Return the path to the best-matching packer binary for the given OS/arch.
pkg/ layout: pkg/<goos>_<goarch>/packer (or packer.exe on Windows)
"""
subdir = bin_dir / "{}_{}" .format(goos, goarch)
candidates = [
subdir / "packer.exe",
subdir / "packer",
]
for path in candidates:
if path.is_file():
return path
return None
def make_zip(binary_path, goos):
# type: (Path, str) -> bytes
"""Zip the binary and return the raw zip bytes."""
binary_name = "packer.exe" if goos == "windows" else "packer"
buf = io.BytesIO()
with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.write(str(binary_path), arcname=binary_name)
return buf.getvalue()
def sha256_bytes(data):
# type: (bytes) -> str
return hashlib.sha256(data).hexdigest()
class ReleaseHandler(http.server.BaseHTTPRequestHandler):
bin_dir = REPO_ROOT / "pkg"
ZIP_RE = re.compile(r"^/packer/([^/]+)/packer_([^/]+)_([^/]+)_([^/]+)\.zip$")
SUMS_RE = re.compile(r"^/packer/([^/]+)/packer_([^/]+)_SHA256SUMS$")
def log_message(self, fmt, *args):
print("[server] {} - {}".format(self.address_string(), fmt % args), file=sys.stderr)
def send_bytes(self, data, content_type="application/octet-stream"):
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
path = self.path.split("?")[0]
m = self.ZIP_RE.match(path)
if m:
version, _, goos, goarch = m.group(1), m.group(2), m.group(3), m.group(4)
binary = find_binary(self.bin_dir, goos, goarch)
if binary is None:
self._not_found("no binary found for {}/{} in {}".format(goos, goarch, self.bin_dir))
return
zip_data = make_zip(binary, goos)
print("[server] serving zip for {}/{} v{} from {} ({} KB)".format(
goos, goarch, version, binary, len(zip_data) // 1024), file=sys.stderr)
self.send_bytes(zip_data, "application/zip")
return
m = self.SUMS_RE.match(path)
if m:
version = m.group(1)
lines = []
# pkg/<goos>_<goarch>/packer[.exe]
for subdir in sorted(self.bin_dir.iterdir()):
if not subdir.is_dir():
continue
parts = subdir.name.split("_", 1)
if len(parts) != 2:
continue
goos, goarch = parts
binary = find_binary(self.bin_dir, goos, goarch)
if binary is None:
continue
zip_data = make_zip(binary, goos)
chk = sha256_bytes(zip_data)
fname = "packer_{}_{}_{}.zip".format(version, goos, goarch)
lines.append("{} {}".format(chk, fname))
body = "\n".join(lines) + "\n"
self.send_bytes(body.encode(), "text/plain")
return
self._not_found("unrecognised path: {}".format(path))
def _not_found(self, reason):
print("[server] 404 {}".format(reason), file=sys.stderr)
self.send_response(404)
self.end_headers()
self.wfile.write("404 not found: {}\n".format(reason).encode())
def main():
parser = argparse.ArgumentParser(description="Local Packer release server")
parser.add_argument("--port", type=int, default=3231)
parser.add_argument("--bin-dir", default=str(REPO_ROOT / "pkg"))
args = parser.parse_args()
ReleaseHandler.bin_dir = Path(args.bin_dir).resolve()
if not ReleaseHandler.bin_dir.is_dir():
print("error: bin-dir {} does not exist".format(ReleaseHandler.bin_dir), file=sys.stderr)
sys.exit(1)
server = http.server.HTTPServer(("127.0.0.1", args.port), ReleaseHandler)
print("[server] listening on http://127.0.0.1:{}".format(args.port), file=sys.stderr)
print("[server] serving binaries from {}".format(ReleaseHandler.bin_dir), file=sys.stderr)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[server] stopped", file=sys.stderr)
if __name__ == "__main__":
main()
Loading…
Cancel
Save