Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ go.work.sum

# the sprue binary
sprue

# local sprue config
config.yaml
24 changes: 16 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,22 @@ Sprue is the upload coordination service for Storacha local development. It rout

**UCAN RPC Service (pkg/service/)**
- `Service` struct wraps a go-ucanto server that handles UCAN RPC requests
- Handlers in `pkg/service/handlers/` implement UCAP capabilities (e.g., `space/blob/add`, `upload/add`)
- Each handler follows the pattern: `With<Capability>Method(s ServiceInterface) server.Option`
- Handler service interfaces define the dependencies each handler needs (e.g., `SpaceBlobAddService`)

**State Management (pkg/state/)**
- `StateStore` interface defines all storage operations (allocations, receipts, auth requests, etc.)
- DynamoDB implementation in `pkg/dynamo/store.go`
- Key types: `Allocation`, `Upload`, `StoredReceipt`, `Provider`, `AuthRequest`, `Provisioning`
- Handlers in `pkg/service/handlers/` implement UCAN capabilities (e.g., `space/blob/add`, `upload/add`)
- Each handler follows the pattern: `With<Capability>Method(stores..., services..., logger) server.Option`
- Handlers receive their store and service dependencies directly as function parameters
- Handlers are registered via fx groups (`group:"ucan_options"`) and collected into the UCAN server

**Stores (pkg/store/)**
- Each domain has its own store interface in `pkg/store/<domain>/`
- Each store has two implementations: AWS (DynamoDB/S3) in `<domain>/aws/` and in-memory in `<domain>/memory/`
- Store interfaces: `agent.Store`, `blob_registry.Store`, `consumer.Store`, `customer.Store`, `delegation.Store`, `metrics.Store`, `replica.Store`, `revocation.Store`, `space_diff.Store`, `storage_provider.Store`, `subscription.Store`, `upload.Store`
- AWS stores are wired in `internal/fx/store/aws/provider.go`, memory stores in `internal/fx/store/memory/provider.go`

**Services (pkg/)**
- `provisioning`: Manages space provisioning (consumers + subscriptions)
- `routing`: Selects storage providers for blob allocation and replication
- `piriclient`: Communicates with Piri storage nodes for blob allocation/acceptance
- `indexerclient`: Communicates with the indexing service

**External Clients (pkg/)**
- `piriclient`: Communicates with Piri storage nodes for blob allocation/acceptance
Expand Down
28 changes: 28 additions & 0 deletions cmd/client/admin/provider/deregister.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package provider

import (
"github.com/spf13/cobra"
"github.com/storacha/go-ucanto/did"
"github.com/storacha/sprue/cmd/client/lib"
)

var deregisterCmd = &cobra.Command{
Use: "deregister <provider-did>",
Aliases: []string{"remove", "rm"},
Short: "Deregister a storage provider from the service",
Args: cobra.ExactArgs(1),
RunE: doDeregister,
}

func doDeregister(cmd *cobra.Command, args []string) error {
c, _, _, id := lib.InitClient(cmd)

providerID, err := did.Parse(args[0])
cobra.CheckErr(err)

_, err = c.AdminProviderDeregister(cmd.Context(), id.Signer, providerID)
cobra.CheckErr(err)

cmd.Println("Provider deregistered successfully")
return nil
}
37 changes: 37 additions & 0 deletions cmd/client/admin/provider/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package provider

import (
"fmt"

"github.com/spf13/cobra"
"github.com/storacha/sprue/cmd/client/lib"
)

var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List registered storage providers",
Args: cobra.NoArgs,
RunE: doList,
}

func doList(cmd *cobra.Command, args []string) error {
c, _, _, id := lib.InitClient(cmd)

res, err := c.AdminProviderList(cmd.Context(), id.Signer)
cobra.CheckErr(err)

if len(res.Providers) == 0 {
cmd.Println("No providers registered")
return nil
}

table := lib.NewTable(cmd.OutOrStdout())
table.SetHeader([]string{"ID", "Weight", "URL"})
for _, p := range res.Providers {
table.Append([]string{p.ID.String(), fmt.Sprintf("%d", p.Weight), p.Endpoint})
}
table.Render()

return nil
}
37 changes: 37 additions & 0 deletions cmd/client/admin/provider/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package provider

import (
"net/url"

"github.com/spf13/cobra"
"github.com/storacha/go-ucanto/core/delegation"
"github.com/storacha/go-ucanto/did"
"github.com/storacha/sprue/cmd/client/lib"
)

var registerCmd = &cobra.Command{
Use: "register <provider-did> <provider-url> <proof>",
Aliases: []string{"add"},
Short: "Register a storage provider with the service",
Args: cobra.ExactArgs(3),
RunE: doRegister,
}

func doRegister(cmd *cobra.Command, args []string) error {
c, _, _, id := lib.InitClient(cmd)

providerID, err := did.Parse(args[0])
cobra.CheckErr(err)

endpoint, err := url.Parse(args[1])
cobra.CheckErr(err)

proof, err := delegation.Parse(args[2])
cobra.CheckErr(err)

_, err = c.AdminProviderRegister(cmd.Context(), id.Signer, providerID, endpoint.String(), proof)
cobra.CheckErr(err)

cmd.Println("Provider registered successfully")
return nil
}
18 changes: 18 additions & 0 deletions cmd/client/admin/provider/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package provider

