From ac67fe39ac17a71b7a429e73958d5038ca7574f1 Mon Sep 17 00:00:00 2001 From: Christian Weichel Date: Wed, 14 Jul 2021 14:53:53 +0000 Subject: [PATCH 1/2] [loadgen] Add benchmark for prod startup time --- dev/loadgen/cmd/benchmark.go | 167 +++++++++++++++++++++++++++ dev/loadgen/cmd/run.go | 4 +- dev/loadgen/go.mod | 1 + dev/loadgen/go.sum | 2 + dev/loadgen/pkg/loadgen/executor.go | 14 ++- dev/loadgen/pkg/loadgen/generator.go | 45 ++++++++ dev/loadgen/pkg/loadgen/loadgen.go | 10 +- dev/loadgen/prod-benchmark.yaml | 42 +++++++ 8 files changed, 277 insertions(+), 8 deletions(-) create mode 100644 dev/loadgen/cmd/benchmark.go create mode 100644 dev/loadgen/prod-benchmark.yaml diff --git a/dev/loadgen/cmd/benchmark.go b/dev/loadgen/cmd/benchmark.go new file mode 100644 index 00000000000000..1e2c5b4820ece6 --- /dev/null +++ b/dev/loadgen/cmd/benchmark.go @@ -0,0 +1,167 @@ +// Copyright (c) 2020 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package cmd + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "io/ioutil" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/protobuf/types/known/timestamppb" + "sigs.k8s.io/yaml" + + "github.com/gitpod-io/gitpod/loadgen/pkg/loadgen" + "github.com/gitpod-io/gitpod/loadgen/pkg/observer" + "github.com/gitpod-io/gitpod/ws-manager/api" +) + +var benchmarkOpts struct { + TLSPath string + Host string +} + +// benchmarkCommand represents the run command +var benchmarkCommand = &cobra.Command{ + Use: "benchmark ", + Short: "starts a bunch of workspaces for benchmarking startup time", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + fn := args[0] + fc, err := ioutil.ReadFile(fn) + if err != nil { + log.WithError(err).WithField("fn", fn).Fatal("cannot read scenario file") + } + var scenario BenchmarkScenario + err = yaml.Unmarshal(fc, &scenario) + if err != nil { + log.WithError(err).WithField("fn", fn).Fatal("cannot unmarshal scenario file") + } + + var load loadgen.LoadGenerator + load = loadgen.NewFixedLoadGenerator(500*time.Millisecond, 300*time.Millisecond) + load = loadgen.NewWorkspaceCountLimitingGenerator(load, scenario.Workspaces) + + template := &api.StartWorkspaceRequest{ + Id: "will-be-overriden", + Metadata: &api.WorkspaceMetadata{ + MetaId: "will-be-overriden", + Owner: "00000000-0000-0000-0000-000000000000", + StartedAt: timestamppb.Now(), + }, + ServicePrefix: "will-be-overriden", + Spec: &api.StartWorkspaceSpec{ + IdeImage: scenario.IDEImage, + Admission: api.AdmissionLevel_ADMIT_OWNER_ONLY, + CheckoutLocation: "gitpod", + Git: &api.GitSpec{ + Email: "test@gitpod.io", + Username: "foobar", + }, + FeatureFlags: []api.WorkspaceFeatureFlag{}, + Timeout: "5m", + WorkspaceImage: "will-be-overriden", + WorkspaceLocation: "gitpod", + Envvars: []*api.EnvironmentVariable{ + { + Name: "THEIA_SUPERVISOR_TOKENS", + Value: `[{"token":"foobar","host":"gitpod-staging.com","scope":["function:getWorkspace","function:getLoggedInUser","function:getPortAuthenticationToken","function:getWorkspaceOwner","function:getWorkspaceUsers","function:isWorkspaceOwner","function:controlAdmission","function:setWorkspaceTimeout","function:getWorkspaceTimeout","function:sendHeartBeat","function:getOpenPorts","function:openPort","function:closePort","function:getLayout","function:generateNewGitpodToken","function:takeSnapshot","function:storeLayout","function:stopWorkspace","resource:workspace::fa498dcc-0a84-448f-9666-79f297ad821a::get/update","resource:workspaceInstance::e0a17083-6a78-441a-9b97-ef90d6aff463::get/update/delete","resource:snapshot::*::create/get","resource:gitpodToken::*::create","resource:userStorage::*::create/get/update"],"expiryDate":"2020-12-01T07:55:12.501Z","reuse":2}]`, + }, + }, + }, + Type: api.WorkspaceType_REGULAR, + } + + var opts []grpc.DialOption + if benchmarkOpts.TLSPath != "" { + ca, err := ioutil.ReadFile(filepath.Join(benchmarkOpts.TLSPath, "ca.crt")) + if err != nil { + log.Fatal(err) + } + capool := x509.NewCertPool() + capool.AppendCertsFromPEM(ca) + cert, err := tls.LoadX509KeyPair(filepath.Join(benchmarkOpts.TLSPath, "tls.crt"), filepath.Join(benchmarkOpts.TLSPath, "tls.key")) + if err != nil { + log.Fatal(err) + } + creds := credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: capool, + ServerName: "ws-manager", + }) + opts = append(opts, grpc.WithTransportCredentials(creds)) + } else { + opts = append(opts, grpc.WithInsecure()) + } + + conn, err := grpc.Dial(benchmarkOpts.Host, opts...) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + session := &loadgen.Session{ + Executor: &loadgen.WsmanExecutor{C: api.NewWorkspaceManagerClient(conn)}, + // Executor: loadgen.NewFakeExecutor(), + Load: load, + Specs: &loadgen.MultiWorkspaceGenerator{ + Template: template, + Repos: scenario.Repos, + }, + Worker: 5, + Observer: []chan<- *loadgen.SessionEvent{ + observer.NewLogObserver(true), + observer.NewProgressBarObserver(scenario.Workspaces), + observer.NewStatsObserver(func(s *observer.Stats) { + fc, err := json.Marshal(s) + if err != nil { + return + } + os.WriteFile("stats.json", fc, 0644) + }), + }, + PostLoadWait: func() { + <-make(chan struct{}) + log.Info("load generation complete - press Ctrl+C to finish of") + + }, + } + + go func() { + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, syscall.SIGINT) + <-sigc + os.Exit(0) + }() + + err = session.Run() + if err != nil { + log.WithError(err).Fatal() + } + + }, +} + +func init() { + rootCmd.AddCommand(benchmarkCommand) + + benchmarkCommand.Flags().StringVar(&benchmarkOpts.TLSPath, "tls", "", "path to ws-manager's TLS certificates") + benchmarkCommand.Flags().StringVar(&benchmarkOpts.Host, "host", "localhost:8080", "ws-manager host to talk to") +} + +type BenchmarkScenario struct { + Workspaces int `json:"workspaces"` + IDEImage string `json:"ideImage"` + Repos []loadgen.WorkspaceCfg `json:"repos"` +} diff --git a/dev/loadgen/cmd/run.go b/dev/loadgen/cmd/run.go index 21d681d2cf41f4..823a5abab69920 100644 --- a/dev/loadgen/cmd/run.go +++ b/dev/loadgen/cmd/run.go @@ -36,7 +36,7 @@ var runCmd = &cobra.Command{ Use: "run", Short: "runs the load generator", Run: func(cmd *cobra.Command, args []string) { - const workspaceCount = 500 + const workspaceCount = 5 var load loadgen.LoadGenerator load = loadgen.NewFixedLoadGenerator(500*time.Millisecond, 300*time.Millisecond) @@ -51,7 +51,7 @@ var runCmd = &cobra.Command{ }, ServicePrefix: "will-be-overriden", Spec: &api.StartWorkspaceSpec{ - IdeImage: "eu.gcr.io/gitpod-dev/ide/theia:master.3206", + IdeImage: "eu.gcr.io/gitpod-core-dev/build/ide/code:commit-8c1466008dedabe79d82cbb91931a16f7ce7994c", Admission: api.AdmissionLevel_ADMIT_OWNER_ONLY, CheckoutLocation: "gitpod", Git: &api.GitSpec{ diff --git a/dev/loadgen/go.mod b/dev/loadgen/go.mod index 7d87f6ccf7a2a7..9973bc9670215f 100644 --- a/dev/loadgen/go.mod +++ b/dev/loadgen/go.mod @@ -12,6 +12,7 @@ require ( github.com/spf13/cobra v1.1.3 google.golang.org/grpc v1.38.0 google.golang.org/protobuf v1.26.0 + sigs.k8s.io/yaml v1.2.0 ) replace github.com/gitpod-io/gitpod/common-go => ../../components/common-go // leeway diff --git a/dev/loadgen/go.sum b/dev/loadgen/go.sum index 28150eb5fedb65..86605a2ed17358 100644 --- a/dev/loadgen/go.sum +++ b/dev/loadgen/go.sum @@ -504,6 +504,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -522,4 +523,5 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/dev/loadgen/pkg/loadgen/executor.go b/dev/loadgen/pkg/loadgen/executor.go index 5c228171795d7c..eda64c5de3cb60 100644 --- a/dev/loadgen/pkg/loadgen/executor.go +++ b/dev/loadgen/pkg/loadgen/executor.go @@ -57,6 +57,7 @@ type FakeExecutor struct { // StartWorkspace starts a new workspace func (fe *FakeExecutor) StartWorkspace(spec *StartWorkspaceSpec) (callDuration time.Duration, err error) { + log.WithField("spec", spec).Info("StartWorkspace") go fe.produceUpdates(spec) callDuration = time.Duration(rand.Uint32()%5000) * time.Millisecond return @@ -100,7 +101,8 @@ func (fe *FakeExecutor) StopAll() error { // WsmanExecutor talks to a ws manager type WsmanExecutor struct { - C api.WorkspaceManagerClient + C api.WorkspaceManagerClient + Sub []context.CancelFunc } // StartWorkspace starts a new workspace @@ -122,11 +124,16 @@ func (w *WsmanExecutor) StartWorkspace(spec *StartWorkspaceSpec) (callDuration t // Observe observes all workspaces started by the excecutor func (w *WsmanExecutor) Observe() (<-chan WorkspaceUpdate, error) { res := make(chan WorkspaceUpdate) - sub, err := w.C.Subscribe(context.Background(), &api.SubscribeRequest{}) + + ctx, cancel := context.WithCancel(context.Background()) + w.Sub = append(w.Sub, cancel) + + sub, err := w.C.Subscribe(ctx, &api.SubscribeRequest{}) if err != nil { return nil, err } go func() { + defer close(res) for { resp, err := sub.Recv() if err != nil { @@ -155,6 +162,9 @@ func (w *WsmanExecutor) Observe() (<-chan WorkspaceUpdate, error) { // StopAll stops all workspaces started by the executor func (w *WsmanExecutor) StopAll() error { + for _, s := range w.Sub { + s() + } fmt.Println("kubectl delete pod -l component=workspace") return nil } diff --git a/dev/loadgen/pkg/loadgen/generator.go b/dev/loadgen/pkg/loadgen/generator.go index ce324299b186fe..2a97f1a488d342 100644 --- a/dev/loadgen/pkg/loadgen/generator.go +++ b/dev/loadgen/pkg/loadgen/generator.go @@ -13,6 +13,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/gitpod-io/gitpod/common-go/namegen" + csapi "github.com/gitpod-io/gitpod/content-service/api" "github.com/gitpod-io/gitpod/ws-manager/api" ) @@ -151,3 +152,47 @@ func (f *FixedLoadGenerator) Close() error { close(f.close) return nil } + +type WorkspaceCfg struct { + CloneURL string `json:"cloneURL"` + WorkspaceImage string `json:"workspaceImage"` +} + +type MultiWorkspaceGenerator struct { + Template *api.StartWorkspaceRequest + Repos []WorkspaceCfg +} + +func (f *MultiWorkspaceGenerator) Generate() (*StartWorkspaceSpec, error) { + instanceID, err := uuid.NewRandom() + if err != nil { + return nil, err + } + workspaceID, err := namegen.GenerateWorkspaceID() + if err != nil { + return nil, err + } + + repo := f.Repos[rand.Intn(len(f.Repos))] + + out := proto.Clone(f.Template).(*api.StartWorkspaceRequest) + out.Id = instanceID.String() + out.Metadata.MetaId = workspaceID + out.ServicePrefix = workspaceID + out.Spec.Initializer = &csapi.WorkspaceInitializer{ + Spec: &csapi.WorkspaceInitializer_Git{ + Git: &csapi.GitInitializer{ + CheckoutLocation: "", + CloneTaget: "main", + RemoteUri: repo.CloneURL, + TargetMode: csapi.CloneTargetMode_REMOTE_BRANCH, + Config: &csapi.GitConfig{ + Authentication: csapi.GitAuthMethod_NO_AUTH, + }, + }, + }, + } + out.Spec.WorkspaceImage = repo.WorkspaceImage + r := StartWorkspaceSpec(*out) + return &r, nil +} diff --git a/dev/loadgen/pkg/loadgen/loadgen.go b/dev/loadgen/pkg/loadgen/loadgen.go index eb0d19aa12526d..085b3cdbd2b30a 100644 --- a/dev/loadgen/pkg/loadgen/loadgen.go +++ b/dev/loadgen/pkg/loadgen/loadgen.go @@ -73,11 +73,11 @@ func (s *Session) Run() error { obs, err := s.Executor.Observe() if err != nil { - close(updates) return err } infraWG.Add(1) go func() { + defer close(updates) defer infraWG.Done() <-start @@ -133,11 +133,13 @@ func (s *Session) Run() error { s.PostLoadWait() } updates <- &SessionEvent{Kind: SessionDone} + err = s.Executor.StopAll() + if err != nil { + return err + } infraWG.Wait() - close(updates) - - return s.Executor.StopAll() + return nil } func (s *Session) distributeUpdates(wg *sync.WaitGroup, updates <-chan *SessionEvent) { diff --git a/dev/loadgen/prod-benchmark.yaml b/dev/loadgen/prod-benchmark.yaml new file mode 100644 index 00000000000000..95eaaf153e234b --- /dev/null +++ b/dev/loadgen/prod-benchmark.yaml @@ -0,0 +1,42 @@ +## start with +## loadgen benchmark prod-benchmark.yaml + +workspaces: 500 +ideImage: eu.gcr.io/gitpod-core-dev/build/ide/code:commit-8c1466008dedabe79d82cbb91931a16f7ce7994c +repos: + - cloneURL: https://github.com/gitpod-io/template-typescript-node/ + # image: gitpod/workspace-mongodb + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:53489ee25aa4d1797edd10485d8ecc2fc7a7456ae37718399a54efb498f7236f + - cloneURL: https://github.com/gitpod-io/template-typescript-react + # image: gitpod/workspace-full:latest + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 + - cloneURL: https://github.com/gitpod-io/template-python-django/ + # image: gitpod/workspace-full:latest + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 + - cloneURL: https://github.com/gitpod-io/template-python-flask + # image: gitpod/workspace-full:latest + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 + - cloneURL: https://github.com/gitpod-io/spring-petclinic/ + # image: gitpod/workspace-full:latest + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 +# - cloneURL: https://github.com/gitpod-io/template-php-drupal-ddev + # image: (dockerfile) +# workspaceImage: +# - cloneURL: https://github.com/gitpod-io/template-php-laravel-mysql + # image: (dockerfile) + #workspaceImage: +# - cloneURL: https://github.com/gitpod-io/template-ruby-on-rails-postgres/ + # image: (dockerfile) + #workspaceImage: + - cloneURL: https://github.com/gitpod-io/template-golang-cli/ + # image: gitpod/workspace-full:latest + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 + - cloneURL: https://github.com/gitpod-io/template-rust-cli/ + # image: gitpod/workspace-full:latest + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 + - cloneURL: https://github.com/gitpod-io/template-dotnet-core-cli-csharp/ + # image: gitpod/workspace-dotnet + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:0b62a3c575c8f00b9130f8a6572d9b4fd935e06de4498cef4ec2db8507cd6159 + - cloneURL: https://github.com/gitpod-io/template-sveltejs/ + # image: gitpod/workspace-full:latest + workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 From f3935afdaad9969283d5b2eed16db89cea3a8195 Mon Sep 17 00:00:00 2001 From: Christian Weichel Date: Fri, 16 Jul 2021 06:30:56 +0000 Subject: [PATCH 2/2] [loadgen] Support per-repo clone targets --- dev/loadgen/pkg/loadgen/generator.go | 3 ++- dev/loadgen/prod-benchmark.yaml | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dev/loadgen/pkg/loadgen/generator.go b/dev/loadgen/pkg/loadgen/generator.go index 2a97f1a488d342..eb0f96f5dd1c66 100644 --- a/dev/loadgen/pkg/loadgen/generator.go +++ b/dev/loadgen/pkg/loadgen/generator.go @@ -156,6 +156,7 @@ func (f *FixedLoadGenerator) Close() error { type WorkspaceCfg struct { CloneURL string `json:"cloneURL"` WorkspaceImage string `json:"workspaceImage"` + CloneTarget string `json:"cloneTarget"` } type MultiWorkspaceGenerator struct { @@ -183,7 +184,7 @@ func (f *MultiWorkspaceGenerator) Generate() (*StartWorkspaceSpec, error) { Spec: &csapi.WorkspaceInitializer_Git{ Git: &csapi.GitInitializer{ CheckoutLocation: "", - CloneTaget: "main", + CloneTaget: repo.CloneTarget, RemoteUri: repo.CloneURL, TargetMode: csapi.CloneTargetMode_REMOTE_BRANCH, Config: &csapi.GitConfig{ diff --git a/dev/loadgen/prod-benchmark.yaml b/dev/loadgen/prod-benchmark.yaml index 95eaaf153e234b..e4246115f22256 100644 --- a/dev/loadgen/prod-benchmark.yaml +++ b/dev/loadgen/prod-benchmark.yaml @@ -2,21 +2,26 @@ ## loadgen benchmark prod-benchmark.yaml workspaces: 500 -ideImage: eu.gcr.io/gitpod-core-dev/build/ide/code:commit-8c1466008dedabe79d82cbb91931a16f7ce7994c +ideImage: eu.gcr.io/gitpod-core-dev/build/ide/code:commit-ff263e14024f00d0ed78386b4417dfa6bcd4ae2f repos: - cloneURL: https://github.com/gitpod-io/template-typescript-node/ + cloneTarget: master # image: gitpod/workspace-mongodb workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:53489ee25aa4d1797edd10485d8ecc2fc7a7456ae37718399a54efb498f7236f - cloneURL: https://github.com/gitpod-io/template-typescript-react + cloneTarget: main # image: gitpod/workspace-full:latest workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 - cloneURL: https://github.com/gitpod-io/template-python-django/ + cloneTarget: master # image: gitpod/workspace-full:latest workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 - cloneURL: https://github.com/gitpod-io/template-python-flask + cloneTarget: master # image: gitpod/workspace-full:latest workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 - cloneURL: https://github.com/gitpod-io/spring-petclinic/ + cloneTarget: main # image: gitpod/workspace-full:latest workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 # - cloneURL: https://github.com/gitpod-io/template-php-drupal-ddev @@ -29,14 +34,18 @@ repos: # image: (dockerfile) #workspaceImage: - cloneURL: https://github.com/gitpod-io/template-golang-cli/ + cloneTarget: master # image: gitpod/workspace-full:latest workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 - cloneURL: https://github.com/gitpod-io/template-rust-cli/ + cloneTarget: main # image: gitpod/workspace-full:latest workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84 - cloneURL: https://github.com/gitpod-io/template-dotnet-core-cli-csharp/ + cloneTarget: main # image: gitpod/workspace-dotnet workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:0b62a3c575c8f00b9130f8a6572d9b4fd935e06de4498cef4ec2db8507cd6159 - cloneURL: https://github.com/gitpod-io/template-sveltejs/ + cloneTarget: main # image: gitpod/workspace-full:latest workspaceImage: eu.gcr.io/gitpod-dev/workspace-images:63bf2cbae693a7ecf60a40fb1eadc90b83a99919a2a010019a336a81b0c54b84