Skip to content

Commit

Permalink
feat(ledger): make stateless
Browse files Browse the repository at this point in the history
  • Loading branch information
gfyrag committed Sep 8, 2024
1 parent b8608f7 commit bd7eaa0
Show file tree
Hide file tree
Showing 228 changed files with 8,222 additions and 10,375 deletions.
2 changes: 1 addition & 1 deletion components/fctl/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
Expand Down
28 changes: 20 additions & 8 deletions components/fctl/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
Expand All @@ -102,6 +104,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Expand Down Expand Up @@ -178,6 +182,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw=
github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
Expand Down Expand Up @@ -213,6 +219,10 @@ github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shomali11/util v0.0.0-20180607005212-e0f70fd665ff h1:A47HTOEURe8GFXu/9ztnUzVgBBo0NlWoKmVPmfJ4LR8=
github.com/shomali11/util v0.0.0-20180607005212-e0f70fd665ff/go.mod h1:WWE2GJM9B5UpdOiwH2val10w/pvJ2cUUQOOA/4LgOng=
github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df h1:SVCDTuzM3KEk8WBwSSw7RTPLw9ajzBaXDg39Bo6xIeU=
github.com/shomali11/xsql v0.0.0-20190608141458-bf76292144df/go.mod h1:K8jR5lDI2MGs9Ky+X2jIF4MwIslI0L8o8ijIlEq7/Vw=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand Down Expand Up @@ -251,12 +261,12 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4=
github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.22.2 h1:iPW+OPxv0G8w75OemJ1RAnTUrF55zOJlXlo1TbJ0Buw=
Expand Down Expand Up @@ -310,8 +320,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand All @@ -330,6 +340,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
31 changes: 19 additions & 12 deletions components/ledger/cmd/buckets.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import (
)

