Add app backend factory wiring

This commit is contained in:
2026-05-31 03:27:49 +00:00
parent 93805bcc8c
commit be4a61fbe6
6 changed files with 124 additions and 21 deletions

View File

@@ -14,7 +14,7 @@ Entries report a logical path, type, and size when available. Entry types are `f
Core packages should depend on `internal/storage`, not adapter packages. Adapter-specific path handling stays behind backend implementations. Core packages should depend on `internal/storage`, not adapter packages. Adapter-specific path handling stays behind backend implementations.
The local adapter lives in `internal/adapters/local`. The fake backend lives in `internal/storage/fake` for tests and is not registered for runtime use. The local adapter lives in `internal/adapters/local`. Runtime backend construction is wired through the app-level backend factory and storage registry. The fake backend lives in `internal/storage/fake` for tests and is not registered for runtime use.
## Paths ## Paths

45
internal/app/backends.go Normal file
View File

@@ -0,0 +1,45 @@
package app
import (
"context"
"fmt"
"gitea.maximumdirect.net/eric/distributor/internal/adapters/local"
"gitea.maximumdirect.net/eric/distributor/internal/config"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
)
const storagePathKey = "path"
type backendFactory struct {
registry *storage.Registry
}
func newBackendFactory() *backendFactory {
registry := storage.NewRegistry()
_ = registry.Register(config.BackendLocal, func(ctx context.Context, cfg storage.OpenConfig) (storage.Backend, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
return local.New(cfg[storagePathKey])
})
return &backendFactory{registry: registry}
}
func (f *backendFactory) openSource(ctx context.Context, source config.Backend) (storage.Backend, error) {
if source.Backend != config.BackendLocal {
return nil, fmt.Errorf("source backend %s is not implemented for execution", source.Backend)
}
return f.registry.Open(ctx, source.Backend, storage.OpenConfig{storagePathKey: source.Path})
}
func (f *backendFactory) openDestination(ctx context.Context, destination config.Destination) (storage.Backend, error) {
if destination.Backend != config.BackendLocal {
return nil, fmt.Errorf("backend %s is not implemented for execution", destination.Backend)
}
return f.registry.Open(ctx, destination.Backend, storage.OpenConfig{storagePathKey: destination.Path})
}
func (f *backendFactory) openLocalPath(ctx context.Context, path string) (storage.Backend, error) {
return f.registry.Open(ctx, config.BackendLocal, storage.OpenConfig{storagePathKey: path})
}

View File

@@ -0,0 +1,72 @@
package app
import (
"context"
"strings"
"testing"
"gitea.maximumdirect.net/eric/distributor/internal/config"
)
func TestBackendFactoryOpensLocalSource(t *testing.T) {
factory := newBackendFactory()
backend, err := factory.openSource(context.Background(), config.Backend{
Backend: config.BackendLocal,
Path: t.TempDir(),
})
if err != nil {
t.Fatalf("openSource() error = %v", err)
}
if backend == nil {
t.Fatal("openSource() backend = nil")
}
}
func TestBackendFactoryOpensLocalDestination(t *testing.T) {
factory := newBackendFactory()
backend, err := factory.openDestination(context.Background(), config.Destination{
Backend: config.BackendLocal,
Path: t.TempDir(),
})
if err != nil {
t.Fatalf("openDestination() error = %v", err)
}
if backend == nil {
t.Fatal("openDestination() backend = nil")
}
}
func TestBackendFactoryOpensDirectLocalPath(t *testing.T) {
factory := newBackendFactory()
backend, err := factory.openLocalPath(context.Background(), t.TempDir())
if err != nil {
t.Fatalf("openLocalPath() error = %v", err)
}
if backend == nil {
t.Fatal("openLocalPath() backend = nil")
}
}
func TestBackendFactoryRejectsUnsupportedSource(t *testing.T) {
factory := newBackendFactory()
_, err := factory.openSource(context.Background(), config.Backend{
Backend: config.BackendSSH,
URI: "ssh://reports@example.com:22",
Path: "/reports",
})
if err == nil || !strings.Contains(err.Error(), "source backend ssh is not implemented for execution") {
t.Fatalf("openSource() error = %v, want not implemented", err)
}
}
func TestBackendFactoryRejectsUnsupportedDestination(t *testing.T) {
factory := newBackendFactory()
_, err := factory.openDestination(context.Background(), config.Destination{
Backend: config.BackendS3,
Endpoint: "https://s3.example.com",
Bucket: "reports",
})
if err == nil || !strings.Contains(err.Error(), "backend s3 is not implemented for execution") {
t.Fatalf("openDestination() error = %v, want not implemented", err)
}
}

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"gitea.maximumdirect.net/eric/distributor/internal/adapters/local"
"gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/bundle"
) )
@@ -18,7 +17,7 @@ func Inspect(ctx context.Context, options InspectOptions) error {
if options.Path == "" { if options.Path == "" {
return fmt.Errorf("inspect command requires a path") return fmt.Errorf("inspect command requires a path")
} }
backend, err := local.New(options.Path) backend, err := newBackendFactory().openLocalPath(ctx, options.Path)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -7,7 +7,6 @@ import (
"io" "io"
"strings" "strings"
"gitea.maximumdirect.net/eric/distributor/internal/adapters/local"
"gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/bundle"
"gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/config"
"gitea.maximumdirect.net/eric/distributor/internal/notify" "gitea.maximumdirect.net/eric/distributor/internal/notify"
@@ -44,18 +43,16 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error
} }
summary := runSummary{dryRun: options.DryRun} summary := runSummary{dryRun: options.DryRun}
var failures runFailures var failures runFailures
backends := newBackendFactory()
if options.Stdout != nil { if options.Stdout != nil {
if _, err := fmt.Fprintf(options.Stdout, "Configured pipelines: %d\n", len(cfg.Pipelines)); err != nil { if _, err := fmt.Fprintf(options.Stdout, "Configured pipelines: %d\n", len(cfg.Pipelines)); err != nil {
return err return err
} }
} }
for _, pipeline := range cfg.Pipelines { for _, pipeline := range cfg.Pipelines {
if pipeline.Source.Backend != config.BackendLocal { sourceBackend, err := backends.openSource(ctx, pipeline.Source)
return fmt.Errorf("pipeline %s source backend %s is not implemented for execution", pipeline.ID, pipeline.Source.Backend)
}
sourceBackend, err := local.New(pipeline.Source.Path)
if err != nil { if err != nil {
return err return fmt.Errorf("pipeline %s: %w", pipeline.ID, err)
} }
bundles, err := bundle.Discover(ctx, sourceBackend, "") bundles, err := bundle.Discover(ctx, sourceBackend, "")
if err != nil { if err != nil {
@@ -68,16 +65,7 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error
} }
for _, sourceBundle := range bundles { for _, sourceBundle := range bundles {
for _, destination := range pipeline.Destinations { for _, destination := range pipeline.Destinations {
if destination.Backend != config.BackendLocal { destinationBackend, err := backends.openDestination(ctx, destination)
err := fmt.Errorf("backend %s is not implemented for execution", destination.Backend)
failures.add(pipeline.ID, destination.ID, displayBundlePath(sourceBundle.RootRelativePath), err)
summary.recordFailure()
if options.Stdout != nil {
writeErrorLine(options.Stdout, sourceBundle.RootRelativePath, destination.ID, err)
}
continue
}
destinationBackend, err := local.New(destination.Path)
if err != nil { if err != nil {
failures.add(pipeline.ID, destination.ID, displayBundlePath(sourceBundle.RootRelativePath), err) failures.add(pipeline.ID, destination.ID, displayBundlePath(sourceBundle.RootRelativePath), err)
summary.recordFailure() summary.recordFailure()

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"gitea.maximumdirect.net/eric/distributor/internal/adapters/local"
"gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/bundle"
) )
@@ -18,7 +17,7 @@ func Validate(ctx context.Context, options ValidateOptions) error {
if options.Path == "" { if options.Path == "" {
return fmt.Errorf("validate command requires a path") return fmt.Errorf("validate command requires a path")
} }
backend, err := local.New(options.Path) backend, err := newBackendFactory().openLocalPath(ctx, options.Path)
if err != nil { if err != nil {
return err return err
} }