From c119dc094231ebb998d90af50d4ee334c959ab01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20BUISSON?= Date: Thu, 5 Dec 2024 11:49:52 +0100 Subject: [PATCH 1/2] feat: add tests + handle TTL --- cmd/serve/command.go | 239 ++-- cmd/serve/config.go | 26 +- cmd/serve/serve.go | 208 ++-- go.mod | 3 +- go.sum | 2 + pkg/handlers/dbmock_test.go | 61 - pkg/handlers/github.go | 45 + pkg/handlers/goproxy.go | 41 + pkg/handlers/handlers.go | 677 +++++------ pkg/handlers/handlers_test.go | 327 +++--- pkg/handlers/mock_gen.go | 2009 +++++++++++++++++++++++++++++++++ pkg/handlers/mock_test.go | 5 + pkg/handlers/module.go | 755 +++++++------ pkg/handlers/module_test.go | 676 +++++++++-- 14 files changed, 3804 insertions(+), 1270 deletions(-) delete mode 100644 pkg/handlers/dbmock_test.go create mode 100644 pkg/handlers/github.go create mode 100644 pkg/handlers/goproxy.go create mode 100644 pkg/handlers/mock_gen.go create mode 100644 pkg/handlers/mock_test.go diff --git a/cmd/serve/command.go b/cmd/serve/command.go index 669b2ea..7949802 100644 --- a/cmd/serve/command.go +++ b/cmd/serve/command.go @@ -1,141 +1,148 @@ package serve import ( - "github.com/ettle/strcase" - "github.com/traefik/plugin-service/cmd/internal" - "github.com/traefik/plugin-service/pkg/tracer" - "github.com/urfave/cli/v2" + "github.com/ettle/strcase" + "github.com/traefik/plugin-service/cmd/internal" + "github.com/traefik/plugin-service/pkg/tracer" + "github.com/urfave/cli/v2" ) const ( - flagAddr = "addr" - flagGHToken = "github-token" + flagAddr = "addr" + flagTTL = "ttl" + flagGHToken = "github-token" - flagTraceServiceURL = "trace-service-url" + flagTraceServiceURL = "trace-service-url" - flagGoProxyURL = "go-proxy-url" - flagGoProxyUsername = "go-proxy-username" - flagGoProxyPassword = "go-proxy-password" + flagGoProxyURL = "go-proxy-url" + flagGoProxyUsername = "go-proxy-username" + flagGoProxyPassword = "go-proxy-password" - flagTracingAddress = "tracing-address" - flagTracingInsecure = "tracing-insecure" - flagTracingUsername = "tracing-username" - flagTracingPassword = "tracing-password" - flagTracingProbability = "tracing-probability" + flagTracingAddress = "tracing-address" + flagTracingInsecure = "tracing-insecure" + flagTracingUsername = "tracing-username" + flagTracingPassword = "tracing-password" + flagTracingProbability = "tracing-probability" ) // Command creates the command for serving the plugin service. func Command() *cli.Command { - cmd := &cli.Command{ - Name: "serve", - Usage: "Serve HTTP", - Description: "Launch plugin service application", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: flagAddr, - Usage: "Addr to listen on.", - EnvVars: []string{strcase.ToSNAKE(flagAddr)}, - }, - &cli.StringFlag{ - Name: flagGHToken, - Usage: "GitHub Token", - EnvVars: []string{strcase.ToSNAKE(flagGHToken)}, - Required: true, - }, - &cli.StringFlag{ - Name: flagTraceServiceURL, - Usage: "URL of the trace service", - EnvVars: []string{strcase.ToSNAKE(flagTraceServiceURL)}, - }, - }, - Action: func(cliCtx *cli.Context) error { - return run(cliCtx.Context, buildConfig(cliCtx)) - }, - } + cmd := &cli.Command{ + Name: "serve", + Usage: "Serve HTTP", + Description: "Launch plugin service application", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: flagAddr, + Usage: "Addr to listen on.", + EnvVars: []string{strcase.ToSNAKE(flagAddr)}, + }, + &cli.StringFlag{ + Name: flagTTL, + Usage: "Control TTL of download responses.", + EnvVars: []string{strcase.ToSNAKE(flagTTL)}, + }, + &cli.StringFlag{ + Name: flagGHToken, + Usage: "GitHub Token", + EnvVars: []string{strcase.ToSNAKE(flagGHToken)}, + Required: true, + }, + &cli.StringFlag{ + Name: flagTraceServiceURL, + Usage: "URL of the trace service", + EnvVars: []string{strcase.ToSNAKE(flagTraceServiceURL)}, + }, + }, + Action: func(cliCtx *cli.Context) error { + return run(cliCtx.Context, buildConfig(cliCtx)) + }, + } - cmd.Flags = append(cmd.Flags, goProxyFlags()...) - cmd.Flags = append(cmd.Flags, tracingFlags()...) - cmd.Flags = append(cmd.Flags, internal.MongoFlags()...) + cmd.Flags = append(cmd.Flags, goProxyFlags()...) + cmd.Flags = append(cmd.Flags, tracingFlags()...) + cmd.Flags = append(cmd.Flags, internal.MongoFlags()...) - return cmd + return cmd } func buildConfig(cliCtx *cli.Context) Config { - return Config{ - MongoDB: internal.BuildMongoConfig(cliCtx), - Tracing: tracer.Config{ - Address: cliCtx.String(flagTracingAddress), - Insecure: cliCtx.Bool(flagTracingInsecure), - Username: cliCtx.String(flagTracingUsername), - Password: cliCtx.String(flagTracingPassword), - Probability: cliCtx.Float64(flagTracingProbability), - ServiceName: "plugin-service", - }, - TraceURL: cliCtx.String(flagTraceServiceURL), - Addr: cliCtx.String(flagAddr), - GitHubToken: cliCtx.String(flagGHToken), - GoProxy: GoProxy{ - URL: cliCtx.String(flagGoProxyURL), - Username: cliCtx.String(flagGoProxyUsername), - Password: cliCtx.String(flagGoProxyPassword), - }, - } + return Config{ + MongoDB: internal.BuildMongoConfig(cliCtx), + Tracing: tracer.Config{ + Address: cliCtx.String(flagTracingAddress), + Insecure: cliCtx.Bool(flagTracingInsecure), + Username: cliCtx.String(flagTracingUsername), + Password: cliCtx.String(flagTracingPassword), + Probability: cliCtx.Float64(flagTracingProbability), + ServiceName: "plugin-service", + }, + TraceURL: cliCtx.String(flagTraceServiceURL), + Addr: cliCtx.String(flagAddr), + GitHubToken: cliCtx.String(flagGHToken), + GoProxy: GoProxy{ + URL: cliCtx.String(flagGoProxyURL), + Username: cliCtx.String(flagGoProxyUsername), + Password: cliCtx.String(flagGoProxyPassword), + }, + TTL: cliCtx.Duration(flagTTL), + } } func goProxyFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: flagGoProxyURL, - Usage: "Go Proxy URL", - EnvVars: []string{strcase.ToSNAKE(flagGoProxyURL)}, - Required: true, - }, - &cli.StringFlag{ - Name: flagGoProxyUsername, - Usage: "Go Proxy Username", - EnvVars: []string{strcase.ToSNAKE(flagGoProxyUsername)}, - Required: true, - }, - &cli.StringFlag{ - Name: flagGoProxyPassword, - Usage: "Go Proxy Password", - EnvVars: []string{strcase.ToSNAKE(flagGoProxyPassword)}, - Required: true, - }, - } + return []cli.Flag{ + &cli.StringFlag{ + Name: flagGoProxyURL, + Usage: "Go Proxy URL", + EnvVars: []string{strcase.ToSNAKE(flagGoProxyURL)}, + Required: true, + }, + &cli.StringFlag{ + Name: flagGoProxyUsername, + Usage: "Go Proxy Username", + EnvVars: []string{strcase.ToSNAKE(flagGoProxyUsername)}, + Required: true, + }, + &cli.StringFlag{ + Name: flagGoProxyPassword, + Usage: "Go Proxy Password", + EnvVars: []string{strcase.ToSNAKE(flagGoProxyPassword)}, + Required: true, + }, + } } func tracingFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: flagTracingAddress, - Usage: "Address to send traces", - EnvVars: []string{strcase.ToSNAKE(flagTracingAddress)}, - Value: "jaeger.jaeger.svc.cluster.local:4318", - }, - &cli.BoolFlag{ - Name: flagTracingInsecure, - Usage: "use HTTP instead of HTTPS", - EnvVars: []string{strcase.ToSNAKE(flagTracingInsecure)}, - Value: true, - }, - &cli.StringFlag{ - Name: flagTracingUsername, - Usage: "Username to connect to Jaeger", - EnvVars: []string{strcase.ToSNAKE(flagTracingUsername)}, - Value: "jaeger", - }, - &cli.StringFlag{ - Name: flagTracingPassword, - Usage: "Password to connect to Jaeger", - EnvVars: []string{strcase.ToSNAKE(flagTracingPassword)}, - Value: "jaeger", - }, - &cli.Float64Flag{ - Name: flagTracingProbability, - Usage: "Probability to send traces", - EnvVars: []string{strcase.ToSNAKE(flagTracingProbability)}, - Value: 0, - }, - } + return []cli.Flag{ + &cli.StringFlag{ + Name: flagTracingAddress, + Usage: "Address to send traces", + EnvVars: []string{strcase.ToSNAKE(flagTracingAddress)}, + Value: "jaeger.jaeger.svc.cluster.local:4318", + }, + &cli.BoolFlag{ + Name: flagTracingInsecure, + Usage: "use HTTP instead of HTTPS", + EnvVars: []string{strcase.ToSNAKE(flagTracingInsecure)}, + Value: true, + }, + &cli.StringFlag{ + Name: flagTracingUsername, + Usage: "Username to connect to Jaeger", + EnvVars: []string{strcase.ToSNAKE(flagTracingUsername)}, + Value: "jaeger", + }, + &cli.StringFlag{ + Name: flagTracingPassword, + Usage: "Password to connect to Jaeger", + EnvVars: []string{strcase.ToSNAKE(flagTracingPassword)}, + Value: "jaeger", + }, + &cli.Float64Flag{ + Name: flagTracingProbability, + Usage: "Probability to send traces", + EnvVars: []string{strcase.ToSNAKE(flagTracingProbability)}, + Value: 0, + }, + } } diff --git a/cmd/serve/config.go b/cmd/serve/config.go index d1d4563..6878d05 100644 --- a/cmd/serve/config.go +++ b/cmd/serve/config.go @@ -1,25 +1,29 @@ package serve import ( - "github.com/traefik/plugin-service/pkg/db/mongodb" - "github.com/traefik/plugin-service/pkg/tracer" + "time" + + "github.com/traefik/plugin-service/pkg/db/mongodb" + "github.com/traefik/plugin-service/pkg/tracer" ) // Config holds the serve configuration. type Config struct { - Addr string - GitHubToken string + Addr string + GitHubToken string + + TraceURL string - TraceURL string + MongoDB mongodb.Config + Tracing tracer.Config + GoProxy GoProxy - MongoDB mongodb.Config - Tracing tracer.Config - GoProxy GoProxy + TTL time.Duration } // GoProxy holds the go-proxy configuration. type GoProxy struct { - URL string - Username string - Password string + URL string + Username string + Password string } diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index c6d0654..ddbceb0 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -1,150 +1,122 @@ package serve import ( - "context" - "fmt" - "net/http" - - "github.com/google/go-github/v57/github" - "github.com/gorilla/mux" - "github.com/julienschmidt/httprouter" - "github.com/ldez/grignotin/goproxy" - "github.com/traefik/hub-trace-kpi/trace" - "github.com/traefik/plugin-service/cmd/internal" - "github.com/traefik/plugin-service/pkg/handlers" - "github.com/traefik/plugin-service/pkg/healthcheck" - "github.com/traefik/plugin-service/pkg/tracer" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - "golang.org/x/oauth2" + "context" + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/julienschmidt/httprouter" + "github.com/traefik/hub-trace-kpi/trace" + "github.com/traefik/plugin-service/cmd/internal" + "github.com/traefik/plugin-service/pkg/handlers" + "github.com/traefik/plugin-service/pkg/healthcheck" + "github.com/traefik/plugin-service/pkg/tracer" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" ) func run(ctx context.Context, cfg Config) error { - stopTracer, err := setupTracing(ctx, cfg.Tracing, cfg.TraceURL) - if err != nil { - return fmt.Errorf("setup tracing provider: %w", err) - } - defer stopTracer() - - store, tearDown, err := internal.CreateMongoClient(ctx, cfg.MongoDB) - if err != nil { - return fmt.Errorf("unable to create MongoDB client: %w", err) - } - defer tearDown() - - if err = store.Bootstrap(); err != nil { - return fmt.Errorf("unable to bootstrap database: %w", err) - } - - gpClient, err := newGoProxyClient(cfg.GoProxy) - if err != nil { - return fmt.Errorf("unable to create go proxy client: %w", err) - } - - var ghClient *github.Client - if cfg.GitHubToken != "" { - ghClient = newGitHubClient(context.Background(), cfg.GitHubToken) - } - - handler := handlers.New(store, gpClient, ghClient) - - healthChecker := healthcheck.Client{DB: store} - - r := http.NewServeMux() - - r.Handle("/public/", buildPublicRouter(handler)) - r.Handle("/internal/", buildInternalRouter(handler)) - r.Handle("/external/", buildExternalRouter(handler)) - r.HandleFunc("/live", healthChecker.Live) - r.HandleFunc("/ready", healthChecker.Ready) - - return http.ListenAndServe(cfg.Addr, r) + stopTracer, err := setupTracing(ctx, cfg.Tracing, cfg.TraceURL) + if err != nil { + return fmt.Errorf("setup tracing provider: %w", err) + } + defer stopTracer() + + store, tearDown, err := internal.CreateMongoClient(ctx, cfg.MongoDB) + if err != nil { + return fmt.Errorf("unable to create MongoDB client: %w", err) + } + defer tearDown() + + if err = store.Bootstrap(); err != nil { + return fmt.Errorf("unable to bootstrap database: %w", err) + } + + var gpClient handlers.GoproxyPluginClient + gpClient, err = handlers.NewGoproxyClient(cfg.GoProxy.URL, cfg.GoProxy.Username, cfg.GoProxy.Password) + if err != nil { + return fmt.Errorf("unable to create go proxy client: %w", err) + } + + var ghClient handlers.GithubPluginClient + if cfg.GitHubToken != "" { + ghClient = handlers.NewGithubClient(context.Background(), cfg.GitHubToken) + } + + handler := handlers.New(store, gpClient, ghClient, cfg.TTL) + + healthChecker := healthcheck.Client{DB: store} + + r := http.NewServeMux() + + r.Handle("/public/", buildPublicRouter(handler)) + r.Handle("/internal/", buildInternalRouter(handler)) + r.Handle("/external/", buildExternalRouter(handler)) + r.HandleFunc("/live", healthChecker.Live) + r.HandleFunc("/ready", healthChecker.Ready) + + return http.ListenAndServe(cfg.Addr, r) } func buildPublicRouter(handler handlers.Handlers) http.Handler { - r := mux.NewRouter() + r := mux.NewRouter() - r.Handle("/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "public_list")) - r.Handle("/download/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Download), "public_download")) - r.Handle("/validate/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Validate), "public_validate")) - r.Handle("/{uuid}", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "public_get")) + r.Handle("/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "public_list")) + r.Handle("/download/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Download), "public_download")) + r.Handle("/validate/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Validate), "public_validate")) + r.Handle("/{uuid}", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "public_get")) - r.NotFoundHandler = http.HandlerFunc(handlers.NotFound) + r.NotFoundHandler = http.HandlerFunc(handlers.NotFound) - return http.StripPrefix("/public", r) + return http.StripPrefix("/public", r) } func buildInternalRouter(handler handlers.Handlers) http.Handler { - r := httprouter.New() + r := httprouter.New() - r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "internal_list")) - r.Handler(http.MethodPost, "/", otelhttp.NewHandler(http.HandlerFunc(handler.Create), "internal_create")) - r.Handler(http.MethodPut, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Update), "internal_update")) - r.Handler(http.MethodDelete, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Delete), "internal_delete")) + r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "internal_list")) + r.Handler(http.MethodPost, "/", otelhttp.NewHandler(http.HandlerFunc(handler.Create), "internal_create")) + r.Handler(http.MethodPut, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Update), "internal_update")) + r.Handler(http.MethodDelete, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Delete), "internal_delete")) - r.NotFound = http.HandlerFunc(handlers.NotFound) - r.PanicHandler = handlers.PanicHandler + r.NotFound = http.HandlerFunc(handlers.NotFound) + r.PanicHandler = handlers.PanicHandler - return http.StripPrefix("/internal", r) + return http.StripPrefix("/internal", r) } func buildExternalRouter(handler handlers.Handlers) http.Handler { - r := httprouter.New() + r := httprouter.New() - r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "external_list")) - r.Handler(http.MethodGet, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "external_get")) - - r.NotFound = http.HandlerFunc(handlers.NotFound) - r.PanicHandler = handlers.PanicHandler - - return http.StripPrefix("/external", r) -} - -func newGoProxyClient(cfg GoProxy) (*goproxy.Client, error) { - gpClient := goproxy.NewClient(cfg.URL) - - if cfg.URL != "" && cfg.Username != "" && cfg.Password != "" { - tr, err := goproxy.NewBasicAuthTransport(cfg.Username, cfg.Password) - if err != nil { - return nil, err - } - - gpClient.HTTPClient = tr.Client() - } - - return gpClient, nil -} + r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "external_list")) + r.Handler(http.MethodGet, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "external_get")) -func newGitHubClient(ctx context.Context, tk string) *github.Client { - if tk == "" { - return github.NewClient(nil) - } + r.NotFound = http.HandlerFunc(handlers.NotFound) + r.PanicHandler = handlers.PanicHandler - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: tk}, - ) - return github.NewClient(oauth2.NewClient(ctx, ts)) + return http.StripPrefix("/external", r) } func setupTracing(ctx context.Context, cfg tracer.Config, traceServiceURL string) (func(), error) { - tracePropagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}) - traceProvider, err := tracer.NewOTLPProvider(ctx, cfg) - if err != nil { - return nil, fmt.Errorf("setup tracing provider: %w", err) - } + tracePropagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}) + traceProvider, err := tracer.NewOTLPProvider(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("setup tracing provider: %w", err) + } - otel.SetTracerProvider(traceProvider) - otel.SetTextMapPropagator(tracePropagator) + otel.SetTracerProvider(traceProvider) + otel.SetTextMapPropagator(tracePropagator) - if traceServiceURL != "" { - provider := trace.NewProvider("plugin-service", traceProvider, http.DefaultClient, traceServiceURL, 2) - provider.StartWorkers(ctx) + if traceServiceURL != "" { + provider := trace.NewProvider("plugin-service", traceProvider, http.DefaultClient, traceServiceURL, 2) + provider.StartWorkers(ctx) - otel.SetTracerProvider(provider) - } + otel.SetTracerProvider(provider) + } - return func() { - _ = traceProvider.Stop(ctx) - }, nil + return func() { + _ = traceProvider.Stop(ctx) + }, nil } diff --git a/go.mod b/go.mod index b33f51b..7aaf79c 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 go.opentelemetry.io/otel/sdk v1.27.0 go.opentelemetry.io/otel/trace v1.27.0 + golang.org/x/mod v0.18.0 golang.org/x/oauth2 v0.21.0 ) @@ -39,6 +40,7 @@ require ( github.com/montanaflynn/stats v0.7.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect @@ -47,7 +49,6 @@ require ( go.opentelemetry.io/otel/metric v1.27.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect golang.org/x/crypto v0.24.0 // indirect - golang.org/x/mod v0.18.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect diff --git a/go.sum b/go.sum index 019f9f2..45aeae6 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/traefik/hub-trace-kpi v0.13.0 h1:2iWUgt77TVBW0XRm02IxNtlathBNO1gtCgTGESUn0SY= diff --git a/pkg/handlers/dbmock_test.go b/pkg/handlers/dbmock_test.go deleted file mode 100644 index eeeecd1..0000000 --- a/pkg/handlers/dbmock_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package handlers - -import ( - "context" - - "github.com/traefik/plugin-service/pkg/db" -) - -type mockDB struct { - getFn func(ctx context.Context, id string) (db.Plugin, error) - deleteFn func(ctx context.Context, id string) error - createFn func(context.Context, db.Plugin) (db.Plugin, error) - listFn func(context.Context, db.Pagination) ([]db.Plugin, string, error) - getByNameFn func(context.Context, string, bool) (db.Plugin, error) - searchByNameFn func(context.Context, string, db.Pagination) ([]db.Plugin, string, error) - updateFn func(context.Context, string, db.Plugin) (db.Plugin, error) - - deleteHashFn func(ctx context.Context, id string) error - createHashFn func(ctx context.Context, module, version, hash string) (db.PluginHash, error) - getHashByNameFn func(ctx context.Context, module, version string) (db.PluginHash, error) -} - -func (m mockDB) Get(ctx context.Context, id string) (db.Plugin, error) { - return m.getFn(ctx, id) -} - -func (m mockDB) Delete(ctx context.Context, id string) error { - return m.deleteFn(ctx, id) -} - -func (m mockDB) Create(ctx context.Context, plugin db.Plugin) (db.Plugin, error) { - return m.createFn(ctx, plugin) -} - -func (m mockDB) List(ctx context.Context, pagination db.Pagination) ([]db.Plugin, string, error) { - return m.listFn(ctx, pagination) -} - -func (m mockDB) GetByName(ctx context.Context, name string, _, filterHidden bool) (db.Plugin, error) { - return m.getByNameFn(ctx, name, filterHidden) -} - -func (m mockDB) SearchByName(ctx context.Context, query string, pagination db.Pagination) ([]db.Plugin, string, error) { - return m.searchByNameFn(ctx, query, pagination) -} - -func (m mockDB) Update(ctx context.Context, id string, plugin db.Plugin) (db.Plugin, error) { - return m.updateFn(ctx, id, plugin) -} - -func (m mockDB) DeleteHash(ctx context.Context, id string) error { - return m.deleteHashFn(ctx, id) -} - -func (m mockDB) CreateHash(ctx context.Context, module, version, hash string) (db.PluginHash, error) { - return m.createHashFn(ctx, module, version, hash) -} - -func (m mockDB) GetHashByName(ctx context.Context, module, version string) (db.PluginHash, error) { - return m.getHashByNameFn(ctx, module, version) -} diff --git a/pkg/handlers/github.go b/pkg/handlers/github.go new file mode 100644 index 0000000..5c8a56d --- /dev/null +++ b/pkg/handlers/github.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "context" + "net/http" + "net/url" + + "github.com/google/go-github/v57/github" + "golang.org/x/oauth2" +) + +type GithubClient struct { + client *github.Client +} + +type GithubPluginClient interface { + Do(ctx context.Context, req *http.Request, v interface{}) (*github.Response, error) + GetArchiveLink(ctx context.Context, owner, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) (*url.URL, *github.Response, error) + GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error) +} + +// NewGithubClient create a new GitHub client. +func NewGithubClient(ctx context.Context, token string) *GithubClient { + if token == "" { + return &GithubClient{github.NewClient(nil)} + } + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + + return &GithubClient{github.NewClient(oauth2.NewClient(ctx, ts))} +} + +func (c GithubClient) Do(ctx context.Context, req *http.Request, v interface{}) (*github.Response, error) { + return c.client.Do(ctx, req, v) +} + +func (c GithubClient) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) (*url.URL, *github.Response, error) { + return c.client.Repositories.GetArchiveLink(ctx, owner, repo, archiveformat, opts, maxRedirects) +} + +func (c GithubClient) GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error) { + return c.client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) +} diff --git a/pkg/handlers/goproxy.go b/pkg/handlers/goproxy.go new file mode 100644 index 0000000..c18873a --- /dev/null +++ b/pkg/handlers/goproxy.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "io" + + "github.com/ldez/grignotin/goproxy" + "golang.org/x/mod/modfile" +) + +type GoproxyClient struct { + client *goproxy.Client +} + +type GoproxyPluginClient interface { + DownloadSources(moduleName, version string) (io.ReadCloser, error) + GetModFile(moduleName, version string) (*modfile.File, error) +} + +// NewGoproxyClient creates a new Goproxy client. +func NewGoproxyClient(url, username, password string) (*GoproxyClient, error) { + gpClient := goproxy.NewClient(url) + + if url != "" && username != "" && password != "" { + tr, err := goproxy.NewBasicAuthTransport(username, password) + if err != nil { + return nil, err + } + + gpClient.HTTPClient = tr.Client() + } + + return &GoproxyClient{client: gpClient}, nil +} + +func (c GoproxyClient) DownloadSources(moduleName, version string) (io.ReadCloser, error) { + return c.client.DownloadSources(moduleName, version) +} + +func (c GoproxyClient) GetModFile(moduleName, version string) (*modfile.File, error) { + return c.client.GetModFile(moduleName, version) +} diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index bc3e277..824a0f6 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -1,401 +1,402 @@ package handlers import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "regexp" - "strconv" - - "github.com/google/go-github/v57/github" - "github.com/ldez/grignotin/goproxy" - "github.com/rs/zerolog/log" - "github.com/traefik/plugin-service/pkg/db" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "regexp" + "strconv" + "time" + + "github.com/rs/zerolog/log" + "github.com/traefik/plugin-service/pkg/db" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" ) const ( - nextPageHeader = "X-Next-Page" - defaultPerPage = 200 // TODO(ldez): we use 200 because currently the UI doesn't use the pagination. Must be changed. + nextPageHeader = "X-Next-Page" + defaultPerPage = 200 // TODO(ldez): we use 200 because currently the UI doesn't use the pagination. Must be changed. ) // PluginStorer is capable of storing plugins. type PluginStorer interface { - Get(ctx context.Context, id string) (db.Plugin, error) - Delete(ctx context.Context, id string) error - Create(context.Context, db.Plugin) (db.Plugin, error) - List(context.Context, db.Pagination) ([]db.Plugin, string, error) - GetByName(context.Context, string, bool, bool) (db.Plugin, error) - SearchByName(context.Context, string, db.Pagination) ([]db.Plugin, string, error) - Update(context.Context, string, db.Plugin) (db.Plugin, error) - - CreateHash(ctx context.Context, module, version, hash string) (db.PluginHash, error) - GetHashByName(ctx context.Context, module, version string) (db.PluginHash, error) + Get(ctx context.Context, id string) (db.Plugin, error) + Delete(ctx context.Context, id string) error + Create(context.Context, db.Plugin) (db.Plugin, error) + List(context.Context, db.Pagination) ([]db.Plugin, string, error) + GetByName(context.Context, string, bool, bool) (db.Plugin, error) + SearchByName(context.Context, string, db.Pagination) ([]db.Plugin, string, error) + Update(context.Context, string, db.Plugin) (db.Plugin, error) + + CreateHash(ctx context.Context, module, version, hash string) (db.PluginHash, error) + GetHashByName(ctx context.Context, module, version string) (db.PluginHash, error) } // Handlers a set of handlers. type Handlers struct { - store PluginStorer - goProxy *goproxy.Client - gh *github.Client - tracer trace.Tracer + store PluginStorer + goProxy GoproxyPluginClient + gh GithubPluginClient + tracer trace.Tracer + ttl int } // New creates all HTTP handlers. -func New(store PluginStorer, goProxy *goproxy.Client, gh *github.Client) Handlers { - return Handlers{ - store: store, - goProxy: goProxy, - gh: gh, - tracer: otel.GetTracerProvider().Tracer("handler"), - } +func New(store PluginStorer, goProxy GoproxyPluginClient, gh GithubPluginClient, ttl time.Duration) Handlers { + return Handlers{ + store: store, + goProxy: goProxy, + gh: gh, + tracer: otel.GetTracerProvider().Tracer("handler"), + ttl: int(ttl.Seconds()), + } } // Get gets a plugin. func (h Handlers) Get(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_get") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - id, err := getPathParam(req.URL) - if err != nil { - span.RecordError(err) - JSONError(rw, http.StatusBadRequest, "Missing plugin id") - return - } - - logger := log.With().Str("plugin_id", id).Logger() - - plugin, err := h.store.Get(ctx, id) - if err != nil { - span.RecordError(err) - - if errors.As(err, &db.NotFoundError{}) { - NotFound(rw, req) - return - } - - logger.Error().Err(err).Msg("Error while trying to get plugin") - JSONInternalServerError(rw) - return - } - - if err := json.NewEncoder(rw).Encode(plugin); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_get") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + id, err := getPathParam(req.URL) + if err != nil { + span.RecordError(err) + JSONError(rw, http.StatusBadRequest, "Missing plugin id") + return + } + + logger := log.With().Str("plugin_id", id).Logger() + + plugin, err := h.store.Get(ctx, id) + if err != nil { + span.RecordError(err) + + if errors.As(err, &db.NotFoundError{}) { + NotFound(rw, req) + return + } + + logger.Error().Err(err).Msg("Error while trying to get plugin") + JSONInternalServerError(rw) + return + } + + if err := json.NewEncoder(rw).Encode(plugin); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin") + JSONInternalServerError(rw) + return + } } // List gets a list of plugins. func (h Handlers) List(rw http.ResponseWriter, req *http.Request) { - rw.Header().Set("Content-Type", "application/json") + rw.Header().Set("Content-Type", "application/json") - if value := req.FormValue("query"); value != "" { - h.searchByName(rw, req) - return - } + if value := req.FormValue("query"); value != "" { + h.searchByName(rw, req) + return + } - if value := req.FormValue("name"); value != "" { - h.getByName(rw, req) - return - } + if value := req.FormValue("name"); value != "" { + h.getByName(rw, req) + return + } - h.list(rw, req) + h.list(rw, req) } // Create creates a plugin. func (h Handlers) Create(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_create") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - body, err := io.ReadAll(req.Body) - if err != nil { - span.RecordError(err) - log.Error().Err(err).Msg("Error reading body for creation") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - if len(body) == 0 { - err = errors.New("empty body") - span.RecordError(err) - log.Error().Err(err).Msg("Error decoding plugin for creation") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - pl := db.Plugin{} - - err = json.Unmarshal(body, &pl) - if err != nil { - span.RecordError(err) - log.Error().Err(err).Msg("Error decoding plugin for creation") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - logger := log.With().Str("module_name", pl.Name).Logger() - - created, err := h.store.Create(ctx, pl) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error persisting plugin") - JSONError(rw, http.StatusInternalServerError, "Could not persist data") - return - } - - rw.WriteHeader(http.StatusCreated) - if err := json.NewEncoder(rw).Encode(created); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error sending create response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_create") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + body, err := io.ReadAll(req.Body) + if err != nil { + span.RecordError(err) + log.Error().Err(err).Msg("Error reading body for creation") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + if len(body) == 0 { + err = errors.New("empty body") + span.RecordError(err) + log.Error().Err(err).Msg("Error decoding plugin for creation") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + pl := db.Plugin{} + + err = json.Unmarshal(body, &pl) + if err != nil { + span.RecordError(err) + log.Error().Err(err).Msg("Error decoding plugin for creation") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + logger := log.With().Str("module_name", pl.Name).Logger() + + created, err := h.store.Create(ctx, pl) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error persisting plugin") + JSONError(rw, http.StatusInternalServerError, "Could not persist data") + return + } + + rw.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(rw).Encode(created); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error sending create response") + JSONInternalServerError(rw) + return + } } // Update updates a plugin. func (h Handlers) Update(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_update") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - id, err := getPathParam(req.URL) - if err != nil { - span.RecordError(err) - log.Error().Err(err).Msg("Missing plugin id") - JSONError(rw, http.StatusBadRequest, "Missing plugin id") - return - } - - logger := log.With().Str("plugin_id", id).Logger() - - input := db.Plugin{} - err = json.NewDecoder(req.Body).Decode(&input) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error reading body for update") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - pg, err := h.store.Update(ctx, id, input) - if err != nil { - span.RecordError(err) - - if errors.As(err, &db.NotFoundError{}) { - span.RecordError(err) - log.Error().Err(err).Msg("Plugin not found") - NotFound(rw, req) - return - } - - logger.Error().Err(err).Msg("Error updating plugin") - JSONInternalServerError(rw) - return - } - - rw.WriteHeader(http.StatusOK) - if err := json.NewEncoder(rw).Encode(pg); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to marshal plugin") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_update") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + id, err := getPathParam(req.URL) + if err != nil { + span.RecordError(err) + log.Error().Err(err).Msg("Missing plugin id") + JSONError(rw, http.StatusBadRequest, "Missing plugin id") + return + } + + logger := log.With().Str("plugin_id", id).Logger() + + input := db.Plugin{} + err = json.NewDecoder(req.Body).Decode(&input) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error reading body for update") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + pg, err := h.store.Update(ctx, id, input) + if err != nil { + span.RecordError(err) + + if errors.As(err, &db.NotFoundError{}) { + span.RecordError(err) + log.Error().Err(err).Msg("Plugin not found") + NotFound(rw, req) + return + } + + logger.Error().Err(err).Msg("Error updating plugin") + JSONInternalServerError(rw) + return + } + + rw.WriteHeader(http.StatusOK) + if err := json.NewEncoder(rw).Encode(pg); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to marshal plugin") + JSONInternalServerError(rw) + return + } } // Delete deletes an instance info. func (h Handlers) Delete(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_delete") - defer span.End() - - id, err := getPathParam(req.URL) - if err != nil { - span.RecordError(err) - log.Warn().Err(err).Msg("Missing plugin id") - JSONError(rw, http.StatusBadRequest, "Missing plugin id") - return - } - - logger := log.With().Str("plugin_id", id).Logger() - - _, err = h.store.Get(ctx, id) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin information") - NotFound(rw, req) - return - } - - err = h.store.Delete(ctx, id) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to delete the plugin info") - JSONError(rw, http.StatusInternalServerError, "Failed to delete plugin info") - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_delete") + defer span.End() + + id, err := getPathParam(req.URL) + if err != nil { + span.RecordError(err) + log.Warn().Err(err).Msg("Missing plugin id") + JSONError(rw, http.StatusBadRequest, "Missing plugin id") + return + } + + logger := log.With().Str("plugin_id", id).Logger() + + _, err = h.store.Get(ctx, id) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin information") + NotFound(rw, req) + return + } + + err = h.store.Delete(ctx, id) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to delete the plugin info") + JSONError(rw, http.StatusInternalServerError, "Failed to delete plugin info") + return + } } func (h Handlers) searchByName(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_searchByName") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - query := unquote(req.FormValue("query")) - start := req.URL.Query().Get("start") - - logger := log.With().Str("search_query", query).Str("search_start", start).Logger() - - plugins, next, err := h.store.SearchByName(ctx, query, db.Pagination{ - Start: start, - Size: defaultPerPage, - }) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Unable to get plugins by name") - JSONError(rw, http.StatusBadRequest, "Unable to get plugins") - return - } - - if len(plugins) == 0 { - if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error sending create response") - JSONInternalServerError(rw) - } - return - } - - rw.Header().Set(nextPageHeader, next) - - if err := json.NewEncoder(rw).Encode(plugins); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error sending create response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_searchByName") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + query := unquote(req.FormValue("query")) + start := req.URL.Query().Get("start") + + logger := log.With().Str("search_query", query).Str("search_start", start).Logger() + + plugins, next, err := h.store.SearchByName(ctx, query, db.Pagination{ + Start: start, + Size: defaultPerPage, + }) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Unable to get plugins by name") + JSONError(rw, http.StatusBadRequest, "Unable to get plugins") + return + } + + if len(plugins) == 0 { + if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error sending create response") + JSONInternalServerError(rw) + } + return + } + + rw.Header().Set(nextPageHeader, next) + + if err := json.NewEncoder(rw).Encode(plugins); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error sending create response") + JSONInternalServerError(rw) + return + } } func (h Handlers) getByName(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_getByName") - defer span.End() - - name := unquote(req.FormValue("name")) - logger := log.With().Str("module_name", name).Logger() - - var filterHidden bool - var err error - if value := req.FormValue("filterHidden"); value != "" { - filterHidden, err = strconv.ParseBool(value) - if err != nil { - logger.Debug().Err(err).Msg("Unable to parse filterHidden field") - JSONInternalServerError(rw) - return - } - } - - plugin, err := h.store.GetByName(ctx, name, true, filterHidden) - if err != nil { - span.RecordError(err) - - if errors.As(err, &db.NotFoundError{}) { - logger.Error().Msg("plugin not found") - NotFound(rw, req) - return - } - - logger.Error().Err(err).Msg("Error while fetch") - JSONInternalServerError(rw) - return - } - - if err := json.NewEncoder(rw).Encode([]*db.Plugin{&plugin}); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to encode response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_getByName") + defer span.End() + + name := unquote(req.FormValue("name")) + logger := log.With().Str("module_name", name).Logger() + + var filterHidden bool + var err error + if value := req.FormValue("filterHidden"); value != "" { + filterHidden, err = strconv.ParseBool(value) + if err != nil { + logger.Debug().Err(err).Msg("Unable to parse filterHidden field") + JSONInternalServerError(rw) + return + } + } + + plugin, err := h.store.GetByName(ctx, name, true, filterHidden) + if err != nil { + span.RecordError(err) + + if errors.As(err, &db.NotFoundError{}) { + logger.Error().Msg("plugin not found") + NotFound(rw, req) + return + } + + logger.Error().Err(err).Msg("Error while fetch") + JSONInternalServerError(rw) + return + } + + if err := json.NewEncoder(rw).Encode([]*db.Plugin{&plugin}); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to encode response") + JSONInternalServerError(rw) + return + } } func (h Handlers) list(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_list") - defer span.End() - - start := req.URL.Query().Get("start") - - logger := log.With().Str("search_start", start).Logger() - - plugins, next, err := h.store.List(ctx, db.Pagination{ - Start: start, - Size: defaultPerPage, - }) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error fetching plugins") - NotFound(rw, req) - return - } - - // TODO: detection of the plugin name changes must be done in piceus. - var cleanPlugins []db.Plugin - for _, plugin := range plugins { - if plugin.Name == "github.com/tommoulard/fail2ban" || plugin.Name == "github.com/tommoulard/htransformation" { - continue - } - - cleanPlugins = append(cleanPlugins, plugin) - } - - if len(cleanPlugins) == 0 { - if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to encode response") - JSONInternalServerError(rw) - } - return - } - - rw.Header().Set(nextPageHeader, next) - - if err := json.NewEncoder(rw).Encode(cleanPlugins); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to encode response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_list") + defer span.End() + + start := req.URL.Query().Get("start") + + logger := log.With().Str("search_start", start).Logger() + + plugins, next, err := h.store.List(ctx, db.Pagination{ + Start: start, + Size: defaultPerPage, + }) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error fetching plugins") + NotFound(rw, req) + return + } + + // TODO: detection of the plugin name changes must be done in piceus. + var cleanPlugins []db.Plugin + for _, plugin := range plugins { + if plugin.Name == "github.com/tommoulard/fail2ban" || plugin.Name == "github.com/tommoulard/htransformation" { + continue + } + + cleanPlugins = append(cleanPlugins, plugin) + } + + if len(cleanPlugins) == 0 { + if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to encode response") + JSONInternalServerError(rw) + } + return + } + + rw.Header().Set(nextPageHeader, next) + + if err := json.NewEncoder(rw).Encode(cleanPlugins); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to encode response") + JSONInternalServerError(rw) + return + } } // NotFound a not found handler. func NotFound(rw http.ResponseWriter, _ *http.Request) { - JSONError(rw, http.StatusNotFound, http.StatusText(http.StatusNotFound)) + JSONError(rw, http.StatusNotFound, http.StatusText(http.StatusNotFound)) } func getPathParam(uri *url.URL) (string, error) { - exp := regexp.MustCompile(`^/([\w-]+)/?$`) - parts := exp.FindStringSubmatch(uri.Path) + exp := regexp.MustCompile(`^/([\w-]+)/?$`) + parts := exp.FindStringSubmatch(uri.Path) - if len(parts) != 2 { - return "", errors.New("missing id") - } + if len(parts) != 2 { + return "", errors.New("missing id") + } - return parts[1], nil + return parts[1], nil } func unquote(value string) string { - unquote, err := strconv.Unquote(value) - if err != nil { - return value - } + unquote, err := strconv.Unquote(value) + if err != nil { + return value + } - return unquote + return unquote } diff --git a/pkg/handlers/handlers_test.go b/pkg/handlers/handlers_test.go index 6b31f41..557bc76 100644 --- a/pkg/handlers/handlers_test.go +++ b/pkg/handlers/handlers_test.go @@ -1,197 +1,184 @@ package handlers import ( - "context" - "net/http" - "net/http/httptest" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/traefik/plugin-service/pkg/db" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/plugin-service/pkg/db" ) func TestHandlers_List(t *testing.T) { - data := []db.Plugin{ - { - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - }, - { - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Block Path", - ID: "2768097807845374", - Import: "github.com/traefik/plugin-blockpath", - LatestVersion: "v0.3.1", - Name: "github.com/traefik/plugin-blockpath", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 3, - Summary: "Block Path plugin", - Type: "middleware", - Versions: []string{"v0.3.1", "v0.2.0", "v0.1.0"}, - }, - } - - testDB := mockDB{ - listFn: func(_ context.Context, _ db.Pagination) ([]db.Plugin, string, error) { - return data, "next", nil - }, - } - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) - - New(testDB, nil, nil).List(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "next", rw.Header().Get(nextPageHeader)) - - file, err := os.ReadFile("./fixtures/get_plugins.json") - require.NoError(t, err) - - assert.JSONEq(t, string(file), rw.Body.String()) + data := []db.Plugin{ + { + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + }, + { + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Block Path", + ID: "2768097807845374", + Import: "github.com/traefik/plugin-blockpath", + LatestVersion: "v0.3.1", + Name: "github.com/traefik/plugin-blockpath", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 3, + Summary: "Block Path plugin", + Type: "middleware", + Versions: []string{"v0.3.1", "v0.2.0", "v0.1.0"}, + }, + } + + testDB := NewPluginStorerMock(t).OnList(db.Pagination{ + Start: "", + Size: 200, + }).Once().TypedReturns(data, "next", nil).Parent + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + + New(testDB, nil, nil, 0).List(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "next", rw.Header().Get(nextPageHeader)) + + file, err := os.ReadFile("./fixtures/get_plugins.json") + require.NoError(t, err) + + assert.JSONEq(t, string(file), rw.Body.String()) } func TestHandlers_List_GetByName(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } - - testDB := mockDB{ - getByNameFn: func(_ context.Context, _ string, _ bool) (db.Plugin, error) { - return data, nil - }, - } - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) - - New(testDB, nil, nil).getByName(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - - file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") - require.NoError(t, err) - - assert.JSONEq(t, string(file), rw.Body.String()) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("Demo Plugin", true, false).Once().TypedReturns(data, nil).Parent + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) + + New(testDB, nil, nil, 0).getByName(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + + file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") + require.NoError(t, err) + + assert.JSONEq(t, string(file), rw.Body.String()) } func TestHandlers_List_GetByName_hidden(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - Hidden: true, - } + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + Hidden: true, + } + + _ = data - testDB := mockDB{ - getByNameFn: func(_ context.Context, _ string, filterHidden bool) (db.Plugin, error) { - if filterHidden { - return data, nil - } - return db.Plugin{}, db.NotFoundError{} - }, - } + testDB := NewPluginStorerMock(t). + OnGetByName("Demo Plugin", true, true).Once().TypedReturns(data, nil). + OnGetByName("Demo Plugin", true, false).Once().TypedReturns(db.Plugin{}, db.NotFoundError{}).Parent - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) - New(testDB, nil, nil).getByName(rw, req) + New(testDB, nil, nil, 0).getByName(rw, req) - assert.Equal(t, http.StatusNotFound, rw.Code) + assert.Equal(t, http.StatusNotFound, rw.Code) - rw = httptest.NewRecorder() + rw = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin&filterHidden=true", http.NoBody) + req = httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin&filterHidden=true", http.NoBody) - New(testDB, nil, nil).getByName(rw, req) + New(testDB, nil, nil, 0).getByName(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusOK, rw.Code) - file, err := os.ReadFile("./fixtures/get_plugin_by_name_hidden.json") - require.NoError(t, err) + file, err := os.ReadFile("./fixtures/get_plugin_by_name_hidden.json") + require.NoError(t, err) - assert.JSONEq(t, string(file), rw.Body.String()) + assert.JSONEq(t, string(file), rw.Body.String()) } func TestHandlers_List_SearchByName(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } - - testDB := mockDB{ - getByNameFn: func(_ context.Context, _ string, _ bool) (db.Plugin, error) { - return data, nil - }, - } - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/?query=demo", http.NoBody) - - New(testDB, nil, nil).getByName(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - - file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") - require.NoError(t, err) - - assert.JSONEq(t, string(file), rw.Body.String()) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("", true, false).Once().TypedReturns(data, nil).Parent + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/?query=demo", http.NoBody) + + New(testDB, nil, nil, 0).getByName(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + + file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") + require.NoError(t, err) + + assert.JSONEq(t, string(file), rw.Body.String()) } diff --git a/pkg/handlers/mock_gen.go b/pkg/handlers/mock_gen.go new file mode 100644 index 0000000..777fbf5 --- /dev/null +++ b/pkg/handlers/mock_gen.go @@ -0,0 +1,2009 @@ +// Code generated by mocktail; DO NOT EDIT. + +package handlers + +import ( + "context" + "io" + "net/http" + "net/url" + "testing" + "time" + + "github.com/google/go-github/v57/github" + "github.com/stretchr/testify/mock" + "github.com/traefik/plugin-service/pkg/db" + "golang.org/x/mod/modfile" +) + +// githubPluginClientMock mock of GithubPluginClient. +type githubPluginClientMock struct{ mock.Mock } + +// NewGithubPluginClientMock creates a new githubPluginClientMock. +func NewGithubPluginClientMock(tb testing.TB) *githubPluginClientMock { + tb.Helper() + + m := &githubPluginClientMock{} + m.Mock.Test(tb) + + tb.Cleanup(func() { m.AssertExpectations(tb) }) + + return m +} + +func (_m *githubPluginClientMock) Do(_ context.Context, req *http.Request, v interface{}) (*github.Response, error) { + _ret := _m.Called(req, v) + + if _rf, ok := _ret.Get(0).(func(*http.Request, interface{}) (*github.Response, error)); ok { + return _rf(req, v) + } + + _ra0, _ := _ret.Get(0).(*github.Response) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *githubPluginClientMock) OnDo(req *http.Request, v interface{}) *githubPluginClientDoCall { + return &githubPluginClientDoCall{Call: _m.Mock.On("Do", req, v), Parent: _m} +} + +func (_m *githubPluginClientMock) OnDoRaw(req interface{}, v interface{}) *githubPluginClientDoCall { + return &githubPluginClientDoCall{Call: _m.Mock.On("Do", req, v), Parent: _m} +} + +type githubPluginClientDoCall struct { + *mock.Call + Parent *githubPluginClientMock +} + +func (_c *githubPluginClientDoCall) Panic(msg string) *githubPluginClientDoCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *githubPluginClientDoCall) Once() *githubPluginClientDoCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *githubPluginClientDoCall) Twice() *githubPluginClientDoCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *githubPluginClientDoCall) Times(i int) *githubPluginClientDoCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *githubPluginClientDoCall) WaitUntil(w <-chan time.Time) *githubPluginClientDoCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *githubPluginClientDoCall) After(d time.Duration) *githubPluginClientDoCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *githubPluginClientDoCall) Run(fn func(args mock.Arguments)) *githubPluginClientDoCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *githubPluginClientDoCall) Maybe() *githubPluginClientDoCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *githubPluginClientDoCall) TypedReturns(a *github.Response, b error) *githubPluginClientDoCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *githubPluginClientDoCall) ReturnsFn(fn func(*http.Request, interface{}) (*github.Response, error)) *githubPluginClientDoCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *githubPluginClientDoCall) TypedRun(fn func(*http.Request, interface{})) *githubPluginClientDoCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _req, _ := args.Get(0).(*http.Request) + _v, _ := args.Get(1).(interface{}) + fn(_req, _v) + }) + return _c +} + +func (_c *githubPluginClientDoCall) OnDo(req *http.Request, v interface{}) *githubPluginClientDoCall { + return _c.Parent.OnDo(req, v) +} + +func (_c *githubPluginClientDoCall) OnGetArchiveLink(owner string, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) *githubPluginClientGetArchiveLinkCall { + return _c.Parent.OnGetArchiveLink(owner, repo, archiveformat, opts, maxRedirects) +} + +func (_c *githubPluginClientDoCall) OnGetReleaseByTag(owner string, repo string, tag string) *githubPluginClientGetReleaseByTagCall { + return _c.Parent.OnGetReleaseByTag(owner, repo, tag) +} + +func (_c *githubPluginClientDoCall) OnDoRaw(req interface{}, v interface{}) *githubPluginClientDoCall { + return _c.Parent.OnDoRaw(req, v) +} + +func (_c *githubPluginClientDoCall) OnGetArchiveLinkRaw(owner interface{}, repo interface{}, archiveformat interface{}, opts interface{}, maxRedirects interface{}) *githubPluginClientGetArchiveLinkCall { + return _c.Parent.OnGetArchiveLinkRaw(owner, repo, archiveformat, opts, maxRedirects) +} + +func (_c *githubPluginClientDoCall) OnGetReleaseByTagRaw(owner interface{}, repo interface{}, tag interface{}) *githubPluginClientGetReleaseByTagCall { + return _c.Parent.OnGetReleaseByTagRaw(owner, repo, tag) +} + +func (_m *githubPluginClientMock) GetArchiveLink(_ context.Context, owner string, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) (*url.URL, *github.Response, error) { + _ret := _m.Called(owner, repo, archiveformat, opts, maxRedirects) + + if _rf, ok := _ret.Get(0).(func(string, string, github.ArchiveFormat, *github.RepositoryContentGetOptions, int) (*url.URL, *github.Response, error)); ok { + return _rf(owner, repo, archiveformat, opts, maxRedirects) + } + + _ra0, _ := _ret.Get(0).(*url.URL) + _rb1, _ := _ret.Get(1).(*github.Response) + _rc2 := _ret.Error(2) + + return _ra0, _rb1, _rc2 +} + +func (_m *githubPluginClientMock) OnGetArchiveLink(owner string, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) *githubPluginClientGetArchiveLinkCall { + return &githubPluginClientGetArchiveLinkCall{Call: _m.Mock.On("GetArchiveLink", owner, repo, archiveformat, opts, maxRedirects), Parent: _m} +} + +func (_m *githubPluginClientMock) OnGetArchiveLinkRaw(owner interface{}, repo interface{}, archiveformat interface{}, opts interface{}, maxRedirects interface{}) *githubPluginClientGetArchiveLinkCall { + return &githubPluginClientGetArchiveLinkCall{Call: _m.Mock.On("GetArchiveLink", owner, repo, archiveformat, opts, maxRedirects), Parent: _m} +} + +type githubPluginClientGetArchiveLinkCall struct { + *mock.Call + Parent *githubPluginClientMock +} + +func (_c *githubPluginClientGetArchiveLinkCall) Panic(msg string) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) Once() *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) Twice() *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) Times(i int) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) WaitUntil(w <-chan time.Time) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) After(d time.Duration) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) Run(fn func(args mock.Arguments)) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) Maybe() *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) TypedReturns(a *url.URL, b *github.Response, c error) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Return(a, b, c) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) ReturnsFn(fn func(string, string, github.ArchiveFormat, *github.RepositoryContentGetOptions, int) (*url.URL, *github.Response, error)) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) TypedRun(fn func(string, string, github.ArchiveFormat, *github.RepositoryContentGetOptions, int)) *githubPluginClientGetArchiveLinkCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _owner := args.String(0) + _repo := args.String(1) + _archiveformat, _ := args.Get(2).(github.ArchiveFormat) + _opts, _ := args.Get(3).(*github.RepositoryContentGetOptions) + _maxRedirects := args.Int(4) + fn(_owner, _repo, _archiveformat, _opts, _maxRedirects) + }) + return _c +} + +func (_c *githubPluginClientGetArchiveLinkCall) OnDo(req *http.Request, v interface{}) *githubPluginClientDoCall { + return _c.Parent.OnDo(req, v) +} + +func (_c *githubPluginClientGetArchiveLinkCall) OnGetArchiveLink(owner string, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) *githubPluginClientGetArchiveLinkCall { + return _c.Parent.OnGetArchiveLink(owner, repo, archiveformat, opts, maxRedirects) +} + +func (_c *githubPluginClientGetArchiveLinkCall) OnGetReleaseByTag(owner string, repo string, tag string) *githubPluginClientGetReleaseByTagCall { + return _c.Parent.OnGetReleaseByTag(owner, repo, tag) +} + +func (_c *githubPluginClientGetArchiveLinkCall) OnDoRaw(req interface{}, v interface{}) *githubPluginClientDoCall { + return _c.Parent.OnDoRaw(req, v) +} + +func (_c *githubPluginClientGetArchiveLinkCall) OnGetArchiveLinkRaw(owner interface{}, repo interface{}, archiveformat interface{}, opts interface{}, maxRedirects interface{}) *githubPluginClientGetArchiveLinkCall { + return _c.Parent.OnGetArchiveLinkRaw(owner, repo, archiveformat, opts, maxRedirects) +} + +func (_c *githubPluginClientGetArchiveLinkCall) OnGetReleaseByTagRaw(owner interface{}, repo interface{}, tag interface{}) *githubPluginClientGetReleaseByTagCall { + return _c.Parent.OnGetReleaseByTagRaw(owner, repo, tag) +} + +func (_m *githubPluginClientMock) GetReleaseByTag(_ context.Context, owner string, repo string, tag string) (*github.RepositoryRelease, *github.Response, error) { + _ret := _m.Called(owner, repo, tag) + + if _rf, ok := _ret.Get(0).(func(string, string, string) (*github.RepositoryRelease, *github.Response, error)); ok { + return _rf(owner, repo, tag) + } + + _ra0, _ := _ret.Get(0).(*github.RepositoryRelease) + _rb1, _ := _ret.Get(1).(*github.Response) + _rc2 := _ret.Error(2) + + return _ra0, _rb1, _rc2 +} + +func (_m *githubPluginClientMock) OnGetReleaseByTag(owner string, repo string, tag string) *githubPluginClientGetReleaseByTagCall { + return &githubPluginClientGetReleaseByTagCall{Call: _m.Mock.On("GetReleaseByTag", owner, repo, tag), Parent: _m} +} + +func (_m *githubPluginClientMock) OnGetReleaseByTagRaw(owner interface{}, repo interface{}, tag interface{}) *githubPluginClientGetReleaseByTagCall { + return &githubPluginClientGetReleaseByTagCall{Call: _m.Mock.On("GetReleaseByTag", owner, repo, tag), Parent: _m} +} + +type githubPluginClientGetReleaseByTagCall struct { + *mock.Call + Parent *githubPluginClientMock +} + +func (_c *githubPluginClientGetReleaseByTagCall) Panic(msg string) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) Once() *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) Twice() *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) Times(i int) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) WaitUntil(w <-chan time.Time) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) After(d time.Duration) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) Run(fn func(args mock.Arguments)) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) Maybe() *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) TypedReturns(a *github.RepositoryRelease, b *github.Response, c error) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Return(a, b, c) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) ReturnsFn(fn func(string, string, string) (*github.RepositoryRelease, *github.Response, error)) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) TypedRun(fn func(string, string, string)) *githubPluginClientGetReleaseByTagCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _owner := args.String(0) + _repo := args.String(1) + _tag := args.String(2) + fn(_owner, _repo, _tag) + }) + return _c +} + +func (_c *githubPluginClientGetReleaseByTagCall) OnDo(req *http.Request, v interface{}) *githubPluginClientDoCall { + return _c.Parent.OnDo(req, v) +} + +func (_c *githubPluginClientGetReleaseByTagCall) OnGetArchiveLink(owner string, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) *githubPluginClientGetArchiveLinkCall { + return _c.Parent.OnGetArchiveLink(owner, repo, archiveformat, opts, maxRedirects) +} + +func (_c *githubPluginClientGetReleaseByTagCall) OnGetReleaseByTag(owner string, repo string, tag string) *githubPluginClientGetReleaseByTagCall { + return _c.Parent.OnGetReleaseByTag(owner, repo, tag) +} + +func (_c *githubPluginClientGetReleaseByTagCall) OnDoRaw(req interface{}, v interface{}) *githubPluginClientDoCall { + return _c.Parent.OnDoRaw(req, v) +} + +func (_c *githubPluginClientGetReleaseByTagCall) OnGetArchiveLinkRaw(owner interface{}, repo interface{}, archiveformat interface{}, opts interface{}, maxRedirects interface{}) *githubPluginClientGetArchiveLinkCall { + return _c.Parent.OnGetArchiveLinkRaw(owner, repo, archiveformat, opts, maxRedirects) +} + +func (_c *githubPluginClientGetReleaseByTagCall) OnGetReleaseByTagRaw(owner interface{}, repo interface{}, tag interface{}) *githubPluginClientGetReleaseByTagCall { + return _c.Parent.OnGetReleaseByTagRaw(owner, repo, tag) +} + +// pluginStorerMock mock of PluginStorer. +type pluginStorerMock struct{ mock.Mock } + +// NewPluginStorerMock creates a new pluginStorerMock. +func NewPluginStorerMock(tb testing.TB) *pluginStorerMock { + tb.Helper() + + m := &pluginStorerMock{} + m.Mock.Test(tb) + + tb.Cleanup(func() { m.AssertExpectations(tb) }) + + return m +} + +func (_m *pluginStorerMock) Create(_ context.Context, bParam db.Plugin) (db.Plugin, error) { + _ret := _m.Called(bParam) + + if _rf, ok := _ret.Get(0).(func(db.Plugin) (db.Plugin, error)); ok { + return _rf(bParam) + } + + _ra0, _ := _ret.Get(0).(db.Plugin) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *pluginStorerMock) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return &pluginStorerCreateCall{Call: _m.Mock.On("Create", bParam), Parent: _m} +} + +func (_m *pluginStorerMock) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return &pluginStorerCreateCall{Call: _m.Mock.On("Create", bParam), Parent: _m} +} + +type pluginStorerCreateCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerCreateCall) Panic(msg string) *pluginStorerCreateCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerCreateCall) Once() *pluginStorerCreateCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerCreateCall) Twice() *pluginStorerCreateCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerCreateCall) Times(i int) *pluginStorerCreateCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerCreateCall) WaitUntil(w <-chan time.Time) *pluginStorerCreateCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerCreateCall) After(d time.Duration) *pluginStorerCreateCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerCreateCall) Run(fn func(args mock.Arguments)) *pluginStorerCreateCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerCreateCall) Maybe() *pluginStorerCreateCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerCreateCall) TypedReturns(a db.Plugin, b error) *pluginStorerCreateCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *pluginStorerCreateCall) ReturnsFn(fn func(db.Plugin) (db.Plugin, error)) *pluginStorerCreateCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerCreateCall) TypedRun(fn func(db.Plugin)) *pluginStorerCreateCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _bParam, _ := args.Get(0).(db.Plugin) + fn(_bParam) + }) + return _c +} + +func (_c *pluginStorerCreateCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerCreateCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerCreateCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerCreateCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerCreateCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerCreateCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerCreateCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerCreateCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerCreateCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerCreateCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerCreateCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerCreateCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerCreateCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerCreateCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerCreateCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerCreateCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerCreateCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerCreateCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) CreateHash(_ context.Context, module string, version string, hash string) (db.PluginHash, error) { + _ret := _m.Called(module, version, hash) + + if _rf, ok := _ret.Get(0).(func(string, string, string) (db.PluginHash, error)); ok { + return _rf(module, version, hash) + } + + _ra0, _ := _ret.Get(0).(db.PluginHash) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *pluginStorerMock) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return &pluginStorerCreateHashCall{Call: _m.Mock.On("CreateHash", module, version, hash), Parent: _m} +} + +func (_m *pluginStorerMock) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return &pluginStorerCreateHashCall{Call: _m.Mock.On("CreateHash", module, version, hash), Parent: _m} +} + +type pluginStorerCreateHashCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerCreateHashCall) Panic(msg string) *pluginStorerCreateHashCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerCreateHashCall) Once() *pluginStorerCreateHashCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerCreateHashCall) Twice() *pluginStorerCreateHashCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerCreateHashCall) Times(i int) *pluginStorerCreateHashCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerCreateHashCall) WaitUntil(w <-chan time.Time) *pluginStorerCreateHashCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerCreateHashCall) After(d time.Duration) *pluginStorerCreateHashCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerCreateHashCall) Run(fn func(args mock.Arguments)) *pluginStorerCreateHashCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerCreateHashCall) Maybe() *pluginStorerCreateHashCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerCreateHashCall) TypedReturns(a db.PluginHash, b error) *pluginStorerCreateHashCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *pluginStorerCreateHashCall) ReturnsFn(fn func(string, string, string) (db.PluginHash, error)) *pluginStorerCreateHashCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerCreateHashCall) TypedRun(fn func(string, string, string)) *pluginStorerCreateHashCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _module := args.String(0) + _version := args.String(1) + _hash := args.String(2) + fn(_module, _version, _hash) + }) + return _c +} + +func (_c *pluginStorerCreateHashCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerCreateHashCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerCreateHashCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerCreateHashCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerCreateHashCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerCreateHashCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerCreateHashCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerCreateHashCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerCreateHashCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerCreateHashCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerCreateHashCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerCreateHashCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerCreateHashCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerCreateHashCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerCreateHashCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerCreateHashCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerCreateHashCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerCreateHashCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) Delete(_ context.Context, id string) error { + _ret := _m.Called(id) + + if _rf, ok := _ret.Get(0).(func(string) error); ok { + return _rf(id) + } + + _ra0 := _ret.Error(0) + + return _ra0 +} + +func (_m *pluginStorerMock) OnDelete(id string) *pluginStorerDeleteCall { + return &pluginStorerDeleteCall{Call: _m.Mock.On("Delete", id), Parent: _m} +} + +func (_m *pluginStorerMock) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return &pluginStorerDeleteCall{Call: _m.Mock.On("Delete", id), Parent: _m} +} + +type pluginStorerDeleteCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerDeleteCall) Panic(msg string) *pluginStorerDeleteCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerDeleteCall) Once() *pluginStorerDeleteCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerDeleteCall) Twice() *pluginStorerDeleteCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerDeleteCall) Times(i int) *pluginStorerDeleteCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerDeleteCall) WaitUntil(w <-chan time.Time) *pluginStorerDeleteCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerDeleteCall) After(d time.Duration) *pluginStorerDeleteCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerDeleteCall) Run(fn func(args mock.Arguments)) *pluginStorerDeleteCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerDeleteCall) Maybe() *pluginStorerDeleteCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerDeleteCall) TypedReturns(a error) *pluginStorerDeleteCall { + _c.Call = _c.Return(a) + return _c +} + +func (_c *pluginStorerDeleteCall) ReturnsFn(fn func(string) error) *pluginStorerDeleteCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerDeleteCall) TypedRun(fn func(string)) *pluginStorerDeleteCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _id := args.String(0) + fn(_id) + }) + return _c +} + +func (_c *pluginStorerDeleteCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerDeleteCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerDeleteCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerDeleteCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerDeleteCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerDeleteCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerDeleteCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerDeleteCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerDeleteCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerDeleteCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerDeleteCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerDeleteCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerDeleteCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerDeleteCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerDeleteCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerDeleteCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerDeleteCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerDeleteCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) Get(_ context.Context, id string) (db.Plugin, error) { + _ret := _m.Called(id) + + if _rf, ok := _ret.Get(0).(func(string) (db.Plugin, error)); ok { + return _rf(id) + } + + _ra0, _ := _ret.Get(0).(db.Plugin) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *pluginStorerMock) OnGet(id string) *pluginStorerGetCall { + return &pluginStorerGetCall{Call: _m.Mock.On("Get", id), Parent: _m} +} + +func (_m *pluginStorerMock) OnGetRaw(id interface{}) *pluginStorerGetCall { + return &pluginStorerGetCall{Call: _m.Mock.On("Get", id), Parent: _m} +} + +type pluginStorerGetCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerGetCall) Panic(msg string) *pluginStorerGetCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerGetCall) Once() *pluginStorerGetCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerGetCall) Twice() *pluginStorerGetCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerGetCall) Times(i int) *pluginStorerGetCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerGetCall) WaitUntil(w <-chan time.Time) *pluginStorerGetCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerGetCall) After(d time.Duration) *pluginStorerGetCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerGetCall) Run(fn func(args mock.Arguments)) *pluginStorerGetCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerGetCall) Maybe() *pluginStorerGetCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerGetCall) TypedReturns(a db.Plugin, b error) *pluginStorerGetCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *pluginStorerGetCall) ReturnsFn(fn func(string) (db.Plugin, error)) *pluginStorerGetCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerGetCall) TypedRun(fn func(string)) *pluginStorerGetCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _id := args.String(0) + fn(_id) + }) + return _c +} + +func (_c *pluginStorerGetCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerGetCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerGetCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerGetCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerGetCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerGetCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerGetCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerGetCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerGetCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerGetCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerGetCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerGetCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerGetCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerGetCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerGetCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerGetCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerGetCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerGetCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) GetByName(_ context.Context, bParam string, cParam bool, dParam bool) (db.Plugin, error) { + _ret := _m.Called(bParam, cParam, dParam) + + if _rf, ok := _ret.Get(0).(func(string, bool, bool) (db.Plugin, error)); ok { + return _rf(bParam, cParam, dParam) + } + + _ra0, _ := _ret.Get(0).(db.Plugin) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *pluginStorerMock) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return &pluginStorerGetByNameCall{Call: _m.Mock.On("GetByName", bParam, cParam, dParam), Parent: _m} +} + +func (_m *pluginStorerMock) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return &pluginStorerGetByNameCall{Call: _m.Mock.On("GetByName", bParam, cParam, dParam), Parent: _m} +} + +type pluginStorerGetByNameCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerGetByNameCall) Panic(msg string) *pluginStorerGetByNameCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerGetByNameCall) Once() *pluginStorerGetByNameCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerGetByNameCall) Twice() *pluginStorerGetByNameCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerGetByNameCall) Times(i int) *pluginStorerGetByNameCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerGetByNameCall) WaitUntil(w <-chan time.Time) *pluginStorerGetByNameCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerGetByNameCall) After(d time.Duration) *pluginStorerGetByNameCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerGetByNameCall) Run(fn func(args mock.Arguments)) *pluginStorerGetByNameCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerGetByNameCall) Maybe() *pluginStorerGetByNameCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerGetByNameCall) TypedReturns(a db.Plugin, b error) *pluginStorerGetByNameCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *pluginStorerGetByNameCall) ReturnsFn(fn func(string, bool, bool) (db.Plugin, error)) *pluginStorerGetByNameCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerGetByNameCall) TypedRun(fn func(string, bool, bool)) *pluginStorerGetByNameCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _bParam := args.String(0) + _cParam := args.Bool(1) + _dParam := args.Bool(2) + fn(_bParam, _cParam, _dParam) + }) + return _c +} + +func (_c *pluginStorerGetByNameCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerGetByNameCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerGetByNameCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerGetByNameCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerGetByNameCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerGetByNameCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerGetByNameCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerGetByNameCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerGetByNameCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerGetByNameCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerGetByNameCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerGetByNameCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerGetByNameCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerGetByNameCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerGetByNameCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerGetByNameCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerGetByNameCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerGetByNameCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) GetHashByName(_ context.Context, module string, version string) (db.PluginHash, error) { + _ret := _m.Called(module, version) + + if _rf, ok := _ret.Get(0).(func(string, string) (db.PluginHash, error)); ok { + return _rf(module, version) + } + + _ra0, _ := _ret.Get(0).(db.PluginHash) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *pluginStorerMock) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return &pluginStorerGetHashByNameCall{Call: _m.Mock.On("GetHashByName", module, version), Parent: _m} +} + +func (_m *pluginStorerMock) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return &pluginStorerGetHashByNameCall{Call: _m.Mock.On("GetHashByName", module, version), Parent: _m} +} + +type pluginStorerGetHashByNameCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerGetHashByNameCall) Panic(msg string) *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) Once() *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerGetHashByNameCall) Twice() *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerGetHashByNameCall) Times(i int) *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) WaitUntil(w <-chan time.Time) *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) After(d time.Duration) *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) Run(fn func(args mock.Arguments)) *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) Maybe() *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerGetHashByNameCall) TypedReturns(a db.PluginHash, b error) *pluginStorerGetHashByNameCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) ReturnsFn(fn func(string, string) (db.PluginHash, error)) *pluginStorerGetHashByNameCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) TypedRun(fn func(string, string)) *pluginStorerGetHashByNameCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _module := args.String(0) + _version := args.String(1) + fn(_module, _version) + }) + return _c +} + +func (_c *pluginStorerGetHashByNameCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerGetHashByNameCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerGetHashByNameCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerGetHashByNameCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerGetHashByNameCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerGetHashByNameCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerGetHashByNameCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerGetHashByNameCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerGetHashByNameCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerGetHashByNameCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) List(_ context.Context, bParam db.Pagination) ([]db.Plugin, string, error) { + _ret := _m.Called(bParam) + + if _rf, ok := _ret.Get(0).(func(db.Pagination) ([]db.Plugin, string, error)); ok { + return _rf(bParam) + } + + _ra0, _ := _ret.Get(0).([]db.Plugin) + _rb1 := _ret.String(1) + _rc2 := _ret.Error(2) + + return _ra0, _rb1, _rc2 +} + +func (_m *pluginStorerMock) OnList(bParam db.Pagination) *pluginStorerListCall { + return &pluginStorerListCall{Call: _m.Mock.On("List", bParam), Parent: _m} +} + +func (_m *pluginStorerMock) OnListRaw(bParam interface{}) *pluginStorerListCall { + return &pluginStorerListCall{Call: _m.Mock.On("List", bParam), Parent: _m} +} + +type pluginStorerListCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerListCall) Panic(msg string) *pluginStorerListCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerListCall) Once() *pluginStorerListCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerListCall) Twice() *pluginStorerListCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerListCall) Times(i int) *pluginStorerListCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerListCall) WaitUntil(w <-chan time.Time) *pluginStorerListCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerListCall) After(d time.Duration) *pluginStorerListCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerListCall) Run(fn func(args mock.Arguments)) *pluginStorerListCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerListCall) Maybe() *pluginStorerListCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerListCall) TypedReturns(a []db.Plugin, b string, c error) *pluginStorerListCall { + _c.Call = _c.Return(a, b, c) + return _c +} + +func (_c *pluginStorerListCall) ReturnsFn(fn func(db.Pagination) ([]db.Plugin, string, error)) *pluginStorerListCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerListCall) TypedRun(fn func(db.Pagination)) *pluginStorerListCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _bParam, _ := args.Get(0).(db.Pagination) + fn(_bParam) + }) + return _c +} + +func (_c *pluginStorerListCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerListCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerListCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerListCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerListCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerListCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerListCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerListCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerListCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerListCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerListCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerListCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerListCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerListCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerListCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerListCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerListCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerListCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) SearchByName(_ context.Context, bParam string, cParam db.Pagination) ([]db.Plugin, string, error) { + _ret := _m.Called(bParam, cParam) + + if _rf, ok := _ret.Get(0).(func(string, db.Pagination) ([]db.Plugin, string, error)); ok { + return _rf(bParam, cParam) + } + + _ra0, _ := _ret.Get(0).([]db.Plugin) + _rb1 := _ret.String(1) + _rc2 := _ret.Error(2) + + return _ra0, _rb1, _rc2 +} + +func (_m *pluginStorerMock) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return &pluginStorerSearchByNameCall{Call: _m.Mock.On("SearchByName", bParam, cParam), Parent: _m} +} + +func (_m *pluginStorerMock) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return &pluginStorerSearchByNameCall{Call: _m.Mock.On("SearchByName", bParam, cParam), Parent: _m} +} + +type pluginStorerSearchByNameCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerSearchByNameCall) Panic(msg string) *pluginStorerSearchByNameCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerSearchByNameCall) Once() *pluginStorerSearchByNameCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerSearchByNameCall) Twice() *pluginStorerSearchByNameCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerSearchByNameCall) Times(i int) *pluginStorerSearchByNameCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerSearchByNameCall) WaitUntil(w <-chan time.Time) *pluginStorerSearchByNameCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerSearchByNameCall) After(d time.Duration) *pluginStorerSearchByNameCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerSearchByNameCall) Run(fn func(args mock.Arguments)) *pluginStorerSearchByNameCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerSearchByNameCall) Maybe() *pluginStorerSearchByNameCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerSearchByNameCall) TypedReturns(a []db.Plugin, b string, c error) *pluginStorerSearchByNameCall { + _c.Call = _c.Return(a, b, c) + return _c +} + +func (_c *pluginStorerSearchByNameCall) ReturnsFn(fn func(string, db.Pagination) ([]db.Plugin, string, error)) *pluginStorerSearchByNameCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerSearchByNameCall) TypedRun(fn func(string, db.Pagination)) *pluginStorerSearchByNameCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _bParam := args.String(0) + _cParam, _ := args.Get(1).(db.Pagination) + fn(_bParam, _cParam) + }) + return _c +} + +func (_c *pluginStorerSearchByNameCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerSearchByNameCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerSearchByNameCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerSearchByNameCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerSearchByNameCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerSearchByNameCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerSearchByNameCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerSearchByNameCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerSearchByNameCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerSearchByNameCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerSearchByNameCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerSearchByNameCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerSearchByNameCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerSearchByNameCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerSearchByNameCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerSearchByNameCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerSearchByNameCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerSearchByNameCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +func (_m *pluginStorerMock) Update(_ context.Context, bParam string, cParam db.Plugin) (db.Plugin, error) { + _ret := _m.Called(bParam, cParam) + + if _rf, ok := _ret.Get(0).(func(string, db.Plugin) (db.Plugin, error)); ok { + return _rf(bParam, cParam) + } + + _ra0, _ := _ret.Get(0).(db.Plugin) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *pluginStorerMock) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return &pluginStorerUpdateCall{Call: _m.Mock.On("Update", bParam, cParam), Parent: _m} +} + +func (_m *pluginStorerMock) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return &pluginStorerUpdateCall{Call: _m.Mock.On("Update", bParam, cParam), Parent: _m} +} + +type pluginStorerUpdateCall struct { + *mock.Call + Parent *pluginStorerMock +} + +func (_c *pluginStorerUpdateCall) Panic(msg string) *pluginStorerUpdateCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *pluginStorerUpdateCall) Once() *pluginStorerUpdateCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *pluginStorerUpdateCall) Twice() *pluginStorerUpdateCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *pluginStorerUpdateCall) Times(i int) *pluginStorerUpdateCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *pluginStorerUpdateCall) WaitUntil(w <-chan time.Time) *pluginStorerUpdateCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *pluginStorerUpdateCall) After(d time.Duration) *pluginStorerUpdateCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *pluginStorerUpdateCall) Run(fn func(args mock.Arguments)) *pluginStorerUpdateCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *pluginStorerUpdateCall) Maybe() *pluginStorerUpdateCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *pluginStorerUpdateCall) TypedReturns(a db.Plugin, b error) *pluginStorerUpdateCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *pluginStorerUpdateCall) ReturnsFn(fn func(string, db.Plugin) (db.Plugin, error)) *pluginStorerUpdateCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *pluginStorerUpdateCall) TypedRun(fn func(string, db.Plugin)) *pluginStorerUpdateCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _bParam := args.String(0) + _cParam, _ := args.Get(1).(db.Plugin) + fn(_bParam, _cParam) + }) + return _c +} + +func (_c *pluginStorerUpdateCall) OnCreate(bParam db.Plugin) *pluginStorerCreateCall { + return _c.Parent.OnCreate(bParam) +} + +func (_c *pluginStorerUpdateCall) OnCreateHash(module string, version string, hash string) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHash(module, version, hash) +} + +func (_c *pluginStorerUpdateCall) OnDelete(id string) *pluginStorerDeleteCall { + return _c.Parent.OnDelete(id) +} + +func (_c *pluginStorerUpdateCall) OnGet(id string) *pluginStorerGetCall { + return _c.Parent.OnGet(id) +} + +func (_c *pluginStorerUpdateCall) OnGetByName(bParam string, cParam bool, dParam bool) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByName(bParam, cParam, dParam) +} + +func (_c *pluginStorerUpdateCall) OnGetHashByName(module string, version string) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByName(module, version) +} + +func (_c *pluginStorerUpdateCall) OnList(bParam db.Pagination) *pluginStorerListCall { + return _c.Parent.OnList(bParam) +} + +func (_c *pluginStorerUpdateCall) OnSearchByName(bParam string, cParam db.Pagination) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByName(bParam, cParam) +} + +func (_c *pluginStorerUpdateCall) OnUpdate(bParam string, cParam db.Plugin) *pluginStorerUpdateCall { + return _c.Parent.OnUpdate(bParam, cParam) +} + +func (_c *pluginStorerUpdateCall) OnCreateRaw(bParam interface{}) *pluginStorerCreateCall { + return _c.Parent.OnCreateRaw(bParam) +} + +func (_c *pluginStorerUpdateCall) OnCreateHashRaw(module interface{}, version interface{}, hash interface{}) *pluginStorerCreateHashCall { + return _c.Parent.OnCreateHashRaw(module, version, hash) +} + +func (_c *pluginStorerUpdateCall) OnDeleteRaw(id interface{}) *pluginStorerDeleteCall { + return _c.Parent.OnDeleteRaw(id) +} + +func (_c *pluginStorerUpdateCall) OnGetRaw(id interface{}) *pluginStorerGetCall { + return _c.Parent.OnGetRaw(id) +} + +func (_c *pluginStorerUpdateCall) OnGetByNameRaw(bParam interface{}, cParam interface{}, dParam interface{}) *pluginStorerGetByNameCall { + return _c.Parent.OnGetByNameRaw(bParam, cParam, dParam) +} + +func (_c *pluginStorerUpdateCall) OnGetHashByNameRaw(module interface{}, version interface{}) *pluginStorerGetHashByNameCall { + return _c.Parent.OnGetHashByNameRaw(module, version) +} + +func (_c *pluginStorerUpdateCall) OnListRaw(bParam interface{}) *pluginStorerListCall { + return _c.Parent.OnListRaw(bParam) +} + +func (_c *pluginStorerUpdateCall) OnSearchByNameRaw(bParam interface{}, cParam interface{}) *pluginStorerSearchByNameCall { + return _c.Parent.OnSearchByNameRaw(bParam, cParam) +} + +func (_c *pluginStorerUpdateCall) OnUpdateRaw(bParam interface{}, cParam interface{}) *pluginStorerUpdateCall { + return _c.Parent.OnUpdateRaw(bParam, cParam) +} + +// goproxyPluginClientMock mock of GoproxyPluginClient. +type goproxyPluginClientMock struct{ mock.Mock } + +// NewGoproxyPluginClientMock creates a new goproxyPluginClientMock. +func NewGoproxyPluginClientMock(tb testing.TB) *goproxyPluginClientMock { + tb.Helper() + + m := &goproxyPluginClientMock{} + m.Mock.Test(tb) + + tb.Cleanup(func() { m.AssertExpectations(tb) }) + + return m +} + +func (_m *goproxyPluginClientMock) DownloadSources(moduleName string, version string) (io.ReadCloser, error) { + _ret := _m.Called(moduleName, version) + + if _rf, ok := _ret.Get(0).(func(string, string) (io.ReadCloser, error)); ok { + return _rf(moduleName, version) + } + + _ra0, _ := _ret.Get(0).(io.ReadCloser) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *goproxyPluginClientMock) OnDownloadSources(moduleName string, version string) *goproxyPluginClientDownloadSourcesCall { + return &goproxyPluginClientDownloadSourcesCall{Call: _m.Mock.On("DownloadSources", moduleName, version), Parent: _m} +} + +func (_m *goproxyPluginClientMock) OnDownloadSourcesRaw(moduleName interface{}, version interface{}) *goproxyPluginClientDownloadSourcesCall { + return &goproxyPluginClientDownloadSourcesCall{Call: _m.Mock.On("DownloadSources", moduleName, version), Parent: _m} +} + +type goproxyPluginClientDownloadSourcesCall struct { + *mock.Call + Parent *goproxyPluginClientMock +} + +func (_c *goproxyPluginClientDownloadSourcesCall) Panic(msg string) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) Once() *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) Twice() *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) Times(i int) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) WaitUntil(w <-chan time.Time) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) After(d time.Duration) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) Run(fn func(args mock.Arguments)) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) Maybe() *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) TypedReturns(a io.ReadCloser, b error) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) ReturnsFn(fn func(string, string) (io.ReadCloser, error)) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) TypedRun(fn func(string, string)) *goproxyPluginClientDownloadSourcesCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _moduleName := args.String(0) + _version := args.String(1) + fn(_moduleName, _version) + }) + return _c +} + +func (_c *goproxyPluginClientDownloadSourcesCall) OnDownloadSources(moduleName string, version string) *goproxyPluginClientDownloadSourcesCall { + return _c.Parent.OnDownloadSources(moduleName, version) +} + +func (_c *goproxyPluginClientDownloadSourcesCall) OnGetModFile(moduleName string, version string) *goproxyPluginClientGetModFileCall { + return _c.Parent.OnGetModFile(moduleName, version) +} + +func (_c *goproxyPluginClientDownloadSourcesCall) OnDownloadSourcesRaw(moduleName interface{}, version interface{}) *goproxyPluginClientDownloadSourcesCall { + return _c.Parent.OnDownloadSourcesRaw(moduleName, version) +} + +func (_c *goproxyPluginClientDownloadSourcesCall) OnGetModFileRaw(moduleName interface{}, version interface{}) *goproxyPluginClientGetModFileCall { + return _c.Parent.OnGetModFileRaw(moduleName, version) +} + +func (_m *goproxyPluginClientMock) GetModFile(moduleName string, version string) (*modfile.File, error) { + _ret := _m.Called(moduleName, version) + + if _rf, ok := _ret.Get(0).(func(string, string) (*modfile.File, error)); ok { + return _rf(moduleName, version) + } + + _ra0, _ := _ret.Get(0).(*modfile.File) + _rb1 := _ret.Error(1) + + return _ra0, _rb1 +} + +func (_m *goproxyPluginClientMock) OnGetModFile(moduleName string, version string) *goproxyPluginClientGetModFileCall { + return &goproxyPluginClientGetModFileCall{Call: _m.Mock.On("GetModFile", moduleName, version), Parent: _m} +} + +func (_m *goproxyPluginClientMock) OnGetModFileRaw(moduleName interface{}, version interface{}) *goproxyPluginClientGetModFileCall { + return &goproxyPluginClientGetModFileCall{Call: _m.Mock.On("GetModFile", moduleName, version), Parent: _m} +} + +type goproxyPluginClientGetModFileCall struct { + *mock.Call + Parent *goproxyPluginClientMock +} + +func (_c *goproxyPluginClientGetModFileCall) Panic(msg string) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.Panic(msg) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) Once() *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.Once() + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) Twice() *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.Twice() + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) Times(i int) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.Times(i) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) WaitUntil(w <-chan time.Time) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.WaitUntil(w) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) After(d time.Duration) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.After(d) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) Run(fn func(args mock.Arguments)) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.Run(fn) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) Maybe() *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.Maybe() + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) TypedReturns(a *modfile.File, b error) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Return(a, b) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) ReturnsFn(fn func(string, string) (*modfile.File, error)) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Return(fn) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) TypedRun(fn func(string, string)) *goproxyPluginClientGetModFileCall { + _c.Call = _c.Call.Run(func(args mock.Arguments) { + _moduleName := args.String(0) + _version := args.String(1) + fn(_moduleName, _version) + }) + return _c +} + +func (_c *goproxyPluginClientGetModFileCall) OnDownloadSources(moduleName string, version string) *goproxyPluginClientDownloadSourcesCall { + return _c.Parent.OnDownloadSources(moduleName, version) +} + +func (_c *goproxyPluginClientGetModFileCall) OnGetModFile(moduleName string, version string) *goproxyPluginClientGetModFileCall { + return _c.Parent.OnGetModFile(moduleName, version) +} + +func (_c *goproxyPluginClientGetModFileCall) OnDownloadSourcesRaw(moduleName interface{}, version interface{}) *goproxyPluginClientDownloadSourcesCall { + return _c.Parent.OnDownloadSourcesRaw(moduleName, version) +} + +func (_c *goproxyPluginClientGetModFileCall) OnGetModFileRaw(moduleName interface{}, version interface{}) *goproxyPluginClientGetModFileCall { + return _c.Parent.OnGetModFileRaw(moduleName, version) +} diff --git a/pkg/handlers/mock_test.go b/pkg/handlers/mock_test.go new file mode 100644 index 0000000..1193aff --- /dev/null +++ b/pkg/handlers/mock_test.go @@ -0,0 +1,5 @@ +package handlers + +// mocktail:GithubPluginClient +// mocktail:PluginStorer +// mocktail:GoproxyPluginClient diff --git a/pkg/handlers/module.go b/pkg/handlers/module.go index cc43eb0..9e5b900 100644 --- a/pkg/handlers/module.go +++ b/pkg/handlers/module.go @@ -1,403 +1,434 @@ package handlers import ( - "bytes" - "context" - "crypto/sha256" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "path" - "path/filepath" - "strings" - - "github.com/google/go-github/v57/github" - "github.com/rs/zerolog/log" - ttrace "github.com/traefik/hub-trace-kpi/trace" - "github.com/traefik/plugin-service/pkg/db" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" + "bytes" + "context" + "crypto/sha256" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/google/go-github/v57/github" + "github.com/rs/zerolog/log" + ttrace "github.com/traefik/hub-trace-kpi/trace" + "github.com/traefik/plugin-service/pkg/db" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) const ( - hashHeader = "X-Plugin-Hash" + hashHeader = "X-Plugin-Hash" + cacheControlHeader = "Cache-Control" + cacheControlNoCache = "no-cache" + cacheControlMaxAge = "max-age" + cacheControlSMaxAge = "s-maxage" ) // Download a plugin archive. func (h Handlers) Download(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_download") - defer span.End() - - if req.Method != http.MethodGet { - span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) - log.Error().Msgf("Unsupported method: %s", req.Method) - JSONErrorf(rw, http.StatusMethodNotAllowed, "unsupported method: %s", req.Method) - return - } - - pluginName, version := extractPluginInfo(req.URL, "/download/") - - attributes := []attribute.KeyValue{ - attribute.String("module.moduleName", pluginName), - attribute.String("module.version", version), - attribute.String("module.ip", getUserIP(req)), - } - - logger := log.With().Str("plugin_name", pluginName).Str("plugin_version", version).Logger() - - plugin, err := h.store.GetByName(ctx, pluginName, false, false) - if err != nil { - span.RecordError(err) - if errors.As(err, &db.NotFoundError{}) { - logger.Warn().Err(err).Msg("Unknown plugin") - JSONErrorf(rw, http.StatusNotFound, "Unknown plugin: %s@%s", pluginName, version) - return - } - - logger.Error().Err(err).Msg("Failed to get plugin") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - - ttrace.Captured(span) - - sum := req.Header.Get(hashHeader) - if sum != "" { - attributes = append(attributes, attribute.String("module.sum", sum)) - ph, errH := h.store.GetHashByName(ctx, pluginName, version) - if errH != nil { - span.RecordError(errH) - if !errors.As(errH, &db.NotFoundError{}) { - logger.Error().Err(errH).Msg("Failed to get plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - } else if ph.Hash == sum { - rw.WriteHeader(http.StatusNotModified) - return - } - - span.AddEvent("module.download", trace.WithAttributes(attributes...)) - logger.Error().Msgf("Someone is trying to hack the archive: %v", sum) - } - - span.SetAttributes(attributes...) - - switch strings.ToLower(plugin.Runtime) { - case "wasm": - // WASM plugins - if h.gh == nil { - logger.Error().Msg("Failed to get plugin: missing GitHub client.") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - - h.downloadGitHub(ctx, pluginName, version, true)(rw, req) - return - - default: - // Yaegi plugins - modFile, err := h.goProxy.GetModFile(pluginName, version) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get module file") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - - // Uses GitHub when there are dependencies because Go proxy archives don't contain vendor folder. - if h.gh != nil && len(modFile.Require) > 0 { - h.downloadGitHub(ctx, pluginName, version, false)(rw, req) - return - } - - h.downloadGoProxy(ctx, pluginName, version)(rw, req) - } + ctx, span := h.tracer.Start(req.Context(), "handler_download") + defer span.End() + + if req.Method != http.MethodGet { + span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) + log.Error().Msgf("Unsupported method: %s", req.Method) + JSONErrorf(rw, http.StatusMethodNotAllowed, "unsupported method: %s", req.Method) + return + } + + pluginName, version := extractPluginInfo(req.URL, "/download/") + + attributes := []attribute.KeyValue{ + attribute.String("module.moduleName", pluginName), + attribute.String("module.version", version), + attribute.String("module.ip", getUserIP(req)), + } + + logger := log.With().Str("plugin_name", pluginName).Str("plugin_version", version).Logger() + + plugin, err := h.store.GetByName(ctx, pluginName, false, false) + if err != nil { + span.RecordError(err) + if errors.As(err, &db.NotFoundError{}) { + logger.Warn().Err(err).Msg("Unknown plugin") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusNotFound, "Unknown plugin: %s@%s", pluginName, version) + return + } + + logger.Error().Err(err).Msg("Failed to get plugin") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + + ttrace.Captured(span) + + sum := req.Header.Get(hashHeader) + if sum != "" { + attributes = append(attributes, attribute.String("module.sum", sum)) + ph, errH := h.store.GetHashByName(ctx, pluginName, version) + if errH != nil { + span.RecordError(errH) + if !errors.As(errH, &db.NotFoundError{}) { + logger.Error().Err(errH).Msg("Failed to get plugin hash") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + } else if ph.Hash == sum { + setCacheControl(rw, h.ttl) + rw.WriteHeader(http.StatusNotModified) + return + } + + span.AddEvent("module.download", trace.WithAttributes(attributes...)) + logger.Error().Msgf("Someone is trying to hack the archive: %v != %v", ph.Hash, sum) + } + + span.SetAttributes(attributes...) + + switch strings.ToLower(plugin.Runtime) { + case "wasm": + // WASM plugins + if h.gh == nil { + logger.Error().Msg("Failed to get plugin: missing GitHub client.") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + + h.downloadGitHub(ctx, pluginName, version, true)(rw, req) + return + + default: + // Yaegi plugins + modFile, err := h.goProxy.GetModFile(pluginName, version) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get module file") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + + // Uses GitHub when there are dependencies because Go proxy archives don't contain vendor folder. + if h.gh != nil && len(modFile.Require) > 0 { + h.downloadGitHub(ctx, pluginName, version, false)(rw, req) + return + } + + h.downloadGoProxy(ctx, pluginName, version)(rw, req) + } } func getUserIP(req *http.Request) string { - ip, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { - ip = req.RemoteAddr - } - - xff := req.Header.Get("X-Forwarded-For") - if xff != "" { - ip = strings.Trim(strings.Split(xff, ",")[0], " ") - } - return ip + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + ip = req.RemoteAddr + } + + xff := req.Header.Get("X-Forwarded-For") + if xff != "" { + ip = strings.Trim(strings.Split(xff, ",")[0], " ") + } + return ip } func (h Handlers) downloadGoProxy(ctx context.Context, moduleName, version string) http.HandlerFunc { - return func(rw http.ResponseWriter, _ *http.Request) { - var span trace.Span - ctx, span = h.tracer.Start(ctx, "handler_downloadGoProxy") - defer span.End() - - logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() - - sources, err := h.goProxy.DownloadSources(moduleName, version) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to download sources") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - defer func() { _ = sources.Close() }() - - _, err = h.store.GetHashByName(ctx, moduleName, version) - if err != nil && !errors.As(err, &db.NotFoundError{}) { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - if err == nil { - _, err = io.Copy(rw, sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - return - } - - raw, err := io.ReadAll(sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to read response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - hash := sha256.New() - - _, err = hash.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to compute hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - sum := fmt.Sprintf("%x", hash.Sum(nil)) - - _, err = h.store.CreateHash(ctx, moduleName, version, sum) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error persisting plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Could not persist data: %s@%s", moduleName, version) - return - } - - _, err = rw.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "failed to get plugin %s@%s", moduleName, version) - return - } - } + return func(rw http.ResponseWriter, _ *http.Request) { + var span trace.Span + ctx, span = h.tracer.Start(ctx, "handler_downloadGoProxy") + defer span.End() + + logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() + + sources, err := h.goProxy.DownloadSources(moduleName, version) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to download sources") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + defer func() { _ = sources.Close() }() + + _, err = h.store.GetHashByName(ctx, moduleName, version) + if err != nil && !errors.As(err, &db.NotFoundError{}) { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + if err == nil { + setCacheControl(rw, h.ttl) + _, err = io.Copy(rw, sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + return + } + + raw, err := io.ReadAll(sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to read response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + hash := sha256.New() + + _, err = hash.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to compute hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + sum := fmt.Sprintf("%x", hash.Sum(nil)) + + _, err = h.store.CreateHash(ctx, moduleName, version, sum) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error persisting plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Could not persist data: %s@%s", moduleName, version) + return + } + + setCacheControl(rw, h.ttl) + _, err = rw.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "failed to get plugin %s@%s", moduleName, version) + return + } + } } func (h Handlers) downloadGitHub(ctx context.Context, moduleName, version string, fromAssets bool) http.HandlerFunc { - return func(rw http.ResponseWriter, _ *http.Request) { - var span trace.Span - ctx, span = h.tracer.Start(ctx, "handler_downloadGitHub") - defer span.End() - - logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() - - var request *http.Request - var err error - if fromAssets { - request, err = h.getAssetLinkRequest(ctx, moduleName, version) - } else { - request, err = h.getArchiveLinkRequest(ctx, moduleName, version) - } - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get archive link") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - _, err = h.store.GetHashByName(ctx, moduleName, version) - if err != nil && !errors.As(err, &db.NotFoundError{}) { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - if err == nil { - _, err = h.gh.Do(ctx, request, rw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - return - } - - sources := bytes.NewBufferString("") - - _, err = h.gh.Do(ctx, request, sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get archive content") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - raw, err := io.ReadAll(sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to read response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - hash := sha256.New() - - _, err = hash.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to compute hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - sum := fmt.Sprintf("%x", hash.Sum(nil)) - - _, err = h.store.CreateHash(ctx, moduleName, version, sum) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error persisting plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - _, err = rw.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - } + return func(rw http.ResponseWriter, _ *http.Request) { + var span trace.Span + ctx, span = h.tracer.Start(ctx, "handler_downloadGitHub") + defer span.End() + + logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() + + var request *http.Request + var err error + if fromAssets { + request, err = h.getAssetLinkRequest(ctx, moduleName, version) + } else { + request, err = h.getArchiveLinkRequest(ctx, moduleName, version) + } + if err != nil { + setCacheControl(rw, 0) + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get archive link") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + _, err = h.store.GetHashByName(ctx, moduleName, version) + if err != nil && !errors.As(err, &db.NotFoundError{}) { + setCacheControl(rw, 0) + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + if err == nil { + setCacheControl(rw, h.ttl) + _, err = h.gh.Do(ctx, request, rw) + if err != nil { + setCacheControl(rw, 0) + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + return + } + + sources := bytes.NewBufferString("") + + _, err = h.gh.Do(ctx, request, sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get archive content") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + raw, err := io.ReadAll(sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to read response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + hash := sha256.New() + + _, err = hash.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to compute hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + sum := fmt.Sprintf("%x", hash.Sum(nil)) + + _, err = h.store.CreateHash(ctx, moduleName, version, sum) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error persisting plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + setCacheControl(rw, h.ttl) + _, err = rw.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + } } func (h Handlers) getArchiveLinkRequest(ctx context.Context, moduleName, version string) (*http.Request, error) { - ctx, span := h.tracer.Start(ctx, "handler_getArchiveLinkRequest") - defer span.End() + ctx, span := h.tracer.Start(ctx, "handler_getArchiveLinkRequest") + defer span.End() - opts := &github.RepositoryContentGetOptions{Ref: version} + opts := &github.RepositoryContentGetOptions{Ref: version} - owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) - owner = strings.TrimSuffix(owner, "/") + owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) + owner = strings.TrimSuffix(owner, "/") - link, _, err := h.gh.Repositories.GetArchiveLink(ctx, owner, repoName, github.Zipball, opts, 3) - if err != nil { - span.RecordError(err) - return nil, fmt.Errorf("failed to get archive link: %w", err) - } + link, _, err := h.gh.GetArchiveLink(ctx, owner, repoName, github.Zipball, opts, 3) + if err != nil { + span.RecordError(err) + return nil, fmt.Errorf("failed to get archive link: %w", err) + } - return http.NewRequestWithContext(ctx, http.MethodGet, link.String(), http.NoBody) + return http.NewRequestWithContext(ctx, http.MethodGet, link.String(), http.NoBody) } func (h Handlers) getAssetLinkRequest(ctx context.Context, moduleName, version string) (*http.Request, error) { - ctx, span := h.tracer.Start(ctx, "handler_getAssetLinkRequest") - defer span.End() - - owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) - owner = strings.TrimSuffix(owner, "/") - - release, _, err := h.gh.Repositories.GetReleaseByTag(ctx, owner, repoName, version) - if err != nil { - span.RecordError(err) - return nil, fmt.Errorf("failed to get release: %w", err) - } - - assets := map[*github.ReleaseAsset]struct{}{} - for _, asset := range release.Assets { - if filepath.Ext(asset.GetName()) == ".zip" { - assets[asset] = struct{}{} - } - } - - if len(assets) > 1 { - return nil, fmt.Errorf("too many zip archive (%d)", len(assets)) - } - - for asset := range assets { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.GetURL(), http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("Accept", "application/octet-stream") - - return req, nil - } - return nil, errors.New("zip archive not found") + ctx, span := h.tracer.Start(ctx, "handler_getAssetLinkRequest") + defer span.End() + + owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) + owner = strings.TrimSuffix(owner, "/") + + release, _, err := h.gh.GetReleaseByTag(ctx, owner, repoName, version) + if err != nil { + span.RecordError(err) + return nil, fmt.Errorf("failed to get release: %w", err) + } + + assets := map[*github.ReleaseAsset]struct{}{} + for _, asset := range release.Assets { + if filepath.Ext(asset.GetName()) == ".zip" { + assets[asset] = struct{}{} + } + } + + if len(assets) > 1 { + return nil, fmt.Errorf("too many zip archive (%d)", len(assets)) + } + + for asset := range assets { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.GetURL(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Accept", "application/octet-stream") + + return req, nil + } + return nil, errors.New("zip archive not found") } // Validate validates a plugin archive. func (h Handlers) Validate(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_getArchiveLinkRequest") - defer span.End() - - if req.Method != http.MethodGet { - span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) - log.Warn().Msgf("Unsupported method: %s", req.Method) - JSONErrorf(rw, http.StatusMethodNotAllowed, "Unsupported method: %s", req.Method) - return - } - - moduleName, version := extractPluginInfo(req.URL, "/validate/") - - logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() - - headerSum := req.Header.Get(hashHeader) - ph, err := h.store.GetHashByName(ctx, moduleName, version) - if err != nil { - if errors.As(err, &db.NotFoundError{}) { - span.RecordError(fmt.Errorf("plugin not found %s@%s", moduleName, version)) - logger.Warn().Err(err).Msg("Plugin not found") - JSONErrorf(rw, http.StatusNotFound, "Plugin not found %s@%s", moduleName, version) - return - } - - span.RecordError(err) - logger.Error().Err(err).Msg("Error while fetch") - JSONInternalServerError(rw) - return - } - - if ph.Hash == headerSum { - rw.WriteHeader(http.StatusOK) - return - } - - rw.WriteHeader(http.StatusNotFound) + ctx, span := h.tracer.Start(req.Context(), "handler_getArchiveLinkRequest") + defer span.End() + + if req.Method != http.MethodGet { + span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) + log.Warn().Msgf("Unsupported method: %s", req.Method) + JSONErrorf(rw, http.StatusMethodNotAllowed, "Unsupported method: %s", req.Method) + return + } + + moduleName, version := extractPluginInfo(req.URL, "/validate/") + + logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() + + headerSum := req.Header.Get(hashHeader) + ph, err := h.store.GetHashByName(ctx, moduleName, version) + if err != nil { + if errors.As(err, &db.NotFoundError{}) { + span.RecordError(fmt.Errorf("plugin not found %s@%s", moduleName, version)) + logger.Warn().Err(err).Msg("Plugin not found") + JSONErrorf(rw, http.StatusNotFound, "Plugin not found %s@%s", moduleName, version) + return + } + + span.RecordError(err) + logger.Error().Err(err).Msg("Error while fetch") + JSONInternalServerError(rw) + return + } + + if ph.Hash == headerSum { + rw.WriteHeader(http.StatusOK) + return + } + + rw.WriteHeader(http.StatusNotFound) } func extractPluginInfo(endpoint *url.URL, sep string) (string, string) { - _, after, _ := strings.Cut(strings.TrimSuffix(endpoint.Path, "/"), sep) - moduleName, version := path.Split(after) + _, after, _ := strings.Cut(strings.TrimSuffix(endpoint.Path, "/"), sep) + moduleName, version := path.Split(after) - return cleanModuleName(moduleName), version + return cleanModuleName(moduleName), version } func cleanModuleName(moduleName string) string { - return strings.TrimSuffix(strings.TrimPrefix(moduleName, "/"), "/") + return strings.TrimSuffix(strings.TrimPrefix(moduleName, "/"), "/") +} + +// cf. https://www.rfc-editor.org/rfc/rfc7231#section-6.1 +// cf. https://www.rfc-editor.org/rfc/rfc7234#section-3 +// This method should only be used when 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 are returned but setting +// Cache-Control to no-cache is the only way this can't be cached. +func setCacheControl(rw http.ResponseWriter, t int) { + if t == 0 { + rw.Header().Set(cacheControlHeader, cacheControlNoCache) + return + } + + rw.Header().Set(cacheControlHeader, strings.Join([]string{ + fmt.Sprintf("%s=%d", cacheControlMaxAge, t), + fmt.Sprintf("%s=%d", cacheControlSMaxAge, t), + }, ",")) } diff --git a/pkg/handlers/module_test.go b/pkg/handlers/module_test.go index 9551584..3c1fd16 100644 --- a/pkg/handlers/module_test.go +++ b/pkg/handlers/module_test.go @@ -1,104 +1,594 @@ package handlers import ( - "net/url" - "testing" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/google/go-github/v57/github" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/traefik/plugin-service/pkg/db" + "golang.org/x/mod/modfile" + "golang.org/x/mod/module" ) func Test_cleanModuleName(t *testing.T) { - testCases := []struct { - name string - expected string - }{ - { - name: "/powpow/", - expected: "powpow", - }, - { - name: "/powpow/v2", - expected: "powpow/v2", - }, - { - name: "powpow/v2", - expected: "powpow/v2", - }, - - { - name: "powpow", - expected: "powpow", - }, - - { - name: "powpow/v2/", - expected: "powpow/v2", - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - name := cleanModuleName(test.name) - assert.Equal(t, test.expected, name) - }) - } + testCases := []struct { + name string + expected string + }{ + { + name: "/powpow/", + expected: "powpow", + }, + { + name: "/powpow/v2", + expected: "powpow/v2", + }, + { + name: "powpow/v2", + expected: "powpow/v2", + }, + + { + name: "powpow", + expected: "powpow", + }, + + { + name: "powpow/v2/", + expected: "powpow/v2", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + name := cleanModuleName(test.name) + assert.Equal(t, test.expected, name) + }) + } } func Test_extractPluginInfo(t *testing.T) { - type expected struct { - moduleName string - version string - } - - testCases := []struct { - desc string - url string - sep string - expected expected - }{ - { - desc: "public URL", - url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6", - sep: "/download/", - expected: expected{ - moduleName: "github.com/tomMoulard/fail2ban", - version: "v0.6.6", - }, - }, - { - desc: "internal URL", - url: "https://plugins.traefik.io/download/github.com/tomMoulard/fail2ban/v0.6.6", - sep: "/download/", - expected: expected{ - moduleName: "github.com/tomMoulard/fail2ban", - version: "v0.6.6", - }, - }, - { - desc: "with extra slash", - url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6/", - sep: "/download/", - expected: expected{ - moduleName: "github.com/tomMoulard/fail2ban", - version: "v0.6.6", - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - endpoint, err := url.Parse(test.url) - require.NoError(t, err) - - moduleName, version := extractPluginInfo(endpoint, test.sep) - - assert.Equal(t, test.expected.moduleName, moduleName) - assert.Equal(t, test.expected.version, version) - }) - } + type expected struct { + moduleName string + version string + } + + testCases := []struct { + desc string + url string + sep string + expected expected + }{ + { + desc: "public URL", + url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6", + sep: "/download/", + expected: expected{ + moduleName: "github.com/tomMoulard/fail2ban", + version: "v0.6.6", + }, + }, + { + desc: "internal URL", + url: "https://plugins.traefik.io/download/github.com/tomMoulard/fail2ban/v0.6.6", + sep: "/download/", + expected: expected{ + moduleName: "github.com/tomMoulard/fail2ban", + version: "v0.6.6", + }, + }, + { + desc: "with extra slash", + url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6/", + sep: "/download/", + expected: expected{ + moduleName: "github.com/tomMoulard/fail2ban", + version: "v0.6.6", + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + endpoint, err := url.Parse(test.url) + require.NoError(t, err) + + moduleName, version := extractPluginInfo(endpoint, test.sep) + + assert.Equal(t, test.expected.moduleName, moduleName) + assert.Equal(t, test.expected.version, version) + }) + } +} + +func Test_Download(t *testing.T) { + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(&modfile.File{}, nil). + OnDownloadSources("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(io.NopCloser(strings.NewReader("test")), nil). + Parent + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_withDifferentHash(t *testing.T) { + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(db.PluginHash{Hash: "yy"}, nil).Twice(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t).OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(&modfile.File{}, nil). + OnDownloadSources("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(io.NopCloser(strings.NewReader("test")), nil). + Parent + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req.Header.Set(hashHeader, "xx") + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_handle_GetByName_Error(t *testing.T) { + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(db.Plugin{}, errors.New("test")).Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_handle_GetByName_Error_dbNotFoundError(t *testing.T) { + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(db.Plugin{}, db.NotFoundError{Err: errors.New("test")}).Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusNotFound, rw.Code) + assert.Equal(t, `{"error":"Unknown plugin: github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_handle_GetHashByName_Error(t *testing.T) { + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{}, errors.New("error")).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req.Header.Set(hashHeader, "xx") + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_handle_GetModFile_Error(t *testing.T) { + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil).Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(&modfile.File{}, errors.New("error")).Parent + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_unhandledMethod(t *testing.T) { + testDB := NewPluginStorerMock(t) + + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodHead, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusMethodNotAllowed, rw.Code) +} + +func Test_Download_wasm(t *testing.T) { + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemowasm", + LatestVersion: "v0.0.1", + Name: "github.com/traefik/plugindemowasm", + Readme: "README", + Runtime: "wasm", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Demo Plugin WASM", + Type: "middleware", + Versions: []string{"v0.0.1"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemowasm", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemowasm", "v0.0.1").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "api.github.com", Path: "/repos/traefik/plugindemowasm/releases/assets/138238821"} + + githubMock := NewGithubPluginClientMock(t). + OnGetReleaseByTag("traefik", "plugindemowasm", "v0.0.1").TypedReturns(&github.RepositoryRelease{ + Assets: []*github.ReleaseAsset{ + {Name: github.String("plugindemowasm_0.0.1_checksums.txt"), URL: github.String("https://api.github.com/repos/traefik/plugindemowasm/releases/assets/138238820")}, + {Name: github.String("plugindemowasm_v0.0.1.zip"), URL: github.String("https://api.github.com/repos/traefik/plugindemowasm/releases/assets/138238821")}, + }}, nil, nil).Once(). + OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { + if req.URL.String() != link.String() { + return false + } + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("test")) + + return true + }), rw).TypedReturns(nil, nil). + Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemowasm/v0.0.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_withHash(t *testing.T) { + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{Hash: "xx"}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req.Header.Set(hashHeader, "xx") + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusNotModified, rw.Code) + assert.Equal(t, "", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_withRequirements(t *testing.T) { + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once(). + OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { + if req.URL.String() != link.String() { + return false + } + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("test")) + + return true + }), rw).TypedReturns(nil, nil).Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_withRequirements_handle_Do_Error(t *testing.T) { + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once(). + OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { + return req.URL.String() == link.String() + }), rw).TypedReturns(nil, errors.New("error")).Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_withRequirements_handle_GetArchiveLink_Error(t *testing.T) { + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil).Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(nil, nil, errors.New("error")).Once().Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) +} + +func Test_Download_withRequirements_handle_GetHashByName_Error(t *testing.T) { + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, errors.New("error")).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once().Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) } From 894c81f14c2e6f3f0df3f9e35ce0122f1a371208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20BUISSON?= Date: Tue, 3 Jun 2025 16:24:32 +0200 Subject: [PATCH 2/2] fix: formatting --- cmd/serve/command.go | 246 ++++---- cmd/serve/config.go | 26 +- cmd/serve/serve.go | 180 +++--- pkg/handlers/github.go | 38 +- pkg/handlers/goproxy.go | 34 +- pkg/handlers/handlers.go | 676 ++++++++++----------- pkg/handlers/handlers_test.go | 284 ++++----- pkg/handlers/module.go | 772 ++++++++++++------------ pkg/handlers/module_test.go | 1060 ++++++++++++++++----------------- 9 files changed, 1658 insertions(+), 1658 deletions(-) diff --git a/cmd/serve/command.go b/cmd/serve/command.go index 7949802..b9696bc 100644 --- a/cmd/serve/command.go +++ b/cmd/serve/command.go @@ -1,148 +1,148 @@ package serve import ( - "github.com/ettle/strcase" - "github.com/traefik/plugin-service/cmd/internal" - "github.com/traefik/plugin-service/pkg/tracer" - "github.com/urfave/cli/v2" + "github.com/ettle/strcase" + "github.com/traefik/plugin-service/cmd/internal" + "github.com/traefik/plugin-service/pkg/tracer" + "github.com/urfave/cli/v2" ) const ( - flagAddr = "addr" - flagTTL = "ttl" - flagGHToken = "github-token" + flagAddr = "addr" + flagTTL = "ttl" + flagGHToken = "github-token" - flagTraceServiceURL = "trace-service-url" + flagTraceServiceURL = "trace-service-url" - flagGoProxyURL = "go-proxy-url" - flagGoProxyUsername = "go-proxy-username" - flagGoProxyPassword = "go-proxy-password" + flagGoProxyURL = "go-proxy-url" + flagGoProxyUsername = "go-proxy-username" + flagGoProxyPassword = "go-proxy-password" - flagTracingAddress = "tracing-address" - flagTracingInsecure = "tracing-insecure" - flagTracingUsername = "tracing-username" - flagTracingPassword = "tracing-password" - flagTracingProbability = "tracing-probability" + flagTracingAddress = "tracing-address" + flagTracingInsecure = "tracing-insecure" + flagTracingUsername = "tracing-username" + flagTracingPassword = "tracing-password" + flagTracingProbability = "tracing-probability" ) // Command creates the command for serving the plugin service. func Command() *cli.Command { - cmd := &cli.Command{ - Name: "serve", - Usage: "Serve HTTP", - Description: "Launch plugin service application", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: flagAddr, - Usage: "Addr to listen on.", - EnvVars: []string{strcase.ToSNAKE(flagAddr)}, - }, - &cli.StringFlag{ - Name: flagTTL, - Usage: "Control TTL of download responses.", - EnvVars: []string{strcase.ToSNAKE(flagTTL)}, - }, - &cli.StringFlag{ - Name: flagGHToken, - Usage: "GitHub Token", - EnvVars: []string{strcase.ToSNAKE(flagGHToken)}, - Required: true, - }, - &cli.StringFlag{ - Name: flagTraceServiceURL, - Usage: "URL of the trace service", - EnvVars: []string{strcase.ToSNAKE(flagTraceServiceURL)}, - }, - }, - Action: func(cliCtx *cli.Context) error { - return run(cliCtx.Context, buildConfig(cliCtx)) - }, - } + cmd := &cli.Command{ + Name: "serve", + Usage: "Serve HTTP", + Description: "Launch plugin service application", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: flagAddr, + Usage: "Addr to listen on.", + EnvVars: []string{strcase.ToSNAKE(flagAddr)}, + }, + &cli.StringFlag{ + Name: flagTTL, + Usage: "Control TTL of download responses.", + EnvVars: []string{strcase.ToSNAKE(flagTTL)}, + }, + &cli.StringFlag{ + Name: flagGHToken, + Usage: "GitHub Token", + EnvVars: []string{strcase.ToSNAKE(flagGHToken)}, + Required: true, + }, + &cli.StringFlag{ + Name: flagTraceServiceURL, + Usage: "URL of the trace service", + EnvVars: []string{strcase.ToSNAKE(flagTraceServiceURL)}, + }, + }, + Action: func(cliCtx *cli.Context) error { + return run(cliCtx.Context, buildConfig(cliCtx)) + }, + } - cmd.Flags = append(cmd.Flags, goProxyFlags()...) - cmd.Flags = append(cmd.Flags, tracingFlags()...) - cmd.Flags = append(cmd.Flags, internal.MongoFlags()...) + cmd.Flags = append(cmd.Flags, goProxyFlags()...) + cmd.Flags = append(cmd.Flags, tracingFlags()...) + cmd.Flags = append(cmd.Flags, internal.MongoFlags()...) - return cmd + return cmd } func buildConfig(cliCtx *cli.Context) Config { - return Config{ - MongoDB: internal.BuildMongoConfig(cliCtx), - Tracing: tracer.Config{ - Address: cliCtx.String(flagTracingAddress), - Insecure: cliCtx.Bool(flagTracingInsecure), - Username: cliCtx.String(flagTracingUsername), - Password: cliCtx.String(flagTracingPassword), - Probability: cliCtx.Float64(flagTracingProbability), - ServiceName: "plugin-service", - }, - TraceURL: cliCtx.String(flagTraceServiceURL), - Addr: cliCtx.String(flagAddr), - GitHubToken: cliCtx.String(flagGHToken), - GoProxy: GoProxy{ - URL: cliCtx.String(flagGoProxyURL), - Username: cliCtx.String(flagGoProxyUsername), - Password: cliCtx.String(flagGoProxyPassword), - }, - TTL: cliCtx.Duration(flagTTL), - } + return Config{ + MongoDB: internal.BuildMongoConfig(cliCtx), + Tracing: tracer.Config{ + Address: cliCtx.String(flagTracingAddress), + Insecure: cliCtx.Bool(flagTracingInsecure), + Username: cliCtx.String(flagTracingUsername), + Password: cliCtx.String(flagTracingPassword), + Probability: cliCtx.Float64(flagTracingProbability), + ServiceName: "plugin-service", + }, + TraceURL: cliCtx.String(flagTraceServiceURL), + Addr: cliCtx.String(flagAddr), + GitHubToken: cliCtx.String(flagGHToken), + GoProxy: GoProxy{ + URL: cliCtx.String(flagGoProxyURL), + Username: cliCtx.String(flagGoProxyUsername), + Password: cliCtx.String(flagGoProxyPassword), + }, + TTL: cliCtx.Duration(flagTTL), + } } func goProxyFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: flagGoProxyURL, - Usage: "Go Proxy URL", - EnvVars: []string{strcase.ToSNAKE(flagGoProxyURL)}, - Required: true, - }, - &cli.StringFlag{ - Name: flagGoProxyUsername, - Usage: "Go Proxy Username", - EnvVars: []string{strcase.ToSNAKE(flagGoProxyUsername)}, - Required: true, - }, - &cli.StringFlag{ - Name: flagGoProxyPassword, - Usage: "Go Proxy Password", - EnvVars: []string{strcase.ToSNAKE(flagGoProxyPassword)}, - Required: true, - }, - } + return []cli.Flag{ + &cli.StringFlag{ + Name: flagGoProxyURL, + Usage: "Go Proxy URL", + EnvVars: []string{strcase.ToSNAKE(flagGoProxyURL)}, + Required: true, + }, + &cli.StringFlag{ + Name: flagGoProxyUsername, + Usage: "Go Proxy Username", + EnvVars: []string{strcase.ToSNAKE(flagGoProxyUsername)}, + Required: true, + }, + &cli.StringFlag{ + Name: flagGoProxyPassword, + Usage: "Go Proxy Password", + EnvVars: []string{strcase.ToSNAKE(flagGoProxyPassword)}, + Required: true, + }, + } } func tracingFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: flagTracingAddress, - Usage: "Address to send traces", - EnvVars: []string{strcase.ToSNAKE(flagTracingAddress)}, - Value: "jaeger.jaeger.svc.cluster.local:4318", - }, - &cli.BoolFlag{ - Name: flagTracingInsecure, - Usage: "use HTTP instead of HTTPS", - EnvVars: []string{strcase.ToSNAKE(flagTracingInsecure)}, - Value: true, - }, - &cli.StringFlag{ - Name: flagTracingUsername, - Usage: "Username to connect to Jaeger", - EnvVars: []string{strcase.ToSNAKE(flagTracingUsername)}, - Value: "jaeger", - }, - &cli.StringFlag{ - Name: flagTracingPassword, - Usage: "Password to connect to Jaeger", - EnvVars: []string{strcase.ToSNAKE(flagTracingPassword)}, - Value: "jaeger", - }, - &cli.Float64Flag{ - Name: flagTracingProbability, - Usage: "Probability to send traces", - EnvVars: []string{strcase.ToSNAKE(flagTracingProbability)}, - Value: 0, - }, - } + return []cli.Flag{ + &cli.StringFlag{ + Name: flagTracingAddress, + Usage: "Address to send traces", + EnvVars: []string{strcase.ToSNAKE(flagTracingAddress)}, + Value: "jaeger.jaeger.svc.cluster.local:4318", + }, + &cli.BoolFlag{ + Name: flagTracingInsecure, + Usage: "use HTTP instead of HTTPS", + EnvVars: []string{strcase.ToSNAKE(flagTracingInsecure)}, + Value: true, + }, + &cli.StringFlag{ + Name: flagTracingUsername, + Usage: "Username to connect to Jaeger", + EnvVars: []string{strcase.ToSNAKE(flagTracingUsername)}, + Value: "jaeger", + }, + &cli.StringFlag{ + Name: flagTracingPassword, + Usage: "Password to connect to Jaeger", + EnvVars: []string{strcase.ToSNAKE(flagTracingPassword)}, + Value: "jaeger", + }, + &cli.Float64Flag{ + Name: flagTracingProbability, + Usage: "Probability to send traces", + EnvVars: []string{strcase.ToSNAKE(flagTracingProbability)}, + Value: 0, + }, + } } diff --git a/cmd/serve/config.go b/cmd/serve/config.go index 6878d05..870edcd 100644 --- a/cmd/serve/config.go +++ b/cmd/serve/config.go @@ -1,29 +1,29 @@ package serve import ( - "time" + "time" - "github.com/traefik/plugin-service/pkg/db/mongodb" - "github.com/traefik/plugin-service/pkg/tracer" + "github.com/traefik/plugin-service/pkg/db/mongodb" + "github.com/traefik/plugin-service/pkg/tracer" ) // Config holds the serve configuration. type Config struct { - Addr string - GitHubToken string + Addr string + GitHubToken string - TraceURL string + TraceURL string - MongoDB mongodb.Config - Tracing tracer.Config - GoProxy GoProxy + MongoDB mongodb.Config + Tracing tracer.Config + GoProxy GoProxy - TTL time.Duration + TTL time.Duration } // GoProxy holds the go-proxy configuration. type GoProxy struct { - URL string - Username string - Password string + URL string + Username string + Password string } diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index ddbceb0..985d589 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -1,122 +1,122 @@ package serve import ( - "context" - "fmt" - "net/http" - - "github.com/gorilla/mux" - "github.com/julienschmidt/httprouter" - "github.com/traefik/hub-trace-kpi/trace" - "github.com/traefik/plugin-service/cmd/internal" - "github.com/traefik/plugin-service/pkg/handlers" - "github.com/traefik/plugin-service/pkg/healthcheck" - "github.com/traefik/plugin-service/pkg/tracer" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" + "context" + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/julienschmidt/httprouter" + "github.com/traefik/hub-trace-kpi/trace" + "github.com/traefik/plugin-service/cmd/internal" + "github.com/traefik/plugin-service/pkg/handlers" + "github.com/traefik/plugin-service/pkg/healthcheck" + "github.com/traefik/plugin-service/pkg/tracer" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" ) func run(ctx context.Context, cfg Config) error { - stopTracer, err := setupTracing(ctx, cfg.Tracing, cfg.TraceURL) - if err != nil { - return fmt.Errorf("setup tracing provider: %w", err) - } - defer stopTracer() - - store, tearDown, err := internal.CreateMongoClient(ctx, cfg.MongoDB) - if err != nil { - return fmt.Errorf("unable to create MongoDB client: %w", err) - } - defer tearDown() - - if err = store.Bootstrap(); err != nil { - return fmt.Errorf("unable to bootstrap database: %w", err) - } - - var gpClient handlers.GoproxyPluginClient - gpClient, err = handlers.NewGoproxyClient(cfg.GoProxy.URL, cfg.GoProxy.Username, cfg.GoProxy.Password) - if err != nil { - return fmt.Errorf("unable to create go proxy client: %w", err) - } - - var ghClient handlers.GithubPluginClient - if cfg.GitHubToken != "" { - ghClient = handlers.NewGithubClient(context.Background(), cfg.GitHubToken) - } - - handler := handlers.New(store, gpClient, ghClient, cfg.TTL) - - healthChecker := healthcheck.Client{DB: store} - - r := http.NewServeMux() - - r.Handle("/public/", buildPublicRouter(handler)) - r.Handle("/internal/", buildInternalRouter(handler)) - r.Handle("/external/", buildExternalRouter(handler)) - r.HandleFunc("/live", healthChecker.Live) - r.HandleFunc("/ready", healthChecker.Ready) - - return http.ListenAndServe(cfg.Addr, r) + stopTracer, err := setupTracing(ctx, cfg.Tracing, cfg.TraceURL) + if err != nil { + return fmt.Errorf("setup tracing provider: %w", err) + } + defer stopTracer() + + store, tearDown, err := internal.CreateMongoClient(ctx, cfg.MongoDB) + if err != nil { + return fmt.Errorf("unable to create MongoDB client: %w", err) + } + defer tearDown() + + if err = store.Bootstrap(); err != nil { + return fmt.Errorf("unable to bootstrap database: %w", err) + } + + var gpClient handlers.GoproxyPluginClient + gpClient, err = handlers.NewGoproxyClient(cfg.GoProxy.URL, cfg.GoProxy.Username, cfg.GoProxy.Password) + if err != nil { + return fmt.Errorf("unable to create go proxy client: %w", err) + } + + var ghClient handlers.GithubPluginClient + if cfg.GitHubToken != "" { + ghClient = handlers.NewGithubClient(context.Background(), cfg.GitHubToken) + } + + handler := handlers.New(store, gpClient, ghClient, cfg.TTL) + + healthChecker := healthcheck.Client{DB: store} + + r := http.NewServeMux() + + r.Handle("/public/", buildPublicRouter(handler)) + r.Handle("/internal/", buildInternalRouter(handler)) + r.Handle("/external/", buildExternalRouter(handler)) + r.HandleFunc("/live", healthChecker.Live) + r.HandleFunc("/ready", healthChecker.Ready) + + return http.ListenAndServe(cfg.Addr, r) } func buildPublicRouter(handler handlers.Handlers) http.Handler { - r := mux.NewRouter() + r := mux.NewRouter() - r.Handle("/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "public_list")) - r.Handle("/download/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Download), "public_download")) - r.Handle("/validate/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Validate), "public_validate")) - r.Handle("/{uuid}", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "public_get")) + r.Handle("/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "public_list")) + r.Handle("/download/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Download), "public_download")) + r.Handle("/validate/{all:.+}", otelhttp.NewHandler(http.HandlerFunc(handler.Validate), "public_validate")) + r.Handle("/{uuid}", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "public_get")) - r.NotFoundHandler = http.HandlerFunc(handlers.NotFound) + r.NotFoundHandler = http.HandlerFunc(handlers.NotFound) - return http.StripPrefix("/public", r) + return http.StripPrefix("/public", r) } func buildInternalRouter(handler handlers.Handlers) http.Handler { - r := httprouter.New() + r := httprouter.New() - r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "internal_list")) - r.Handler(http.MethodPost, "/", otelhttp.NewHandler(http.HandlerFunc(handler.Create), "internal_create")) - r.Handler(http.MethodPut, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Update), "internal_update")) - r.Handler(http.MethodDelete, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Delete), "internal_delete")) + r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "internal_list")) + r.Handler(http.MethodPost, "/", otelhttp.NewHandler(http.HandlerFunc(handler.Create), "internal_create")) + r.Handler(http.MethodPut, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Update), "internal_update")) + r.Handler(http.MethodDelete, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Delete), "internal_delete")) - r.NotFound = http.HandlerFunc(handlers.NotFound) - r.PanicHandler = handlers.PanicHandler + r.NotFound = http.HandlerFunc(handlers.NotFound) + r.PanicHandler = handlers.PanicHandler - return http.StripPrefix("/internal", r) + return http.StripPrefix("/internal", r) } func buildExternalRouter(handler handlers.Handlers) http.Handler { - r := httprouter.New() + r := httprouter.New() - r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "external_list")) - r.Handler(http.MethodGet, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "external_get")) + r.Handler(http.MethodGet, "/", otelhttp.NewHandler(http.HandlerFunc(handler.List), "external_list")) + r.Handler(http.MethodGet, "/:uuid", otelhttp.NewHandler(http.HandlerFunc(handler.Get), "external_get")) - r.NotFound = http.HandlerFunc(handlers.NotFound) - r.PanicHandler = handlers.PanicHandler + r.NotFound = http.HandlerFunc(handlers.NotFound) + r.PanicHandler = handlers.PanicHandler - return http.StripPrefix("/external", r) + return http.StripPrefix("/external", r) } func setupTracing(ctx context.Context, cfg tracer.Config, traceServiceURL string) (func(), error) { - tracePropagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}) - traceProvider, err := tracer.NewOTLPProvider(ctx, cfg) - if err != nil { - return nil, fmt.Errorf("setup tracing provider: %w", err) - } + tracePropagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}) + traceProvider, err := tracer.NewOTLPProvider(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("setup tracing provider: %w", err) + } - otel.SetTracerProvider(traceProvider) - otel.SetTextMapPropagator(tracePropagator) + otel.SetTracerProvider(traceProvider) + otel.SetTextMapPropagator(tracePropagator) - if traceServiceURL != "" { - provider := trace.NewProvider("plugin-service", traceProvider, http.DefaultClient, traceServiceURL, 2) - provider.StartWorkers(ctx) + if traceServiceURL != "" { + provider := trace.NewProvider("plugin-service", traceProvider, http.DefaultClient, traceServiceURL, 2) + provider.StartWorkers(ctx) - otel.SetTracerProvider(provider) - } + otel.SetTracerProvider(provider) + } - return func() { - _ = traceProvider.Stop(ctx) - }, nil + return func() { + _ = traceProvider.Stop(ctx) + }, nil } diff --git a/pkg/handlers/github.go b/pkg/handlers/github.go index 5c8a56d..f38eb3f 100644 --- a/pkg/handlers/github.go +++ b/pkg/handlers/github.go @@ -1,45 +1,45 @@ package handlers import ( - "context" - "net/http" - "net/url" + "context" + "net/http" + "net/url" - "github.com/google/go-github/v57/github" - "golang.org/x/oauth2" + "github.com/google/go-github/v57/github" + "golang.org/x/oauth2" ) type GithubClient struct { - client *github.Client + client *github.Client } type GithubPluginClient interface { - Do(ctx context.Context, req *http.Request, v interface{}) (*github.Response, error) - GetArchiveLink(ctx context.Context, owner, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) (*url.URL, *github.Response, error) - GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error) + Do(ctx context.Context, req *http.Request, v interface{}) (*github.Response, error) + GetArchiveLink(ctx context.Context, owner, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) (*url.URL, *github.Response, error) + GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error) } // NewGithubClient create a new GitHub client. func NewGithubClient(ctx context.Context, token string) *GithubClient { - if token == "" { - return &GithubClient{github.NewClient(nil)} - } + if token == "" { + return &GithubClient{github.NewClient(nil)} + } - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) - return &GithubClient{github.NewClient(oauth2.NewClient(ctx, ts))} + return &GithubClient{github.NewClient(oauth2.NewClient(ctx, ts))} } func (c GithubClient) Do(ctx context.Context, req *http.Request, v interface{}) (*github.Response, error) { - return c.client.Do(ctx, req, v) + return c.client.Do(ctx, req, v) } func (c GithubClient) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat github.ArchiveFormat, opts *github.RepositoryContentGetOptions, maxRedirects int) (*url.URL, *github.Response, error) { - return c.client.Repositories.GetArchiveLink(ctx, owner, repo, archiveformat, opts, maxRedirects) + return c.client.Repositories.GetArchiveLink(ctx, owner, repo, archiveformat, opts, maxRedirects) } func (c GithubClient) GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error) { - return c.client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) + return c.client.Repositories.GetReleaseByTag(ctx, owner, repo, tag) } diff --git a/pkg/handlers/goproxy.go b/pkg/handlers/goproxy.go index c18873a..80a2270 100644 --- a/pkg/handlers/goproxy.go +++ b/pkg/handlers/goproxy.go @@ -1,41 +1,41 @@ package handlers import ( - "io" + "io" - "github.com/ldez/grignotin/goproxy" - "golang.org/x/mod/modfile" + "github.com/ldez/grignotin/goproxy" + "golang.org/x/mod/modfile" ) type GoproxyClient struct { - client *goproxy.Client + client *goproxy.Client } type GoproxyPluginClient interface { - DownloadSources(moduleName, version string) (io.ReadCloser, error) - GetModFile(moduleName, version string) (*modfile.File, error) + DownloadSources(moduleName, version string) (io.ReadCloser, error) + GetModFile(moduleName, version string) (*modfile.File, error) } // NewGoproxyClient creates a new Goproxy client. func NewGoproxyClient(url, username, password string) (*GoproxyClient, error) { - gpClient := goproxy.NewClient(url) + gpClient := goproxy.NewClient(url) - if url != "" && username != "" && password != "" { - tr, err := goproxy.NewBasicAuthTransport(username, password) - if err != nil { - return nil, err - } + if url != "" && username != "" && password != "" { + tr, err := goproxy.NewBasicAuthTransport(username, password) + if err != nil { + return nil, err + } - gpClient.HTTPClient = tr.Client() - } + gpClient.HTTPClient = tr.Client() + } - return &GoproxyClient{client: gpClient}, nil + return &GoproxyClient{client: gpClient}, nil } func (c GoproxyClient) DownloadSources(moduleName, version string) (io.ReadCloser, error) { - return c.client.DownloadSources(moduleName, version) + return c.client.DownloadSources(moduleName, version) } func (c GoproxyClient) GetModFile(moduleName, version string) (*modfile.File, error) { - return c.client.GetModFile(moduleName, version) + return c.client.GetModFile(moduleName, version) } diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index 824a0f6..e0b3b67 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -1,402 +1,402 @@ package handlers import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/url" - "regexp" - "strconv" - "time" - - "github.com/rs/zerolog/log" - "github.com/traefik/plugin-service/pkg/db" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "regexp" + "strconv" + "time" + + "github.com/rs/zerolog/log" + "github.com/traefik/plugin-service/pkg/db" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" ) const ( - nextPageHeader = "X-Next-Page" - defaultPerPage = 200 // TODO(ldez): we use 200 because currently the UI doesn't use the pagination. Must be changed. + nextPageHeader = "X-Next-Page" + defaultPerPage = 200 // TODO(ldez): we use 200 because currently the UI doesn't use the pagination. Must be changed. ) // PluginStorer is capable of storing plugins. type PluginStorer interface { - Get(ctx context.Context, id string) (db.Plugin, error) - Delete(ctx context.Context, id string) error - Create(context.Context, db.Plugin) (db.Plugin, error) - List(context.Context, db.Pagination) ([]db.Plugin, string, error) - GetByName(context.Context, string, bool, bool) (db.Plugin, error) - SearchByName(context.Context, string, db.Pagination) ([]db.Plugin, string, error) - Update(context.Context, string, db.Plugin) (db.Plugin, error) - - CreateHash(ctx context.Context, module, version, hash string) (db.PluginHash, error) - GetHashByName(ctx context.Context, module, version string) (db.PluginHash, error) + Get(ctx context.Context, id string) (db.Plugin, error) + Delete(ctx context.Context, id string) error + Create(context.Context, db.Plugin) (db.Plugin, error) + List(context.Context, db.Pagination) ([]db.Plugin, string, error) + GetByName(context.Context, string, bool, bool) (db.Plugin, error) + SearchByName(context.Context, string, db.Pagination) ([]db.Plugin, string, error) + Update(context.Context, string, db.Plugin) (db.Plugin, error) + + CreateHash(ctx context.Context, module, version, hash string) (db.PluginHash, error) + GetHashByName(ctx context.Context, module, version string) (db.PluginHash, error) } // Handlers a set of handlers. type Handlers struct { - store PluginStorer - goProxy GoproxyPluginClient - gh GithubPluginClient - tracer trace.Tracer - ttl int + store PluginStorer + goProxy GoproxyPluginClient + gh GithubPluginClient + tracer trace.Tracer + ttl int } // New creates all HTTP handlers. func New(store PluginStorer, goProxy GoproxyPluginClient, gh GithubPluginClient, ttl time.Duration) Handlers { - return Handlers{ - store: store, - goProxy: goProxy, - gh: gh, - tracer: otel.GetTracerProvider().Tracer("handler"), - ttl: int(ttl.Seconds()), - } + return Handlers{ + store: store, + goProxy: goProxy, + gh: gh, + tracer: otel.GetTracerProvider().Tracer("handler"), + ttl: int(ttl.Seconds()), + } } // Get gets a plugin. func (h Handlers) Get(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_get") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - id, err := getPathParam(req.URL) - if err != nil { - span.RecordError(err) - JSONError(rw, http.StatusBadRequest, "Missing plugin id") - return - } - - logger := log.With().Str("plugin_id", id).Logger() - - plugin, err := h.store.Get(ctx, id) - if err != nil { - span.RecordError(err) - - if errors.As(err, &db.NotFoundError{}) { - NotFound(rw, req) - return - } - - logger.Error().Err(err).Msg("Error while trying to get plugin") - JSONInternalServerError(rw) - return - } - - if err := json.NewEncoder(rw).Encode(plugin); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_get") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + id, err := getPathParam(req.URL) + if err != nil { + span.RecordError(err) + JSONError(rw, http.StatusBadRequest, "Missing plugin id") + return + } + + logger := log.With().Str("plugin_id", id).Logger() + + plugin, err := h.store.Get(ctx, id) + if err != nil { + span.RecordError(err) + + if errors.As(err, &db.NotFoundError{}) { + NotFound(rw, req) + return + } + + logger.Error().Err(err).Msg("Error while trying to get plugin") + JSONInternalServerError(rw) + return + } + + if err := json.NewEncoder(rw).Encode(plugin); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin") + JSONInternalServerError(rw) + return + } } // List gets a list of plugins. func (h Handlers) List(rw http.ResponseWriter, req *http.Request) { - rw.Header().Set("Content-Type", "application/json") + rw.Header().Set("Content-Type", "application/json") - if value := req.FormValue("query"); value != "" { - h.searchByName(rw, req) - return - } + if value := req.FormValue("query"); value != "" { + h.searchByName(rw, req) + return + } - if value := req.FormValue("name"); value != "" { - h.getByName(rw, req) - return - } + if value := req.FormValue("name"); value != "" { + h.getByName(rw, req) + return + } - h.list(rw, req) + h.list(rw, req) } // Create creates a plugin. func (h Handlers) Create(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_create") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - body, err := io.ReadAll(req.Body) - if err != nil { - span.RecordError(err) - log.Error().Err(err).Msg("Error reading body for creation") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - if len(body) == 0 { - err = errors.New("empty body") - span.RecordError(err) - log.Error().Err(err).Msg("Error decoding plugin for creation") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - pl := db.Plugin{} - - err = json.Unmarshal(body, &pl) - if err != nil { - span.RecordError(err) - log.Error().Err(err).Msg("Error decoding plugin for creation") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - logger := log.With().Str("module_name", pl.Name).Logger() - - created, err := h.store.Create(ctx, pl) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error persisting plugin") - JSONError(rw, http.StatusInternalServerError, "Could not persist data") - return - } - - rw.WriteHeader(http.StatusCreated) - if err := json.NewEncoder(rw).Encode(created); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error sending create response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_create") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + body, err := io.ReadAll(req.Body) + if err != nil { + span.RecordError(err) + log.Error().Err(err).Msg("Error reading body for creation") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + if len(body) == 0 { + err = errors.New("empty body") + span.RecordError(err) + log.Error().Err(err).Msg("Error decoding plugin for creation") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + pl := db.Plugin{} + + err = json.Unmarshal(body, &pl) + if err != nil { + span.RecordError(err) + log.Error().Err(err).Msg("Error decoding plugin for creation") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + logger := log.With().Str("module_name", pl.Name).Logger() + + created, err := h.store.Create(ctx, pl) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error persisting plugin") + JSONError(rw, http.StatusInternalServerError, "Could not persist data") + return + } + + rw.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(rw).Encode(created); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error sending create response") + JSONInternalServerError(rw) + return + } } // Update updates a plugin. func (h Handlers) Update(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_update") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - id, err := getPathParam(req.URL) - if err != nil { - span.RecordError(err) - log.Error().Err(err).Msg("Missing plugin id") - JSONError(rw, http.StatusBadRequest, "Missing plugin id") - return - } - - logger := log.With().Str("plugin_id", id).Logger() - - input := db.Plugin{} - err = json.NewDecoder(req.Body).Decode(&input) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error reading body for update") - JSONError(rw, http.StatusBadRequest, err.Error()) - return - } - - pg, err := h.store.Update(ctx, id, input) - if err != nil { - span.RecordError(err) - - if errors.As(err, &db.NotFoundError{}) { - span.RecordError(err) - log.Error().Err(err).Msg("Plugin not found") - NotFound(rw, req) - return - } - - logger.Error().Err(err).Msg("Error updating plugin") - JSONInternalServerError(rw) - return - } - - rw.WriteHeader(http.StatusOK) - if err := json.NewEncoder(rw).Encode(pg); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to marshal plugin") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_update") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + id, err := getPathParam(req.URL) + if err != nil { + span.RecordError(err) + log.Error().Err(err).Msg("Missing plugin id") + JSONError(rw, http.StatusBadRequest, "Missing plugin id") + return + } + + logger := log.With().Str("plugin_id", id).Logger() + + input := db.Plugin{} + err = json.NewDecoder(req.Body).Decode(&input) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error reading body for update") + JSONError(rw, http.StatusBadRequest, err.Error()) + return + } + + pg, err := h.store.Update(ctx, id, input) + if err != nil { + span.RecordError(err) + + if errors.As(err, &db.NotFoundError{}) { + span.RecordError(err) + log.Error().Err(err).Msg("Plugin not found") + NotFound(rw, req) + return + } + + logger.Error().Err(err).Msg("Error updating plugin") + JSONInternalServerError(rw) + return + } + + rw.WriteHeader(http.StatusOK) + if err := json.NewEncoder(rw).Encode(pg); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to marshal plugin") + JSONInternalServerError(rw) + return + } } // Delete deletes an instance info. func (h Handlers) Delete(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_delete") - defer span.End() - - id, err := getPathParam(req.URL) - if err != nil { - span.RecordError(err) - log.Warn().Err(err).Msg("Missing plugin id") - JSONError(rw, http.StatusBadRequest, "Missing plugin id") - return - } - - logger := log.With().Str("plugin_id", id).Logger() - - _, err = h.store.Get(ctx, id) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin information") - NotFound(rw, req) - return - } - - err = h.store.Delete(ctx, id) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to delete the plugin info") - JSONError(rw, http.StatusInternalServerError, "Failed to delete plugin info") - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_delete") + defer span.End() + + id, err := getPathParam(req.URL) + if err != nil { + span.RecordError(err) + log.Warn().Err(err).Msg("Missing plugin id") + JSONError(rw, http.StatusBadRequest, "Missing plugin id") + return + } + + logger := log.With().Str("plugin_id", id).Logger() + + _, err = h.store.Get(ctx, id) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin information") + NotFound(rw, req) + return + } + + err = h.store.Delete(ctx, id) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to delete the plugin info") + JSONError(rw, http.StatusInternalServerError, "Failed to delete plugin info") + return + } } func (h Handlers) searchByName(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_searchByName") - defer span.End() - - rw.Header().Set("Content-Type", "application/json") - - query := unquote(req.FormValue("query")) - start := req.URL.Query().Get("start") - - logger := log.With().Str("search_query", query).Str("search_start", start).Logger() - - plugins, next, err := h.store.SearchByName(ctx, query, db.Pagination{ - Start: start, - Size: defaultPerPage, - }) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Unable to get plugins by name") - JSONError(rw, http.StatusBadRequest, "Unable to get plugins") - return - } - - if len(plugins) == 0 { - if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error sending create response") - JSONInternalServerError(rw) - } - return - } - - rw.Header().Set(nextPageHeader, next) - - if err := json.NewEncoder(rw).Encode(plugins); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error sending create response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_searchByName") + defer span.End() + + rw.Header().Set("Content-Type", "application/json") + + query := unquote(req.FormValue("query")) + start := req.URL.Query().Get("start") + + logger := log.With().Str("search_query", query).Str("search_start", start).Logger() + + plugins, next, err := h.store.SearchByName(ctx, query, db.Pagination{ + Start: start, + Size: defaultPerPage, + }) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Unable to get plugins by name") + JSONError(rw, http.StatusBadRequest, "Unable to get plugins") + return + } + + if len(plugins) == 0 { + if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error sending create response") + JSONInternalServerError(rw) + } + return + } + + rw.Header().Set(nextPageHeader, next) + + if err := json.NewEncoder(rw).Encode(plugins); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error sending create response") + JSONInternalServerError(rw) + return + } } func (h Handlers) getByName(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_getByName") - defer span.End() - - name := unquote(req.FormValue("name")) - logger := log.With().Str("module_name", name).Logger() - - var filterHidden bool - var err error - if value := req.FormValue("filterHidden"); value != "" { - filterHidden, err = strconv.ParseBool(value) - if err != nil { - logger.Debug().Err(err).Msg("Unable to parse filterHidden field") - JSONInternalServerError(rw) - return - } - } - - plugin, err := h.store.GetByName(ctx, name, true, filterHidden) - if err != nil { - span.RecordError(err) - - if errors.As(err, &db.NotFoundError{}) { - logger.Error().Msg("plugin not found") - NotFound(rw, req) - return - } - - logger.Error().Err(err).Msg("Error while fetch") - JSONInternalServerError(rw) - return - } - - if err := json.NewEncoder(rw).Encode([]*db.Plugin{&plugin}); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to encode response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_getByName") + defer span.End() + + name := unquote(req.FormValue("name")) + logger := log.With().Str("module_name", name).Logger() + + var filterHidden bool + var err error + if value := req.FormValue("filterHidden"); value != "" { + filterHidden, err = strconv.ParseBool(value) + if err != nil { + logger.Debug().Err(err).Msg("Unable to parse filterHidden field") + JSONInternalServerError(rw) + return + } + } + + plugin, err := h.store.GetByName(ctx, name, true, filterHidden) + if err != nil { + span.RecordError(err) + + if errors.As(err, &db.NotFoundError{}) { + logger.Error().Msg("plugin not found") + NotFound(rw, req) + return + } + + logger.Error().Err(err).Msg("Error while fetch") + JSONInternalServerError(rw) + return + } + + if err := json.NewEncoder(rw).Encode([]*db.Plugin{&plugin}); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to encode response") + JSONInternalServerError(rw) + return + } } func (h Handlers) list(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_list") - defer span.End() - - start := req.URL.Query().Get("start") - - logger := log.With().Str("search_start", start).Logger() - - plugins, next, err := h.store.List(ctx, db.Pagination{ - Start: start, - Size: defaultPerPage, - }) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error fetching plugins") - NotFound(rw, req) - return - } - - // TODO: detection of the plugin name changes must be done in piceus. - var cleanPlugins []db.Plugin - for _, plugin := range plugins { - if plugin.Name == "github.com/tommoulard/fail2ban" || plugin.Name == "github.com/tommoulard/htransformation" { - continue - } - - cleanPlugins = append(cleanPlugins, plugin) - } - - if len(cleanPlugins) == 0 { - if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to encode response") - JSONInternalServerError(rw) - } - return - } - - rw.Header().Set(nextPageHeader, next) - - if err := json.NewEncoder(rw).Encode(cleanPlugins); err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to encode response") - JSONInternalServerError(rw) - return - } + ctx, span := h.tracer.Start(req.Context(), "handler_list") + defer span.End() + + start := req.URL.Query().Get("start") + + logger := log.With().Str("search_start", start).Logger() + + plugins, next, err := h.store.List(ctx, db.Pagination{ + Start: start, + Size: defaultPerPage, + }) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error fetching plugins") + NotFound(rw, req) + return + } + + // TODO: detection of the plugin name changes must be done in piceus. + var cleanPlugins []db.Plugin + for _, plugin := range plugins { + if plugin.Name == "github.com/tommoulard/fail2ban" || plugin.Name == "github.com/tommoulard/htransformation" { + continue + } + + cleanPlugins = append(cleanPlugins, plugin) + } + + if len(cleanPlugins) == 0 { + if err := json.NewEncoder(rw).Encode(make([]*db.Plugin, 0)); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to encode response") + JSONInternalServerError(rw) + } + return + } + + rw.Header().Set(nextPageHeader, next) + + if err := json.NewEncoder(rw).Encode(cleanPlugins); err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to encode response") + JSONInternalServerError(rw) + return + } } // NotFound a not found handler. func NotFound(rw http.ResponseWriter, _ *http.Request) { - JSONError(rw, http.StatusNotFound, http.StatusText(http.StatusNotFound)) + JSONError(rw, http.StatusNotFound, http.StatusText(http.StatusNotFound)) } func getPathParam(uri *url.URL) (string, error) { - exp := regexp.MustCompile(`^/([\w-]+)/?$`) - parts := exp.FindStringSubmatch(uri.Path) + exp := regexp.MustCompile(`^/([\w-]+)/?$`) + parts := exp.FindStringSubmatch(uri.Path) - if len(parts) != 2 { - return "", errors.New("missing id") - } + if len(parts) != 2 { + return "", errors.New("missing id") + } - return parts[1], nil + return parts[1], nil } func unquote(value string) string { - unquote, err := strconv.Unquote(value) - if err != nil { - return value - } + unquote, err := strconv.Unquote(value) + if err != nil { + return value + } - return unquote + return unquote } diff --git a/pkg/handlers/handlers_test.go b/pkg/handlers/handlers_test.go index 557bc76..32e7630 100644 --- a/pkg/handlers/handlers_test.go +++ b/pkg/handlers/handlers_test.go @@ -1,184 +1,184 @@ package handlers import ( - "net/http" - "net/http/httptest" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/traefik/plugin-service/pkg/db" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/plugin-service/pkg/db" ) func TestHandlers_List(t *testing.T) { - data := []db.Plugin{ - { - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - }, - { - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Block Path", - ID: "2768097807845374", - Import: "github.com/traefik/plugin-blockpath", - LatestVersion: "v0.3.1", - Name: "github.com/traefik/plugin-blockpath", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 3, - Summary: "Block Path plugin", - Type: "middleware", - Versions: []string{"v0.3.1", "v0.2.0", "v0.1.0"}, - }, - } - - testDB := NewPluginStorerMock(t).OnList(db.Pagination{ - Start: "", - Size: 200, - }).Once().TypedReturns(data, "next", nil).Parent - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) - - New(testDB, nil, nil, 0).List(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "next", rw.Header().Get(nextPageHeader)) - - file, err := os.ReadFile("./fixtures/get_plugins.json") - require.NoError(t, err) - - assert.JSONEq(t, string(file), rw.Body.String()) + data := []db.Plugin{ + { + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + }, + { + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Block Path", + ID: "2768097807845374", + Import: "github.com/traefik/plugin-blockpath", + LatestVersion: "v0.3.1", + Name: "github.com/traefik/plugin-blockpath", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 3, + Summary: "Block Path plugin", + Type: "middleware", + Versions: []string{"v0.3.1", "v0.2.0", "v0.1.0"}, + }, + } + + testDB := NewPluginStorerMock(t).OnList(db.Pagination{ + Start: "", + Size: 200, + }).Once().TypedReturns(data, "next", nil).Parent + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + + New(testDB, nil, nil, 0).List(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "next", rw.Header().Get(nextPageHeader)) + + file, err := os.ReadFile("./fixtures/get_plugins.json") + require.NoError(t, err) + + assert.JSONEq(t, string(file), rw.Body.String()) } func TestHandlers_List_GetByName(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } - testDB := NewPluginStorerMock(t).OnGetByName("Demo Plugin", true, false).Once().TypedReturns(data, nil).Parent + testDB := NewPluginStorerMock(t).OnGetByName("Demo Plugin", true, false).Once().TypedReturns(data, nil).Parent - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) - New(testDB, nil, nil, 0).getByName(rw, req) + New(testDB, nil, nil, 0).getByName(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusOK, rw.Code) - file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") - require.NoError(t, err) + file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") + require.NoError(t, err) - assert.JSONEq(t, string(file), rw.Body.String()) + assert.JSONEq(t, string(file), rw.Body.String()) } func TestHandlers_List_GetByName_hidden(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - Hidden: true, - } + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + Hidden: true, + } - _ = data + _ = data - testDB := NewPluginStorerMock(t). - OnGetByName("Demo Plugin", true, true).Once().TypedReturns(data, nil). - OnGetByName("Demo Plugin", true, false).Once().TypedReturns(db.Plugin{}, db.NotFoundError{}).Parent + testDB := NewPluginStorerMock(t). + OnGetByName("Demo Plugin", true, true).Once().TypedReturns(data, nil). + OnGetByName("Demo Plugin", true, false).Once().TypedReturns(db.Plugin{}, db.NotFoundError{}).Parent - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin", http.NoBody) - New(testDB, nil, nil, 0).getByName(rw, req) + New(testDB, nil, nil, 0).getByName(rw, req) - assert.Equal(t, http.StatusNotFound, rw.Code) + assert.Equal(t, http.StatusNotFound, rw.Code) - rw = httptest.NewRecorder() + rw = httptest.NewRecorder() - req = httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin&filterHidden=true", http.NoBody) + req = httptest.NewRequest(http.MethodGet, "/?name=Demo%20Plugin&filterHidden=true", http.NoBody) - New(testDB, nil, nil, 0).getByName(rw, req) + New(testDB, nil, nil, 0).getByName(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusOK, rw.Code) - file, err := os.ReadFile("./fixtures/get_plugin_by_name_hidden.json") - require.NoError(t, err) + file, err := os.ReadFile("./fixtures/get_plugin_by_name_hidden.json") + require.NoError(t, err) - assert.JSONEq(t, string(file), rw.Body.String()) + assert.JSONEq(t, string(file), rw.Body.String()) } func TestHandlers_List_SearchByName(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } - testDB := NewPluginStorerMock(t).OnGetByName("", true, false).Once().TypedReturns(data, nil).Parent + testDB := NewPluginStorerMock(t).OnGetByName("", true, false).Once().TypedReturns(data, nil).Parent - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/?query=demo", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/?query=demo", http.NoBody) - New(testDB, nil, nil, 0).getByName(rw, req) + New(testDB, nil, nil, 0).getByName(rw, req) - assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, http.StatusOK, rw.Code) - file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") - require.NoError(t, err) + file, err := os.ReadFile("./fixtures/get_plugin_by_name.json") + require.NoError(t, err) - assert.JSONEq(t, string(file), rw.Body.String()) + assert.JSONEq(t, string(file), rw.Body.String()) } diff --git a/pkg/handlers/module.go b/pkg/handlers/module.go index 9e5b900..f84f733 100644 --- a/pkg/handlers/module.go +++ b/pkg/handlers/module.go @@ -1,420 +1,420 @@ package handlers import ( - "bytes" - "context" - "crypto/sha256" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "path" - "path/filepath" - "strings" - - "github.com/google/go-github/v57/github" - "github.com/rs/zerolog/log" - ttrace "github.com/traefik/hub-trace-kpi/trace" - "github.com/traefik/plugin-service/pkg/db" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" + "bytes" + "context" + "crypto/sha256" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/google/go-github/v57/github" + "github.com/rs/zerolog/log" + ttrace "github.com/traefik/hub-trace-kpi/trace" + "github.com/traefik/plugin-service/pkg/db" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) const ( - hashHeader = "X-Plugin-Hash" - cacheControlHeader = "Cache-Control" - cacheControlNoCache = "no-cache" - cacheControlMaxAge = "max-age" - cacheControlSMaxAge = "s-maxage" + hashHeader = "X-Plugin-Hash" + cacheControlHeader = "Cache-Control" + cacheControlNoCache = "no-cache" + cacheControlMaxAge = "max-age" + cacheControlSMaxAge = "s-maxage" ) // Download a plugin archive. func (h Handlers) Download(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_download") - defer span.End() - - if req.Method != http.MethodGet { - span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) - log.Error().Msgf("Unsupported method: %s", req.Method) - JSONErrorf(rw, http.StatusMethodNotAllowed, "unsupported method: %s", req.Method) - return - } - - pluginName, version := extractPluginInfo(req.URL, "/download/") - - attributes := []attribute.KeyValue{ - attribute.String("module.moduleName", pluginName), - attribute.String("module.version", version), - attribute.String("module.ip", getUserIP(req)), - } - - logger := log.With().Str("plugin_name", pluginName).Str("plugin_version", version).Logger() - - plugin, err := h.store.GetByName(ctx, pluginName, false, false) - if err != nil { - span.RecordError(err) - if errors.As(err, &db.NotFoundError{}) { - logger.Warn().Err(err).Msg("Unknown plugin") - setCacheControl(rw, 0) - JSONErrorf(rw, http.StatusNotFound, "Unknown plugin: %s@%s", pluginName, version) - return - } - - logger.Error().Err(err).Msg("Failed to get plugin") - setCacheControl(rw, 0) - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - - ttrace.Captured(span) - - sum := req.Header.Get(hashHeader) - if sum != "" { - attributes = append(attributes, attribute.String("module.sum", sum)) - ph, errH := h.store.GetHashByName(ctx, pluginName, version) - if errH != nil { - span.RecordError(errH) - if !errors.As(errH, &db.NotFoundError{}) { - logger.Error().Err(errH).Msg("Failed to get plugin hash") - setCacheControl(rw, 0) - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - } else if ph.Hash == sum { - setCacheControl(rw, h.ttl) - rw.WriteHeader(http.StatusNotModified) - return - } - - span.AddEvent("module.download", trace.WithAttributes(attributes...)) - logger.Error().Msgf("Someone is trying to hack the archive: %v != %v", ph.Hash, sum) - } - - span.SetAttributes(attributes...) - - switch strings.ToLower(plugin.Runtime) { - case "wasm": - // WASM plugins - if h.gh == nil { - logger.Error().Msg("Failed to get plugin: missing GitHub client.") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - - h.downloadGitHub(ctx, pluginName, version, true)(rw, req) - return - - default: - // Yaegi plugins - modFile, err := h.goProxy.GetModFile(pluginName, version) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get module file") - setCacheControl(rw, 0) - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) - return - } - - // Uses GitHub when there are dependencies because Go proxy archives don't contain vendor folder. - if h.gh != nil && len(modFile.Require) > 0 { - h.downloadGitHub(ctx, pluginName, version, false)(rw, req) - return - } - - h.downloadGoProxy(ctx, pluginName, version)(rw, req) - } + ctx, span := h.tracer.Start(req.Context(), "handler_download") + defer span.End() + + if req.Method != http.MethodGet { + span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) + log.Error().Msgf("Unsupported method: %s", req.Method) + JSONErrorf(rw, http.StatusMethodNotAllowed, "unsupported method: %s", req.Method) + return + } + + pluginName, version := extractPluginInfo(req.URL, "/download/") + + attributes := []attribute.KeyValue{ + attribute.String("module.moduleName", pluginName), + attribute.String("module.version", version), + attribute.String("module.ip", getUserIP(req)), + } + + logger := log.With().Str("plugin_name", pluginName).Str("plugin_version", version).Logger() + + plugin, err := h.store.GetByName(ctx, pluginName, false, false) + if err != nil { + span.RecordError(err) + if errors.As(err, &db.NotFoundError{}) { + logger.Warn().Err(err).Msg("Unknown plugin") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusNotFound, "Unknown plugin: %s@%s", pluginName, version) + return + } + + logger.Error().Err(err).Msg("Failed to get plugin") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + + ttrace.Captured(span) + + sum := req.Header.Get(hashHeader) + if sum != "" { + attributes = append(attributes, attribute.String("module.sum", sum)) + ph, errH := h.store.GetHashByName(ctx, pluginName, version) + if errH != nil { + span.RecordError(errH) + if !errors.As(errH, &db.NotFoundError{}) { + logger.Error().Err(errH).Msg("Failed to get plugin hash") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + } else if ph.Hash == sum { + setCacheControl(rw, h.ttl) + rw.WriteHeader(http.StatusNotModified) + return + } + + span.AddEvent("module.download", trace.WithAttributes(attributes...)) + logger.Error().Msgf("Someone is trying to hack the archive: %v != %v", ph.Hash, sum) + } + + span.SetAttributes(attributes...) + + switch strings.ToLower(plugin.Runtime) { + case "wasm": + // WASM plugins + if h.gh == nil { + logger.Error().Msg("Failed to get plugin: missing GitHub client.") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + + h.downloadGitHub(ctx, pluginName, version, true)(rw, req) + return + + default: + // Yaegi plugins + modFile, err := h.goProxy.GetModFile(pluginName, version) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get module file") + setCacheControl(rw, 0) + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", pluginName, version) + return + } + + // Uses GitHub when there are dependencies because Go proxy archives don't contain vendor folder. + if h.gh != nil && len(modFile.Require) > 0 { + h.downloadGitHub(ctx, pluginName, version, false)(rw, req) + return + } + + h.downloadGoProxy(ctx, pluginName, version)(rw, req) + } } func getUserIP(req *http.Request) string { - ip, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { - ip = req.RemoteAddr - } - - xff := req.Header.Get("X-Forwarded-For") - if xff != "" { - ip = strings.Trim(strings.Split(xff, ",")[0], " ") - } - return ip + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + ip = req.RemoteAddr + } + + xff := req.Header.Get("X-Forwarded-For") + if xff != "" { + ip = strings.Trim(strings.Split(xff, ",")[0], " ") + } + return ip } func (h Handlers) downloadGoProxy(ctx context.Context, moduleName, version string) http.HandlerFunc { - return func(rw http.ResponseWriter, _ *http.Request) { - var span trace.Span - ctx, span = h.tracer.Start(ctx, "handler_downloadGoProxy") - defer span.End() - - logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() - - sources, err := h.goProxy.DownloadSources(moduleName, version) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to download sources") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - defer func() { _ = sources.Close() }() - - _, err = h.store.GetHashByName(ctx, moduleName, version) - if err != nil && !errors.As(err, &db.NotFoundError{}) { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - if err == nil { - setCacheControl(rw, h.ttl) - _, err = io.Copy(rw, sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - return - } - - raw, err := io.ReadAll(sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to read response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - hash := sha256.New() - - _, err = hash.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to compute hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - sum := fmt.Sprintf("%x", hash.Sum(nil)) - - _, err = h.store.CreateHash(ctx, moduleName, version, sum) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error persisting plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Could not persist data: %s@%s", moduleName, version) - return - } - - setCacheControl(rw, h.ttl) - _, err = rw.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "failed to get plugin %s@%s", moduleName, version) - return - } - } + return func(rw http.ResponseWriter, _ *http.Request) { + var span trace.Span + ctx, span = h.tracer.Start(ctx, "handler_downloadGoProxy") + defer span.End() + + logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() + + sources, err := h.goProxy.DownloadSources(moduleName, version) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to download sources") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + defer func() { _ = sources.Close() }() + + _, err = h.store.GetHashByName(ctx, moduleName, version) + if err != nil && !errors.As(err, &db.NotFoundError{}) { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + if err == nil { + setCacheControl(rw, h.ttl) + _, err = io.Copy(rw, sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + return + } + + raw, err := io.ReadAll(sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to read response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + hash := sha256.New() + + _, err = hash.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to compute hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + sum := fmt.Sprintf("%x", hash.Sum(nil)) + + _, err = h.store.CreateHash(ctx, moduleName, version, sum) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error persisting plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Could not persist data: %s@%s", moduleName, version) + return + } + + setCacheControl(rw, h.ttl) + _, err = rw.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "failed to get plugin %s@%s", moduleName, version) + return + } + } } func (h Handlers) downloadGitHub(ctx context.Context, moduleName, version string, fromAssets bool) http.HandlerFunc { - return func(rw http.ResponseWriter, _ *http.Request) { - var span trace.Span - ctx, span = h.tracer.Start(ctx, "handler_downloadGitHub") - defer span.End() - - logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() - - var request *http.Request - var err error - if fromAssets { - request, err = h.getAssetLinkRequest(ctx, moduleName, version) - } else { - request, err = h.getArchiveLinkRequest(ctx, moduleName, version) - } - if err != nil { - setCacheControl(rw, 0) - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get archive link") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - _, err = h.store.GetHashByName(ctx, moduleName, version) - if err != nil && !errors.As(err, &db.NotFoundError{}) { - setCacheControl(rw, 0) - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - if err == nil { - setCacheControl(rw, h.ttl) - _, err = h.gh.Do(ctx, request, rw) - if err != nil { - setCacheControl(rw, 0) - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - return - } - - sources := bytes.NewBufferString("") - - _, err = h.gh.Do(ctx, request, sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to get archive content") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - raw, err := io.ReadAll(sources) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to read response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - hash := sha256.New() - - _, err = hash.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to compute hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - sum := fmt.Sprintf("%x", hash.Sum(nil)) - - _, err = h.store.CreateHash(ctx, moduleName, version, sum) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Error persisting plugin hash") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - - setCacheControl(rw, h.ttl) - _, err = rw.Write(raw) - if err != nil { - span.RecordError(err) - logger.Error().Err(err).Msg("Failed to write response body") - JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) - return - } - } + return func(rw http.ResponseWriter, _ *http.Request) { + var span trace.Span + ctx, span = h.tracer.Start(ctx, "handler_downloadGitHub") + defer span.End() + + logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() + + var request *http.Request + var err error + if fromAssets { + request, err = h.getAssetLinkRequest(ctx, moduleName, version) + } else { + request, err = h.getArchiveLinkRequest(ctx, moduleName, version) + } + if err != nil { + setCacheControl(rw, 0) + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get archive link") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + _, err = h.store.GetHashByName(ctx, moduleName, version) + if err != nil && !errors.As(err, &db.NotFoundError{}) { + setCacheControl(rw, 0) + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + if err == nil { + setCacheControl(rw, h.ttl) + _, err = h.gh.Do(ctx, request, rw) + if err != nil { + setCacheControl(rw, 0) + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + return + } + + sources := bytes.NewBufferString("") + + _, err = h.gh.Do(ctx, request, sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to get archive content") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + raw, err := io.ReadAll(sources) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to read response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + hash := sha256.New() + + _, err = hash.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to compute hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + sum := fmt.Sprintf("%x", hash.Sum(nil)) + + _, err = h.store.CreateHash(ctx, moduleName, version, sum) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Error persisting plugin hash") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + + setCacheControl(rw, h.ttl) + _, err = rw.Write(raw) + if err != nil { + span.RecordError(err) + logger.Error().Err(err).Msg("Failed to write response body") + JSONErrorf(rw, http.StatusInternalServerError, "Failed to get plugin %s@%s", moduleName, version) + return + } + } } func (h Handlers) getArchiveLinkRequest(ctx context.Context, moduleName, version string) (*http.Request, error) { - ctx, span := h.tracer.Start(ctx, "handler_getArchiveLinkRequest") - defer span.End() + ctx, span := h.tracer.Start(ctx, "handler_getArchiveLinkRequest") + defer span.End() - opts := &github.RepositoryContentGetOptions{Ref: version} + opts := &github.RepositoryContentGetOptions{Ref: version} - owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) - owner = strings.TrimSuffix(owner, "/") + owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) + owner = strings.TrimSuffix(owner, "/") - link, _, err := h.gh.GetArchiveLink(ctx, owner, repoName, github.Zipball, opts, 3) - if err != nil { - span.RecordError(err) - return nil, fmt.Errorf("failed to get archive link: %w", err) - } + link, _, err := h.gh.GetArchiveLink(ctx, owner, repoName, github.Zipball, opts, 3) + if err != nil { + span.RecordError(err) + return nil, fmt.Errorf("failed to get archive link: %w", err) + } - return http.NewRequestWithContext(ctx, http.MethodGet, link.String(), http.NoBody) + return http.NewRequestWithContext(ctx, http.MethodGet, link.String(), http.NoBody) } func (h Handlers) getAssetLinkRequest(ctx context.Context, moduleName, version string) (*http.Request, error) { - ctx, span := h.tracer.Start(ctx, "handler_getAssetLinkRequest") - defer span.End() - - owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) - owner = strings.TrimSuffix(owner, "/") - - release, _, err := h.gh.GetReleaseByTag(ctx, owner, repoName, version) - if err != nil { - span.RecordError(err) - return nil, fmt.Errorf("failed to get release: %w", err) - } - - assets := map[*github.ReleaseAsset]struct{}{} - for _, asset := range release.Assets { - if filepath.Ext(asset.GetName()) == ".zip" { - assets[asset] = struct{}{} - } - } - - if len(assets) > 1 { - return nil, fmt.Errorf("too many zip archive (%d)", len(assets)) - } - - for asset := range assets { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.GetURL(), http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("Accept", "application/octet-stream") - - return req, nil - } - return nil, errors.New("zip archive not found") + ctx, span := h.tracer.Start(ctx, "handler_getAssetLinkRequest") + defer span.End() + + owner, repoName := path.Split(strings.TrimPrefix(moduleName, "github.com/")) + owner = strings.TrimSuffix(owner, "/") + + release, _, err := h.gh.GetReleaseByTag(ctx, owner, repoName, version) + if err != nil { + span.RecordError(err) + return nil, fmt.Errorf("failed to get release: %w", err) + } + + assets := map[*github.ReleaseAsset]struct{}{} + for _, asset := range release.Assets { + if filepath.Ext(asset.GetName()) == ".zip" { + assets[asset] = struct{}{} + } + } + + if len(assets) > 1 { + return nil, fmt.Errorf("too many zip archive (%d)", len(assets)) + } + + for asset := range assets { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.GetURL(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Accept", "application/octet-stream") + + return req, nil + } + return nil, errors.New("zip archive not found") } // Validate validates a plugin archive. func (h Handlers) Validate(rw http.ResponseWriter, req *http.Request) { - ctx, span := h.tracer.Start(req.Context(), "handler_getArchiveLinkRequest") - defer span.End() - - if req.Method != http.MethodGet { - span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) - log.Warn().Msgf("Unsupported method: %s", req.Method) - JSONErrorf(rw, http.StatusMethodNotAllowed, "Unsupported method: %s", req.Method) - return - } - - moduleName, version := extractPluginInfo(req.URL, "/validate/") - - logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() - - headerSum := req.Header.Get(hashHeader) - ph, err := h.store.GetHashByName(ctx, moduleName, version) - if err != nil { - if errors.As(err, &db.NotFoundError{}) { - span.RecordError(fmt.Errorf("plugin not found %s@%s", moduleName, version)) - logger.Warn().Err(err).Msg("Plugin not found") - JSONErrorf(rw, http.StatusNotFound, "Plugin not found %s@%s", moduleName, version) - return - } - - span.RecordError(err) - logger.Error().Err(err).Msg("Error while fetch") - JSONInternalServerError(rw) - return - } - - if ph.Hash == headerSum { - rw.WriteHeader(http.StatusOK) - return - } - - rw.WriteHeader(http.StatusNotFound) + ctx, span := h.tracer.Start(req.Context(), "handler_getArchiveLinkRequest") + defer span.End() + + if req.Method != http.MethodGet { + span.RecordError(fmt.Errorf("unsupported method: %s", req.Method)) + log.Warn().Msgf("Unsupported method: %s", req.Method) + JSONErrorf(rw, http.StatusMethodNotAllowed, "Unsupported method: %s", req.Method) + return + } + + moduleName, version := extractPluginInfo(req.URL, "/validate/") + + logger := log.With().Str("module_name", moduleName).Str("module_version", version).Logger() + + headerSum := req.Header.Get(hashHeader) + ph, err := h.store.GetHashByName(ctx, moduleName, version) + if err != nil { + if errors.As(err, &db.NotFoundError{}) { + span.RecordError(fmt.Errorf("plugin not found %s@%s", moduleName, version)) + logger.Warn().Err(err).Msg("Plugin not found") + JSONErrorf(rw, http.StatusNotFound, "Plugin not found %s@%s", moduleName, version) + return + } + + span.RecordError(err) + logger.Error().Err(err).Msg("Error while fetch") + JSONInternalServerError(rw) + return + } + + if ph.Hash == headerSum { + rw.WriteHeader(http.StatusOK) + return + } + + rw.WriteHeader(http.StatusNotFound) } func extractPluginInfo(endpoint *url.URL, sep string) (string, string) { - _, after, _ := strings.Cut(strings.TrimSuffix(endpoint.Path, "/"), sep) - moduleName, version := path.Split(after) + _, after, _ := strings.Cut(strings.TrimSuffix(endpoint.Path, "/"), sep) + moduleName, version := path.Split(after) - return cleanModuleName(moduleName), version + return cleanModuleName(moduleName), version } func cleanModuleName(moduleName string) string { - return strings.TrimSuffix(strings.TrimPrefix(moduleName, "/"), "/") + return strings.TrimSuffix(strings.TrimPrefix(moduleName, "/"), "/") } // cf. https://www.rfc-editor.org/rfc/rfc7231#section-6.1 @@ -422,13 +422,13 @@ func cleanModuleName(moduleName string) string { // This method should only be used when 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 are returned but setting // Cache-Control to no-cache is the only way this can't be cached. func setCacheControl(rw http.ResponseWriter, t int) { - if t == 0 { - rw.Header().Set(cacheControlHeader, cacheControlNoCache) - return - } - - rw.Header().Set(cacheControlHeader, strings.Join([]string{ - fmt.Sprintf("%s=%d", cacheControlMaxAge, t), - fmt.Sprintf("%s=%d", cacheControlSMaxAge, t), - }, ",")) + if t == 0 { + rw.Header().Set(cacheControlHeader, cacheControlNoCache) + return + } + + rw.Header().Set(cacheControlHeader, strings.Join([]string{ + fmt.Sprintf("%s=%d", cacheControlMaxAge, t), + fmt.Sprintf("%s=%d", cacheControlSMaxAge, t), + }, ",")) } diff --git a/pkg/handlers/module_test.go b/pkg/handlers/module_test.go index 3c1fd16..b3f2857 100644 --- a/pkg/handlers/module_test.go +++ b/pkg/handlers/module_test.go @@ -1,594 +1,594 @@ package handlers import ( - "errors" - "io" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "time" - - "github.com/google/go-github/v57/github" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/traefik/plugin-service/pkg/db" - "golang.org/x/mod/modfile" - "golang.org/x/mod/module" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/google/go-github/v57/github" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/traefik/plugin-service/pkg/db" + "golang.org/x/mod/modfile" + "golang.org/x/mod/module" ) func Test_cleanModuleName(t *testing.T) { - testCases := []struct { - name string - expected string - }{ - { - name: "/powpow/", - expected: "powpow", - }, - { - name: "/powpow/v2", - expected: "powpow/v2", - }, - { - name: "powpow/v2", - expected: "powpow/v2", - }, - - { - name: "powpow", - expected: "powpow", - }, - - { - name: "powpow/v2/", - expected: "powpow/v2", - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - name := cleanModuleName(test.name) - assert.Equal(t, test.expected, name) - }) - } + testCases := []struct { + name string + expected string + }{ + { + name: "/powpow/", + expected: "powpow", + }, + { + name: "/powpow/v2", + expected: "powpow/v2", + }, + { + name: "powpow/v2", + expected: "powpow/v2", + }, + + { + name: "powpow", + expected: "powpow", + }, + + { + name: "powpow/v2/", + expected: "powpow/v2", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + name := cleanModuleName(test.name) + assert.Equal(t, test.expected, name) + }) + } } func Test_extractPluginInfo(t *testing.T) { - type expected struct { - moduleName string - version string - } - - testCases := []struct { - desc string - url string - sep string - expected expected - }{ - { - desc: "public URL", - url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6", - sep: "/download/", - expected: expected{ - moduleName: "github.com/tomMoulard/fail2ban", - version: "v0.6.6", - }, - }, - { - desc: "internal URL", - url: "https://plugins.traefik.io/download/github.com/tomMoulard/fail2ban/v0.6.6", - sep: "/download/", - expected: expected{ - moduleName: "github.com/tomMoulard/fail2ban", - version: "v0.6.6", - }, - }, - { - desc: "with extra slash", - url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6/", - sep: "/download/", - expected: expected{ - moduleName: "github.com/tomMoulard/fail2ban", - version: "v0.6.6", - }, - }, - } - - for _, test := range testCases { - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - endpoint, err := url.Parse(test.url) - require.NoError(t, err) - - moduleName, version := extractPluginInfo(endpoint, test.sep) - - assert.Equal(t, test.expected.moduleName, moduleName) - assert.Equal(t, test.expected.version, version) - }) - } + type expected struct { + moduleName string + version string + } + + testCases := []struct { + desc string + url string + sep string + expected expected + }{ + { + desc: "public URL", + url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6", + sep: "/download/", + expected: expected{ + moduleName: "github.com/tomMoulard/fail2ban", + version: "v0.6.6", + }, + }, + { + desc: "internal URL", + url: "https://plugins.traefik.io/download/github.com/tomMoulard/fail2ban/v0.6.6", + sep: "/download/", + expected: expected{ + moduleName: "github.com/tomMoulard/fail2ban", + version: "v0.6.6", + }, + }, + { + desc: "with extra slash", + url: "https://plugins.traefik.io/public/download/github.com/tomMoulard/fail2ban/v0.6.6/", + sep: "/download/", + expected: expected{ + moduleName: "github.com/tomMoulard/fail2ban", + version: "v0.6.6", + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + endpoint, err := url.Parse(test.url) + require.NoError(t, err) + + moduleName, version := extractPluginInfo(endpoint, test.sep) + + assert.Equal(t, test.expected.moduleName, moduleName) + assert.Equal(t, test.expected.version, version) + }) + } } func Test_Download(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{}, nil).Once(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t). - OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(&modfile.File{}, nil). - OnDownloadSources("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(io.NopCloser(strings.NewReader("test")), nil). - Parent - githubMock := NewGithubPluginClientMock(t) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "test", rw.Body.String()) - assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(&modfile.File{}, nil). + OnDownloadSources("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(io.NopCloser(strings.NewReader("test")), nil). + Parent + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) } func Test_Download_withDifferentHash(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(db.PluginHash{Hash: "yy"}, nil).Twice(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t).OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(&modfile.File{}, nil). - OnDownloadSources("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(io.NopCloser(strings.NewReader("test")), nil). - Parent - githubMock := NewGithubPluginClientMock(t) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - req.Header.Set(hashHeader, "xx") - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "test", rw.Body.String()) - assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(db.PluginHash{Hash: "yy"}, nil).Twice(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t).OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(&modfile.File{}, nil). + OnDownloadSources("github.com/traefik/plugindemo", "v0.2.1").Once().TypedReturns(io.NopCloser(strings.NewReader("test")), nil). + Parent + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req.Header.Set(hashHeader, "xx") + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) } func Test_Download_handle_GetByName_Error(t *testing.T) { - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(db.Plugin{}, errors.New("test")).Parent + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(db.Plugin{}, errors.New("test")).Parent - goproxyMock := NewGoproxyPluginClientMock(t) - githubMock := NewGithubPluginClientMock(t) + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - assert.Equal(t, http.StatusInternalServerError, rw.Code) - assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) - assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) } func Test_Download_handle_GetByName_Error_dbNotFoundError(t *testing.T) { - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(db.Plugin{}, db.NotFoundError{Err: errors.New("test")}).Parent + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(db.Plugin{}, db.NotFoundError{Err: errors.New("test")}).Parent - goproxyMock := NewGoproxyPluginClientMock(t) - githubMock := NewGithubPluginClientMock(t) + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - assert.Equal(t, http.StatusNotFound, rw.Code) - assert.Equal(t, `{"error":"Unknown plugin: github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) - assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) + assert.Equal(t, http.StatusNotFound, rw.Code) + assert.Equal(t, `{"error":"Unknown plugin: github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) } func Test_Download_handle_GetHashByName_Error(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{}, errors.New("error")).Once(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t) - githubMock := NewGithubPluginClientMock(t) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - req.Header.Set(hashHeader, "xx") - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusInternalServerError, rw.Code) - assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) - assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{}, errors.New("error")).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req.Header.Set(hashHeader, "xx") + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) } func Test_Download_handle_GetModFile_Error(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil).Parent - - goproxyMock := NewGoproxyPluginClientMock(t). - OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(&modfile.File{}, errors.New("error")).Parent - githubMock := NewGithubPluginClientMock(t) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusInternalServerError, rw.Code) - assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) - assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil).Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(&modfile.File{}, errors.New("error")).Parent + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/traefik/plugindemo@v0.2.1"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) } func Test_Download_unhandledMethod(t *testing.T) { - testDB := NewPluginStorerMock(t) + testDB := NewPluginStorerMock(t) - goproxyMock := NewGoproxyPluginClientMock(t) - githubMock := NewGithubPluginClientMock(t) + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) - rw := httptest.NewRecorder() + rw := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodHead, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req := httptest.NewRequest(http.MethodHead, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - assert.Equal(t, http.StatusMethodNotAllowed, rw.Code) + assert.Equal(t, http.StatusMethodNotAllowed, rw.Code) } func Test_Download_wasm(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemowasm", - LatestVersion: "v0.0.1", - Name: "github.com/traefik/plugindemowasm", - Readme: "README", - Runtime: "wasm", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "Demo Plugin WASM", - Type: "middleware", - Versions: []string{"v0.0.1"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemowasm", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/traefik/plugindemowasm", "v0.0.1").TypedReturns(db.PluginHash{}, nil).Once(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t) - - rw := httptest.NewRecorder() - link := &url.URL{Scheme: "https", Host: "api.github.com", Path: "/repos/traefik/plugindemowasm/releases/assets/138238821"} - - githubMock := NewGithubPluginClientMock(t). - OnGetReleaseByTag("traefik", "plugindemowasm", "v0.0.1").TypedReturns(&github.RepositoryRelease{ - Assets: []*github.ReleaseAsset{ - {Name: github.String("plugindemowasm_0.0.1_checksums.txt"), URL: github.String("https://api.github.com/repos/traefik/plugindemowasm/releases/assets/138238820")}, - {Name: github.String("plugindemowasm_v0.0.1.zip"), URL: github.String("https://api.github.com/repos/traefik/plugindemowasm/releases/assets/138238821")}, - }}, nil, nil).Once(). - OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { - if req.URL.String() != link.String() { - return false - } - rw.WriteHeader(http.StatusOK) - _, _ = rw.Write([]byte("test")) - - return true - }), rw).TypedReturns(nil, nil). - Parent - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemowasm/v0.0.1", http.NoBody) - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "test", rw.Body.String()) - assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemowasm", + LatestVersion: "v0.0.1", + Name: "github.com/traefik/plugindemowasm", + Readme: "README", + Runtime: "wasm", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Demo Plugin WASM", + Type: "middleware", + Versions: []string{"v0.0.1"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemowasm", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemowasm", "v0.0.1").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "api.github.com", Path: "/repos/traefik/plugindemowasm/releases/assets/138238821"} + + githubMock := NewGithubPluginClientMock(t). + OnGetReleaseByTag("traefik", "plugindemowasm", "v0.0.1").TypedReturns(&github.RepositoryRelease{ + Assets: []*github.ReleaseAsset{ + {Name: github.String("plugindemowasm_0.0.1_checksums.txt"), URL: github.String("https://api.github.com/repos/traefik/plugindemowasm/releases/assets/138238820")}, + {Name: github.String("plugindemowasm_v0.0.1.zip"), URL: github.String("https://api.github.com/repos/traefik/plugindemowasm/releases/assets/138238821")}, + }}, nil, nil).Once(). + OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { + if req.URL.String() != link.String() { + return false + } + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("test")) + + return true + }), rw).TypedReturns(nil, nil). + Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemowasm/v0.0.1", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) } func Test_Download_withHash(t *testing.T) { - data := db.Plugin{ - Author: "traefik", - Compatibility: "v2", - CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), - DisplayName: "Demo Plugin", - ID: "276809780784267776", - Import: "github.com/traefik/plugindemo", - LatestVersion: "v0.2.1", - Name: "github.com/traefik/plugindemo", - Readme: "README", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "[Demo] Add Request Header", - Type: "middleware", - Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{Hash: "xx"}, nil).Once(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t) - githubMock := NewGithubPluginClientMock(t) - - rw := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) - req.Header.Set(hashHeader, "xx") - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusNotModified, rw.Code) - assert.Equal(t, "", rw.Body.String()) - assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "traefik", + Compatibility: "v2", + CreatedAt: time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC), + DisplayName: "Demo Plugin", + ID: "276809780784267776", + Import: "github.com/traefik/plugindemo", + LatestVersion: "v0.2.1", + Name: "github.com/traefik/plugindemo", + Readme: "README", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "[Demo] Add Request Header", + Type: "middleware", + Versions: []string{"v0.2.1", "v0.2.0", "v0.1.0"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/traefik/plugindemo", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/traefik/plugindemo", "v0.2.1").TypedReturns(db.PluginHash{Hash: "xx"}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t) + githubMock := NewGithubPluginClientMock(t) + + rw := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/traefik/plugindemo/v0.2.1", http.NoBody) + req.Header.Set(hashHeader, "xx") + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusNotModified, rw.Code) + assert.Equal(t, "", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) } func Test_Download_withRequirements(t *testing.T) { - data := db.Plugin{ - Author: "maxlerebourg", - Compatibility: "", - CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), - DisplayName: "Crowdsec Bouncer Traefik Plugin", - ID: "6335346ca4caa9ddeffda116", - Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - LatestVersion: "v1.3.5", - Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", - Type: "middleware", - Versions: []string{"v1.3.5", "v1.3.4"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, nil).Once(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t). - OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( - &modfile.File{ - Require: []*modfile.Require{ - {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, - {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, - }, - }, nil). - Parent - - rw := httptest.NewRecorder() - link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} - githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once(). - OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { - if req.URL.String() != link.String() { - return false - } - rw.WriteHeader(http.StatusOK) - _, _ = rw.Write([]byte("test")) - - return true - }), rw).TypedReturns(nil, nil).Parent - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusOK, rw.Code) - assert.Equal(t, "test", rw.Body.String()) - assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once(). + OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { + if req.URL.String() != link.String() { + return false + } + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("test")) + + return true + }), rw).TypedReturns(nil, nil).Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusOK, rw.Code) + assert.Equal(t, "test", rw.Body.String()) + assert.Equal(t, "max-age=10,s-maxage=10", rw.Header().Get(cacheControlHeader)) } func Test_Download_withRequirements_handle_Do_Error(t *testing.T) { - data := db.Plugin{ - Author: "maxlerebourg", - Compatibility: "", - CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), - DisplayName: "Crowdsec Bouncer Traefik Plugin", - ID: "6335346ca4caa9ddeffda116", - Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - LatestVersion: "v1.3.5", - Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", - Type: "middleware", - Versions: []string{"v1.3.5", "v1.3.4"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, nil).Once(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t). - OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( - &modfile.File{ - Require: []*modfile.Require{ - {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, - {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, - }, - }, nil). - Parent - - rw := httptest.NewRecorder() - link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} - githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once(). - OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { - return req.URL.String() == link.String() - }), rw).TypedReturns(nil, errors.New("error")).Parent - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusInternalServerError, rw.Code) - assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) - assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, nil).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once(). + OnDoRaw(mock.MatchedBy(func(req *http.Request) bool { + return req.URL.String() == link.String() + }), rw).TypedReturns(nil, errors.New("error")).Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) } func Test_Download_withRequirements_handle_GetArchiveLink_Error(t *testing.T) { - data := db.Plugin{ - Author: "maxlerebourg", - Compatibility: "", - CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), - DisplayName: "Crowdsec Bouncer Traefik Plugin", - ID: "6335346ca4caa9ddeffda116", - Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - LatestVersion: "v1.3.5", - Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", - Type: "middleware", - Versions: []string{"v1.3.5", "v1.3.4"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil).Parent - - goproxyMock := NewGoproxyPluginClientMock(t). - OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( - &modfile.File{ - Require: []*modfile.Require{ - {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, - {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, - }, - }, nil). - Parent - - rw := httptest.NewRecorder() - githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(nil, nil, errors.New("error")).Once().Parent - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusInternalServerError, rw.Code) - assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) - assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil).Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(nil, nil, errors.New("error")).Once().Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) } func Test_Download_withRequirements_handle_GetHashByName_Error(t *testing.T) { - data := db.Plugin{ - Author: "maxlerebourg", - Compatibility: "", - CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), - DisplayName: "Crowdsec Bouncer Traefik Plugin", - ID: "6335346ca4caa9ddeffda116", - Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - LatestVersion: "v1.3.5", - Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", - Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", - Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, - Stars: 22, - Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", - Type: "middleware", - Versions: []string{"v1.3.5", "v1.3.4"}, - } - - testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). - OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, errors.New("error")).Once(). - Parent - - goproxyMock := NewGoproxyPluginClientMock(t). - OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( - &modfile.File{ - Require: []*modfile.Require{ - {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, - {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, - }, - }, nil). - Parent - - rw := httptest.NewRecorder() - link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} - githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once().Parent - - req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) - - New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) - - assert.Equal(t, http.StatusInternalServerError, rw.Code) - assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) - assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) + data := db.Plugin{ + Author: "maxlerebourg", + Compatibility: "", + CreatedAt: time.Date(2020, 9, 29, 6, 0, 12, 517000, time.UTC), + DisplayName: "Crowdsec Bouncer Traefik Plugin", + ID: "6335346ca4caa9ddeffda116", + Import: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + LatestVersion: "v1.3.5", + Name: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", + Readme: "![GitHub](https://img.shields.io/github/license/maxlerebourg/crowdsec-bouncer-traefik-plugin)\n...", + Snippet: map[string]interface{}{"toml": "toml", "yaml": "yaml"}, + Stars: 22, + Summary: "Middleware plugin which forwards the request IP to local Crowdsec agent, which can be used to allow/deny the request", + Type: "middleware", + Versions: []string{"v1.3.5", "v1.3.4"}, + } + + testDB := NewPluginStorerMock(t).OnGetByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", false, false).Once().TypedReturns(data, nil). + OnGetHashByName("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns(db.PluginHash{}, errors.New("error")).Once(). + Parent + + goproxyMock := NewGoproxyPluginClientMock(t). + OnGetModFile("github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin", "v1.3.5").TypedReturns( + &modfile.File{ + Require: []*modfile.Require{ + {Mod: module.Version{Path: "github.com/leprosus/golang-ttl-map", Version: "v1.1.7"}, Indirect: false}, + {Mod: module.Version{Path: "github.com/maxlerebourg/simpleredis", Version: "v1.0.11"}, Indirect: false}, + }, + }, nil). + Parent + + rw := httptest.NewRecorder() + link := &url.URL{Scheme: "https", Host: "codeload.github.com", Path: "/maxlerebourg/crowdsec-bouncer-traefik-plugin/legacy.zip/refs/tags/v1.3.5"} + githubMock := NewGithubPluginClientMock(t).OnGetArchiveLink("maxlerebourg", "crowdsec-bouncer-traefik-plugin", "zipball", &github.RepositoryContentGetOptions{Ref: "v1.3.5"}, 3).TypedReturns(link, &github.Response{Response: &http.Response{StatusCode: http.StatusFound, Header: map[string][]string{"location": {link.String()}}}}, nil).Once().Parent + + req := httptest.NewRequest(http.MethodGet, "/public/download/github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/v1.3.5", http.NoBody) + + New(testDB, goproxyMock, githubMock, 10*time.Second).Download(rw, req) + + assert.Equal(t, http.StatusInternalServerError, rw.Code) + assert.Equal(t, `{"error":"Failed to get plugin github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin@v1.3.5"}`+"\n", rw.Body.String()) + assert.Equal(t, "no-cache", rw.Header().Get(cacheControlHeader)) }