Skip to content

Commit 351544b

Browse files
authored
fixed audit logging with saves and loads (#727)
1 parent 7d7adca commit 351544b

8 files changed

Lines changed: 252 additions & 33 deletions

File tree

cmd/hauler/cli/store.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,8 @@ func addStoreLoad(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
166166
if err != nil {
167167
return err
168168
}
169-
_ = s
170169

171-
return store.LoadCmd(ctx, o, rso, ro)
170+
return store.LoadCmd(ctx, o, s, rso, ro)
172171
},
173172
}
174173
o.AddFlags(cmd)
@@ -252,9 +251,8 @@ func addStoreSave(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
252251
if err != nil {
253252
return err
254253
}
255-
_ = s
256254

257-
return store.SaveCmd(ctx, o, rso, ro)
255+
return store.SaveCmd(ctx, o, s, rso, ro)
258256
},
259257
}
260258
o.AddFlags(cmd)

cmd/hauler/cli/store/lifecycle_test.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestLifecycle_FileArtifact_AddSaveLoadCopy(t *testing.T) {
4444
// Step 3: SaveCmd -> archive (absolute paths required).
4545
archivePath := filepath.Join(t.TempDir(), "lifecycle-file.tar.zst")
4646
saveOpts := newSaveOpts(storeA.Root, archivePath)
47-
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
47+
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
4848
t.Fatalf("SaveCmd: %v", err)
4949
}
5050

@@ -58,11 +58,15 @@ func TestLifecycle_FileArtifact_AddSaveLoadCopy(t *testing.T) {
5858

5959
// Step 4: LoadCmd -> store B.
6060
storeBDir := t.TempDir()
61+
storeBPreLoad, err := store.NewLayout(storeBDir)
62+
if err != nil {
63+
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
64+
}
6165
loadOpts := &flags.LoadOpts{
6266
StoreRootOpts: defaultRootOpts(storeBDir),
6367
FileName: []string{archivePath},
6468
}
65-
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
69+
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
6670
t.Fatalf("LoadCmd: %v", err)
6771
}
6872

@@ -122,17 +126,21 @@ func TestLifecycle_Image_AddSaveLoadCopyRegistry(t *testing.T) {
122126
// Step 3: SaveCmd -> archive.
123127
archivePath := filepath.Join(t.TempDir(), "lifecycle-image.tar.zst")
124128
saveOpts := newSaveOpts(storeA.Root, archivePath)
125-
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
129+
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
126130
t.Fatalf("SaveCmd: %v", err)
127131
}
128132

129133
// Step 4: LoadCmd -> store B.
130134
storeBDir := t.TempDir()
135+
storeBPreLoad, err := store.NewLayout(storeBDir)
136+
if err != nil {
137+
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
138+
}
131139
loadOpts := &flags.LoadOpts{
132140
StoreRootOpts: defaultRootOpts(storeBDir),
133141
FileName: []string{archivePath},
134142
}
135-
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
143+
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
136144
t.Fatalf("LoadCmd: %v", err)
137145
}
138146

@@ -190,17 +198,21 @@ func TestLifecycle_Chart_AddSaveLoadExtract(t *testing.T) {
190198
// Step 2: SaveCmd -> archive.
191199
archivePath := filepath.Join(t.TempDir(), "lifecycle-chart.tar.zst")
192200
saveOpts := newSaveOpts(storeA.Root, archivePath)
193-
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
201+
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
194202
t.Fatalf("SaveCmd: %v", err)
195203
}
196204

197205
// Step 3: LoadCmd -> new store.
198206
storeBDir := t.TempDir()
207+
storeBPreLoad, err := store.NewLayout(storeBDir)
208+
if err != nil {
209+
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
210+
}
199211
loadOpts := &flags.LoadOpts{
200212
StoreRootOpts: defaultRootOpts(storeBDir),
201213
FileName: []string{archivePath},
202214
}
203-
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
215+
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
204216
t.Fatalf("LoadCmd: %v", err)
205217
}
206218

@@ -274,17 +286,21 @@ func TestLifecycle_DigestOnlyImage_AddSaveLoad(t *testing.T) {
274286
// Step 3: SaveCmd -> archive
275287
archivePath := filepath.Join(t.TempDir(), "lifecycle-digestonly.tar.zst")
276288
saveOpts := newSaveOpts(storeA.Root, archivePath)
277-
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
289+
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
278290
t.Fatalf("SaveCmd: %v", err)
279291
}
280292