import (
"github.com/spf13/cobra"
"github.com/storacha/sprue/cmd/client/admin/provider/weight"
)

var Cmd = &cobra.Command{
Use: "provider",
Short: "Manage storage providers",
}

func init() {
Cmd.AddCommand(deregisterCmd)
Cmd.AddCommand(listCmd)
Cmd.AddCommand(registerCmd)
Cmd.AddCommand(weight.Cmd)
}
14 changes: 14 additions & 0 deletions cmd/client/admin/provider/weight/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package weight

import (
"github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
Use: "weight",
Short: "Manage storage provider weights",
}

func init() {
Cmd.AddCommand(setCmd)
}
35 changes: 35 additions & 0 deletions cmd/client/admin/provider/weight/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package weight

import (
"strconv"

"github.com/spf13/cobra"
"github.com/storacha/go-ucanto/did"
"github.com/storacha/sprue/cmd/client/lib"
)

var setCmd = &cobra.Command{
Use: "set <provider-did> <weight> <replication-weight>",
Short: "Set the weight of a storage provider",
Args: cobra.ExactArgs(3),
RunE: doSet,
}

func doSet(cmd *cobra.Command, args []string) error {
c, _, _, id := lib.InitClient(cmd)

providerID, err := did.Parse(args[0])
cobra.CheckErr(err)

weight, err := strconv.ParseInt(args[1], 10, 0)
cobra.CheckErr(err)

replicationWeight, err := strconv.ParseInt(args[2], 10, 0)
cobra.CheckErr(err)

_, err = c.AdminProviderWeightSet(cmd.Context(), id.Signer, providerID, int(weight), int(replicationWeight))
cobra.CheckErr(err)

cmd.Println("Provider weight set successfully")
return nil
}
15 changes: 15 additions & 0 deletions cmd/client/admin/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package admin

import (
"github.com/spf13/cobra"
"github.com/storacha/sprue/cmd/client/admin/provider"
)

var Cmd = &cobra.Command{
Use: "admin",
Short: "Administrate a running sprue via UCAN invocations",
}

func init() {
Cmd.AddCommand(provider.Cmd)
}
34 changes: 34 additions & 0 deletions cmd/client/lib/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package lib

import (
"fmt"

"github.com/spf13/cobra"
"github.com/storacha/sprue/internal/config"
"github.com/storacha/sprue/internal/fx"
"github.com/storacha/sprue/pkg/client"
"github.com/storacha/sprue/pkg/identity"
"go.uber.org/zap"
)

func InitClient(cmd *cobra.Command) (*client.Client, *config.Config, *zap.Logger, *identity.Identity) {
var configFile string
configFlag := cmd.InheritedFlags().Lookup("config")
if configFlag != nil {
configFile = configFlag.Value.String()
}
cfg, err := config.Load(configFile)
cobra.CheckErr(err)

logger, err := fx.NewLogger(cfg)
cobra.CheckErr(err)
id, err := fx.NewIdentity(cfg, logger)
cobra.CheckErr(err)

c, err := client.New(
id.Signer.DID(),
client.WithServiceURL(fmt.Sprintf("http://%s:%d", cfg.Server.Host, cfg.Server.Port)),
)
cobra.CheckErr(err)
return c, cfg, logger, id
}
22 changes: 22 additions & 0 deletions cmd/client/lib/table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package lib

import (
"io"

"github.com/olekukonko/tablewriter"
)

func NewTable(w io.Writer) *tablewriter.Table {
table := tablewriter.NewWriter(w)
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetTablePadding("\t")
table.SetNoWhiteSpace(true)
return table
}
15 changes: 15 additions & 0 deletions cmd/client/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package client

import (
"github.com/spf13/cobra"
"github.com/storacha/sprue/cmd/client/admin"
)

var Cmd = &cobra.Command{
Use: "client",
Short: "Interact with a running sprue via UCAN invocations",
}

func init() {
Cmd.AddCommand(admin.Cmd)
}
20 changes: 13 additions & 7 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import (

"github.com/spf13/cobra"
"go.uber.org/fx"
"go.uber.org/fx/fxevent"
"go.uber.org/zap"

"github.com/storacha/sprue/cmd/client"
"github.com/storacha/sprue/internal/config"
appfx "github.com/storacha/sprue/internal/fx"
)

Expand All @@ -27,6 +31,7 @@ Routes blob allocations to Piri nodes and tracks upload state in DynamoDB.`,
}

rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(client.Cmd)

// Global flags
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (default: looks for config.yaml in current dir)")
Expand All @@ -38,16 +43,17 @@ Routes blob allocations to Piri nodes and tracks upload state in DynamoDB.`,
}

func runServe(cmd *cobra.Command, args []string) error {
cfg, err := config.Load(cfgFile)
cobra.CheckErr(err)

app := fx.New(
fx.Supply(appfx.ConfigParams{
ConfigFile: cfgFile,
appfx.AppModule(cfg),
// Suppress fx's default logging and use our own zap logger
fx.WithLogger(func(log *zap.Logger) fxevent.Logger {
return &fxevent.ZapLogger{Logger: log}
}),
appfx.ConfigModule,
appfx.AppModule,
// Suppress fx's default logging, we use our own zap logger
fx.NopLogger,
)

app.Run()

return nil
}
Loading
Loading