From da6f9066f955e4d777989896f76264289f5573f8 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 25 Sep 2023 16:17:56 +0200 Subject: [PATCH 1/8] chore: add backend providers Signed-off-by: Miguel Martinez Trivino --- app/artifact-cas/api/cas/v1/status.pb.go | 54 +++++++++++-------- app/artifact-cas/api/cas/v1/status.proto | 33 ++++++------ app/artifact-cas/cmd/main.go | 47 +++++++++++----- app/artifact-cas/cmd/wire.go | 6 ++- app/artifact-cas/cmd/wire_gen.go | 12 ++--- app/artifact-cas/internal/server/grpc.go | 5 +- app/artifact-cas/internal/server/http.go | 5 +- app/artifact-cas/internal/service/status.go | 15 ++++-- .../internal/service/status_test.go | 11 +++- internal/blobmanager/oci/provider.go | 4 +- 10 files changed, 121 insertions(+), 71 deletions(-) diff --git a/app/artifact-cas/api/cas/v1/status.pb.go b/app/artifact-cas/api/cas/v1/status.pb.go index 3e5bae98a..2814e7bd5 100644 --- a/app/artifact-cas/api/cas/v1/status.pb.go +++ b/app/artifact-cas/api/cas/v1/status.pb.go @@ -79,7 +79,8 @@ type InfozResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + Backends []string `protobuf:"bytes,2,rep,name=backends,proto3" json:"backends,omitempty"` } func (x *InfozResponse) Reset() { @@ -121,6 +122,13 @@ func (x *InfozResponse) GetVersion() string { return "" } +func (x *InfozResponse) GetBackends() []string { + if x != nil { + return x.Backends + } + return nil +} + type StatuszRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -216,29 +224,31 @@ var file_cas_v1_status_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x63, 0x61, 0x73, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0e, 0x0a, 0x0c, 0x49, - 0x6e, 0x66, 0x6f, 0x7a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x29, 0x0a, 0x0d, 0x49, + 0x6e, 0x66, 0x6f, 0x7a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x45, 0x0a, 0x0d, 0x49, 0x6e, 0x66, 0x6f, 0x7a, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x2e, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x7a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, - 0x69, 0x6e, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x61, - 0x64, 0x69, 0x6e, 0x65, 0x73, 0x73, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x7a, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xa3, 0x01, 0x0a, 0x0d, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x05, 0x49, - 0x6e, 0x66, 0x6f, 0x7a, 0x12, 0x14, 0x2e, 0x63, 0x61, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, - 0x66, 0x6f, 0x7a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x63, 0x61, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x7a, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x0e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x08, 0x12, 0x06, 0x2f, 0x69, 0x6e, 0x66, 0x6f, - 0x7a, 0x12, 0x4c, 0x0a, 0x07, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x12, 0x16, 0x2e, 0x63, - 0x61, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x61, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x7a, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x10, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x42, - 0x43, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, - 0x63, 0x74, 0x2d, 0x63, 0x61, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x73, 0x2f, 0x76, - 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, + 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, + 0x64, 0x73, 0x22, 0x2e, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x65, 0x73, + 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x61, 0x64, 0x69, 0x6e, 0x65, + 0x73, 0x73, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xa3, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x05, 0x49, 0x6e, 0x66, 0x6f, 0x7a, + 0x12, 0x14, 0x2e, 0x63, 0x61, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x7a, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x63, 0x61, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x49, 0x6e, 0x66, 0x6f, 0x7a, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0e, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x08, 0x12, 0x06, 0x2f, 0x69, 0x6e, 0x66, 0x6f, 0x7a, 0x12, 0x4c, 0x0a, + 0x07, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x12, 0x16, 0x2e, 0x63, 0x61, 0x73, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x17, 0x2e, 0x63, 0x61, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x7a, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x10, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x0a, 0x12, 0x08, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x7a, 0x42, 0x43, 0x5a, 0x41, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, + 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, + 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2d, 0x63, + 0x61, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x61, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/artifact-cas/api/cas/v1/status.proto b/app/artifact-cas/api/cas/v1/status.proto index b84ad6792..87500f464 100644 --- a/app/artifact-cas/api/cas/v1/status.proto +++ b/app/artifact-cas/api/cas/v1/status.proto @@ -17,34 +17,31 @@ syntax = "proto3"; package cas.v1; -option go_package = "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1;v1"; - import "google/api/annotations.proto"; +option go_package = "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1;v1"; + service StatusService { - rpc Infoz (InfozRequest) returns (InfozResponse) { - option (google.api.http) = { - get: "/infoz" - }; - } - rpc Statusz (StatuszRequest) returns (StatuszResponse) { - option (google.api.http) = { - get: "/statusz" - }; - } + rpc Infoz(InfozRequest) returns (InfozResponse) { + option (google.api.http) = {get: "/infoz"}; + } + rpc Statusz(StatuszRequest) returns (StatuszResponse) { + option (google.api.http) = {get: "/statusz"}; + } } -message InfozRequest { } +message InfozRequest {} message InfozResponse { - string version = 1; + string version = 1; + repeated string backends = 2; } message StatuszRequest { - // Parameter that can be used by readiness probes - // The main difference is that readiness probes will take into account that all - // dependent services are up and ready - bool readiness = 1; + // Parameter that can be used by readiness probes + // The main difference is that readiness probes will take into account that all + // dependent services are up and ready + bool readiness = 1; } message StatuszResponse {} diff --git a/app/artifact-cas/cmd/main.go b/app/artifact-cas/cmd/main.go index 82f9f26a1..41b350b3c 100644 --- a/app/artifact-cas/cmd/main.go +++ b/app/artifact-cas/cmd/main.go @@ -24,6 +24,8 @@ import ( "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/conf" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/server" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" + "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" "github.com/chainloop-dev/chainloop/internal/credentials" "github.com/chainloop-dev/chainloop/internal/credentials/manager" "github.com/chainloop-dev/chainloop/internal/servicelogger" @@ -56,19 +58,36 @@ func init() { flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml") } -func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer) *kratos.App { - return kratos.New( - kratos.ID(id), - kratos.Name(Name), - kratos.Version(Version), - kratos.Metadata(map[string]string{}), - kratos.Logger(logger), - kratos.Server( - gs, - hs, - ms, +type app struct { + *kratos.App + *backend.Providers +} + +func loadCASBackendProviders(creader credentials.Reader) *backend.Providers { + // Currently only OCI is supported + // Here we will load the rest of providers, S3, GCS, etc + p := oci.NewBackendProvider(creader) + return &backend.Providers{ + p.ID(): p, + } +} + +func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer, providers *backend.Providers) *app { + return &app{ + kratos.New( + kratos.ID(id), + kratos.Name(Name), + kratos.Version(Version), + kratos.Metadata(map[string]string{}), + kratos.Logger(logger), + kratos.Server( + gs, + hs, + ms, + ), ), - ) + providers, + } } func main() { @@ -115,6 +134,10 @@ func main() { } defer cleanup() + for k, _ := range *app.Providers { + _ = logger.Log(log.LevelInfo, "msg", "CAS backend provider loaded", "provider", k) + } + // start and wait for stop signal if err := app.Run(); err != nil { panic(err) diff --git a/app/artifact-cas/cmd/wire.go b/app/artifact-cas/cmd/wire.go index 83a6b79f9..e8368e92c 100644 --- a/app/artifact-cas/cmd/wire.go +++ b/app/artifact-cas/cmd/wire.go @@ -27,19 +27,21 @@ import ( backend "github.com/chainloop-dev/chainloop/internal/blobmanager" "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" "github.com/chainloop-dev/chainloop/internal/credentials" - "github.com/go-kratos/kratos/v2" "github.com/go-kratos/kratos/v2/log" "github.com/google/wire" ) // wireApp init kratos application. -func wireApp(*conf.Server, *conf.Auth, credentials.Reader, log.Logger) (*kratos.App, func(), error) { +func wireApp(*conf.Server, *conf.Auth, credentials.Reader, log.Logger) (*app, func(), error) { panic( wire.Build( server.ProviderSet, service.ProviderSet, + // DEPRECATED wire.Bind(new(backend.Provider), new(*oci.BackendProvider)), oci.NewBackendProvider, + // EO DEPRECATED + loadCASBackendProviders, newApp, serviceOpts, ), diff --git a/app/artifact-cas/cmd/wire_gen.go b/app/artifact-cas/cmd/wire_gen.go index d27e662f1..6393c6acd 100644 --- a/app/artifact-cas/cmd/wire_gen.go +++ b/app/artifact-cas/cmd/wire_gen.go @@ -12,7 +12,6 @@ import ( "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/service" "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" "github.com/chainloop-dev/chainloop/internal/credentials" - "github.com/go-kratos/kratos/v2" "github.com/go-kratos/kratos/v2/log" ) @@ -23,17 +22,18 @@ import ( // Injectors from wire.go: // wireApp init kratos application. -func wireApp(confServer *conf.Server, auth *conf.Auth, reader credentials.Reader, logger log.Logger) (*kratos.App, func(), error) { +func wireApp(confServer *conf.Server, auth *conf.Auth, reader credentials.Reader, logger log.Logger) (*app, func(), error) { backendProvider := oci.NewBackendProvider(reader) v := serviceOpts(logger) byteStreamService := service.NewByteStreamService(backendProvider, v...) resourceService := service.NewResourceService(backendProvider, v...) - grpcServer, err := server.NewGRPCServer(confServer, auth, byteStreamService, resourceService, logger) + providers := loadCASBackendProviders(reader) + grpcServer, err := server.NewGRPCServer(confServer, auth, byteStreamService, resourceService, providers, logger) if err != nil { return nil, nil, err } downloadService := service.NewDownloadService(backendProvider, v...) - httpServer, err := server.NewHTTPServer(confServer, auth, downloadService, logger) + httpServer, err := server.NewHTTPServer(confServer, auth, downloadService, providers, logger) if err != nil { return nil, nil, err } @@ -41,8 +41,8 @@ func wireApp(confServer *conf.Server, auth *conf.Auth, reader credentials.Reader if err != nil { return nil, nil, err } - app := newApp(logger, grpcServer, httpServer, httpMetricsServer) - return app, func() { + mainApp := newApp(logger, grpcServer, httpServer, httpMetricsServer, providers) + return mainApp, func() { }, nil } diff --git a/app/artifact-cas/internal/server/grpc.go b/app/artifact-cas/internal/server/grpc.go index d0d69fde5..3ce839f5c 100644 --- a/app/artifact-cas/internal/server/grpc.go +++ b/app/artifact-cas/internal/server/grpc.go @@ -25,6 +25,7 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/conf" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/service" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" "github.com/getsentry/sentry-go" "github.com/go-kratos/kratos/v2/errors" @@ -43,7 +44,7 @@ import ( ) // NewGRPCServer new a gRPC server. -func NewGRPCServer(c *conf.Server, authConf *conf.Auth, byteService *service.ByteStreamService, rSvc *service.ResourceService, logger log.Logger) (*grpc.Server, error) { +func NewGRPCServer(c *conf.Server, authConf *conf.Auth, byteService *service.ByteStreamService, rSvc *service.ResourceService, providers *backend.Providers, logger log.Logger) (*grpc.Server, error) { log := log.NewHelper(logger) // Load the key on initialization instead of on every request // TODO: implement jwks endpoint @@ -119,7 +120,7 @@ func NewGRPCServer(c *conf.Server, authConf *conf.Auth, byteService *service.Byt bytestream.RegisterByteStreamServer(srv.Server, byteService) v1.RegisterResourceServiceServer(srv.Server, rSvc) - v1.RegisterStatusServiceServer(srv.Server, service.NewStatusService(Version)) + v1.RegisterStatusServiceServer(srv.Server, service.NewStatusService(Version, providers)) // Register and set metrics to 0 grpc_prometheus.Register(srv.Server) diff --git a/app/artifact-cas/internal/server/http.go b/app/artifact-cas/internal/server/http.go index ec1007336..fd540524b 100644 --- a/app/artifact-cas/internal/server/http.go +++ b/app/artifact-cas/internal/server/http.go @@ -27,6 +27,7 @@ import ( nhttp "net/http" api "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" "github.com/go-kratos/kratos/v2/log" jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt" "github.com/go-kratos/kratos/v2/middleware/logging" @@ -35,7 +36,7 @@ import ( ) // NewHTTPServer new a HTTP server. -func NewHTTPServer(c *conf.Server, authConf *conf.Auth, downloadSvc *service.DownloadService, logger log.Logger) (*http.Server, error) { +func NewHTTPServer(c *conf.Server, authConf *conf.Auth, downloadSvc *service.DownloadService, providers *backend.Providers, logger log.Logger) (*http.Server, error) { var opts = []http.ServerOption{ http.Middleware( recovery.Recovery(), @@ -68,7 +69,7 @@ func NewHTTPServer(c *conf.Server, authConf *conf.Auth, downloadSvc *service.Dow srv := http.NewServer(opts...) srv.Handle(service.DownloadPath, authFromQueryMiddleware(loadPublicKey(rawKey), casJWT.SigningMethod, downloadSvc)) - api.RegisterStatusServiceHTTPServer(srv, service.NewStatusService(Version)) + api.RegisterStatusServiceHTTPServer(srv, service.NewStatusService(Version, providers)) return srv, nil } diff --git a/app/artifact-cas/internal/service/status.go b/app/artifact-cas/internal/service/status.go index e185dbf5d..f034abbe4 100644 --- a/app/artifact-cas/internal/service/status.go +++ b/app/artifact-cas/internal/service/status.go @@ -19,15 +19,17 @@ import ( "context" pb "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" ) type StatusService struct { - version string + version string + providers *backend.Providers pb.UnimplementedStatusServiceServer } -func NewStatusService(version string) *StatusService { - return &StatusService{version: version} +func NewStatusService(version string, providers *backend.Providers) *StatusService { + return &StatusService{version: version, providers: providers} } func (s *StatusService) Statusz(_ context.Context, _ *pb.StatuszRequest) (*pb.StatuszResponse, error) { @@ -35,5 +37,10 @@ func (s *StatusService) Statusz(_ context.Context, _ *pb.StatuszRequest) (*pb.St } func (s *StatusService) Infoz(_ context.Context, _ *pb.InfozRequest) (*pb.InfozResponse, error) { - return &pb.InfozResponse{Version: s.version}, nil + var backends = make([]string, 0, len(*s.providers)) + for k, _ := range *s.providers { + backends = append(backends, k) + } + + return &pb.InfozResponse{Version: s.version, Backends: backends}, nil } diff --git a/app/artifact-cas/internal/service/status_test.go b/app/artifact-cas/internal/service/status_test.go index 7bd0088c3..2682dc8d8 100644 --- a/app/artifact-cas/internal/service/status_test.go +++ b/app/artifact-cas/internal/service/status_test.go @@ -20,17 +20,24 @@ import ( "testing" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/service" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" "github.com/stretchr/testify/assert" ) +var enabledProviders *backend.Providers = &backend.Providers{ + "OCI": nil, + "GCS": nil, +} + func TestInfoz(t *testing.T) { want := "v1.0.0" - got, err := service.NewStatusService(want).Infoz(context.Background(), nil) + got, err := service.NewStatusService(want, enabledProviders).Infoz(context.Background(), nil) assert.NoError(t, err) assert.Equal(t, want, got.Version) + assert.Equal(t, []string{"OCI", "GCS"}, got.Backends) } func TestStatusz(t *testing.T) { - _, err := service.NewStatusService("1.1.1").Statusz(context.Background(), nil) + _, err := service.NewStatusService("1.1.1", enabledProviders).Statusz(context.Background(), nil) assert.NoError(t, err) } diff --git a/internal/blobmanager/oci/provider.go b/internal/blobmanager/oci/provider.go index cb5eeab1d..3111253f4 100644 --- a/internal/blobmanager/oci/provider.go +++ b/internal/blobmanager/oci/provider.go @@ -35,8 +35,10 @@ func NewBackendProvider(cReader credentials.Reader) *BackendProvider { return &BackendProvider{cReader: cReader} } +const ProviderID = "OCI" + func (p *BackendProvider) ID() string { - return "OCI" + return ProviderID } func (p *BackendProvider) FromCredentials(ctx context.Context, secretName string) (backend.UploaderDownloader, error) { From b36deb47f4c881e1b79e910397f453931c3379bd Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 25 Sep 2023 16:39:55 +0200 Subject: [PATCH 2/8] feat: multiple cas-backends support in proxy Signed-off-by: Miguel Martinez Trivino --- app/artifact-cas/cmd/main.go | 10 ++++----- app/artifact-cas/cmd/wire.go | 6 ------ app/artifact-cas/cmd/wire_gen.go | 10 ++++----- app/artifact-cas/internal/server/grpc.go | 2 +- app/artifact-cas/internal/server/http.go | 2 +- .../internal/service/bytestream.go | 21 ++++++++++++++++--- app/artifact-cas/internal/service/download.go | 13 ++++++++++-- app/artifact-cas/internal/service/resource.go | 12 +++++++++-- app/artifact-cas/internal/service/service.go | 18 +++++++++++++--- app/artifact-cas/internal/service/status.go | 8 +++---- .../internal/service/status_test.go | 2 +- 11 files changed, 70 insertions(+), 34 deletions(-) diff --git a/app/artifact-cas/cmd/main.go b/app/artifact-cas/cmd/main.go index 41b350b3c..9ccc27e7a 100644 --- a/app/artifact-cas/cmd/main.go +++ b/app/artifact-cas/cmd/main.go @@ -60,19 +60,19 @@ func init() { type app struct { *kratos.App - *backend.Providers + backend.Providers } -func loadCASBackendProviders(creader credentials.Reader) *backend.Providers { +func loadCASBackendProviders(creader credentials.Reader) backend.Providers { // Currently only OCI is supported // Here we will load the rest of providers, S3, GCS, etc p := oci.NewBackendProvider(creader) - return &backend.Providers{ + return backend.Providers{ p.ID(): p, } } -func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer, providers *backend.Providers) *app { +func newApp(logger log.Logger, gs *grpc.Server, hs *http.Server, ms *server.HTTPMetricsServer, providers backend.Providers) *app { return &app{ kratos.New( kratos.ID(id), @@ -134,7 +134,7 @@ func main() { } defer cleanup() - for k, _ := range *app.Providers { + for k, _ := range app.Providers { _ = logger.Log(log.LevelInfo, "msg", "CAS backend provider loaded", "provider", k) } diff --git a/app/artifact-cas/cmd/wire.go b/app/artifact-cas/cmd/wire.go index e8368e92c..d02fd236a 100644 --- a/app/artifact-cas/cmd/wire.go +++ b/app/artifact-cas/cmd/wire.go @@ -24,8 +24,6 @@ import ( "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/conf" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/server" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/service" - backend "github.com/chainloop-dev/chainloop/internal/blobmanager" - "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" "github.com/chainloop-dev/chainloop/internal/credentials" "github.com/go-kratos/kratos/v2/log" "github.com/google/wire" @@ -37,10 +35,6 @@ func wireApp(*conf.Server, *conf.Auth, credentials.Reader, log.Logger) (*app, fu wire.Build( server.ProviderSet, service.ProviderSet, - // DEPRECATED - wire.Bind(new(backend.Provider), new(*oci.BackendProvider)), - oci.NewBackendProvider, - // EO DEPRECATED loadCASBackendProviders, newApp, serviceOpts, diff --git a/app/artifact-cas/cmd/wire_gen.go b/app/artifact-cas/cmd/wire_gen.go index 6393c6acd..a447a17f3 100644 --- a/app/artifact-cas/cmd/wire_gen.go +++ b/app/artifact-cas/cmd/wire_gen.go @@ -10,7 +10,6 @@ import ( "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/conf" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/server" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/service" - "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" "github.com/chainloop-dev/chainloop/internal/credentials" "github.com/go-kratos/kratos/v2/log" ) @@ -23,16 +22,15 @@ import ( // wireApp init kratos application. func wireApp(confServer *conf.Server, auth *conf.Auth, reader credentials.Reader, logger log.Logger) (*app, func(), error) { - backendProvider := oci.NewBackendProvider(reader) - v := serviceOpts(logger) - byteStreamService := service.NewByteStreamService(backendProvider, v...) - resourceService := service.NewResourceService(backendProvider, v...) providers := loadCASBackendProviders(reader) + v := serviceOpts(logger) + byteStreamService := service.NewByteStreamService(providers, v...) + resourceService := service.NewResourceService(providers, v...) grpcServer, err := server.NewGRPCServer(confServer, auth, byteStreamService, resourceService, providers, logger) if err != nil { return nil, nil, err } - downloadService := service.NewDownloadService(backendProvider, v...) + downloadService := service.NewDownloadService(providers, v...) httpServer, err := server.NewHTTPServer(confServer, auth, downloadService, providers, logger) if err != nil { return nil, nil, err diff --git a/app/artifact-cas/internal/server/grpc.go b/app/artifact-cas/internal/server/grpc.go index 3ce839f5c..ea764bed9 100644 --- a/app/artifact-cas/internal/server/grpc.go +++ b/app/artifact-cas/internal/server/grpc.go @@ -44,7 +44,7 @@ import ( ) // NewGRPCServer new a gRPC server. -func NewGRPCServer(c *conf.Server, authConf *conf.Auth, byteService *service.ByteStreamService, rSvc *service.ResourceService, providers *backend.Providers, logger log.Logger) (*grpc.Server, error) { +func NewGRPCServer(c *conf.Server, authConf *conf.Auth, byteService *service.ByteStreamService, rSvc *service.ResourceService, providers backend.Providers, logger log.Logger) (*grpc.Server, error) { log := log.NewHelper(logger) // Load the key on initialization instead of on every request // TODO: implement jwks endpoint diff --git a/app/artifact-cas/internal/server/http.go b/app/artifact-cas/internal/server/http.go index fd540524b..0b62a8d32 100644 --- a/app/artifact-cas/internal/server/http.go +++ b/app/artifact-cas/internal/server/http.go @@ -36,7 +36,7 @@ import ( ) // NewHTTPServer new a HTTP server. -func NewHTTPServer(c *conf.Server, authConf *conf.Auth, downloadSvc *service.DownloadService, providers *backend.Providers, logger log.Logger) (*http.Server, error) { +func NewHTTPServer(c *conf.Server, authConf *conf.Auth, downloadSvc *service.DownloadService, providers backend.Providers, logger log.Logger) (*http.Server, error) { var opts = []http.ServerOption{ http.Middleware( recovery.Recovery(), diff --git a/app/artifact-cas/internal/service/bytestream.go b/app/artifact-cas/internal/service/bytestream.go index fbdb9d3af..54d32092f 100644 --- a/app/artifact-cas/internal/service/bytestream.go +++ b/app/artifact-cas/internal/service/bytestream.go @@ -26,6 +26,7 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" + "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" kerrors "github.com/go-kratos/kratos/v2/errors" @@ -41,7 +42,7 @@ type ByteStreamService struct { *commonService } -func NewByteStreamService(bp backend.Provider, opts ...NewOpt) *ByteStreamService { +func NewByteStreamService(bp backend.Providers, opts ...NewOpt) *ByteStreamService { return &ByteStreamService{ commonService: newCommonService(bp, opts...), } @@ -70,8 +71,15 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro return kerrors.BadRequest("resource name", err.Error()) } + // For now we only support OCI + // TODO: select per-request + backendProvider, err := s.selectProvider(oci.ProviderID) + if err != nil { + return kerrors.NotFound("not found", err.Error()) + } + // Load OCI backend based on a reference stored in the token - backend, err := s.backendP.FromCredentials(ctx, info.StoredSecretID) + backend, err := backendProvider.FromCredentials(ctx, info.StoredSecretID) if err != nil { return sl.LogAndMaskErr(err, s.log) } @@ -142,8 +150,15 @@ func (s *ByteStreamService) Read(req *bytestream.ReadRequest, stream bytestream. return kerrors.BadRequest("resource name", "empty resource name") } + // For now we only support OCI + // TODO: select per-request + backendProvider, err := s.selectProvider(oci.ProviderID) + if err != nil { + return kerrors.NotFound("not found", err.Error()) + } + // Retrieve the OCI backend from where to download the file - backend, err := s.backendP.FromCredentials(ctx, info.StoredSecretID) + backend, err := backendProvider.FromCredentials(ctx, info.StoredSecretID) if err != nil { return sl.LogAndMaskErr(err, s.log) } diff --git a/app/artifact-cas/internal/service/download.go b/app/artifact-cas/internal/service/download.go index 343f71d3c..d8344dcf9 100644 --- a/app/artifact-cas/internal/service/download.go +++ b/app/artifact-cas/internal/service/download.go @@ -24,6 +24,7 @@ import ( "code.cloudfoundry.org/bytefmt" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" + "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" cr_v1 "github.com/google/go-containerregistry/pkg/v1" @@ -37,7 +38,7 @@ type DownloadService struct { *commonService } -func NewDownloadService(bp backend.Provider, opts ...NewOpt) *DownloadService { +func NewDownloadService(bp backend.Providers, opts ...NewOpt) *DownloadService { return &DownloadService{ commonService: newCommonService(bp, opts...), } @@ -69,8 +70,16 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // For now we only support OCI + // TODO: select per-request + backendProvider, err := s.selectProvider(oci.ProviderID) + if err != nil { + http.Error(w, "backend not found", http.StatusNotFound) + return + } + // Retrieve the CAS backend from where to download the file - b, err := s.backendP.FromCredentials(ctx, auth.StoredSecretID) + b, err := backendProvider.FromCredentials(ctx, auth.StoredSecretID) if err != nil { http.Error(w, sl.LogAndMaskErr(err, s.log).Error(), http.StatusInternalServerError) return diff --git a/app/artifact-cas/internal/service/resource.go b/app/artifact-cas/internal/service/resource.go index 910514197..0db296b5c 100644 --- a/app/artifact-cas/internal/service/resource.go +++ b/app/artifact-cas/internal/service/resource.go @@ -20,6 +20,7 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" + "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" "github.com/go-kratos/kratos/v2/errors" ) @@ -29,7 +30,7 @@ type ResourceService struct { *commonService } -func NewResourceService(bp backend.Provider, opts ...NewOpt) *ResourceService { +func NewResourceService(bp backend.Providers, opts ...NewOpt) *ResourceService { return &ResourceService{ commonService: newCommonService(bp, opts...), } @@ -42,7 +43,14 @@ func (s *ResourceService) Describe(ctx context.Context, req *v1.ResourceServiceD return nil, err } - b, err := s.backendP.FromCredentials(ctx, info.StoredSecretID) + // For now we only support OCI + // TODO: select per-request + backendProvider, err := s.selectProvider(oci.ProviderID) + if err != nil { + return nil, errors.NotFound("not found", err.Error()) + } + + b, err := backendProvider.FromCredentials(ctx, info.StoredSecretID) if err != nil { return nil, sl.LogAndMaskErr(err, s.log) } diff --git a/app/artifact-cas/internal/service/service.go b/app/artifact-cas/internal/service/service.go index def31d3cc..169b6ee08 100644 --- a/app/artifact-cas/internal/service/service.go +++ b/app/artifact-cas/internal/service/service.go @@ -17,6 +17,7 @@ package service import ( "context" + "fmt" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" @@ -32,7 +33,18 @@ var ProviderSet = wire.NewSet(NewByteStreamService, NewResourceService, NewDownl type commonService struct { log *log.Helper - backendP backend.Provider + backends backend.Providers +} + +func (s *commonService) selectProvider(id string) (backend.Provider, error) { + // get the OCI provider from the map + p, ok := s.backends[id] + if !ok { + return nil, fmt.Errorf("provider %s not found", id) + } + + s.log.Infow("msg", "selected provider", "provider", id) + return p, nil } type NewOpt func(s *commonService) @@ -43,10 +55,10 @@ func WithLogger(logger log.Logger) NewOpt { } } -func newCommonService(bp backend.Provider, opts ...NewOpt) *commonService { +func newCommonService(backends backend.Providers, opts ...NewOpt) *commonService { s := &commonService{ log: servicelogger.EmptyLogger(), - backendP: bp, + backends: backends, } for _, opt := range opts { diff --git a/app/artifact-cas/internal/service/status.go b/app/artifact-cas/internal/service/status.go index f034abbe4..82b6bb7cd 100644 --- a/app/artifact-cas/internal/service/status.go +++ b/app/artifact-cas/internal/service/status.go @@ -24,11 +24,11 @@ import ( type StatusService struct { version string - providers *backend.Providers + providers backend.Providers pb.UnimplementedStatusServiceServer } -func NewStatusService(version string, providers *backend.Providers) *StatusService { +func NewStatusService(version string, providers backend.Providers) *StatusService { return &StatusService{version: version, providers: providers} } @@ -37,8 +37,8 @@ func (s *StatusService) Statusz(_ context.Context, _ *pb.StatuszRequest) (*pb.St } func (s *StatusService) Infoz(_ context.Context, _ *pb.InfozRequest) (*pb.InfozResponse, error) { - var backends = make([]string, 0, len(*s.providers)) - for k, _ := range *s.providers { + var backends = make([]string, 0, len(s.providers)) + for k, _ := range s.providers { backends = append(backends, k) } diff --git a/app/artifact-cas/internal/service/status_test.go b/app/artifact-cas/internal/service/status_test.go index 2682dc8d8..b8a207f6b 100644 --- a/app/artifact-cas/internal/service/status_test.go +++ b/app/artifact-cas/internal/service/status_test.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/assert" ) -var enabledProviders *backend.Providers = &backend.Providers{ +var enabledProviders backend.Providers = backend.Providers{ "OCI": nil, "GCS": nil, } From 1d77bdec4d6af0d83f12b9174d9af274433ca9ae Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 25 Sep 2023 21:22:06 +0200 Subject: [PATCH 3/8] feat: fix tests Signed-off-by: Miguel Martinez Trivino --- app/artifact-cas/cmd/main.go | 2 +- .../internal/service/bytestream_test.go | 9 ++++--- .../internal/service/resource_test.go | 25 ++++++++++--------- app/artifact-cas/internal/service/status.go | 2 +- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/artifact-cas/cmd/main.go b/app/artifact-cas/cmd/main.go index 9ccc27e7a..0f2a5a04c 100644 --- a/app/artifact-cas/cmd/main.go +++ b/app/artifact-cas/cmd/main.go @@ -134,7 +134,7 @@ func main() { } defer cleanup() - for k, _ := range app.Providers { + for k := range app.Providers { _ = logger.Log(log.LevelInfo, "msg", "CAS backend provider loaded", "provider", k) } diff --git a/app/artifact-cas/internal/service/bytestream_test.go b/app/artifact-cas/internal/service/bytestream_test.go index e4b3a9db0..0fe734d6d 100644 --- a/app/artifact-cas/internal/service/bytestream_test.go +++ b/app/artifact-cas/internal/service/bytestream_test.go @@ -26,6 +26,7 @@ import ( "testing" v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" "github.com/chainloop-dev/chainloop/internal/blobmanager/mocks" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" "github.com/go-kratos/kratos/v2/log" @@ -275,13 +276,15 @@ func (s *bytestreamSuite) SetupTest() { ), ), ) - backendProvider := mocks.NewProvider(s.T()) + ociBackendProvider := mocks.NewProvider(s.T()) ociBackend := mocks.NewUploaderDownloader(s.T()) - backendProvider.On("FromCredentials", mock.Anything, mock.Anything).Maybe().Return(ociBackend, nil) + ociBackendProvider.On("FromCredentials", mock.Anything, mock.Anything).Maybe().Return(ociBackend, nil) bytestream.RegisterByteStreamServer( server, - NewByteStreamService(backendProvider, WithLogger(log.DefaultLogger)), + NewByteStreamService(backend.Providers{ + "OCI": ociBackendProvider, + }, WithLogger(log.DefaultLogger)), ) go func() { _ = server.Serve(l) diff --git a/app/artifact-cas/internal/service/resource_test.go b/app/artifact-cas/internal/service/resource_test.go index 297cb51c7..1a058f20e 100644 --- a/app/artifact-cas/internal/service/resource_test.go +++ b/app/artifact-cas/internal/service/resource_test.go @@ -21,6 +21,7 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" "github.com/chainloop-dev/chainloop/app/artifact-cas/internal/service" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" "github.com/chainloop-dev/chainloop/internal/blobmanager/mocks" casjwt "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" jwtmiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt" @@ -31,12 +32,10 @@ import ( func (s *resourceSuite) TestDescribe() { want := &v1.CASResource{FileName: "test.txt", Digest: "deadbeef"} - s.ociProvider.On("Describe", mock.Anything, "deadbeef"). - Return(want, nil) - + s.ociBackend.On("Describe", mock.Anything, "deadbeef").Return(want, nil) ctx := jwtmiddleware.NewContext(context.Background(), &casjwt.Claims{StoredSecretID: "secret-id"}) - svc := service.NewResourceService(s.backendProvider) + svc := service.NewResourceService(s.backendProviders) got, err := svc.Describe(ctx, &v1.ResourceServiceDescribeRequest{ Digest: "deadbeef", }) @@ -47,18 +46,20 @@ func (s *resourceSuite) TestDescribe() { type resourceSuite struct { suite.Suite - ociProvider *mocks.UploaderDownloader - backendProvider *mocks.Provider + ociBackend *mocks.UploaderDownloader + backendProviders backend.Providers } func (s *resourceSuite) SetupTest() { - backendProvider := mocks.NewProvider(s.T()) - ociProvider := mocks.NewUploaderDownloader(s.T()) - backendProvider.On("FromCredentials", mock.Anything, "secret-id"). - Return(ociProvider, nil) + ociBackendProvider := mocks.NewProvider(s.T()) + ociBackend := mocks.NewUploaderDownloader(s.T()) + ociBackendProvider.On("FromCredentials", mock.Anything, "secret-id"). + Return(ociBackend, nil) - s.ociProvider = ociProvider - s.backendProvider = backendProvider + s.ociBackend = ociBackend + s.backendProviders = backend.Providers{ + "OCI": ociBackendProvider, + } } // Run the tests diff --git a/app/artifact-cas/internal/service/status.go b/app/artifact-cas/internal/service/status.go index 82b6bb7cd..26a48ac95 100644 --- a/app/artifact-cas/internal/service/status.go +++ b/app/artifact-cas/internal/service/status.go @@ -38,7 +38,7 @@ func (s *StatusService) Statusz(_ context.Context, _ *pb.StatuszRequest) (*pb.St func (s *StatusService) Infoz(_ context.Context, _ *pb.InfozRequest) (*pb.InfozResponse, error) { var backends = make([]string, 0, len(s.providers)) - for k, _ := range s.providers { + for k := range s.providers { backends = append(backends, k) } From 95dc8f731741ceefdc911d442e4692fe42771ff5 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Mon, 25 Sep 2023 22:10:38 +0200 Subject: [PATCH 4/8] feat: send backend in OCI token Signed-off-by: Miguel Martinez Trivino --- .../internal/service/bytestream.go | 4 +-- app/artifact-cas/internal/service/download.go | 5 +--- app/artifact-cas/internal/service/resource.go | 5 +--- app/artifact-cas/internal/service/service.go | 8 ++++++ app/controlplane/internal/biz/attestation.go | 4 +-- app/controlplane/internal/biz/casclient.go | 16 ++++++------ .../internal/biz/cascredentials.go | 10 ++++++-- .../internal/dispatcher/dispatcher.go | 17 +++++++------ .../internal/service/attestation.go | 10 +++++--- .../internal/service/cascredential.go | 3 ++- .../internal/service/casredirect.go | 3 ++- internal/robotaccount/cas/robotaccount.go | 25 ++++++++++++++++--- .../robotaccount/cas/robotaccount_test.go | 2 +- 13 files changed, 72 insertions(+), 40 deletions(-) diff --git a/app/artifact-cas/internal/service/bytestream.go b/app/artifact-cas/internal/service/bytestream.go index 54d32092f..a6812daa5 100644 --- a/app/artifact-cas/internal/service/bytestream.go +++ b/app/artifact-cas/internal/service/bytestream.go @@ -71,9 +71,7 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro return kerrors.BadRequest("resource name", err.Error()) } - // For now we only support OCI - // TODO: select per-request - backendProvider, err := s.selectProvider(oci.ProviderID) + backendProvider, err := s.selectProvider(info.BackendType) if err != nil { return kerrors.NotFound("not found", err.Error()) } diff --git a/app/artifact-cas/internal/service/download.go b/app/artifact-cas/internal/service/download.go index d8344dcf9..c8e37ca2e 100644 --- a/app/artifact-cas/internal/service/download.go +++ b/app/artifact-cas/internal/service/download.go @@ -24,7 +24,6 @@ import ( "code.cloudfoundry.org/bytefmt" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" - "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" cr_v1 "github.com/google/go-containerregistry/pkg/v1" @@ -70,9 +69,7 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // For now we only support OCI - // TODO: select per-request - backendProvider, err := s.selectProvider(oci.ProviderID) + backendProvider, err := s.selectProvider(auth.BackendType) if err != nil { http.Error(w, "backend not found", http.StatusNotFound) return diff --git a/app/artifact-cas/internal/service/resource.go b/app/artifact-cas/internal/service/resource.go index 0db296b5c..760ba1db0 100644 --- a/app/artifact-cas/internal/service/resource.go +++ b/app/artifact-cas/internal/service/resource.go @@ -20,7 +20,6 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" - "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" "github.com/go-kratos/kratos/v2/errors" ) @@ -43,9 +42,7 @@ func (s *ResourceService) Describe(ctx context.Context, req *v1.ResourceServiceD return nil, err } - // For now we only support OCI - // TODO: select per-request - backendProvider, err := s.selectProvider(oci.ProviderID) + backendProvider, err := s.selectProvider(info.BackendType) if err != nil { return nil, errors.NotFound("not found", err.Error()) } diff --git a/app/artifact-cas/internal/service/service.go b/app/artifact-cas/internal/service/service.go index 169b6ee08..b87b16af1 100644 --- a/app/artifact-cas/internal/service/service.go +++ b/app/artifact-cas/internal/service/service.go @@ -20,6 +20,7 @@ import ( "fmt" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" + "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" "github.com/chainloop-dev/chainloop/internal/servicelogger" kerrors "github.com/go-kratos/kratos/v2/errors" @@ -37,6 +38,13 @@ type commonService struct { } func (s *commonService) selectProvider(id string) (backend.Provider, error) { + // if no provider is specified, default to OCI + // this is done for backward compatibility + if id == "" { + s.log.Warn("provider not set, defaulting to OCI") + id = oci.ProviderID + } + // get the OCI provider from the map p, ok := s.backends[id] if !ok { diff --git a/app/controlplane/internal/biz/attestation.go b/app/controlplane/internal/biz/attestation.go index 1f98b1f2b..395bcd2d6 100644 --- a/app/controlplane/internal/biz/attestation.go +++ b/app/controlplane/internal/biz/attestation.go @@ -45,7 +45,7 @@ func NewAttestationUseCase(client CASClient, logger log.Logger) *AttestationUseC } } -func (uc *AttestationUseCase) UploadToCAS(ctx context.Context, envelope *dsse.Envelope, secretID, workflowRunID string) (*cr_v1.Hash, error) { +func (uc *AttestationUseCase) UploadToCAS(ctx context.Context, envelope *dsse.Envelope, backend *CASBackend, workflowRunID string) (*cr_v1.Hash, error) { filename := fmt.Sprintf("attestation-%s.json", workflowRunID) jsonContent, err := json.Marshal(envelope) if err != nil { @@ -57,7 +57,7 @@ func (uc *AttestationUseCase) UploadToCAS(ctx context.Context, envelope *dsse.En return nil, fmt.Errorf("calculating the digest: %w", err) } - if err := uc.CASClient.Upload(ctx, secretID, bytes.NewBuffer(jsonContent), filename, h.String()); err != nil { + if err := uc.CASClient.Upload(ctx, string(backend.Provider), backend.SecretName, bytes.NewBuffer(jsonContent), filename, h.String()); err != nil { return nil, fmt.Errorf("uploading to CAS: %w", err) } diff --git a/app/controlplane/internal/biz/casclient.go b/app/controlplane/internal/biz/casclient.go index f9ac9e399..c779d435a 100644 --- a/app/controlplane/internal/biz/casclient.go +++ b/app/controlplane/internal/biz/casclient.go @@ -40,11 +40,11 @@ type CASClientUseCase struct { } type CASUploader interface { - Upload(ctx context.Context, secretID string, content io.Reader, filename, digest string) error + Upload(ctx context.Context, backendType, secretID string, content io.Reader, filename, digest string) error } type CASDownloader interface { - Download(ctx context.Context, secretID string, w io.Writer, digest string) error + Download(ctx context.Context, backendType, secretID string, w io.Writer, digest string) error } type CASClient interface { @@ -97,11 +97,11 @@ func NewCASClientUseCase(credsProvider *CASCredentialsUseCase, config *conf.Boot } // The secretID is embedded in the JWT token and is used to identify the secret by the CAS server -func (uc *CASClientUseCase) Upload(ctx context.Context, secretID string, content io.Reader, filename, digest string) error { +func (uc *CASClientUseCase) Upload(ctx context.Context, backendType, secretID string, content io.Reader, filename, digest string) error { uc.logger.Infow("msg", "upload initialized", "filename", filename, "digest", digest) // client with temporary set of credentials - client, closeFn, err := uc.casAPIClient(secretID, casJWT.Uploader) + client, closeFn, err := uc.casAPIClient(&CASCredsOpts{BackendType: backendType, SecretPath: secretID, Role: casJWT.Uploader}) if err != nil { return fmt.Errorf("failed to create cas client: %w", err) } @@ -117,10 +117,10 @@ func (uc *CASClientUseCase) Upload(ctx context.Context, secretID string, content return nil } -func (uc *CASClientUseCase) Download(ctx context.Context, secretID string, w io.Writer, digest string) error { +func (uc *CASClientUseCase) Download(ctx context.Context, backendType, secretID string, w io.Writer, digest string) error { uc.logger.Infow("msg", "download initialized", "digest", digest) - client, closeFn, err := uc.casAPIClient(secretID, casJWT.Downloader) + client, closeFn, err := uc.casAPIClient(&CASCredsOpts{BackendType: backendType, SecretPath: secretID, Role: casJWT.Downloader}) if err != nil { return fmt.Errorf("failed to create cas client: %w", err) } @@ -136,8 +136,8 @@ func (uc *CASClientUseCase) Download(ctx context.Context, secretID string, w io. } // create a client with a temporary set of credentials for a specific operation -func (uc *CASClientUseCase) casAPIClient(secretID string, role casJWT.Role) (casclient.DownloaderUploader, func(), error) { - token, err := uc.credsProvider.GenerateTemporaryCredentials(secretID, role) +func (uc *CASClientUseCase) casAPIClient(backendRef *CASCredsOpts) (casclient.DownloaderUploader, func(), error) { + token, err := uc.credsProvider.GenerateTemporaryCredentials(backendRef) if err != nil { return nil, nil, fmt.Errorf("failed to generate temporary credentials: %w", err) } diff --git a/app/controlplane/internal/biz/cascredentials.go b/app/controlplane/internal/biz/cascredentials.go index ac47e109c..4f1dbe013 100644 --- a/app/controlplane/internal/biz/cascredentials.go +++ b/app/controlplane/internal/biz/cascredentials.go @@ -43,6 +43,12 @@ func NewCASCredentialsUseCase(c *conf.Auth) (*CASCredentialsUseCase, error) { return &CASCredentialsUseCase{builder}, nil } -func (uc *CASCredentialsUseCase) GenerateTemporaryCredentials(secretID string, role robotaccount.Role) (string, error) { - return uc.jwtBuilder.GenerateJWT(secretID, jwt.CASAudience, role) +type CASCredsOpts struct { + BackendType string // i.e OCI, S3 + SecretPath string // path to for example the OCI secret in the vault + Role robotaccount.Role +} + +func (uc *CASCredentialsUseCase) GenerateTemporaryCredentials(backendRef *CASCredsOpts) (string, error) { + return uc.jwtBuilder.GenerateJWT(backendRef.BackendType, backendRef.SecretPath, jwt.CASAudience, backendRef.Role) } diff --git a/app/controlplane/internal/dispatcher/dispatcher.go b/app/controlplane/internal/dispatcher/dispatcher.go index d7007c993..1a50fda06 100644 --- a/app/controlplane/internal/dispatcher/dispatcher.go +++ b/app/controlplane/internal/dispatcher/dispatcher.go @@ -68,11 +68,12 @@ type dispatchItem struct { type dispatchQueue []*dispatchItem type RunOpts struct { - Envelope *dsse.Envelope - OrgID string - WorkflowID string - WorkflowRunID string - DownloadSecretName string + Envelope *dsse.Envelope + OrgID string + WorkflowID string + WorkflowRunID string + DownloadBackendType string + DownloadSecretName string } func (d *FanOutDispatcher) Run(ctx context.Context, opts *RunOpts) error { @@ -90,7 +91,7 @@ func (d *FanOutDispatcher) Run(ctx context.Context, opts *RunOpts) error { } // 2. Hydrate the dispatch queue with the actual inputs - if err := d.loadInputs(ctx, queue, opts.Envelope, opts.DownloadSecretName); err != nil { + if err := d.loadInputs(ctx, queue, opts.Envelope, opts.DownloadBackendType, opts.DownloadSecretName); err != nil { return fmt.Errorf("loading materials: %w", err) } @@ -194,7 +195,7 @@ func (d *FanOutDispatcher) initDispatchQueue(ctx context.Context, orgID, workflo } // Load the inputs for the dispatchItem, both materials and attestation -func (d *FanOutDispatcher) loadInputs(ctx context.Context, queue dispatchQueue, att *dsse.Envelope, secretName string) error { +func (d *FanOutDispatcher) loadInputs(ctx context.Context, queue dispatchQueue, att *dsse.Envelope, backendType, secretName string) error { if att == nil { return fmt.Errorf("attestation is nil") } @@ -249,7 +250,7 @@ func (d *FanOutDispatcher) loadInputs(ctx context.Context, queue dispatchQueue, // It's a downloadable and has not been downloaded yet if !downloaded && material.Hash != nil && material.UploadedToCAS { buf := bytes.NewBuffer(nil) - if err := d.casClient.Download(ctx, secretName, buf, material.Hash.String()); err != nil { + if err := d.casClient.Download(ctx, backendType, secretName, buf, material.Hash.String()); err != nil { return fmt.Errorf("downloading from CAS: %w", err) } diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index bbf2f5b85..5841c05e5 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -198,7 +198,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe func() error { // reset context ctx := context.Background() - d, err := s.attestationUseCase.UploadToCAS(ctx, envelope, casBackend.SecretName, req.WorkflowRunId) + d, err := s.attestationUseCase.UploadToCAS(ctx, envelope, casBackend, req.WorkflowRunId) if err != nil { return err } @@ -237,7 +237,10 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe // Run integrations dispatcher go func() { if err := s.integrationDispatcher.Run(context.TODO(), &dispatcher.RunOpts{ - Envelope: envelope, OrgID: robotAccount.OrgID, WorkflowID: robotAccount.WorkflowID, DownloadSecretName: secretName, WorkflowRunID: req.WorkflowRunId, + Envelope: envelope, OrgID: robotAccount.OrgID, WorkflowID: robotAccount.WorkflowID, + DownloadBackendType: string(casBackend.Provider), + DownloadSecretName: secretName, + WorkflowRunID: req.WorkflowRunId, }); err != nil { _ = sl.LogAndMaskErr(err, s.log) } @@ -329,7 +332,8 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte // Return the backend information and associated credentials (if applicable) resp := &cpAPI.AttestationServiceGetUploadCredsResponse_Result{Backend: bizCASBackendToPb(backend)} if backend.SecretName != "" { - t, err := s.casCredsUseCase.GenerateTemporaryCredentials(backend.SecretName, casJWT.Uploader) + ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Uploader} + t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref) if err != nil { return nil, sl.LogAndMaskErr(err, s.log) } diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index 68f06fea5..e8ea852a0 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -91,7 +91,8 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS return nil, errors.BadRequest("invalid argument", "cannot upload or download artifacts from an inline CAS backend") } - t, err := s.casUC.GenerateTemporaryCredentials(backend.SecretName, role) + ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: role} + t, err := s.casUC.GenerateTemporaryCredentials(ref) if err != nil { return nil, sl.LogAndMaskErr(err, s.log) } diff --git a/app/controlplane/internal/service/casredirect.go b/app/controlplane/internal/service/casredirect.go index 9c1cc1494..9235b483c 100644 --- a/app/controlplane/internal/service/casredirect.go +++ b/app/controlplane/internal/service/casredirect.go @@ -105,7 +105,8 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown // 2- add authentication token to the query params ?t=[token] if backend.SecretName != "" { - t, err := s.casCredsUseCase.GenerateTemporaryCredentials(backend.SecretName, casJWT.Downloader) + ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Downloader} + t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref) if err != nil { return nil, sl.LogAndMaskErr(err, s.log) } diff --git a/internal/robotaccount/cas/robotaccount.go b/internal/robotaccount/cas/robotaccount.go index 28a2c22cf..8799de215 100644 --- a/internal/robotaccount/cas/robotaccount.go +++ b/internal/robotaccount/cas/robotaccount.go @@ -32,11 +32,11 @@ type Builder struct { issuer string expiration *time.Duration } - type Claims struct { jwt.RegisteredClaims Role Role `json:"role"` // either downloader or uploader StoredSecretID string `json:"secret-id"` // path to the OCI secret in the vault + BackendType string `json:"backend"` // backend to use, i.e OCI } type Role string @@ -102,10 +102,29 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) { return b, nil } -func (ra *Builder) GenerateJWT(secretID, audience string, role Role) (string, error) { +func (ra *Builder) GenerateJWT(backendType, secretID, audience string, role Role) (string, error) { + if backendType == "" { + return "", fmt.Errorf("backend type is required") + } + + if secretID == "" { + return "", fmt.Errorf("secret id is required") + } + + if audience == "" { + return "", fmt.Errorf("audience is required") + } + + if role != Downloader && role != Uploader { + return "", fmt.Errorf("invalid role") + } + claims := &Claims{ - Role: role, + Role: role, + // Credentials to instantiate the backend StoredSecretID: secretID, + // Identifier for the backend, i.e OCI + BackendType: backendType, RegisteredClaims: jwt.RegisteredClaims{ Issuer: ra.issuer, Audience: jwt.ClaimStrings{audience}, diff --git a/internal/robotaccount/cas/robotaccount_test.go b/internal/robotaccount/cas/robotaccount_test.go index a1763f1f8..b17cdcbe3 100644 --- a/internal/robotaccount/cas/robotaccount_test.go +++ b/internal/robotaccount/cas/robotaccount_test.go @@ -150,7 +150,7 @@ func TestGenerateJWT(t *testing.T) { ) require.NoError(t, err) - token, err := b.GenerateJWT("secret-id", JWTAudience, Uploader) + token, err := b.GenerateJWT("OCI", "secret-id", JWTAudience, Uploader) assert.NoError(t, err) assert.NotEmpty(t, token) From 9c9788485ad26243f8553aa803eecd9ffb49af5c Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 26 Sep 2023 10:56:27 +0200 Subject: [PATCH 5/8] feat: send backend in OCI token Signed-off-by: Miguel Martinez Trivino --- app/artifact-cas/internal/server/grpc_test.go | 3 +- .../internal/service/bytestream.go | 23 ++----- .../internal/service/bytestream_test.go | 7 +- app/artifact-cas/internal/service/download.go | 12 ++-- app/artifact-cas/internal/service/resource.go | 9 +-- app/artifact-cas/internal/service/service.go | 37 ++++++---- .../internal/service/service_test.go | 68 +++++++++++++++---- .../internal/biz/mocks/CASClient.go | 34 +++------- .../internal/dispatcher/dispatcher_test.go | 8 +-- 9 files changed, 112 insertions(+), 89 deletions(-) diff --git a/app/artifact-cas/internal/server/grpc_test.go b/app/artifact-cas/internal/server/grpc_test.go index ad2b94881..9deb42328 100644 --- a/app/artifact-cas/internal/server/grpc_test.go +++ b/app/artifact-cas/internal/server/grpc_test.go @@ -96,7 +96,7 @@ func TestJWTAuthFunc(t *testing.T) { b, err := robotaccount.NewBuilder(opts...) require.NoError(t, err) - token, err := b.GenerateJWT("secret-id", tc.audience, robotaccount.Downloader) + token, err := b.GenerateJWT("backend-type", "secret-id", tc.audience, robotaccount.Downloader) require.NoError(t, err) // add bearer token to context @@ -127,6 +127,7 @@ func TestJWTAuthFunc(t *testing.T) { claims := infoFromAuth(ctx, t) assert.NoError(t, claims.Valid()) assert.Equal(t, "secret-id", claims.StoredSecretID) + assert.Equal(t, "backend-type", claims.BackendType) assert.Equal(t, robotaccount.Downloader, claims.Role) assert.Equal(t, "my-issuer", claims.Issuer) assert.Contains(t, claims.Audience, "artifact-cas.chainloop") diff --git a/app/artifact-cas/internal/service/bytestream.go b/app/artifact-cas/internal/service/bytestream.go index a6812daa5..8f6840f4c 100644 --- a/app/artifact-cas/internal/service/bytestream.go +++ b/app/artifact-cas/internal/service/bytestream.go @@ -26,7 +26,6 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" - "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" kerrors "github.com/go-kratos/kratos/v2/errors" @@ -71,15 +70,11 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro return kerrors.BadRequest("resource name", err.Error()) } - backendProvider, err := s.selectProvider(info.BackendType) - if err != nil { - return kerrors.NotFound("not found", err.Error()) - } - - // Load OCI backend based on a reference stored in the token - backend, err := backendProvider.FromCredentials(ctx, info.StoredSecretID) + backend, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID) if err != nil { return sl.LogAndMaskErr(err, s.log) + } else if backend == nil { + return kerrors.NotFound("not found", err.Error()) } // We check if the file already exists even before we wait for the whole buffer to be filled @@ -148,17 +143,11 @@ func (s *ByteStreamService) Read(req *bytestream.ReadRequest, stream bytestream. return kerrors.BadRequest("resource name", "empty resource name") } - // For now we only support OCI - // TODO: select per-request - backendProvider, err := s.selectProvider(oci.ProviderID) - if err != nil { - return kerrors.NotFound("not found", err.Error()) - } - - // Retrieve the OCI backend from where to download the file - backend, err := backendProvider.FromCredentials(ctx, info.StoredSecretID) + backend, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID) if err != nil { return sl.LogAndMaskErr(err, s.log) + } else if backend == nil { + return kerrors.NotFound("not found", err.Error()) } // streamwriter will stream chunks of data to the client diff --git a/app/artifact-cas/internal/service/bytestream_test.go b/app/artifact-cas/internal/service/bytestream_test.go index 0fe734d6d..569aa8a1f 100644 --- a/app/artifact-cas/internal/service/bytestream_test.go +++ b/app/artifact-cas/internal/service/bytestream_test.go @@ -249,6 +249,7 @@ func (s *bytestreamSuite) TearDownTest() { } func (s *bytestreamSuite) SetupTest() { + const backendType = "backend-type" // 1 MB buffer l := bufconn.Listen(1 << 20) server := grpc.NewServer( @@ -261,7 +262,9 @@ func (s *bytestreamSuite) SetupTest() { return ctx, nil } - claims := &casJWT.Claims{} + claims := &casJWT.Claims{ + StoredSecretID: "secret-id", BackendType: backendType, + } if roles := md.Get("role"); len(roles) > 0 { if roles[0] == "downloader" { @@ -283,7 +286,7 @@ func (s *bytestreamSuite) SetupTest() { bytestream.RegisterByteStreamServer( server, NewByteStreamService(backend.Providers{ - "OCI": ociBackendProvider, + backendType: ociBackendProvider, }, WithLogger(log.DefaultLogger)), ) go func() { diff --git a/app/artifact-cas/internal/service/download.go b/app/artifact-cas/internal/service/download.go index c8e37ca2e..1bbf961a6 100644 --- a/app/artifact-cas/internal/service/download.go +++ b/app/artifact-cas/internal/service/download.go @@ -69,17 +69,13 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - backendProvider, err := s.selectProvider(auth.BackendType) - if err != nil { - http.Error(w, "backend not found", http.StatusNotFound) - return - } - - // Retrieve the CAS backend from where to download the file - b, err := backendProvider.FromCredentials(ctx, auth.StoredSecretID) + b, err := s.loadBackend(ctx, auth.BackendType, auth.StoredSecretID) if err != nil { http.Error(w, sl.LogAndMaskErr(err, s.log).Error(), http.StatusInternalServerError) return + } else if b == nil { + http.Error(w, "backend not found", http.StatusNotFound) + return } info, err := b.Describe(ctx, hash.Hex) diff --git a/app/artifact-cas/internal/service/resource.go b/app/artifact-cas/internal/service/resource.go index 760ba1db0..ff505c129 100644 --- a/app/artifact-cas/internal/service/resource.go +++ b/app/artifact-cas/internal/service/resource.go @@ -42,14 +42,11 @@ func (s *ResourceService) Describe(ctx context.Context, req *v1.ResourceServiceD return nil, err } - backendProvider, err := s.selectProvider(info.BackendType) - if err != nil { - return nil, errors.NotFound("not found", err.Error()) - } - - b, err := backendProvider.FromCredentials(ctx, info.StoredSecretID) + b, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID) if err != nil { return nil, sl.LogAndMaskErr(err, s.log) + } else if b == nil { + return nil, errors.NotFound("not found", err.Error()) } res, err := b.Describe(ctx, req.Digest) diff --git a/app/artifact-cas/internal/service/service.go b/app/artifact-cas/internal/service/service.go index b87b16af1..224065cfa 100644 --- a/app/artifact-cas/internal/service/service.go +++ b/app/artifact-cas/internal/service/service.go @@ -20,7 +20,6 @@ import ( "fmt" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" - "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" "github.com/chainloop-dev/chainloop/internal/servicelogger" kerrors "github.com/go-kratos/kratos/v2/errors" @@ -37,22 +36,22 @@ type commonService struct { backends backend.Providers } -func (s *commonService) selectProvider(id string) (backend.Provider, error) { - // if no provider is specified, default to OCI - // this is done for backward compatibility - if id == "" { - s.log.Warn("provider not set, defaulting to OCI") - id = oci.ProviderID - } - +func (s *commonService) loadBackend(ctx context.Context, providerType, secretID string) (backend.UploaderDownloader, error) { // get the OCI provider from the map - p, ok := s.backends[id] + p, ok := s.backends[providerType] if !ok { - return nil, fmt.Errorf("provider %s not found", id) + return nil, nil + } + + s.log.Infow("msg", "selected provider", "provider", providerType) + + // Retrieve the OCI backend from where to download the file + backend, err := p.FromCredentials(ctx, secretID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve backend: %w", err) } - s.log.Infow("msg", "selected provider", "provider", id) - return p, nil + return backend, nil } type NewOpt func(s *commonService) @@ -88,5 +87,17 @@ func infoFromAuth(ctx context.Context) (*casJWT.Claims, error) { return nil, kerrors.Unauthorized("cas", "invalid authentication information") } + if claims.StoredSecretID == "" { + return nil, kerrors.Unauthorized("cas", "missing secret reference") + } + + if claims.BackendType == "" { + return nil, kerrors.Unauthorized("cas", "missing backend type") + } + + if claims.Role == "" { + return nil, kerrors.Unauthorized("cas", "missing role") + } + return claims, nil } diff --git a/app/artifact-cas/internal/service/service_test.go b/app/artifact-cas/internal/service/service_test.go index 0376887bb..779f3dd33 100644 --- a/app/artifact-cas/internal/service/service_test.go +++ b/app/artifact-cas/internal/service/service_test.go @@ -26,20 +26,60 @@ import ( ) func TestInfoFromAuth(t *testing.T) { - t.Run("no claims", func(t *testing.T) { - _, err := infoFromAuth(jwtm.NewContext(context.Background(), nil)) - assert.Error(t, err) - }) + testCases := []struct { + name string + // input + claims jwt.Claims + wantErr bool + }{ + { + name: "valid claims", + claims: &casJWT.Claims{ + Role: "test", + StoredSecretID: "test", + BackendType: "backend-type", + }, + }, + { + name: "missing secretID", + claims: &casJWT.Claims{ + Role: "test", + BackendType: "backend-type", + }, + wantErr: true, + }, + { + name: "missing role", + claims: &casJWT.Claims{ + StoredSecretID: "test", + BackendType: "backend-type", + }, + wantErr: true, + }, + { + name: "missing backend type", + claims: &casJWT.Claims{ + StoredSecretID: "test", + Role: "test", + }, + wantErr: true, + }, + } - t.Run("invalid claims", func(t *testing.T) { - _, err := infoFromAuth(jwtm.NewContext(context.Background(), &jwt.RegisteredClaims{})) - assert.Error(t, err) - }) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + info, err := infoFromAuth(jwtm.NewContext(context.Background(), tc.claims)) + if tc.wantErr { + assert.Error(t, err) + return + } - t.Run("valid claims", func(t *testing.T) { - want := &casJWT.Claims{Role: "test", StoredSecretID: "test"} - got, err := infoFromAuth(jwtm.NewContext(context.Background(), want)) - assert.NoError(t, err) - assert.Equal(t, want, got) - }) + assert.NoError(t, err) + assert.Equal(t, tc.claims, info) + }) + } +} + +func TestLoadBackend(t *testing.T) { + t.Fail() } diff --git a/app/controlplane/internal/biz/mocks/CASClient.go b/app/controlplane/internal/biz/mocks/CASClient.go index f0f9c22ee..23792601e 100644 --- a/app/controlplane/internal/biz/mocks/CASClient.go +++ b/app/controlplane/internal/biz/mocks/CASClient.go @@ -14,27 +14,13 @@ type CASClient struct { mock.Mock } -// Configured provides a mock function with given fields: -func (_m *CASClient) Configured() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// Download provides a mock function with given fields: ctx, secretID, w, digest -func (_m *CASClient) Download(ctx context.Context, secretID string, w io.Writer, digest string) error { - ret := _m.Called(ctx, secretID, w, digest) +// Download provides a mock function with given fields: ctx, backendType, secretID, w, digest +func (_m *CASClient) Download(ctx context.Context, backendType string, secretID string, w io.Writer, digest string) error { + ret := _m.Called(ctx, backendType, secretID, w, digest) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, io.Writer, string) error); ok { - r0 = rf(ctx, secretID, w, digest) + if rf, ok := ret.Get(0).(func(context.Context, string, string, io.Writer, string) error); ok { + r0 = rf(ctx, backendType, secretID, w, digest) } else { r0 = ret.Error(0) } @@ -42,13 +28,13 @@ func (_m *CASClient) Download(ctx context.Context, secretID string, w io.Writer, return r0 } -// Upload provides a mock function with given fields: ctx, secretID, content, filename, digest -func (_m *CASClient) Upload(ctx context.Context, secretID string, content io.Reader, filename string, digest string) error { - ret := _m.Called(ctx, secretID, content, filename, digest) +// Upload provides a mock function with given fields: ctx, backendType, secretID, content, filename, digest +func (_m *CASClient) Upload(ctx context.Context, backendType string, secretID string, content io.Reader, filename string, digest string) error { + ret := _m.Called(ctx, backendType, secretID, content, filename, digest) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, io.Reader, string, string) error); ok { - r0 = rf(ctx, secretID, content, filename, digest) + if rf, ok := ret.Get(0).(func(context.Context, string, string, io.Reader, string, string) error); ok { + r0 = rf(ctx, backendType, secretID, content, filename, digest) } else { r0 = ret.Error(0) } diff --git a/app/controlplane/internal/dispatcher/dispatcher_test.go b/app/controlplane/internal/dispatcher/dispatcher_test.go index 3ff61e0bc..4943269a1 100644 --- a/app/controlplane/internal/dispatcher/dispatcher_test.go +++ b/app/controlplane/internal/dispatcher/dispatcher_test.go @@ -53,7 +53,7 @@ func (s *dispatcherTestSuite) TestLoadInputsEnvelope() { envelope, err := testEnvelope("testdata/attestation.json") require.NoError(s.T(), err) - err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "secret-name") + err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "backend-type", "secret-name") assert.NoError(s.T(), err) // Only one integration is registered @@ -88,14 +88,14 @@ func (s *dispatcherTestSuite) TestLoadInputsMaterials() { require.NoError(s.T(), err) // Simulate SBOM download - s.casClient.On("Download", mock.Anything, "secret-name", mock.Anything, mock.Anything). + s.casClient.On("Download", mock.Anything, "backend-type", "secret-name", mock.Anything, mock.Anything). Return(nil).Run(func(args mock.Arguments) { buf := bytes.NewBuffer([]byte("SBOM Content")) - _, err := io.Copy(args.Get(2).(io.Writer), buf) + _, err := io.Copy(args.Get(3).(io.Writer), buf) s.NoError(err) }) - err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "secret-name") + err = s.dispatcher.loadInputs(context.TODO(), queue, envelope, "backend-type", "secret-name") assert.NoError(s.T(), err) require.Len(s.T(), queue, 3) From 2ca507fb57e828b33a93a9614cf7fa217bf6320b Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 26 Sep 2023 11:07:03 +0200 Subject: [PATCH 6/8] feat: send backend in OCI token Signed-off-by: Miguel Martinez Trivino --- .golangci.yml | 6 +++++- .../internal/usercontext/orgrequirements_middleware_test.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6f82bf6fd..23bd25b34 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -71,4 +71,8 @@ severity: - linters: - staticcheck text: "SA1019:" - severity: info \ No newline at end of file + severity: info +issues: + exclude-rules: + - path: _test\.go + text: "Potential hardcoded credentials" diff --git a/app/controlplane/internal/usercontext/orgrequirements_middleware_test.go b/app/controlplane/internal/usercontext/orgrequirements_middleware_test.go index 9ea497ca0..8db1c6522 100644 --- a/app/controlplane/internal/usercontext/orgrequirements_middleware_test.go +++ b/app/controlplane/internal/usercontext/orgrequirements_middleware_test.go @@ -64,7 +64,7 @@ func TestShouldRevalidate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { repo := &biz.CASBackend{ ValidationStatus: tc.repoStatus, - ValidatedAt: &tc.repoValidatedAt, + ValidatedAt: toTimePtr(tc.repoValidatedAt), } assert.Equal(t, tc.expected, shouldRevalidate(repo)) From 15e756020e4cc0c4e7c03303c074f68951992db7 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 26 Sep 2023 11:45:25 +0200 Subject: [PATCH 7/8] feat: send backend in OCI token Signed-off-by: Miguel Martinez Trivino --- .../internal/service/bytestream.go | 12 +-- app/artifact-cas/internal/service/download.go | 9 ++- app/artifact-cas/internal/service/resource.go | 6 +- .../internal/service/resource_test.go | 4 +- app/artifact-cas/internal/service/service.go | 8 +- .../internal/service/service_test.go | 78 ++++++++++++++++++- 6 files changed, 95 insertions(+), 22 deletions(-) diff --git a/app/artifact-cas/internal/service/bytestream.go b/app/artifact-cas/internal/service/bytestream.go index 8f6840f4c..c94575926 100644 --- a/app/artifact-cas/internal/service/bytestream.go +++ b/app/artifact-cas/internal/service/bytestream.go @@ -71,10 +71,10 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro } backend, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID) - if err != nil { + if err != nil && kerrors.IsNotFound(err) { + return err + } else if err != nil { return sl.LogAndMaskErr(err, s.log) - } else if backend == nil { - return kerrors.NotFound("not found", err.Error()) } // We check if the file already exists even before we wait for the whole buffer to be filled @@ -144,10 +144,10 @@ func (s *ByteStreamService) Read(req *bytestream.ReadRequest, stream bytestream. } backend, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID) - if err != nil { + if err != nil && kerrors.IsNotFound(err) { + return err + } else if err != nil { return sl.LogAndMaskErr(err, s.log) - } else if backend == nil { - return kerrors.NotFound("not found", err.Error()) } // streamwriter will stream chunks of data to the client diff --git a/app/artifact-cas/internal/service/download.go b/app/artifact-cas/internal/service/download.go index 1bbf961a6..38948584f 100644 --- a/app/artifact-cas/internal/service/download.go +++ b/app/artifact-cas/internal/service/download.go @@ -26,6 +26,7 @@ import ( backend "github.com/chainloop-dev/chainloop/internal/blobmanager" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" sl "github.com/chainloop-dev/chainloop/internal/servicelogger" + kerrors "github.com/go-kratos/kratos/v2/errors" cr_v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/gorilla/mux" ) @@ -70,12 +71,12 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { } b, err := s.loadBackend(ctx, auth.BackendType, auth.StoredSecretID) - if err != nil { - http.Error(w, sl.LogAndMaskErr(err, s.log).Error(), http.StatusInternalServerError) - return - } else if b == nil { + if err != nil && kerrors.IsNotFound(err) { http.Error(w, "backend not found", http.StatusNotFound) return + } else if err != nil { + http.Error(w, sl.LogAndMaskErr(err, s.log).Error(), http.StatusInternalServerError) + return } info, err := b.Describe(ctx, hash.Hex) diff --git a/app/artifact-cas/internal/service/resource.go b/app/artifact-cas/internal/service/resource.go index ff505c129..ad3b661fe 100644 --- a/app/artifact-cas/internal/service/resource.go +++ b/app/artifact-cas/internal/service/resource.go @@ -43,10 +43,10 @@ func (s *ResourceService) Describe(ctx context.Context, req *v1.ResourceServiceD } b, err := s.loadBackend(ctx, info.BackendType, info.StoredSecretID) - if err != nil { + if err != nil && errors.IsNotFound(err) { + return nil, err + } else if err != nil { return nil, sl.LogAndMaskErr(err, s.log) - } else if b == nil { - return nil, errors.NotFound("not found", err.Error()) } res, err := b.Describe(ctx, req.Digest) diff --git a/app/artifact-cas/internal/service/resource_test.go b/app/artifact-cas/internal/service/resource_test.go index 1a058f20e..2b0f1cf80 100644 --- a/app/artifact-cas/internal/service/resource_test.go +++ b/app/artifact-cas/internal/service/resource_test.go @@ -33,7 +33,7 @@ func (s *resourceSuite) TestDescribe() { want := &v1.CASResource{FileName: "test.txt", Digest: "deadbeef"} s.ociBackend.On("Describe", mock.Anything, "deadbeef").Return(want, nil) - ctx := jwtmiddleware.NewContext(context.Background(), &casjwt.Claims{StoredSecretID: "secret-id"}) + ctx := jwtmiddleware.NewContext(context.Background(), &casjwt.Claims{StoredSecretID: "secret-id", BackendType: "backend1", Role: casjwt.Downloader}) svc := service.NewResourceService(s.backendProviders) got, err := svc.Describe(ctx, &v1.ResourceServiceDescribeRequest{ @@ -58,7 +58,7 @@ func (s *resourceSuite) SetupTest() { s.ociBackend = ociBackend s.backendProviders = backend.Providers{ - "OCI": ociBackendProvider, + "backend1": ociBackendProvider, } } diff --git a/app/artifact-cas/internal/service/service.go b/app/artifact-cas/internal/service/service.go index 224065cfa..c09651a77 100644 --- a/app/artifact-cas/internal/service/service.go +++ b/app/artifact-cas/internal/service/service.go @@ -39,8 +39,8 @@ type commonService struct { func (s *commonService) loadBackend(ctx context.Context, providerType, secretID string) (backend.UploaderDownloader, error) { // get the OCI provider from the map p, ok := s.backends[providerType] - if !ok { - return nil, nil + if !ok || p == nil { + return nil, kerrors.NotFound("backend provider", providerType) } s.log.Infow("msg", "selected provider", "provider", providerType) @@ -95,8 +95,8 @@ func infoFromAuth(ctx context.Context) (*casJWT.Claims, error) { return nil, kerrors.Unauthorized("cas", "missing backend type") } - if claims.Role == "" { - return nil, kerrors.Unauthorized("cas", "missing role") + if claims.Role != casJWT.Uploader && claims.Role != casJWT.Downloader { + return nil, kerrors.Unauthorized("cas", "invalid role") } return claims, nil diff --git a/app/artifact-cas/internal/service/service_test.go b/app/artifact-cas/internal/service/service_test.go index 779f3dd33..efc517d95 100644 --- a/app/artifact-cas/internal/service/service_test.go +++ b/app/artifact-cas/internal/service/service_test.go @@ -17,12 +17,17 @@ package service import ( "context" + "errors" "testing" + backend "github.com/chainloop-dev/chainloop/internal/blobmanager" + "github.com/chainloop-dev/chainloop/internal/blobmanager/mocks" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" + kerrors "github.com/go-kratos/kratos/v2/errors" jwtm "github.com/go-kratos/kratos/v2/middleware/auth/jwt" "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestInfoFromAuth(t *testing.T) { @@ -33,12 +38,29 @@ func TestInfoFromAuth(t *testing.T) { wantErr bool }{ { - name: "valid claims", + name: "valid claims downloader", claims: &casJWT.Claims{ - Role: "test", + Role: casJWT.Downloader, + StoredSecretID: "test", + BackendType: "backend-type", + }, + }, + { + name: "valid claims uploader", + claims: &casJWT.Claims{ + Role: casJWT.Uploader, + StoredSecretID: "test", + BackendType: "backend-type", + }, + }, + { + name: "invalid role", + claims: &casJWT.Claims{ + Role: "invalid", StoredSecretID: "test", BackendType: "backend-type", }, + wantErr: true, }, { name: "missing secretID", @@ -81,5 +103,55 @@ func TestInfoFromAuth(t *testing.T) { } func TestLoadBackend(t *testing.T) { - t.Fail() + testCases := []struct { + name string + // input + providerType string + secretID string + wantErr bool + is404Err bool + }{ + { + name: "valid", + providerType: "test", + secretID: "test", + }, + { + name: "invalid provider type", + providerType: "invalid", + wantErr: true, + is404Err: true, + }, + { + name: "invalid secretID", + providerType: "test", + secretID: "invalid", + wantErr: true, + }, + } + + backendProvider := mocks.NewProvider(t) + b := mocks.NewUploaderDownloader(t) + backendProvider.On("FromCredentials", mock.Anything, "test").Maybe().Return(b, nil) + backendProvider.On("FromCredentials", mock.Anything, "invalid").Maybe().Return(nil, errors.New("backend not found")) + // Initialize common service with backends + providers := backend.Providers{ + "test": backendProvider, + } + + s := newCommonService(providers) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := s.loadBackend(context.Background(), tc.providerType, tc.secretID) + if tc.wantErr { + assert.Error(t, err) + assert.Equal(t, tc.is404Err, kerrors.IsNotFound(err)) + return + } + + assert.NoError(t, err) + assert.Equal(t, b, got) + }) + } } From 3a73c685a4b234f7d3e6ba8fba98edd3882b3fd0 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Tue, 26 Sep 2023 11:55:41 +0200 Subject: [PATCH 8/8] fix flaky test Signed-off-by: Miguel Martinez Trivino --- app/artifact-cas/internal/service/status.go | 3 +++ app/artifact-cas/internal/service/status_test.go | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/artifact-cas/internal/service/status.go b/app/artifact-cas/internal/service/status.go index 26a48ac95..b642695f7 100644 --- a/app/artifact-cas/internal/service/status.go +++ b/app/artifact-cas/internal/service/status.go @@ -17,6 +17,7 @@ package service import ( "context" + "sort" pb "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" backend "github.com/chainloop-dev/chainloop/internal/blobmanager" @@ -42,5 +43,7 @@ func (s *StatusService) Infoz(_ context.Context, _ *pb.InfozRequest) (*pb.InfozR backends = append(backends, k) } + sort.Strings(backends) + return &pb.InfozResponse{Version: s.version, Backends: backends}, nil } diff --git a/app/artifact-cas/internal/service/status_test.go b/app/artifact-cas/internal/service/status_test.go index b8a207f6b..382637a5d 100644 --- a/app/artifact-cas/internal/service/status_test.go +++ b/app/artifact-cas/internal/service/status_test.go @@ -27,6 +27,7 @@ import ( var enabledProviders backend.Providers = backend.Providers{ "OCI": nil, "GCS": nil, + "ABC": nil, } func TestInfoz(t *testing.T) { @@ -34,7 +35,7 @@ func TestInfoz(t *testing.T) { got, err := service.NewStatusService(want, enabledProviders).Infoz(context.Background(), nil) assert.NoError(t, err) assert.Equal(t, want, got.Version) - assert.Equal(t, []string{"OCI", "GCS"}, got.Backends) + assert.Equal(t, []string{"ABC", "GCS", "OCI"}, got.Backends) } func TestStatusz(t *testing.T) {