diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index 692483540..eff564722 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -16,7 +16,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" - "github.com/klauspost/compress/zstd" "io" "os" "path/filepath" @@ -64,7 +63,8 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } func (p *Provisioner) Provision( - ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, + ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, + generatedData map[string]interface{}, ) error { ui.Say( fmt.Sprintf("Starting to provision with hcp-sbom using source: %s", @@ -77,13 +77,15 @@ func (p *Provisioner) Provision( } p.config.ctx.Data = generatedData - // Download the file - destPath, downloadErr := p.downloadSBOM(ui, comm) - // defer os.Remove(destPath) + // Download the file for Packer + destPath, downloadErr := p.downloadSBOMForPacker(ui, comm, generatedData) if downloadErr != nil { return fmt.Errorf("failed to download file: %w", downloadErr) } + // Download the file for user + p.downloadSBOMForUser(ui, comm) + // Validate the file ui.Say(fmt.Sprintf("Validating SBOM file %s", destPath)) validationErr := p.validateSBOM(ui, destPath) @@ -91,29 +93,21 @@ func (p *Provisioner) Provision( return fmt.Errorf("failed to validate SBOM file: %w", validationErr) } - // Compress the file - ui.Say(fmt.Sprintf("Compressing SBOM file %s", destPath)) - _, compessionErr := p.compressFile(ui, destPath) - if compessionErr != nil { - return fmt.Errorf("failed to compress file: %w", compessionErr) - } - - // Future: send compressedData to the internal API as per RFC - // ... - return nil } -// downloadSBOM downloads a Software Bill of Materials (SBOM) from a specified -// source to a local destination. It works with all communicators from packersdk. -// The method returns the path to the downloaded file or an error if any issues -// occur during the download process. -func (p *Provisioner) downloadSBOM(ui packersdk.Ui, comm packersdk.Communicator) (string, error) { +// downloadSBOMForPacker downloads SBOM from a specified source to a local +// destination set by internal SBOM provisioner. It works with all communicators +// from packersdk. +func (p *Provisioner) downloadSBOMForPacker( + ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, +) (string, error) { src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { return p.config.Destination, fmt.Errorf("error interpolating source: %s", err) } + // FIXME:: Do we really need this? // Check if the source is a JSON file if filepath.Ext(src) != ".json" { return p.config.Destination, fmt.Errorf( @@ -121,40 +115,13 @@ func (p *Provisioner) downloadSBOM(ui packersdk.Ui, comm packersdk.Communicator) ) } - // Determine the destination path - dst := p.config.Destination - if dst == "" { - tmpFile, err := os.CreateTemp("", "packer-sbom-*.json") - if err != nil { - return dst, fmt.Errorf( - "failed to create file for Packer SBOM: %s", err, - ) - } - dst = tmpFile.Name() - tmpFile.Close() - } else { - dst, err = interpolate.Render(dst, &p.config.ctx) - if err != nil { - return dst, fmt.Errorf("error interpolating Packer SBOM destination: %s", err) - } - - if strings.HasSuffix(dst, "/") { - info, err := os.Stat(dst) - if err != nil { - return dst, fmt.Errorf("failed to stat destination for Packer SBOM: %s", err) - } - - if info.IsDir() { - tmpFile, err := os.CreateTemp(dst, "packer-sbom-*.json") - if err != nil { - return dst, fmt.Errorf("failed to create file for Packer SBOM: %s", err) - } - dst = tmpFile.Name() - tmpFile.Close() - } - } + // Download the file for Packer + desti, ok := generatedData["dst"] // this has been set by HCPSBOMInternalProvisioner.Provision + if !ok { + return "", fmt.Errorf("failed to find location for Packer SBOM file") } + dst := fmt.Sprintf("%v", desti) // Ensure the destination directory exists dir := filepath.Dir(dst) if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { @@ -172,37 +139,81 @@ func (p *Provisioner) downloadSBOM(ui packersdk.Ui, comm packersdk.Communicator) pf := io.MultiWriter(f) // Download the file - ui.Say(fmt.Sprintf("Downloading SBOM file %s => %s", src, dst)) + ui.Say(fmt.Sprintf("Downloading SBOM file %s for Packer => %s", src, dst)) if err = comm.Download(src, pf); err != nil { - ui.Error(fmt.Sprintf("download failed for SBOM file: %s", err)) + ui.Error(fmt.Sprintf("download failed for Packer SBOM file: %s", err)) return dst, err } return dst, nil } -func (p *Provisioner) compressFile(ui packersdk.Ui, filePath string) ([]byte, error) { - sourceFile, err := os.Open(filePath) +// downloadSBOMForUser downloads a SBOM from a specified source to a local +// destination given by user. It works with all communicators from packersdk. +func (p *Provisioner) downloadSBOMForUser( + ui packersdk.Ui, comm packersdk.Communicator, +) { + src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { - return nil, err - } - defer sourceFile.Close() - - data, err := io.ReadAll(sourceFile) - if err != nil { - return nil, err + ui.Say(fmt.Sprintf("error interpolating source: %s", err)) + return } - encoder, err := zstd.NewWriter(nil) - if err != nil { - return nil, err + // Determine the destination path + dst := p.config.Destination + if dst == "" { + ui.Say("skipped downloading SBOM file for user because 'Destination' is not provided") + return } - defer encoder.Close() - compressedData := encoder.EncodeAll(data, nil) + dst, err = interpolate.Render(dst, &p.config.ctx) + if err != nil { + ui.Say(fmt.Sprintf("error interpolating SBOM file destination: %s", err)) + return + } - ui.Say(fmt.Sprintf("SBOM file compressed successfully. Size: %d bytes", len(compressedData))) - return compressedData, nil + if strings.HasSuffix(dst, "/") { + info, err := os.Stat(dst) + if err != nil { + ui.Say(fmt.Sprintf("failed to stat destination for SBOM: %s", err)) + return + } + + if info.IsDir() { + tmpFile, err := os.CreateTemp(dst, "packer-user-sbom-*.json") + if err != nil { + ui.Say(fmt.Sprintf("failed to create file for Packer SBOM: %s", err)) + return + } + dst = tmpFile.Name() + tmpFile.Close() + } + } + + // Ensure the destination directory exists + dir := filepath.Dir(dst) + if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { + ui.Say(fmt.Sprintf("failed to create destination directory for Packer SBOM: %s", err)) + return + } + + // Open the destination file + f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + ui.Say(fmt.Sprintf("failed to open destination file: %s", err)) + return + } + defer f.Close() + + // Create MultiWriter for the current progress + pf := io.MultiWriter(f) + + // Download the file + ui.Say(fmt.Sprintf("Downloading SBOM file for user %s => %s", src, dst)) + if err = comm.Download(src, pf); err != nil { + ui.Error(fmt.Sprintf("download failed for user SBOM file: %s", err)) + return + } } type SBOM struct { diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go index b9adb5821..c09747a91 100644 --- a/provisioner/hcp_sbom/provisioner_test.go +++ b/provisioner/hcp_sbom/provisioner_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "github.com/hashicorp/packer-plugin-sdk/packer" - "github.com/klauspost/compress/zstd" "io" "os" "testing" @@ -94,8 +93,8 @@ func TestDownloadSBOM(t *testing.T) { provisioner := &Provisioner{ config: tt.config, } - - destPath, err := provisioner.downloadSBOM(ui, comm) + generatedData := map[string]interface{}{} + destPath, err := provisioner.downloadSBOMForPacker(ui, comm, generatedData) if tt.expectError { if err == nil { t.Fatalf("expected error, got none") @@ -173,47 +172,3 @@ func TestValidateSBOM(t *testing.T) { }) } } - -func TestCompressFile(t *testing.T) { - ui := &MockUi{} - provisioner := &Provisioner{} - validSBOM := SBOM{ - BomFormat: "CycloneDX", - SpecVersion: "1.0", - } - data, _ := json.Marshal(validSBOM) - filePath := "data.json" - //os.WriteFile(filePath, data, 0644) - //defer os.Remove(filePath) - - sourceFile, err := os.Open(filePath) - if err != nil { - t.Fatalf("expected no error:%v", err) - } - defer sourceFile.Close() - - data, err = io.ReadAll(sourceFile) - if err != nil { - t.Fatalf("expected no error:%v", err) - } - - compressedData, err := provisioner.compressFile(ui, filePath) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - decoder, err := zstd.NewReader(nil) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - defer decoder.Close() - - decompressedData, err := decoder.DecodeAll(compressedData, nil) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if string(decompressedData) != string(data) { - t.Fatalf("expected decompressed data to be '%s', got %s", data, decompressedData) - } -}