internal/archive/archive.go (110 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package archive
import (
"archive/zip"
"encoding/json"
"io"
"log"
"os"
"strings"
"time"
"k8s.io/apimachinery/pkg/util/errors"
)
const archivePathSeparator = '/'
// Path joins elem to form a (ZIP) archive path.
func Path(elem ...string) string {
// ZIP files use / as separator on all platforms
return strings.Join(elem, string(archivePathSeparator))
}
// RootDir returns the top level directory in a ZIP archive path.
func RootDir(name string) string {
if len(name) == 0 {
return name
}
i := 1
for i < len(name) && name[i] != archivePathSeparator {
i++
}
// cover case where name is not a directory at all
if i == len(name) && name[0] != archivePathSeparator {
return string(archivePathSeparator)
}
return name[0:i]
}
// ZipFile wraps a zip.Writer to add a few convenience functions and implement resource closing.
type ZipFile struct {
*zip.Writer
underlying io.Closer
manifest DiagnosticManifest
errs []error
log *log.Logger
}
// NewZipFile creates a new zip file named fileName.
func NewZipFile(fileName string, version string, log *log.Logger) (*ZipFile, error) {
f, err := os.Create(fileName)
if err != nil {
return nil, err
}
w := zip.NewWriter(f)
return &ZipFile{
Writer: w,
underlying: f,
manifest: NewDiagnosticManifest(version),
log: log,
}, nil
}
// Create adds a file to the zip file using the provided name similarly to https://pkg.go.dev/archive/zip#Writer.Create,
// with the addition of the file modification time set to now.
func (z *ZipFile) Create(filename string) (io.Writer, error) {
header := &zip.FileHeader{
Name: filename,
Method: zip.Deflate,
Modified: time.Now(),
}
return z.CreateHeader(header)
}
// Close closes the zip.Writer and the underlying file.
func (z *ZipFile) Close() error {
errs := []error{
z.writeManifest(),
z.writeErrorsToFile(),
z.Writer.Close(),
z.underlying.Close(),
}
return errors.NewAggregate(errs)
}
// Add takes a map of file names and functions to evaluate with the intent to add the result of the evaluation to the
// zip file at the name used as key in the map.
func (z *ZipFile) Add(fns map[string]func(io.Writer) error) {
for k, f := range fns {
fw, err := z.Create(k)
if err != nil {
z.errs = append(z.errs, err)
return
}
z.errs = append(z.errs, f(fw))
}
}
// AddError records an error to be persistent in the ZipFile.
func (z *ZipFile) AddError(err error) {
if err == nil {
return
}
// log errors immediately to give user early feedback
log.Print(err.Error())
z.errs = append(z.errs, err)
}
func (z *ZipFile) AddManifestEntry(manifest StackDiagnosticManifest) {
z.manifest.IncludedDiagnostics = append(z.manifest.IncludedDiagnostics, manifest)
}
func (z *ZipFile) writeManifest() error {
bytes, err := json.Marshal(z.manifest)
if err != nil {
return err
}
writer, err := z.Create("manifest.json")
if err != nil {
return err
}
_, err = writer.Write(bytes)
return err
}
// writeErrorsToFile writes the accumulated errors to a file inside the ZipFile.
func (z *ZipFile) writeErrorsToFile() error {
aggregate := errors.NewAggregate(z.errs)
if aggregate == nil {
return nil
}
out, err := z.Create("eck-diagnostic-errors.txt")
if err != nil {
return err
}
errorString := aggregate.Error()
// errors have been logged already just include in zip archive to inform support
_, err = out.Write([]byte(errorString))
return err
}