281293
// Step 4: LoadCmd -> fresh store B
282294
storeBDir := t.TempDir()
295+
storeBPreLoad, err := store.NewLayout(storeBDir)
296+
if err != nil {
297+
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
298+
}
283299
loadOpts := &flags.LoadOpts{
284300
StoreRootOpts: defaultRootOpts(storeBDir),
285301
FileName: []string{archivePath},
286302
}
287-
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
303+
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
288304
t.Fatalf("LoadCmd: %v", err)
289305
}
290306

@@ -339,17 +355,21 @@ func TestLifecycle_Remove_ThenSave(t *testing.T) {
339355
// Step 3: SaveCmd -> archive.
340356
archivePath := filepath.Join(t.TempDir(), "lifecycle-remove.tar.zst")
341357
saveOpts := newSaveOpts(storeA.Root, archivePath)
342-
if err := SaveCmd(ctx, saveOpts, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
358+
if err := SaveCmd(ctx, saveOpts, storeA, defaultRootOpts(storeA.Root), defaultCliOpts()); err != nil {
343359
t.Fatalf("SaveCmd: %v", err)
344360
}
345361

346362
// Step 4: LoadCmd -> new store.
347363
storeBDir := t.TempDir()
364+
storeBPreLoad, err := store.NewLayout(storeBDir)
365+
if err != nil {
366+
t.Fatalf("store.NewLayout(storeB pre-load): %v", err)
367+
}
348368
loadOpts := &flags.LoadOpts{
349369
StoreRootOpts: defaultRootOpts(storeBDir),
350370
FileName: []string{archivePath},
351371
}
352-
if err := LoadCmd(ctx, loadOpts, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
372+
if err := LoadCmd(ctx, loadOpts, storeBPreLoad, defaultRootOpts(storeBDir), defaultCliOpts()); err != nil {
353373
t.Fatalf("LoadCmd: %v", err)
354374
}
355375

cmd/hauler/cli/store/load.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"hauler.dev/go/hauler/v2/internal/flags"
1616
"hauler.dev/go/hauler/v2/pkg/archives"
17+
"hauler.dev/go/hauler/v2/pkg/audit"
1718
"hauler.dev/go/hauler/v2/pkg/consts"
1819
"hauler.dev/go/hauler/v2/pkg/content"
1920
"hauler.dev/go/hauler/v2/pkg/getter"
@@ -28,7 +29,7 @@ import (
2829
var legacyChunkRe = regexp.MustCompile(`_\d+\.`)
2930

3031
// extracts the contents of an archived oci layout to an existing oci layout
31-
func LoadCmd(ctx context.Context, o *flags.LoadOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
32+
func LoadCmd(ctx context.Context, o *flags.LoadOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
3233
l := log.FromContext(ctx)
3334

3435
tempOverride := rso.TempOverride
@@ -66,6 +67,25 @@ func LoadCmd(ctx context.Context, o *flags.LoadOpts, rso *flags.StoreRootOpts, r
6667
if err != nil {
6768
return err
6869
}
70+
71+
if auditLevel(ro) != "none" {
72+
e := audit.Entry{
73+
StoreID: s.StoreID,
74+
Store: s.Root,
75+
Command: "store load",
76+
Reference: audit.SanitizeURL(resolved),
77+
}
78+
if auditLevel(ro) == "verbose" {
79+
sys := audit.BuildSystem()
80+
g := audit.BuildGlobal(ro, rso)
81+
e.System = &sys
82+
e.Global = &g
83+
}
84+
if err := audit.Append(ro.HaulerDir, e); err != nil {
85+
l.Warnf("failed to write audit entry: %v", err)
86+
}
87+
}
88+
6989
clearDir(tempDir)
7090
}
7191

@@ -242,8 +262,17 @@ func unarchiveLayoutTo(ctx context.Context, haulPath string, dest string, tempDi
242262
return err
243263
}
244264

245-
_, err = s.CopyAll(ctx, ts, nil)
246-
return err
265+
if _, err := s.CopyAll(ctx, ts, nil); err != nil {
266+
return err
267+
}
268+
269+
// CopyAll only copies OCI content; the haul's audit.log sits alongside
270+
// it, not in it, so it has to be merged in separately.
271+
if err := audit.MergeStoreLog(tempDir, dest); err != nil {
272+
l.Warnf("failed to merge audit log from haul: %v", err)
273+
}
274+
275+
return nil
247276
}
248277

249278
// matches the <path>.NNN chunk suffix that is used to filter resolveHaulPath

cmd/hauler/cli/store/load_test.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,47 @@ func TestUnarchiveLayoutTo(t *testing.T) {
103103
}
104104
}
105105

106+
// TestUnarchiveLayoutTo_MergesAuditLog checks a haul's audit.log survives
107+
// the load and merges into (not overwrites) whatever dest already has.
108+
func TestUnarchiveLayoutTo_MergesAuditLog(t *testing.T) {
109+
srcDir := t.TempDir()
110+
srcLayout, err := store.NewLayout(srcDir)
111+
if err != nil {
112+
t.Fatalf("store.NewLayout(srcDir): %v", err)
113+
}
114+
// NewLayout doesn't write index.json until something forces a save --
115+
// unarchiveLayoutTo requires one to exist in the archive.
116+
if err := srcLayout.OCI.SaveIndex(); err != nil {
117+
t.Fatalf("SaveIndex(srcDir): %v", err)
118+
}
119+
if err := os.WriteFile(filepath.Join(srcDir, "audit.log"), []byte(`{"command":"from-haul"}`+"\n"), 0o644); err != nil {
120+
t.Fatalf("seed source audit.log: %v", err)
121+
}
122+
123+
archivePath := filepath.Join(t.TempDir(), "haul.tar.zst")
124+
if err := createRootLevelArchive(srcDir, archivePath); err != nil {
125+
t.Fatalf("createRootLevelArchive: %v", err)
126+
}
127+
128+
destDir := t.TempDir()
129+
if err := os.WriteFile(filepath.Join(destDir, "audit.log"), []byte(`{"command":"already-here"}`+"\n"), 0o644); err != nil {
130+
t.Fatalf("seed dest audit.log: %v", err)
131+
}
132+
133+
ctx := newTestContext(t)
134+
if err := unarchiveLayoutTo(ctx, archivePath, destDir, t.TempDir(), defaultCliOpts(), false); err != nil {
135+
t.Fatalf("unarchiveLayoutTo: %v", err)
136+
}
137+
138+
data, err := os.ReadFile(filepath.Join(destDir, "audit.log"))
139+
if err != nil {
140+
t.Fatalf("ReadFile dest audit.log: %v", err)
141+
}
142+
if !strings.Contains(string(data), "already-here") || !strings.Contains(string(data), "from-haul") {
143+
t.Fatalf("expected both entries preserved after load, got: %s", data)
144+
}
145+
}
146+
106147
// --------------------------------------------------------------------------
107148
// TestLoadCmd_LocalFile
108149
// --------------------------------------------------------------------------
@@ -114,16 +155,22 @@ func TestLoadCmd_LocalFile(t *testing.T) {
114155

115156
t.Run("single archive", func(t *testing.T) {
116157
destDir := t.TempDir()
158+
s, err := store.NewLayout(destDir)
159+
if err != nil {
160+
t.Fatalf("store.NewLayout: %v", err)
161+
}
117162
o := &flags.LoadOpts{
118163
StoreRootOpts: defaultRootOpts(destDir),
119164
FileName: []string{testHaulArchive},
120165
}
121-
if err := LoadCmd(ctx, o, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
166+
if err := LoadCmd(ctx, o, s, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
122167
t.Fatalf("LoadCmd: %v", err)
123168
}
124-
s, err := store.NewLayout(destDir)
169+
// re-open: LoadCmd writes through its own content.OCI instance, so s's
170+
// in-memory index predates the load.
171+
s, err = store.NewLayout(destDir)
125172
if err != nil {
126-
t.Fatalf("store.NewLayout: %v", err)
173+
t.Fatalf("store.NewLayout (post-load): %v", err)
127174
}
128175
if countArtifactsInStore(t, s) == 0 {
129176
t.Error("expected artifacts in store after LoadCmd")
@@ -135,11 +182,15 @@ func TestLoadCmd_LocalFile(t *testing.T) {
135182
// silently discarded by the OCI pusher. The descriptor count after two
136183
// loads must equal the count after a single load.
137184
singleDir := t.TempDir()
185+
singlePreLoad, err := store.NewLayout(singleDir)
186+
if err != nil {
187+
t.Fatalf("store.NewLayout single (pre-load): %v", err)
188+
}
138189
singleOpts := &flags.LoadOpts{
139190
StoreRootOpts: defaultRootOpts(singleDir),
140191
FileName: []string{testHaulArchive},
141192
}
142-
if err := LoadCmd(ctx, singleOpts, defaultRootOpts(singleDir), defaultCliOpts()); err != nil {
193+
if err := LoadCmd(ctx, singleOpts, singlePreLoad, defaultRootOpts(singleDir), defaultCliOpts()); err != nil {
143194
t.Fatalf("LoadCmd single: %v", err)
144195
}
145196
singleStore, err := store.NewLayout(singleDir)
@@ -149,11 +200,15 @@ func TestLoadCmd_LocalFile(t *testing.T) {
149200
singleCount := countArtifactsInStore(t, singleStore)
150201

151202
doubleDir := t.TempDir()
203+
doublePreLoad, err := store.NewLayout(doubleDir)
204+
if err != nil {
205+
t.Fatalf("store.NewLayout double (pre-load): %v", err)
206+
}
152207
doubleOpts := &flags.LoadOpts{
153208
StoreRootOpts: defaultRootOpts(doubleDir),
154209
FileName: []string{testHaulArchive, testHaulArchive},
155210
}
156-
if err := LoadCmd(ctx, doubleOpts, defaultRootOpts(doubleDir), defaultCliOpts()); err != nil {
211+
if err := LoadCmd(ctx, doubleOpts, doublePreLoad, defaultRootOpts(doubleDir), defaultCliOpts()); err != nil {
157212
t.Fatalf("LoadCmd double: %v", err)
158213
}
159214
doubleStore, err := store.NewLayout(doubleDir)
@@ -192,12 +247,16 @@ func TestLoadCmd_RemoteArchive(t *testing.T) {
192247
destDir := t.TempDir()
193248
remoteURL := srv.URL + "/haul.tar.zst"
194249

250+
preLoad, err := store.NewLayout(destDir)
251+
if err != nil {
252+
t.Fatalf("store.NewLayout (pre-load): %v", err)
253+
}
195254
o := &flags.LoadOpts{
196255
StoreRootOpts: defaultRootOpts(destDir),
197256
FileName: []string{remoteURL},
198257
}
199258

200-
if err := LoadCmd(ctx, o, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
259+
if err := LoadCmd(ctx, o, preLoad, defaultRootOpts(destDir), defaultCliOpts()); err != nil {
201260
t.Fatalf("LoadCmd remote: %v", err)
202261
}
203262

cmd/hauler/cli/store/save.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import (
2222

2323
"hauler.dev/go/hauler/v2/internal/flags"
2424
"hauler.dev/go/hauler/v2/pkg/archives"
25+
"hauler.dev/go/hauler/v2/pkg/audit"
2526
"hauler.dev/go/hauler/v2/pkg/consts"
2627
"hauler.dev/go/hauler/v2/pkg/log"
28+
"hauler.dev/go/hauler/v2/pkg/store"
2729
)
2830

2931
// saves a content store to store archives
30-
func SaveCmd(ctx context.Context, o *flags.SaveOpts, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
32+
func SaveCmd(ctx context.Context, o *flags.SaveOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
3133
l := log.FromContext(ctx)
3234

3335
// maps to handle compression and archival types
@@ -94,6 +96,29 @@ func SaveCmd(ctx context.Context, o *flags.SaveOpts, rso *flags.StoreRootOpts, r
9496
l.Infof("saving store [%s] to archive [%s]", o.StoreDir, o.FileName)
9597
}
9698

99+
if auditLevel(ro) != "none" {
100+
e := audit.Entry{
101+
StoreID: s.StoreID,
102+
Store: s.Root,
103+
Command: "store save",
104+
Reference: o.FileName,
105+
}
106+
if auditLevel(ro) == "verbose" {
107+
sys := audit.BuildSystem()
108+
g := audit.BuildGlobal(ro, rso)
109+
e.System = &sys
110+
e.Global = &g
111+
e.Flags = map[string]any{
112+
"platform": o.Platform,
113+
"containerd": o.ContainerdCompatibility,
114+
"chunk-size": o.ChunkSize,
115+
}
116+
}
117+
if err := audit.Append(ro.HaulerDir, e); err != nil {
118+
l.Warnf("failed to write audit entry: %v", err)
119+
}
120+
}
121+
97122
return nil
98123
}
99124

0 commit comments

Comments
 (0)