func NewBucket() *cobra.Command {
return &cobra.Command{
ret := &cobra.Command{
Use: "buckets",
Aliases: []string{"storage"},
}
service.AddFlags(ret.PersistentFlags())
bunconnect.AddFlags(ret.PersistentFlags())

return ret
}

func NewBucketUpgrade() *cobra.Command {
Expand All @@ -26,24 +30,22 @@ func NewBucketUpgrade() *cobra.Command {
return err
}

driver := driver.New(*connectionOptions)
if err := driver.Initialize(cmd.Context()); err != nil {
db, err := bunconnect.OpenSQLDB(cmd.Context(), *connectionOptions)
if err != nil {
return err
}
defer func() {
_ = driver.Close()
_ = db.Close()
}()

name := args[0]

bucket, err := driver.OpenBucket(cmd.Context(), name)
if err != nil {
driver := driver.New(db)
if err := driver.Initialize(cmd.Context()); err != nil {
return err
}

logger := logging.NewDefaultLogger(cmd.OutOrStdout(), service.IsDebug(cmd), false)

return bucket.Migrate(logging.ContextWithLogger(cmd.Context(), logger))
return driver.UpgradeBucket(logging.ContextWithLogger(cmd.Context(), logger), args[0])
},
}
return cmd
Expand All @@ -58,13 +60,18 @@ func upgradeAll(cmd *cobra.Command, _ []string) error {
return err
}

driver := driver.New(*connectionOptions)
if err := driver.Initialize(ctx); err != nil {
db, err := bunconnect.OpenSQLDB(cmd.Context(), *connectionOptions)
if err != nil {
return err
}
defer func() {
_ = driver.Close()
_ = db.Close()
}()

driver := driver.New(db)
if err := driver.Initialize(ctx); err != nil {
return err
}

return driver.UpgradeAllBuckets(ctx)
}
39 changes: 0 additions & 39 deletions components/ledger/cmd/container.go

This file was deleted.

22 changes: 1 addition & 21 deletions components/ledger/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,11 @@ import (
"github.com/formancehq/stack/libs/go-libs/service"
"github.com/uptrace/bun"

"github.com/formancehq/stack/libs/go-libs/aws/iam"
"github.com/formancehq/stack/libs/go-libs/bun/bunconnect"

"github.com/formancehq/ledger/internal/storage/systemstore"
"github.com/formancehq/stack/libs/go-libs/auth"
"github.com/formancehq/stack/libs/go-libs/otlp/otlpmetrics"
"github.com/formancehq/stack/libs/go-libs/otlp/otlptraces"
"github.com/formancehq/stack/libs/go-libs/publish"
"github.com/spf13/cobra"
)

const (
BindFlag = "bind"
ServiceName = "ledger"
)

var (
Expand Down Expand Up @@ -49,18 +41,6 @@ func NewRootCommand() *cobra.Command {

root.AddCommand(NewDocCommand())

root.PersistentFlags().String(BindFlag, "0.0.0.0:3068", "API bind address")

service.AddFlags(root.PersistentFlags())
otlpmetrics.AddFlags(root.PersistentFlags())
otlptraces.AddFlags(root.PersistentFlags())
auth.AddFlags(root.PersistentFlags())
publish.AddFlags(ServiceName, root.PersistentFlags(), func(cd *publish.ConfigDefault) {
cd.PublisherCircuitBreakerSchema = systemstore.Schema
})
bunconnect.AddFlags(root.PersistentFlags())
iam.AddFlags(root.PersistentFlags())

return root
}

Expand Down
126 changes: 67 additions & 59 deletions components/ledger/cmd/serve.go
Original file line number Diff line number Diff line change
@@ -1,93 +1,101 @@
package cmd

import (
"net/http"

"github.com/formancehq/stack/libs/go-libs/time"

"github.com/formancehq/ledger/internal/storage/driver"
ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger"
"github.com/formancehq/ledger/internal/controller/ledger/writer"
systemcontroller "github.com/formancehq/ledger/internal/controller/system"
"github.com/formancehq/ledger/internal/storage"
"github.com/formancehq/stack/libs/go-libs/auth"
"github.com/formancehq/stack/libs/go-libs/aws/iam"
"github.com/formancehq/stack/libs/go-libs/bun/bunconnect"
"github.com/formancehq/stack/libs/go-libs/otlp/otlpmetrics"
"github.com/formancehq/stack/libs/go-libs/otlp/otlptraces"
"github.com/formancehq/stack/libs/go-libs/publish"

"github.com/formancehq/ledger/internal/api"

systemstore "github.com/formancehq/ledger/internal/storage/system"
"github.com/formancehq/stack/libs/go-libs/ballast"
"github.com/formancehq/stack/libs/go-libs/httpserver"
"github.com/formancehq/stack/libs/go-libs/logging"
"github.com/formancehq/stack/libs/go-libs/service"
"github.com/go-chi/chi/v5"
"github.com/spf13/cobra"
"go.uber.org/fx"
)

const (
BindFlag = "bind"
BallastSizeInBytesFlag = "ballast-size"
NumscriptCacheMaxCountFlag = "numscript-cache-max-count"
ledgerBatchSizeFlag = "ledger-batch-size"
ReadOnlyFlag = "read-only"
AutoUpgradeFlag = "auto-upgrade"
)

func NewServe() *cobra.Command {
cmd := &cobra.Command{
Use: "serve",
RunE: func(cmd *cobra.Command, args []string) error {
readOnly, _ := cmd.Flags().GetBool(ReadOnlyFlag)
autoUpgrade, _ := cmd.Flags().GetBool(AutoUpgradeFlag)
ballastSize, _ := cmd.Flags().GetUint(BallastSizeInBytesFlag)
bind, _ := cmd.Flags().GetString(BindFlag)
serveConfiguration, err := discoverServeConfiguration(cmd)
if err != nil {
return err
}

return service.New(cmd.OutOrStdout(), resolveOptions(
cmd,
ballast.Module(ballastSize),
api.Module(api.Config{
Version: Version,
ReadOnly: readOnly,
Debug: service.IsDebug(cmd),
}),
fx.Invoke(func(lc fx.Lifecycle, driver *driver.Driver) {
if autoUpgrade {
lc.Append(fx.Hook{
OnStart: driver.UpgradeAllBuckets,
})
}
}),
fx.Invoke(func(lc fx.Lifecycle, h chi.Router, logger logging.Logger) {

wrappedRouter := chi.NewRouter()
wrappedRouter.Use(func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(logging.ContextWithLogger(r.Context(), logger))
handler.ServeHTTP(w, r)
})
})
wrappedRouter.Use(Log())
wrappedRouter.Mount("/", h)
connectionOptions, err := bunconnect.ConnectionOptionsFromFlags(cmd)
if err != nil {
return err
}

lc.Append(httpserver.NewHook(wrappedRouter, httpserver.WithAddress(bind)))
return service.New(cmd.OutOrStdout(),
fx.NopLogger,
publish.FXModuleFromFlags(cmd, service.IsDebug(cmd)),
otlptraces.FXModuleFromFlags(cmd),
otlpmetrics.FXModuleFromFlags(cmd),
auth.FXModuleFromFlags(cmd),
bunconnect.Module(*connectionOptions, service.IsDebug(cmd)),
storage.NewFXModule(serveConfiguration.autoUpgrade),
systemcontroller.NewFXModule(),
ledgercontroller.NewFXModule(ledgercontroller.ModuleConfiguration{
NSCacheConfiguration: writer.CacheConfiguration{
MaxCount: serveConfiguration.numscriptCacheMaxCount,
},
}),
ballast.Module(serveConfiguration.ballastSize),
api.Module(api.Config{
Version: Version,
Debug: service.IsDebug(cmd),
Bind: serveConfiguration.bind,
}),
)...).Run(cmd)
).Run(cmd)
},
}
cmd.Flags().Uint(BallastSizeInBytesFlag, 0, "Ballast size in bytes, default to 0")
cmd.Flags().Int(NumscriptCacheMaxCountFlag, 1024, "Numscript cache max count")
cmd.Flags().Int(ledgerBatchSizeFlag, 50, "ledger batch size")
cmd.Flags().Bool(ReadOnlyFlag, false, "Read only mode")
cmd.Flags().Bool(AutoUpgradeFlag, false, "Automatically upgrade all schemas")
cmd.Flags().String(BindFlag, "0.0.0.0:3068", "API bind address")

service.AddFlags(cmd.Flags())
bunconnect.AddFlags(cmd.Flags())
otlpmetrics.AddFlags(cmd.Flags())
otlptraces.AddFlags(cmd.Flags())
auth.AddFlags(cmd.Flags())
publish.AddFlags(ServiceName, cmd.Flags(), func(cd *publish.ConfigDefault) {
cd.PublisherCircuitBreakerSchema = systemstore.Schema
})
iam.AddFlags(cmd.Flags())

return cmd
}

func Log() func(h http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
h.ServeHTTP(w, r)
latency := time.Since(start)
logging.FromContext(r.Context()).WithFields(map[string]interface{}{
"method": r.Method,
"path": r.URL.Path,
"latency": latency,
"user_agent": r.UserAgent(),
"params": r.URL.Query().Encode(),
}).Debug("Request")
})
}
type serveConfiguration struct {
ballastSize uint
numscriptCacheMaxCount uint
autoUpgrade bool
bind string
}

func discoverServeConfiguration(cmd *cobra.Command) (*serveConfiguration, error) {
ret := &serveConfiguration{}
ret.ballastSize, _ = cmd.Flags().GetUint(BallastSizeInBytesFlag)
ret.numscriptCacheMaxCount, _ = cmd.Flags().GetUint(NumscriptCacheMaxCountFlag)
ret.autoUpgrade, _ = cmd.Flags().GetBool(AutoUpgradeFlag)
ret.bind, _ = cmd.Flags().GetString(BindFlag)

return ret, nil
}
Loading

0 comments on commit bd7eaa0

Please sign in to comment.