Skip to content

Commit fc95939

Browse files
committed
internal/gomote, internal/gomote/protos: add the upload file endpoint
This change adds the upload file endpoint which will be used by the gomote clients to upload files to GCS before they are retrieved by a gomote instance. The endpoint generates a signed URL and associated fields which must be used in the upload. For golang/go#47521 Updates golang/go#48742 Change-Id: Id85a55b41b8211b3aae8c2e30245a0b71ecfa238 Reviewed-on: https://go-review.googlesource.com/c/build/+/397595 Trust: Carlos Amedee <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]>
1 parent a7bbee6 commit fc95939

File tree

7 files changed

+327
-197
lines changed

7 files changed

+327
-197
lines changed

buildenv/envs.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ type Environment struct {
143143
// services used by IAP enabled HTTP paths.
144144
// map[backend-service-name]service_id
145145
iapServiceIDs map[string]string
146+
147+
// GomoteTransferBucket is the bucket used by the gomote GRPC service
148+
// to transfer files between gomote clients and the gomote instances.
149+
GomoteTransferBucket string
146150
}
147151

148152
// ComputePrefix returns the URI prefix for Compute Engine resources in a project.
@@ -304,6 +308,7 @@ var Production = &Environment{
304308
"coordinator-internal-iap": "7963570695201399464",
305309
"relui-internal": "155577380958854618",
306310
},
311+
GomoteTransferBucket: "gomote-transfer",
307312
}
308313

