diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 21fe766dac..8053fe3b8e 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/TBD54566975/ftl/backend/controller/artefacts" "hash" "io" "net/http" @@ -221,6 +222,7 @@ type Service struct { tasks *scheduledtask.Scheduler cronJobs *cronjobs.Service pubSub *pubsub.Service + registry *artefacts.Service timeline *timeline.Service controllerListListeners []ControllerListListener @@ -275,6 +277,8 @@ func New(ctx context.Context, conn *sql.DB, config Config, devel bool, runnerSca pubSub := pubsub.New(conn, encryption, svc.tasks, optional.Some[pubsub.AsyncCallListener](svc)) svc.pubSub = pubSub + svc.registry = artefacts.New(ctx, conn) + svc.dal = dal.New(ctx, conn, encryption, pubSub) timelineSvc := timeline.New(ctx, conn, encryption) @@ -1094,7 +1098,7 @@ func (s *Service) GetArtefactDiffs(ctx context.Context, req *connect.Request[ftl if err != nil { return nil, err } - need, err := s.dal.GetMissingArtefacts(ctx, byteDigests) + _, need, err := s.registry.GetDigestsKeys(ctx, byteDigests) if err != nil { return nil, err } @@ -1105,7 +1109,7 @@ func (s *Service) GetArtefactDiffs(ctx context.Context, req *connect.Request[ftl func (s *Service) UploadArtefact(ctx context.Context, req *connect.Request[ftlv1.UploadArtefactRequest]) (*connect.Response[ftlv1.UploadArtefactResponse], error) { logger := log.FromContext(ctx) - digest, err := s.dal.CreateArtefact(ctx, req.Msg.Content) + digest, err := s.registry.Upload(ctx, artefacts.Artefact{Content: req.Msg.Content}) if err != nil { return nil, err } diff --git a/backend/controller/dal/dal.go b/backend/controller/dal/dal.go index f0cd7ff317..0970e920dd 100644 --- a/backend/controller/dal/dal.go +++ b/backend/controller/dal/dal.go @@ -6,13 +6,11 @@ import ( "encoding/json" "errors" "fmt" - "io" - "strings" + "github.com/TBD54566975/ftl/backend/controller/artefacts" "time" "github.com/alecthomas/types/optional" inprocesspubsub "github.com/alecthomas/types/pubsub" - sets "github.com/deckarep/golang-set/v2" xmaps "golang.org/x/exp/maps" "google.golang.org/protobuf/proto" @@ -55,10 +53,12 @@ type Reservation interface { func New(ctx context.Context, conn libdal.Connection, encryption *encryption.Service, pubsub *pubsub.Service) *DAL { var d *DAL + db := dalsql.New(conn) d = &DAL{ leaser: dbleaser.NewDatabaseLeaser(conn), - db: dalsql.New(conn), + db: db, encryption: encryption, + registry: artefacts.New(ctx, conn), Handle: libdal.New(conn, func(h *libdal.Handle[DAL]) *DAL { return &DAL{ Handle: h, @@ -66,6 +66,7 @@ func New(ctx context.Context, conn libdal.Connection, encryption *encryption.Ser leaser: dbleaser.NewDatabaseLeaser(h.Connection), pubsub: pubsub, encryption: d.encryption, + registry: artefacts.New(ctx, h.Connection), DeploymentChanges: d.DeploymentChanges, } }), @@ -82,6 +83,7 @@ type DAL struct { leaser *dbleaser.DatabaseLeaser pubsub *pubsub.Service encryption *encryption.Service + registry *artefacts.Service // DeploymentChanges is a Topic that receives changes to the deployments table. DeploymentChanges *inprocesspubsub.Topic[DeploymentNotification] @@ -194,25 +196,6 @@ func (d *DAL) UpsertModule(ctx context.Context, language, name string) (err erro return libdal.TranslatePGError(err) } -// GetMissingArtefacts returns the digests of the artefacts that are missing from the database. -func (d *DAL) GetMissingArtefacts(ctx context.Context, digests []sha256.SHA256) ([]sha256.SHA256, error) { - have, err := d.db.GetArtefactDigests(ctx, sha256esToBytes(digests)) - if err != nil { - return nil, libdal.TranslatePGError(err) - } - haveStr := slices.Map(have, func(in dalsql.GetArtefactDigestsRow) sha256.SHA256 { - return sha256.FromBytes(in.Digest) - }) - return sets.NewSet(digests...).Difference(sets.NewSet(haveStr...)).ToSlice(), nil -} - -// CreateArtefact inserts a new artefact into the database and returns its ID. -func (d *DAL) CreateArtefact(ctx context.Context, content []byte) (digest sha256.SHA256, err error) { - sha256digest := sha256.Sum(content) - _, err = d.db.CreateArtefact(ctx, sha256digest[:], content) - return sha256digest, libdal.TranslatePGError(err) -} - type IngressRoutingEntry struct { Verb string Method string @@ -281,19 +264,21 @@ func (d *DAL) CreateDeployment(ctx context.Context, language string, moduleSchem return model.DeploymentKey{}, fmt.Errorf("failed to create deployment: %w", libdal.TranslatePGError(err)) } - uploadedDigests := slices.Map(artefacts, func(in dalmodel.DeploymentArtefact) []byte { return in.Digest[:] }) - artefactDigests, err := tx.db.GetArtefactDigests(ctx, uploadedDigests) + uploadedDigests := slices.Map(artefacts, func(in dalmodel.DeploymentArtefact) sha256.SHA256 { return sha256.FromBytes(in.Digest[:]) }) + keys, missing, err := tx.registry.GetDigestsKeys(ctx, uploadedDigests) if err != nil { return model.DeploymentKey{}, fmt.Errorf("failed to get artefact digests: %w", err) } - if len(artefactDigests) != len(artefacts) { - missingDigests := strings.Join(slices.Map(artefacts, func(in dalmodel.DeploymentArtefact) string { return in.Digest.String() }), ", ") - return model.DeploymentKey{}, fmt.Errorf("missing %d artefacts: %s", len(artefacts)-len(artefactDigests), missingDigests) + if len(missing) > 0 { + m := slices.Reduce(missing, "", func(join string, in sha256.SHA256) string { + return fmt.Sprintf("%s, %s", join, in.String()) + }) + return model.DeploymentKey{}, fmt.Errorf("missing digests %s", m) } // Associate the artefacts with the deployment - for _, row := range artefactDigests { - artefact := artefactsByDigest[sha256.FromBytes(row.Digest)] + for _, row := range keys { + artefact := artefactsByDigest[row.Digest] err = tx.db.AssociateArtefactWithDeployment(ctx, dalsql.AssociateArtefactWithDeploymentParams{ Key: deploymentKey, ArtefactID: row.ID, @@ -701,18 +686,27 @@ func (d *DAL) loadDeployment(ctx context.Context, deployment dalsql.GetDeploymen Key: deployment.Deployment.Key, Schema: deployment.Deployment.Schema, } - artefacts, err := d.db.GetDeploymentArtefacts(ctx, deployment.Deployment.ID) + darts, err := d.db.GetDeploymentArtefacts(ctx, deployment.Deployment.ID) + if err != nil { return nil, libdal.TranslatePGError(err) } - out.Artefacts = slices.Map(artefacts, func(row dalsql.GetDeploymentArtefactsRow) *model.Artefact { + out.Artefacts, err = slices.MapErr(darts, func(row dalsql.GetDeploymentArtefactsRow) (*model.Artefact, error) { + digest := sha256.FromBytes(row.Digest) + content, err := d.registry.Download(ctx, digest) + if err != nil { + return nil, fmt.Errorf("artefact download failed: %w", libdal.TranslatePGError(err)) + } return &model.Artefact{ Path: row.Path, Executable: row.Executable, - Content: &artefactReader{id: row.ID, db: d.db}, - Digest: sha256.FromBytes(row.Digest), - } + Content: content, + Digest: digest, + }, nil }) + if err != nil { + return nil, fmt.Errorf("an artefact download failed: %w", err) + } return out, nil } @@ -779,25 +773,3 @@ func (*DAL) checkForExistingDeployments(ctx context.Context, tx *DAL, moduleSche func sha256esToBytes(digests []sha256.SHA256) [][]byte { return slices.Map(digests, func(digest sha256.SHA256) []byte { return digest[:] }) } - -type artefactReader struct { - id int64 - db dalsql.Querier - offset int32 -} - -func (r *artefactReader) Close() error { return nil } - -func (r *artefactReader) Read(p []byte) (n int, err error) { - content, err := r.db.GetArtefactContentRange(context.Background(), r.offset+1, int32(len(p)), r.id) - if err != nil { - return 0, libdal.TranslatePGError(err) - } - copy(p, content) - clen := len(content) - r.offset += int32(clen) - if clen == 0 { - err = io.EOF - } - return clen, err -} diff --git a/internal/buildengine/testdata/alpha/go.mod b/internal/buildengine/testdata/alpha/go.mod index 2306019093..ecf69388c0 100644 --- a/internal/buildengine/testdata/alpha/go.mod +++ b/internal/buildengine/testdata/alpha/go.mod @@ -11,15 +11,19 @@ require ( github.com/XSAM/otelsql v0.34.0 // indirect github.com/alecthomas/atomic v0.1.0-alpha2 // indirect github.com/alecthomas/concurrency v0.0.2 // indirect + github.com/alecthomas/kong v1.2.1 // indirect github.com/alecthomas/participle/v2 v2.1.1 // indirect github.com/alecthomas/types v0.16.0 // indirect github.com/alessio/shellescape v1.4.2 // indirect github.com/benbjohnson/clock v1.3.5 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/cronexpr v1.1.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -34,8 +38,15 @@ require ( github.com/swaggest/refl v1.3.0 // indirect github.com/zalando/go-keyring v0.2.5 // indirect go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.30.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.21.0 // indirect @@ -43,6 +54,9 @@ require ( golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.2 // indirect google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/internal/buildengine/testdata/alpha/go.sum b/internal/buildengine/testdata/alpha/go.sum index f001543d0e..51cef7fe59 100644 --- a/internal/buildengine/testdata/alpha/go.sum +++ b/internal/buildengine/testdata/alpha/go.sum @@ -14,6 +14,8 @@ github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELk github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= @@ -28,6 +30,8 @@ github.com/bool64/dev v0.2.35 h1:M17TLsO/pV2J7PYI/gpe3Ua26ETkzZGb+dC06eoMqlk= github.com/bool64/dev v0.2.35/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -66,6 +70,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -110,6 +116,8 @@ github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -143,6 +151,12 @@ github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8L github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0 h1:WypxHH02KX2poqqbaadmkMYalGyy/vil4HE4PM4nRJc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0/go.mod h1:U79SV99vtvGSEBeeHnpgGJfTsnsdkWLpPN/CcHAzBSI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0 h1:m0yTiGDLUvVYaTFbAvCkVYIYcvwKt3G7OLoN77NUs/8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.30.0/go.mod h1:wBQbT4UekBfegL2nx0Xk1vBcnzyBPsIVm9hRG4fYcr4= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= @@ -151,6 +165,12 @@ go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792j go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= @@ -172,6 +192,12 @@ golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=