mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-27 14:07:17 +00:00
fixed audit logging with saves and loads (#727)
This commit is contained in:
@@ -166,9 +166,8 @@ func addStoreLoad(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s
|
||||
|
||||
return store.LoadCmd(ctx, o, rso, ro)
|
||||
return store.LoadCmd(ctx, o, s, rso, ro)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
@@ -252,9 +251,8 @@ func addStoreSave(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s
|
||||
|
||||
return store.SaveCmd(ctx, o, rso, ro)
|
||||
return store.SaveCmd(ctx, o, s, rso, ro)
|
||||
},
|
||||
}
|
||||
o.AddFlags(cmd)
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestLifecycle_FileArtifact_AddSaveLoadCopy(t *testing.T) {
|
||||
// Step 3: SaveCmd -> archive (absolute paths required).
|
||||
archivePath := filepath.Join(t.TempDir(), "lifecycle-file.tar.zst")
|
||||
saveOpts := newSaveOpts(storeA.Root, archivePath)
|
||||
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd: %v", err)
|
||||
}
|
||||
|
||||
@@ -58,11 +58,15 @@ func TestLifecycle_FileArtifact_AddSaveLoadCopy(t *testing.T) {
|
||||
|
||||
// Step 4: LoadCmd -> store B.
|
||||
storeBDir := t.TempDir()
|
||||
storeBPreLoad, err := store.NewLayout(storeBDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
|
||||
}
|
||||
loadOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(storeBDir),
|
||||
FileName: []string{archivePath},
|
||||
}
|
||||
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd: %v", err)
|
||||
}
|
||||
|
||||
@@ -122,17 +126,21 @@ func TestLifecycle_Image_AddSaveLoadCopyRegistry(t *testing.T) {
|
||||
// Step 3: SaveCmd -> archive.
|
||||
archivePath := filepath.Join(t.TempDir(), "lifecycle-image.tar.zst")
|
||||
saveOpts := newSaveOpts(storeA.Root, archivePath)
|
||||
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: LoadCmd -> store B.
|
||||
storeBDir := t.TempDir()
|
||||
storeBPreLoad, err := store.NewLayout(storeBDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
|
||||
}
|
||||
loadOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(storeBDir),
|
||||
FileName: []string{archivePath},
|
||||
}
|
||||
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd: %v", err)
|
||||
}
|
||||
|
||||
@@ -190,17 +198,21 @@ func TestLifecycle_Chart_AddSaveLoadExtract(t *testing.T) {
|
||||
// Step 2: SaveCmd -> archive.
|
||||
archivePath := filepath.Join(t.TempDir(), "lifecycle-chart.tar.zst")
|
||||
saveOpts := newSaveOpts(storeA.Root, archivePath)
|
||||
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: LoadCmd -> new store.
|
||||
storeBDir := t.TempDir()
|
||||
storeBPreLoad, err := store.NewLayout(storeBDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
|
||||
}
|
||||
loadOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(storeBDir),
|
||||
FileName: []string{archivePath},
|
||||
}
|
||||
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd: %v", err)
|
||||
}
|
||||
|
||||
@@ -274,17 +286,21 @@ func TestLifecycle_DigestOnlyImage_AddSaveLoad(t *testing.T) {
|
||||
// Step 3: SaveCmd -> archive
|
||||
archivePath := filepath.Join(t.TempDir(), "lifecycle-digestonly.tar.zst")
|
||||
saveOpts := newSaveOpts(storeA.Root, archivePath)
|
||||
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: LoadCmd -> fresh store B
|
||||
storeBDir := t.TempDir()
|
||||
storeBPreLoad, err := store.NewLayout(storeBDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
|
||||
}
|
||||
loadOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(storeBDir),
|
||||
FileName: []string{archivePath},
|
||||
}
|
||||
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd: %v", err)
|
||||
}
|
||||
|
||||
@@ -339,17 +355,21 @@ func TestLifecycle_Remove_ThenSave(t *testing.T) {
|
||||
// Step 3: SaveCmd -> archive.
|
||||
archivePath := filepath.Join(t.TempDir(), "lifecycle-remove.tar.zst")
|
||||
saveOpts := newSaveOpts(storeA.Root, archivePath)
|
||||
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd: %v", err)
|
||||
}
|
||||
|
||||
// Step 4: LoadCmd -> new store.
|
||||
storeBDir := t.TempDir()
|
||||
storeBPreLoad, err := store.NewLayout(storeBDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
|
||||
}
|
||||
loadOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(storeBDir),
|
||||
FileName: []string{archivePath},
|
||||
}
|
||||
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"hauler.dev/go/hauler/v2/internal/flags"
|
||||
"hauler.dev/go/hauler/v2/pkg/archives"
|
||||
"hauler.dev/go/hauler/v2/pkg/audit"
|
||||
"hauler.dev/go/hauler/v2/pkg/consts"
|
||||
"hauler.dev/go/hauler/v2/pkg/content"
|
||||
"hauler.dev/go/hauler/v2/pkg/getter"
|
||||
@@ -28,7 +29,7 @@ import (
|
||||
var legacyChunkRe = regexp.MustCompile(`_\d+\.`)
|
||||
|
||||
// extracts the contents of an archived oci layout to an existing oci layout
|
||||
func LoadCmd(ctx context.Context, o *flags.LoadOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
func LoadCmd(ctx context.Context, o *flags.LoadOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
tempOverride := rso.TempOverride
|
||||
@@ -66,6 +67,25 @@ func LoadCmd(ctx context.Context, o *flags.LoadOpts, rso *flags.StoreRootOpts, r
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if auditLevel(ro) != "none" {
|
||||
e := audit.Entry{
|
||||
StoreID: s.StoreID,
|
||||
Store: s.Root,
|
||||
Command: "store load",
|
||||
Reference: audit.SanitizeURL(resolved),
|
||||
}
|
||||
if auditLevel(ro) == "verbose" {
|
||||
sys := audit.BuildSystem()
|
||||
g := audit.BuildGlobal(ro, rso)
|
||||
e.System = &sys
|
||||
e.Global = &g
|
||||
}
|
||||
if err := audit.Append(ro.HaulerDir, e); err != nil {
|
||||
l.Warnf("failed to write audit entry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
clearDir(tempDir)
|
||||
}
|
||||
|
||||
@@ -242,8 +262,17 @@ func unarchiveLayoutTo(ctx context.Context, haulPath string, dest string, tempDi
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.CopyAll(ctx, ts, nil)
|
||||
return err
|
||||
if _, err := s.CopyAll(ctx, ts, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// CopyAll only copies OCI content; the haul's audit.log sits alongside
|
||||
// it, not in it, so it has to be merged in separately.
|
||||
if err := audit.MergeStoreLog(tempDir, dest); err != nil {
|
||||
l.Warnf("failed to merge audit log from haul: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// matches the <path>.NNN chunk suffix that is used to filter resolveHaulPath
|
||||
|
||||
@@ -103,6 +103,47 @@ func TestUnarchiveLayoutTo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnarchiveLayoutTo_MergesAuditLog checks a haul's audit.log survives
|
||||
// the load and merges into (not overwrites) whatever dest already has.
|
||||
func TestUnarchiveLayoutTo_MergesAuditLog(t *testing.T) {
|
||||
srcDir := t.TempDir()
|
||||
srcLayout, err := store.NewLayout(srcDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout(srcDir): %v", err)
|
||||
}
|
||||
// NewLayout doesn't write index.json until something forces a save --
|
||||
// unarchiveLayoutTo requires one to exist in the archive.
|
||||
if err := srcLayout.OCI.SaveIndex(); err != nil {
|
||||
t.Fatalf("SaveIndex(srcDir): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(srcDir, "audit.log"), []byte(`{"command":"from-haul"}`+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("seed source audit.log: %v", err)
|
||||
}
|
||||
|
||||
archivePath := filepath.Join(t.TempDir(), "haul.tar.zst")
|
||||
if err := createRootLevelArchive(srcDir, archivePath); err != nil {
|
||||
t.Fatalf("createRootLevelArchive: %v", err)
|
||||
}
|
||||
|
||||
destDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(destDir, "audit.log"), []byte(`{"command":"already-here"}`+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("seed dest audit.log: %v", err)
|
||||
}
|
||||
|
||||
ctx := newTestContext(t)
|
||||
if err := unarchiveLayoutTo(ctx, archivePath, destDir, t.TempDir(), defaultCliOpts(), false); err != nil {
|
||||
t.Fatalf("unarchiveLayoutTo: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(destDir, "audit.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile dest audit.log: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "already-here") || !strings.Contains(string(data), "from-haul") {
|
||||
t.Fatalf("expected both entries preserved after load, got: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// TestLoadCmd_LocalFile
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -114,16 +155,22 @@ func TestLoadCmd_LocalFile(t *testing.T) {
|
||||
|
||||
t.Run("single archive", func(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
s, err := store.NewLayout(destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout: %v", err)
|
||||
}
|
||||
o := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(destDir),
|
||||
FileName: []string{testHaulArchive},
|
||||
}
|
||||
if err := LoadCmd(ctx, o, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, o, s, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd: %v", err)
|
||||
}
|
||||
s, err := store.NewLayout(destDir)
|
||||
// re-open: LoadCmd writes through its own content.OCI instance, so s's
|
||||
// in-memory index predates the load.
|
||||
s, err = store.NewLayout(destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout: %v", err)
|
||||
t.Fatalf("store.NewLayout (post-load): %v", err)
|
||||
}
|
||||
if countArtifactsInStore(t, s) == 0 {
|
||||
t.Error("expected artifacts in store after LoadCmd")
|
||||
@@ -135,11 +182,15 @@ func TestLoadCmd_LocalFile(t *testing.T) {
|
||||
// silently discarded by the OCI pusher. The descriptor count after two
|
||||
// loads must equal the count after a single load.
|
||||
singleDir := t.TempDir()
|
||||
singlePreLoad, err := store.NewLayout(singleDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout single (pre-load): %v", err)
|
||||
}
|
||||
singleOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(singleDir),
|
||||
FileName: []string{testHaulArchive},
|
||||
}
|
||||
if err := LoadCmd(ctx, singleOpts, defaultRootOpts(singleDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, singleOpts, singlePreLoad, defaultRootOpts(singleDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd single: %v", err)
|
||||
}
|
||||
singleStore, err := store.NewLayout(singleDir)
|
||||
@@ -149,11 +200,15 @@ func TestLoadCmd_LocalFile(t *testing.T) {
|
||||
singleCount := countArtifactsInStore(t, singleStore)
|
||||
|
||||
doubleDir := t.TempDir()
|
||||
doublePreLoad, err := store.NewLayout(doubleDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout double (pre-load): %v", err)
|
||||
}
|
||||
doubleOpts := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(doubleDir),
|
||||
FileName: []string{testHaulArchive, testHaulArchive},
|
||||
}
|
||||
if err := LoadCmd(ctx, doubleOpts, defaultRootOpts(doubleDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, doubleOpts, doublePreLoad, defaultRootOpts(doubleDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd double: %v", err)
|
||||
}
|
||||
doubleStore, err := store.NewLayout(doubleDir)
|
||||
@@ -192,12 +247,16 @@ func TestLoadCmd_RemoteArchive(t *testing.T) {
|
||||
destDir := t.TempDir()
|
||||
remoteURL := srv.URL + "/haul.tar.zst"
|
||||
|
||||
preLoad, err := store.NewLayout(destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("store.NewLayout (pre-load): %v", err)
|
||||
}
|
||||
o := &flags.LoadOpts{
|
||||
StoreRootOpts: defaultRootOpts(destDir),
|
||||
FileName: []string{remoteURL},
|
||||
}
|
||||
|
||||
if err := LoadCmd(ctx, o, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
|
||||
if err := LoadCmd(ctx, o, preLoad, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("LoadCmd remote: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,14 @@ import (
|
||||
|
||||
"hauler.dev/go/hauler/v2/internal/flags"
|
||||
"hauler.dev/go/hauler/v2/pkg/archives"
|
||||
"hauler.dev/go/hauler/v2/pkg/audit"
|
||||
"hauler.dev/go/hauler/v2/pkg/consts"
|
||||
"hauler.dev/go/hauler/v2/pkg/log"
|
||||
"hauler.dev/go/hauler/v2/pkg/store"
|
||||
)
|
||||
|
||||
// saves a content store to store archives
|
||||
func SaveCmd(ctx context.Context, o *flags.SaveOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
func SaveCmd(ctx context.Context, o *flags.SaveOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
// maps to handle compression and archival types
|
||||
@@ -94,6 +96,29 @@ func SaveCmd(ctx context.Context, o *flags.SaveOpts, rso *flags.StoreRootOpts, r
|
||||
l.Infof("saving store [%s] to archive [%s]", o.StoreDir, o.FileName)
|
||||
}
|
||||
|
||||
if auditLevel(ro) != "none" {
|
||||
e := audit.Entry{
|
||||
StoreID: s.StoreID,
|
||||
Store: s.Root,
|
||||
Command: "store save",
|
||||
Reference: o.FileName,
|
||||
}
|
||||
if auditLevel(ro) == "verbose" {
|
||||
sys := audit.BuildSystem()
|
||||
g := audit.BuildGlobal(ro, rso)
|
||||
e.System = &sys
|
||||
e.Global = &g
|
||||
e.Flags = map[string]any{
|
||||
"platform": o.Platform,
|
||||
"containerd": o.ContainerdCompatibility,
|
||||
"chunk-size": o.ChunkSize,
|
||||
}
|
||||
}
|
||||
if err := audit.Append(ro.HaulerDir, e); err != nil {
|
||||
l.Warnf("failed to write audit entry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestSaveCmd(t *testing.T) {
|
||||
archivePath := filepath.Join(t.TempDir(), "haul.tar.zst")
|
||||
o := newSaveOpts(s.Root, archivePath)
|
||||
|
||||
if err := SaveCmd(ctx, o, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, o, s, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd: %v", err)
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func TestSaveCmd_ContainerdCompatibility(t *testing.T) {
|
||||
o := newSaveOpts(s.Root, archivePath)
|
||||
o.ContainerdCompatibility = true
|
||||
|
||||
if err := SaveCmd(ctx, o, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, o, s, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd ContainerdCompatibility: %v", err)
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ func TestSaveCmd_EmptyStore(t *testing.T) {
|
||||
archivePath := filepath.Join(t.TempDir(), "haul-empty.tar.zst")
|
||||
o := newSaveOpts(s.Root, archivePath)
|
||||
|
||||
if err := SaveCmd(ctx, o, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, o, s, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd empty store: %v", err)
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ func TestSaveCmd_ChunkSize(t *testing.T) {
|
||||
o := newSaveOpts(s.Root, archivePath)
|
||||
o.ChunkSize = "1K"
|
||||
|
||||
if err := SaveCmd(ctx, o, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
if err := SaveCmd(ctx, o, s, defaultRootOpts(s.Root), defaultCliOpts()); err != nil {
|
||||
t.Fatalf("SaveCmd with chunk-size: %v", err)
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ func TestSaveCmd_ChunkSize_Invalid(t *testing.T) {
|
||||
o := newSaveOpts(s.Root, filepath.Join(t.TempDir(), "haul.tar.zst"))
|
||||
o.ChunkSize = "0"
|
||||
|
||||
if err := SaveCmd(ctx, o, defaultRootOpts(s.Root), defaultCliOpts()); err == nil {
|
||||
if err := SaveCmd(ctx, o, s, defaultRootOpts(s.Root), defaultCliOpts()); err == nil {
|
||||
t.Fatal("SaveCmd: expected error for chunk-size=0, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
+36
-4
@@ -200,9 +200,13 @@ func Append(haulerDir string, e Entry) error {
|
||||
return globalErr
|
||||
}
|
||||
|
||||
// appendMu serializes appendLine calls: os.OpenFile with O_APPEND is only
|
||||
// atomic for a single write() syscall on POSIX, and concurrent `store sync`
|
||||
// image jobs (runImageJobs) can each call this at once without it.
|
||||
// LogFileName is the audit log's filename, under both haulerDir and a store's Root.
|
||||
const LogFileName = "audit.log"
|
||||
|
||||
// appendMu serializes appendLine/MergeStoreLog calls: os.OpenFile with
|
||||
// O_APPEND is only atomic for a single write() syscall on POSIX, and
|
||||
// concurrent `store sync` image jobs (runImageJobs) can each call this at
|
||||
// once without it.
|
||||
var appendMu sync.Mutex
|
||||
|
||||
func appendLine(dir string, v any) error {
|
||||
@@ -216,7 +220,7 @@ func appendLine(dir string, v any) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("audit: marshal: %w", err)
|
||||
}
|
||||
f, err := os.OpenFile(filepath.Join(dir, "audit.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
f, err := os.OpenFile(filepath.Join(dir, LogFileName), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("audit: open log: %w", err)
|
||||
}
|
||||
@@ -225,6 +229,34 @@ func appendLine(dir string, v any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeStoreLog appends tempDir's staged audit.log onto destDir's. No-op if tempDir has none.
|
||||
func MergeStoreLog(tempDir, destDir string) error {
|
||||
data, err := os.ReadFile(filepath.Join(tempDir, LogFileName))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("audit: read staged log: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
appendMu.Lock()
|
||||
defer appendMu.Unlock()
|
||||
|
||||
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
||||
return fmt.Errorf("audit: ensure dir: %w", err)
|
||||
}
|
||||
f, err := os.OpenFile(filepath.Join(destDir, LogFileName), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("audit: open log: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func resolveDir(haulerDir string) string {
|
||||
if haulerDir != "" {
|
||||
return haulerDir
|
||||
|
||||
@@ -186,6 +186,62 @@ func TestAppend_MultipleEntries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeStoreLog_AppendsOntoExisting(t *testing.T) {
|
||||
temp, dest := t.TempDir(), t.TempDir()
|
||||
|
||||
if err := os.WriteFile(filepath.Join(dest, LogFileName), []byte(`{"command":"original"}`+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("seed dest log: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(temp, LogFileName), []byte(`{"command":"from-haul"}`+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("seed temp log: %v", err)
|
||||
}
|
||||
|
||||
if err := MergeStoreLog(temp, dest); err != nil {
|
||||
t.Fatalf("MergeStoreLog: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dest, LogFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "original") || !strings.Contains(string(data), "from-haul") {
|
||||
t.Fatalf("expected both entries preserved, got: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeStoreLog_CreatesDestWhenMissing(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
dest := filepath.Join(t.TempDir(), "not-yet-created")
|
||||
|
||||
if err := os.WriteFile(filepath.Join(temp, LogFileName), []byte(`{"command":"from-haul"}`+"\n"), 0o644); err != nil {
|
||||
t.Fatalf("seed temp log: %v", err)
|
||||
}
|
||||
|
||||
if err := MergeStoreLog(temp, dest); err != nil {
|
||||
t.Fatalf("MergeStoreLog: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dest, LogFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "from-haul") {
|
||||
t.Fatalf("expected dest log created with haul's entry, got: %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeStoreLog_NoopWhenTempHasNoLog(t *testing.T) {
|
||||
temp, dest := t.TempDir(), t.TempDir()
|
||||
|
||||
if err := MergeStoreLog(temp, dest); err != nil {
|
||||
t.Fatalf("MergeStoreLog: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dest, LogFileName)); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected no dest log to be created, stat err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortFileRef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user