309314
var Development = &Environment{

cmd/coordinator/coordinator.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import (
6666
"golang.org/x/build/revdial/v2"
6767
"golang.org/x/build/types"
6868
"golang.org/x/time/rate"
69+
"google.golang.org/api/option"
6970
"google.golang.org/grpc"
7071
"google.golang.org/grpc/credentials"
7172
)
@@ -343,13 +344,15 @@ func main() {
343344
}
344345
sshCA := mustRetrieveSSHCertificateAuthority()
345346

347+
var gomoteBucket string
346348
var opts []grpc.ServerOption
347349
if *buildEnvName == "" && *mode != "dev" && metadata.OnGCE() {
348350
projectID, err := metadata.ProjectID()
349351
if err != nil {
350352
log.Fatalf("metadata.ProjectID() = %v", err)
351353
}
352354
env := buildenv.ByProjectID(projectID)
355+
gomoteBucket = env.GomoteTransferBucket
353356
var coordinatorBackend, serviceID = "coordinator-internal-iap", ""
354357
if serviceID = env.IAPServiceID(coordinatorBackend); serviceID == "" {
355358
log.Fatalf("unable to retrieve Service ID for backend service=%q", coordinatorBackend)
@@ -363,7 +366,7 @@ func main() {
363366
dashV1 := legacydash.Handler(gce.GoDSClient(), maintnerClient, string(masterKey()), grpcServer)
364367
dashV2 := &builddash.Handler{Datastore: gce.GoDSClient(), Maintner: maintnerClient}
365368
gs := &gRPCServer{dashboardURL: "https://build.golang.org"}
366-
gomoteServer := gomote.New(remote.NewSessionPool(context.Background()), sched, sshCA)
369+
gomoteServer := gomote.New(remote.NewSessionPool(context.Background()), sched, sshCA, gomoteBucket, mustStorageClient())
367370
protos.RegisterCoordinatorServer(grpcServer, gs)
368371
gomoteprotos.RegisterGomoteServiceServer(grpcServer, gomoteServer)
369372
mux.HandleFunc("/", grpcHandlerFunc(grpcServer, handleStatus)) // Serve a status page at farmer.golang.org.
@@ -2202,3 +2205,14 @@ func mustRetrieveSSHCertificateAuthority() (privateKey []byte) {
22022205
}
22032206
return
22042207
}
2208+
2209+
func mustStorageClient() *storage.Client {
2210+
if metadata.OnGCE() {
2211+
return pool.NewGCEConfiguration().StorageClient()
2212+
}
2213+
storageClient, err := storage.NewClient(context.Background(), option.WithoutAuthentication())
2214+
if err != nil {
2215+
log.Fatalf("unable to create storage client: %s", err)
2216+
}
2217+
return storageClient
2218+
}

internal/gomote/gomote.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"strings"
1717
"time"
1818

19+
"cloud.google.com/go/storage"
20+
"github.com/google/uuid"
1921
"golang.org/x/build/buildlet"
2022
"golang.org/x/build/dashboard"
2123
"golang.org/x/build/internal/access"
@@ -34,24 +36,33 @@ type scheduler interface {
3436
GetBuildlet(ctx context.Context, si *schedule.SchedItem) (buildlet.Client, error)
3537
}
3638

39+
// bucketHandle interface used to enable testing of the storage.bucketHandle.
40+
type bucketHandle interface {
41+
GenerateSignedPostPolicyV4(object string, opts *storage.PostPolicyV4Options) (*storage.PostPolicyV4, error)
42+
}
43+
3744
// Server is a gomote server implementation.
3845
type Server struct {
3946
// embed the unimplemented server.
4047
protos.UnimplementedGomoteServiceServer
4148

49+
bucket bucketHandle
4250
buildlets *remote.SessionPool
51+
gceBucketName string
4352
scheduler scheduler
4453
sshCertificateAuthority ssh.Signer
4554
}
4655

4756
// New creates a gomote server. If the rawCAPriKey is invalid, the program will exit.
48-
func New(rsp *remote.SessionPool, sched *schedule.Scheduler, rawCAPriKey []byte) *Server {
57+
func New(rsp *remote.SessionPool, sched *schedule.Scheduler, rawCAPriKey []byte, gomoteGCSBucket string, storageClient *storage.Client) *Server {
4958
signer, err := ssh.ParsePrivateKey(rawCAPriKey)
5059
if err != nil {
5160
log.Fatalf("unable to parse raw certificate authority private key into signer=%s", err)
5261
}
5362
return &Server{
63+
bucket: storageClient.Bucket(gomoteGCSBucket),
5464
buildlets: rsp,
65+
gceBucketName: gomoteGCSBucket,
5566
scheduler: sched,
5667
sshCertificateAuthority: signer,
5768
}
@@ -336,6 +347,39 @@ func (s *Server) SignSSHKey(ctx context.Context, req *protos.SignSSHKeyRequest)
336347
}, nil
337348
}
338349

350+
// UploadFile creates a URL and a set of HTTP post fields which are used to upload a file to a staging GCS bucket. Uploaded files are made available to the
351+
// gomote instances via a subsequent call to one of the WriteFromURL endpoints.
352+
func (s *Server) UploadFile(ctx context.Context, req *protos.UploadFileRequest) (*protos.UploadFileResponse, error) {
353+
_, err := access.IAPFromContext(ctx)
354+
if err != nil {
355+
return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
356+
}
357+
url, fields, err := s.signURLForUpload(uuid.NewString())
358+
if err != nil {
359+
log.Printf("unable to create signed URL: %s", err)
360+
return nil, status.Errorf(codes.Internal, "unable to create signed url")
361+
}
362+
return &protos.UploadFileResponse{
363+
Url: url,
364+
Fields: fields,
365+
}, nil
366+
}
367+
368+
// signURLForUpload generates a signed URL and a set of http Post fields to be used to upload an object to GCS without authenticating.
369+
func (s *Server) signURLForUpload(object string) (url string, fields map[string]string, err error) {
370+
if object == "" {
371+
return "", nil, errors.New("invalid object name")
372+
}
373+
pv4, err := s.bucket.GenerateSignedPostPolicyV4(object, &storage.PostPolicyV4Options{
374+
Expires: time.Now().Add(10 * time.Minute),
375+
Insecure: false,
376+
})
377+
if err != nil {
378+
return "", nil, fmt.Errorf("unable to generate signed url: %w", err)
379+
}
380+
return pv4.URL, pv4.Fields, nil
381+
}
382+
339383
// WriteTGZFromURL will instruct the gomote instance to download the tar.gz from the provided URL. The tar.gz file will be unpacked in the work directory
340384
// relative to the directory provided.
341385
func (s *Server) WriteTGZFromURL(ctx context.Context, req *protos.WriteTGZFromURLRequest) (*protos.WriteTGZFromURLResponse, error) {

internal/gomote/gomote_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ package gomote
99

1010
import (
1111
"context"
12+
"errors"
1213
"fmt"
1314
"io"
1415
"testing"
1516
"time"
1617

18+
"cloud.google.com/go/storage"
1719
"github.com/google/go-cmp/cmp"
1820
"golang.org/x/build/internal/access"
1921
"golang.org/x/build/internal/coordinator/remote"
@@ -27,13 +29,17 @@ import (
2729
"google.golang.org/protobuf/testing/protocmp"
2830
)
2931

32+
const testBucketName = "unit-testing-bucket"
33+
3034
func fakeGomoteServer(t *testing.T, ctx context.Context) protos.GomoteServiceServer {
3135
signer, err := ssh.ParsePrivateKey([]byte(devCertCAPrivate))
3236
if err != nil {
3337
t.Fatalf("unable to parse raw certificate authority private key into signer=%s", err)
3438
}
3539
return &Server{
40+
bucket: &fakeBucketHandler{bucketName: testBucketName},
3641
buildlets: remote.NewSessionPool(ctx),
42+
gceBucketName: testBucketName,
3743
scheduler: schedule.NewFake(),
3844
sshCertificateAuthority: signer,
3945
}
@@ -675,6 +681,48 @@ func TestSignSSHKeyError(t *testing.T) {
675681
}
676682
}
677683

684+
func TestUploadFile(t *testing.T) {
685+
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
686+
client := setupGomoteTest(t, context.Background())
687+
_ = mustCreateInstance(t, client, fakeIAP())
688+
if _, err := client.UploadFile(ctx, &protos.UploadFileRequest{}); err != nil {
689+
t.Fatalf("client.UploadFile(ctx, req) = response, %s; want no error", err)
690+
}
691+
}
692+
693+
func TestUploadFileError(t *testing.T) {
694+
// This test will create a gomote instance and attempt to call UploadFile.
695+
// If overrideID is set to true, the test will use a different gomoteID than the
696+
// the one created for the test.
697+
testCases := []struct {
698+
desc string
699+
ctx context.Context
700+
overrideID bool
701+
filename string
702+
wantCode codes.Code
703+
}{
704+
{
705+
desc: "unauthenticated request",
706+
ctx: context.Background(),
707+
wantCode: codes.Unauthenticated,
708+
},
709+
}
710+
for _, tc := range testCases {
711+
t.Run(tc.desc, func(t *testing.T) {
712+
client := setupGomoteTest(t, context.Background())
713+
_ = mustCreateInstance(t, client, fakeIAP())
714+
req := &protos.UploadFileRequest{}
715+
got, err := client.UploadFile(tc.ctx, req)
716+
if err != nil && status.Code(err) != tc.wantCode {
717+
t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode)
718+
}
719+
if err == nil {
720+
t.Fatalf("client.UploadFile(ctx, %v) = %v, nil; want error", req, got)
721+
}
722+
})
723+
}
724+
}
725+
678726
func TestWriteTGZFromURL(t *testing.T) {
679727
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
680728
client := setupGomoteTest(t, context.Background())
@@ -871,3 +919,17 @@ OfjWFhdu6e4JYiVfN7ZYAAAAE3Rlc3R1c2VyQGdvbGFuZy5vcmcBAg==
871919
// devCertCAPublic is a public SSH CA certificate to be used for development.
872920
devCertCAPublic = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJV3YUncNv+hXneJEO3VEuxxOfjWFhdu6e4JYiVfN7ZY [email protected]`
873921
)
922+
923+
type fakeBucketHandler struct{ bucketName string }
924+
925+
func (fbc *fakeBucketHandler) GenerateSignedPostPolicyV4(object string, opts *storage.PostPolicyV4Options) (*storage.PostPolicyV4, error) {
926+
if object == "" || opts == nil {
927+
return nil, errors.New("invalid arguments")
928+
}
929+
return &storage.PostPolicyV4{
930+
URL: fmt.Sprintf("https://localhost/%s/%s", fbc.bucketName, object),
931+
Fields: map[string]string{
932+
"x-permission-to-post": "granted",
933+
},
934+
}, nil
935+
}

0 commit comments

Comments
 (0)