Centralize managed state targets
This commit is contained in:
@@ -234,22 +234,10 @@ func (b *Backend) DeleteManagedBundle(ctx context.Context, bundlePath string, ma
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := storage.ValidatePrefix(bundlePath); err != nil {
|
targets, err := storage.ManagedBundleTargets(bundlePath, managedOutputPaths)
|
||||||
return err
|
|
||||||
}
|
|
||||||
targets := make([]string, 0, len(managedOutputPaths)+1)
|
|
||||||
for _, outputPath := range managedOutputPaths {
|
|
||||||
target, err := storage.Join(bundlePath, outputPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
targets = append(targets, target)
|
|
||||||
}
|
|
||||||
statePath, err := storage.StatePath(bundlePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
targets = append(targets, statePath)
|
|
||||||
|
|
||||||
for _, logicalPath := range targets {
|
for _, logicalPath := range targets {
|
||||||
nativePath, err := b.nativePath(logicalPath, false)
|
nativePath, err := b.nativePath(logicalPath, false)
|
||||||
|
|||||||
@@ -145,16 +145,20 @@ func TestBackendManagedDeletion(t *testing.T) {
|
|||||||
backend := newBackend(t)
|
backend := newBackend(t)
|
||||||
mustWrite(t, backend, "bundle/report.html", "html")
|
mustWrite(t, backend, "bundle/report.html", "html")
|
||||||
mustWrite(t, backend, "bundle/keep.txt", "keep")
|
mustWrite(t, backend, "bundle/keep.txt", "keep")
|
||||||
mustWrite(t, backend, "bundle/.distributor.json", "{}")
|
statePath, err := storage.StatePath("bundle")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StatePath() error = %v", err)
|
||||||
|
}
|
||||||
|
mustWrite(t, backend, statePath, "{}")
|
||||||
|
|
||||||
err := backend.DeleteManagedBundle(context.Background(), "bundle", []string{"report.html"}, storage.DeleteOptions{PruneEmptyDirs: true})
|
err = backend.DeleteManagedBundle(context.Background(), "bundle", []string{"report.html"}, storage.DeleteOptions{PruneEmptyDirs: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DeleteManagedBundle() error = %v", err)
|
t.Fatalf("DeleteManagedBundle() error = %v", err)
|
||||||
}
|
}
|
||||||
if _, err := backend.Stat(context.Background(), "bundle/report.html"); !storage.IsNotFound(err) {
|
if _, err := backend.Stat(context.Background(), "bundle/report.html"); !storage.IsNotFound(err) {
|
||||||
t.Fatalf("managed output stat error = %v, want not found", err)
|
t.Fatalf("managed output stat error = %v, want not found", err)
|
||||||
}
|
}
|
||||||
if _, err := backend.Stat(context.Background(), "bundle/.distributor.json"); !storage.IsNotFound(err) {
|
if _, err := backend.Stat(context.Background(), statePath); !storage.IsNotFound(err) {
|
||||||
t.Fatalf("state stat error = %v, want not found", err)
|
t.Fatalf("state stat error = %v, want not found", err)
|
||||||
}
|
}
|
||||||
if _, err := backend.Stat(context.Background(), "bundle/keep.txt"); err != nil {
|
if _, err := backend.Stat(context.Background(), "bundle/keep.txt"); err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
||||||
"gitea.maximumdirect.net/eric/distributor/internal/notify"
|
"gitea.maximumdirect.net/eric/distributor/internal/notify"
|
||||||
"gitea.maximumdirect.net/eric/distributor/internal/state"
|
"gitea.maximumdirect.net/eric/distributor/internal/state"
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRunDryRunPrintsConfigSummary(t *testing.T) {
|
func TestRunDryRunPrintsConfigSummary(t *testing.T) {
|
||||||
@@ -63,7 +64,7 @@ func TestRunPublishesNewLocalBundle(t *testing.T) {
|
|||||||
if _, err := os.Stat(filepath.Join(destinationRoot, "manifest.json")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(destinationRoot, "manifest.json")); !os.IsNotExist(err) {
|
||||||
t.Fatalf("destination manifest stat error = %v, want not exist", err)
|
t.Fatalf("destination manifest stat error = %v, want not exist", err)
|
||||||
}
|
}
|
||||||
destinationState := readStateFile(t, filepath.Join(destinationRoot, ".distributor.json"))
|
destinationState := readStateFile(t, filepath.Join(destinationRoot, storage.StateFileName))
|
||||||
if destinationState.PipelineID != "reports" || destinationState.DestinationID != "archive" {
|
if destinationState.PipelineID != "reports" || destinationState.DestinationID != "archive" {
|
||||||
t.Fatalf("state identity = %s/%s", destinationState.PipelineID, destinationState.DestinationID)
|
t.Fatalf("state identity = %s/%s", destinationState.PipelineID, destinationState.DestinationID)
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@ func TestRunNotifiesAfterPublication(t *testing.T) {
|
|||||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||||
notifier := &recordingNotifier{
|
notifier := &recordingNotifier{
|
||||||
check: func() {
|
check: func() {
|
||||||
if _, err := os.Stat(filepath.Join(destinationRoot, ".distributor.json")); err != nil {
|
if _, err := os.Stat(filepath.Join(destinationRoot, storage.StateFileName)); err != nil {
|
||||||
t.Fatalf("state stat during notify: %v", err)
|
t.Fatalf("state stat during notify: %v", err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -217,7 +218,7 @@ func TestRunPublishesHTMLOnly(t *testing.T) {
|
|||||||
if _, err := os.Stat(filepath.Join(destinationRoot, "report.md")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(destinationRoot, "report.md")); !os.IsNotExist(err) {
|
||||||
t.Fatalf("report.md stat error = %v, want not exist", err)
|
t.Fatalf("report.md stat error = %v, want not exist", err)
|
||||||
}
|
}
|
||||||
destinationState := readStateFile(t, filepath.Join(destinationRoot, ".distributor.json"))
|
destinationState := readStateFile(t, filepath.Join(destinationRoot, storage.StateFileName))
|
||||||
if got, want := len(destinationState.Outputs), 1; got != want {
|
if got, want := len(destinationState.Outputs), 1; got != want {
|
||||||
t.Fatalf("state output count = %d, want %d", got, want)
|
t.Fatalf("state output count = %d, want %d", got, want)
|
||||||
}
|
}
|
||||||
@@ -239,7 +240,7 @@ func TestRunPublishesSourceAndHTML(t *testing.T) {
|
|||||||
assertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nSunny.\n")
|
assertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nSunny.\n")
|
||||||
assertFileContains(t, filepath.Join(destinationRoot, "report.html"), "<p>Sunny.</p>")
|
assertFileContains(t, filepath.Join(destinationRoot, "report.html"), "<p>Sunny.</p>")
|
||||||
assertFile(t, filepath.Join(destinationRoot, "summary.txt"), "Summary\n")
|
assertFile(t, filepath.Join(destinationRoot, "summary.txt"), "Summary\n")
|
||||||
destinationState := readStateFile(t, filepath.Join(destinationRoot, ".distributor.json"))
|
destinationState := readStateFile(t, filepath.Join(destinationRoot, storage.StateFileName))
|
||||||
if got, want := len(destinationState.Outputs), 3; got != want {
|
if got, want := len(destinationState.Outputs), 3; got != want {
|
||||||
t.Fatalf("state output count = %d, want %d", got, want)
|
t.Fatalf("state output count = %d, want %d", got, want)
|
||||||
}
|
}
|
||||||
@@ -567,7 +568,7 @@ func writeDestinationState(t *testing.T, root, relative string, manifest bundle.
|
|||||||
t.Fatalf("marshal state: %v", err)
|
t.Fatalf("marshal state: %v", err)
|
||||||
}
|
}
|
||||||
data = append(data, '\n')
|
data = append(data, '\n')
|
||||||
if err := os.WriteFile(filepath.Join(bundleRoot, ".distributor.json"), data, 0o600); err != nil {
|
if err := os.WriteFile(filepath.Join(bundleRoot, storage.StateFileName), data, 0o600); err != nil {
|
||||||
t.Fatalf("write state: %v", err)
|
t.Fatalf("write state: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseManifestValid(t *testing.T) {
|
func TestParseManifestValid(t *testing.T) {
|
||||||
@@ -71,7 +73,7 @@ func TestParseManifestRejectsUnsafeFilePaths(t *testing.T) {
|
|||||||
`"path": "/report.md"`,
|
`"path": "/report.md"`,
|
||||||
`"path": "nested/../report.md"`,
|
`"path": "nested/../report.md"`,
|
||||||
`"path": "manifest.json"`,
|
`"path": "manifest.json"`,
|
||||||
`"path": ".distributor.json"`,
|
`"path": "` + storage.StateFileName + `"`,
|
||||||
}
|
}
|
||||||
for _, replacement := range tests {
|
for _, replacement := range tests {
|
||||||
t.Run(replacement, func(t *testing.T) {
|
t.Run(replacement, func(t *testing.T) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func ValidateSourcePath(path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch path {
|
switch path {
|
||||||
case ManifestName, ".distributor.json":
|
case ManifestName, storage.StateFileName:
|
||||||
return fmt.Errorf("%q is reserved", path)
|
return fmt.Errorf("%q is reserved", path)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExecuteRootHelp(t *testing.T) {
|
func TestExecuteRootHelp(t *testing.T) {
|
||||||
@@ -128,7 +130,7 @@ pipelines:
|
|||||||
if code != exitOK {
|
if code != exitOK {
|
||||||
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
|
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filepath.Join(destinationRoot, ".distributor.json")); err != nil {
|
if _, err := os.Stat(filepath.Join(destinationRoot, storage.StateFileName)); err != nil {
|
||||||
t.Fatalf("state stat error = %v", err)
|
t.Fatalf("state stat error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,22 +181,10 @@ func (b *Backend) DeleteManagedBundle(ctx context.Context, bundlePath string, ma
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := storage.ValidatePrefix(bundlePath); err != nil {
|
targets, err := storage.ManagedBundleTargets(bundlePath, managedOutputPaths)
|
||||||
return err
|
|
||||||
}
|
|
||||||
targets := make([]string, 0, len(managedOutputPaths)+1)
|
|
||||||
for _, outputPath := range managedOutputPaths {
|
|
||||||
target, err := storage.Join(bundlePath, outputPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
targets = append(targets, target)
|
|
||||||
}
|
|
||||||
statePath, err := storage.StatePath(bundlePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
targets = append(targets, statePath)
|
|
||||||
|
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if _, ok := b.dirs[target]; ok {
|
if _, ok := b.dirs[target]; ok {
|
||||||
|
|||||||
@@ -116,21 +116,28 @@ func TestBackendManagedDeletion(t *testing.T) {
|
|||||||
backend := New()
|
backend := New()
|
||||||
mustWrite(t, backend, "bundle/report.html", "html")
|
mustWrite(t, backend, "bundle/report.html", "html")
|
||||||
mustWrite(t, backend, "bundle/keep.txt", "keep")
|
mustWrite(t, backend, "bundle/keep.txt", "keep")
|
||||||
mustWrite(t, backend, "bundle/.distributor.json", "{}")
|
statePath, err := storage.StatePath("bundle")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StatePath() error = %v", err)
|
||||||
|
}
|
||||||
|
mustWrite(t, backend, statePath, "{}")
|
||||||
|
|
||||||
err := backend.DeleteManagedBundle(context.Background(), "bundle", []string{"report.html"}, storage.DeleteOptions{PruneEmptyDirs: true})
|
err = backend.DeleteManagedBundle(context.Background(), "bundle", []string{"report.html"}, storage.DeleteOptions{PruneEmptyDirs: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DeleteManagedBundle() error = %v", err)
|
t.Fatalf("DeleteManagedBundle() error = %v", err)
|
||||||
}
|
}
|
||||||
if _, err := backend.Stat(context.Background(), "bundle/report.html"); !storage.IsNotFound(err) {
|
if _, err := backend.Stat(context.Background(), "bundle/report.html"); !storage.IsNotFound(err) {
|
||||||
t.Fatalf("managed output stat error = %v, want not found", err)
|
t.Fatalf("managed output stat error = %v, want not found", err)
|
||||||
}
|
}
|
||||||
if _, err := backend.Stat(context.Background(), "bundle/.distributor.json"); !storage.IsNotFound(err) {
|
if _, err := backend.Stat(context.Background(), statePath); !storage.IsNotFound(err) {
|
||||||
t.Fatalf("state stat error = %v, want not found", err)
|
t.Fatalf("state stat error = %v, want not found", err)
|
||||||
}
|
}
|
||||||
if _, err := backend.Stat(context.Background(), "bundle/keep.txt"); err != nil {
|
if _, err := backend.Stat(context.Background(), "bundle/keep.txt"); err != nil {
|
||||||
t.Fatalf("unlisted file stat error = %v", err)
|
t.Fatalf("unlisted file stat error = %v", err)
|
||||||
}
|
}
|
||||||
|
if err := backend.DeleteManagedBundle(context.Background(), "bundle", []string{""}, storage.DeleteOptions{}); !storage.IsInvalidPath(err) {
|
||||||
|
t.Fatalf("DeleteManagedBundle invalid output error = %v, want invalid path", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackendHasAnyAndWalkStop(t *testing.T) {
|
func TestBackendHasAnyAndWalkStop(t *testing.T) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const stateFileName = ".distributor.json"
|
const StateFileName = ".distributor.json"
|
||||||
|
|
||||||
func ValidatePath(value string) error {
|
func ValidatePath(value string) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -37,9 +37,29 @@ func Join(base, child string) (string, error) {
|
|||||||
|
|
||||||
func StatePath(bundlePath string) (string, error) {
|
func StatePath(bundlePath string) (string, error) {
|
||||||
if bundlePath == "" {
|
if bundlePath == "" {
|
||||||
return stateFileName, nil
|
return StateFileName, nil
|
||||||
}
|
}
|
||||||
return Join(bundlePath, stateFileName)
|
return Join(bundlePath, StateFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ManagedBundleTargets(bundlePath string, managedOutputPaths []string) ([]string, error) {
|
||||||
|
if err := ValidatePrefix(bundlePath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targets := make([]string, 0, len(managedOutputPaths)+1)
|
||||||
|
for _, outputPath := range managedOutputPaths {
|
||||||
|
target, err := Join(bundlePath, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targets = append(targets, target)
|
||||||
|
}
|
||||||
|
statePath, err := StatePath(bundlePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targets = append(targets, statePath)
|
||||||
|
return targets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SortEntries(entries []Entry) {
|
func SortEntries(entries []Entry) {
|
||||||
|
|||||||
@@ -49,6 +49,67 @@ func TestValidatePrefixAllowsRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStatePath(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"": StateFileName,
|
||||||
|
"bundle": "bundle/" + StateFileName,
|
||||||
|
}
|
||||||
|
for bundlePath, want := range tests {
|
||||||
|
t.Run(bundlePath, func(t *testing.T) {
|
||||||
|
got, err := StatePath(bundlePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StatePath(%q) error = %v", bundlePath, err)
|
||||||
|
}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("StatePath(%q) = %q, want %q", bundlePath, got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagedBundleTargets(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bundlePath string
|
||||||
|
outputs []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "root",
|
||||||
|
outputs: []string{"report.html", "assets/style.css"},
|
||||||
|
want: []string{"report.html", "assets/style.css", StateFileName},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested",
|
||||||
|
bundlePath: "daily",
|
||||||
|
outputs: []string{"report.html"},
|
||||||
|
want: []string{"daily/report.html", "daily/" + StateFileName},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := ManagedBundleTargets(tt.bundlePath, tt.outputs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ManagedBundleTargets() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Fatalf("targets = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Fatalf("targets = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagedBundleTargetsRejectsInvalidOutputPath(t *testing.T) {
|
||||||
|
if _, err := ManagedBundleTargets("bundle", []string{"../outside"}); !IsInvalidPath(err) {
|
||||||
|
t.Fatalf("ManagedBundleTargets() error = %v, want invalid path", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListSortsEntries(t *testing.T) {
|
func TestListSortsEntries(t *testing.T) {
|
||||||
backend := walkBackend{
|
backend := walkBackend{
|
||||||
entries: []Entry{
|
entries: []Entry{
|
||||||
|
|||||||
Reference in New Issue
Block a user