├── secrets └── not-empty ├── configuration.txt ├── utils ├── .gitignore ├── helm-2-tiller-sa-crb.yaml └── create-gcp-resources.go ├── requirements.yaml ├── .gitpod.yml ├── values ├── database.yaml ├── https.yaml ├── minio.yaml ├── gcp │ ├── database.yaml │ └── buckets.yaml ├── oauth.yaml ├── workspace-sizing.yaml ├── registry.yaml ├── node-affinity.yaml └── node-layout.yaml ├── Chart.yaml ├── requirements.lock ├── templates ├── image-builder-registry-secret.yaml ├── gcp │ ├── cloudsql-key.yaml │ ├── server-storage-key.yaml │ └── ws-sync-key.yaml └── proxy-certificates-secret.yaml ├── database ├── 01-create-user.sql ├── 02-create-and-init-sessions-db.sql └── 03-recreate-gitpod-db.sql ├── .helmignore ├── README.md ├── values.yaml └── .gitpod.Dockerfile /secrets/not-empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /configuration.txt: -------------------------------------------------------------------------------- 1 | values.yaml 2 | -------------------------------------------------------------------------------- /utils/.gitignore: -------------------------------------------------------------------------------- 1 | cloud_sql_proxy 2 | helm -------------------------------------------------------------------------------- /requirements.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: gitpod 3 | version: 0.4.0 4 | repository: https://charts.gitpod.io/ 5 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | tasks: 4 | - command: | 5 | gp open values.yaml 6 | ./utils/create-gcp-resources.go 7 | -------------------------------------------------------------------------------- /values/database.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | db: 3 | host: db 4 | port: 3306 5 | password: your-password-goes-here 6 | 7 | mysql: 8 | enabled: false -------------------------------------------------------------------------------- /Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: 0.1.5 3 | description: The configuration chart for your Gitpod installation 4 | name: gitpod-selfhosted 5 | version: 0.4.0 6 | -------------------------------------------------------------------------------- /values/https.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | certificatesSecret: 3 | secretName: proxy-config-certificates 4 | path: secrets/https-certificates/* 5 | 6 | gitpod_selfhosted: 7 | variants: 8 | customCerts: true -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: gitpod 3 | repository: https://charts.gitpod.io/ 4 | version: 0.4.0 5 | digest: sha256:5e8265f04d6e8cf721397608ab2dd17adab10608ed01d20f8f02dc255bf33cd5 6 | generated: "2020-05-06T15:14:44.005215887Z" 7 | -------------------------------------------------------------------------------- /values/minio.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | wsSync: 3 | remoteStorage: 4 | kind: minio 5 | minio: 6 | endpoint: minio:9000 7 | accessKey: EXAMPLEvalue 8 | secretKey: Someone.Should/ReallyChangeThisKey!! 9 | tmpdir: /tmp 10 | 11 | minio: 12 | enabled: false -------------------------------------------------------------------------------- /templates/image-builder-registry-secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.gitpod_selfhosted -}} 2 | {{- if .Values.gitpod_selfhosted.variants -}} 3 | {{- if .Values.gitpod_selfhosted.variants.customRegistry -}} 4 | {{ include "gitpod.pull-secret" (dict "root" . "secret" .Values.gitpod.components.imageBuilder.registry) }} 5 | {{- end -}} 6 | {{- end -}} 7 | {{- end -}} -------------------------------------------------------------------------------- /values/gcp/database.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | db: 3 | # 4 | password: YourPasswordGoesHere 5 | 6 | components: 7 | db: 8 | gcloudSqlProxy: 9 | enabled: true 10 | # Set it to the name of your CloudSQL instance. 11 | instance: YourDBConnectionStringGoesHere 12 | credentials: secrets/your-key-file.json 13 | 14 | mysql: 15 | enabled: false -------------------------------------------------------------------------------- /database/01-create-user.sql: -------------------------------------------------------------------------------- 1 | -- must be idempotent 2 | 3 | -- create user (parameterized) 4 | SET @statementStr = CONCAT( 5 | 'CREATE USER IF NOT EXISTS "gitpod"@"%" IDENTIFIED BY "', @gitpodDbPassword, '";' 6 | ); 7 | SELECT @statementStr ; 8 | PREPARE stmt FROM @statementStr; EXECUTE stmt; DEALLOCATE PREPARE stmt; 9 | 10 | -- Grant privileges 11 | GRANT ALL ON `gitpod%`.* TO "gitpod"@"%"; 12 | -------------------------------------------------------------------------------- /values/oauth.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | authProviders: 3 | - id: "Git-Hosting" 4 | host: "github.com" 5 | protocol: "https" 6 | type: "GitHub" # alt. "GitLab" 7 | oauth: 8 | clientId: "your-client-ID-here" 9 | clientSecret: "your-client-secret-here" 10 | callBackUrl: "https://your-domain.com/auth/github/callback" 11 | settingsUrl: "https://github.com/settings/connections/applications/your-client-ID-here" 12 | -------------------------------------------------------------------------------- /utils/helm-2-tiller-sa-crb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: tiller 5 | namespace: kube-system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: tiller 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: cluster-admin 15 | subjects: 16 | - kind: ServiceAccount 17 | name: tiller 18 | namespace: kube-system -------------------------------------------------------------------------------- /database/02-create-and-init-sessions-db.sql: -------------------------------------------------------------------------------- 1 | -- must be idempotent 2 | 3 | CREATE DATABASE IF NOT EXISTS `gitpod-sessions` CHARSET utf8mb4; 4 | 5 | USE `gitpod-sessions`; 6 | 7 | CREATE TABLE IF NOT EXISTS sessions ( 8 | `session_id` varchar(128) COLLATE utf8mb4_bin NOT NULL, 9 | `expires` int(11) unsigned NOT NULL, 10 | `data` text COLLATE utf8mb4_bin, 11 | `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), 12 | PRIMARY KEY (`session_id`) 13 | ); 14 | -------------------------------------------------------------------------------- /.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | utils/helm* 23 | utils/cloud_sql_proxy 24 | 25 | # Files commonly created 26 | cloud_sql_proxy -------------------------------------------------------------------------------- /values/workspace-sizing.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | # workspaceSizing configures the resources available to each workspace. These settings directly impact how 3 | # desenly we pack workspaces on nodes where workspacesPerNode = memoryAvailable(node) / memoryRequest. 4 | # 5 | # Beware: if a workspace exceeds its memoryLimit, some of its processes may be terminated (OOM-kill) which 6 | # results in a broken user experience. 7 | workspaceSizing: 8 | # in MiB 9 | memoryRequest: 2150 10 | # in MiB 11 | memoryLimit: 11444 -------------------------------------------------------------------------------- /templates/gcp/cloudsql-key.yaml: -------------------------------------------------------------------------------- 1 | {{- $comp := .Values.gitpod.components.db -}} 2 | {{- if $comp.gcloudSqlProxy.enabled }} 3 | kind: Secret 4 | apiVersion: v1 5 | metadata: 6 | creationTimestamp: 7 | labels: 8 | app: {{ template "gitpod.fullname" . }} 9 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 10 | release: "{{ .Release.Name }}" 11 | heritage: "{{ .Release.Service }}" 12 | name: gcloud-sql-token 13 | namespace: default 14 | data: 15 | credentials.json: {{ (.Files.Get $comp.gcloudSqlProxy.credentials) | b64enc }} 16 | {{- end }} -------------------------------------------------------------------------------- /templates/gcp/server-storage-key.yaml: -------------------------------------------------------------------------------- 1 | {{- $comp := .Values.gitpod.components.server -}} 2 | {{- if $comp.storage.keyFilePath }} 3 | apiVersion: v1 4 | kind: Secret 5 | metadata: 6 | name: gcp-server-storage-key 7 | labels: 8 | app: {{ template "gitpod.fullname" . }} 9 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 10 | release: "{{ .Release.Name }}" 11 | heritage: "{{ .Release.Service }}" 12 | type: Opaque 13 | data: 14 | "{{ base $comp.storage.keyFilePath }}": {{ $.Files.Get $comp.storage.keyFilePath | b64enc | quote }} 15 | {{- end }} -------------------------------------------------------------------------------- /database/03-recreate-gitpod-db.sql: -------------------------------------------------------------------------------- 1 | -- must be idempotent 2 | 3 | -- @gitpodDB contains name of the DB the script manipulates, 'gitpod' by default. 4 | -- Prepend the script with "SET @gitpodDB = '``'" if needed otherwise 5 | SET @gitpodDB = IFNULL(@gitpodDB, '`gitpod`'); 6 | 7 | SET @statementStr = CONCAT('DROP DATABASE IF EXISTS ', @gitpodDB); 8 | PREPARE statement FROM @statementStr; 9 | EXECUTE statement; 10 | 11 | SET @statementStr = CONCAT('CREATE DATABASE ', @gitpodDB, ' CHARSET utf8mb4'); 12 | PREPARE statement FROM @statementStr; 13 | EXECUTE statement; 14 | -------------------------------------------------------------------------------- /templates/gcp/ws-sync-key.yaml: -------------------------------------------------------------------------------- 1 | {{- $comp := .Values.gitpod.components.wsSync -}} 2 | {{- if (eq $comp.remoteStorage.kind "gcloud") }} 3 | apiVersion: v1 4 | kind: Secret 5 | metadata: 6 | name: gcp-ws-sync-key 7 | labels: 8 | app: {{ template "gitpod.fullname" . }} 9 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 10 | release: "{{ .Release.Name }}" 11 | heritage: "{{ .Release.Service }}" 12 | type: Opaque 13 | data: 14 | "{{ base $comp.remoteStorage.gcloud.credentialsFile }}": {{ $.Files.Get (print "secrets/" (base $comp.remoteStorage.gcloud.credentialsFile)) | b64enc | quote }} 15 | {{- end }} -------------------------------------------------------------------------------- /values/registry.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | components: 3 | imageBuilder: 4 | registryCerts: [] 5 | registry: 6 | # name must not end with a "/" 7 | name: your.registry.com/gitpod 8 | secretName: image-builder-registry-secret 9 | path: secrets/registry-auth.json 10 | 11 | # server: 12 | # defaultBaseImageRegistryWhitelist: 13 | # - some.registry.domain.com 14 | 15 | workspace: 16 | pullSecret: 17 | secretName: image-builder-registry-secret 18 | 19 | docker-registry: 20 | enabled: false 21 | 22 | gitpod_selfhosted: 23 | variants: 24 | customRegistry: true -------------------------------------------------------------------------------- /templates/proxy-certificates-secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.gitpod_selfhosted -}} 2 | {{- if .Values.gitpod_selfhosted.variants -}} 3 | {{- if .Values.gitpod_selfhosted.variants.customCerts -}} 4 | apiVersion: v1 5 | kind: Secret 6 | metadata: 7 | name: proxy-config-certificates 8 | labels: 9 | app: {{ template "gitpod.fullname" $ }} 10 | chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" 11 | release: "{{ $.Release.Name }}" 12 | heritage: "{{ $.Release.Service }}" 13 | annotations: 14 | checksum/checksd-config: {{ ($.Files.Get .Values.gitpod.certificatesSecret.path) | sha256sum }} 15 | data: 16 | {{ ($.Files.Glob .Values.gitpod.certificatesSecret.path).AsSecrets | nindent 2 }} 17 | {{- end -}} 18 | {{- end -}} 19 | {{- end -}} -------------------------------------------------------------------------------- /values/node-affinity.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | affinity: 3 | nodeAffinity: 4 | requiredDuringSchedulingIgnoredDuringExecution: 5 | nodeSelectorTerms: 6 | - matchExpressions: 7 | - key: gitpod.io/workload_meta 8 | operator: In 9 | values: 10 | - "true" 11 | 12 | components: 13 | workspace: 14 | template: 15 | spec: 16 | affinity: 17 | nodeAffinity: 18 | requiredDuringSchedulingIgnoredDuringExecution: 19 | nodeSelectorTerms: 20 | - matchExpressions: 21 | - key: gitpod.io/workload_workspace 22 | operator: In 23 | values: 24 | - "true" -------------------------------------------------------------------------------- /values/node-layout.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | components: 3 | nodeDaemon: 4 | # Gitpod copies Theia to each node. This setting configures where Theia is copied to. 5 | # We'll copy Theia to $theiaHostBasePath/theia/theia-$version 6 | # The faster this location is (in terms of IO) the faster nodes will become available and the faster workspaces will start. 7 | theiaHostBasePath: /mnt/disks/ssd0 8 | imageBuilder: 9 | # The image builder deploys a Docker-in-Docker-daemon. By default that Docker daemon works in an empty-dir on the node. 10 | # Depending on the types of node you operate that may cause image builds to fail or not perform well. We recommend you give the Docker daemon 11 | # fast storage on the node, e.g. an SSD. 12 | hostDindData: /mnt/disks/ssd0/docker 13 | wsSync: 14 | # Workspace data is stored on the nodes. This setting configures where on the ndoe the workspace data lives. 15 | # The faster this location is (in terms of IO) the faster workspaces will initialize. 16 | hostWorkspaceArea: /mnt/disks/ssd0/workspaces 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitpod Self-Hosted 2 | 3 | **DEPRECATED since Gitpod 0.5.0; use https://github.com/gitpod-io/gitpod/tree/master/chart and https://github.com/gitpod-io/gitpod/tree/master/install/helm** 4 | 5 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/gitpod-io/self-hosted) 6 | 7 | This repository contains the configuration templates and installation scripts to deploy Gitpod on your own infrastructure. Learn more about [Gitpod Self-Hosted](https://www.gitpod.io/docs/self-hosted/latest/self-hosted/). 8 | 9 | ## Install on GCP 10 | 11 | If you want to deploy Gitpod Self-Hosted on Google Cloud Platform, you can start the installation process in one click: 12 | 13 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/gitpod-io/self-hosted) 14 | 15 | Alternatively, you can run this process locally like so: 16 | 1. Install [Google Cloud SDK](https://cloud.google.com/sdk/install) 17 | 2. Install [Go](https://golang.org/doc/install) 18 | 3. Clone this repository 19 | 4. Run `./utils/create-gcp-resources.go` 20 | 21 | ## Install on AWS 22 | 23 | Coming soon. 24 | 25 | ## Install on Azure 26 | 27 | Coming soon. 28 | 29 | ## Install on OpenShift 30 | 31 | Coming soon. 32 | 33 | ## Install on any Kubernetes system 34 | 35 | Please see the [installation instructions for vanilla Kubernetes](https://www.gitpod.io/docs/self-hosted/latest/install/install-on-kubernetes/). 36 | -------------------------------------------------------------------------------- /values/gcp/buckets.yaml: -------------------------------------------------------------------------------- 1 | # Use GCP Buckets for workspace backups. 2 | # There is only one value you need to change in this file: projectId 3 | 4 | gitpod: 5 | components: 6 | wsSync: 7 | volumes: 8 | - name: gcloud-creds 9 | secret: 10 | secretName: gcp-ws-sync-key 11 | - name: gcloud-tmp 12 | hostPath: 13 | path: /mnt/disks/ssd0/sync-tmp 14 | type: DirectoryOrCreate 15 | volumeMounts: 16 | - mountPath: /credentials 17 | name: gcloud-creds 18 | - mountPath: /mnt/sync-tmp 19 | name: gcloud-tmp 20 | remoteStorage: 21 | kind: gcloud 22 | gcloud: 23 | # You need to set your GCP project ID here. 24 | # Beware: the name of your project is not the same as its ID. You can find the project ID under the "Home" page of your GCP project. 25 | projectId: some-gcp-project-id 26 | # The GCP region you want the workspace content to be stored in. This should ideally be in the same region as your cluster. 27 | region: some-gcp-region 28 | # You shouldn't have to change the values below if you're using the templates that ship with this chart. 29 | credentialsFile: /credentials/gitpod-workspace-syncer-key.json 30 | tmpdir: /mnt/sync-tmp 31 | parallelUpload: 6 32 | server: 33 | storage: 34 | secretName: gcp-server-storage-key 35 | keyFilePath: secrets/gitpod-workspace-syncer-key.json 36 | 37 | minio: 38 | enabled: false -------------------------------------------------------------------------------- /values.yaml: -------------------------------------------------------------------------------- 1 | gitpod: 2 | # This field must be set to your domain name. Leaving it set to its default value will result in 3 | # a non-functional installation. 4 | hostname: your-domain.com 5 | 6 | # If you have a static IP that your domain resolves to, set it here. 7 | # Leaving this field set to its default value is fine. Kubernetes will assign you an IP address 8 | # during deployment. 9 | components: 10 | proxy: 11 | loadBalancerIP: null 12 | 13 | # Gitpod needs at least one auth provider to allow users to log in. 14 | # The auth providers below are examples only. Please change/remove them to fit your installation. 15 | authProviders: 16 | - id: "Example Github" 17 | host: "github.com" 18 | protocol: "https" 19 | type: "GitHub" 20 | oauth: 21 | clientId: "your-client-ID-here" 22 | clientSecret: "your-client-secret-here" 23 | callBackUrl: "https://your-domain.com/auth/github/callback" 24 | settingsUrl: "https://github.com/settings/connections/applications/your-client-ID-here" 25 | - id: "Example Gitlab" 26 | host: "gitlab.com" 27 | protocol: "https" 28 | type: "GitLab" 29 | oauth: 30 | clientId: "your-application-ID-here" 31 | clientSecret: "your-secret-here" 32 | callBackUrl: "https://your-domain.com/auth/gitlab/callback" 33 | settingsUrl: "gitlab.com/profile/applications" 34 | 35 | # RBAC is enabled by default. If your cluster does not use RBAC, set this flag to false so that 36 | # we do not attempt to install PodSecurityPolicies and the likes. 37 | installPodSecurityPolicies: true 38 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | USER root 4 | 5 | ### cloud_sql_proxy ### 6 | ARG CLOUD_SQL_PROXY=/usr/local/bin/cloud_sql_proxy 7 | RUN curl -fsSL https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 > $CLOUD_SQL_PROXY \ 8 | && chmod +x $CLOUD_SQL_PROXY 9 | 10 | ### Docker client ### 11 | RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \ 12 | # 'cosmic' not supported 13 | && add-apt-repository -yu "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable" \ 14 | && apt-get install -yq docker-ce-cli=5:18.09.0~3-0~ubuntu-bionic \ 15 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* 16 | 17 | ### Helm ### 18 | RUN curl -fsSL https://get.helm.sh/helm-v3.0.1-linux-amd64.tar.gz \ 19 | | tar -xzvC /usr/local/bin --strip-components=1 \ 20 | && helm completion bash > /usr/share/bash-completion/completions/helm 21 | 22 | ### kubernetes ### 23 | RUN mkdir -p /usr/local/kubernetes/ && \ 24 | curl -fsSL https://github.com/kubernetes/kubernetes/releases/download/v1.16.2/kubernetes.tar.gz \ 25 | | tar -xzvC /usr/local/kubernetes/ --strip-components=1 && \ 26 | KUBERNETES_SKIP_CONFIRM=true /usr/local/kubernetes/cluster/get-kube-binaries.sh && \ 27 | chown gitpod:gitpod -R /usr/local/kubernetes 28 | ENV PATH=$PATH:/usr/local/kubernetes/cluster/:/usr/local/kubernetes/client/bin/ 29 | 30 | RUN curl -o /usr/bin/kubectx https://raw.githubusercontent.com/ahmetb/kubectx/master/kubectx && chmod +x /usr/bin/kubectx \ 31 | && curl -o /usr/bin/kubens https://raw.githubusercontent.com/ahmetb/kubectx/master/kubens && chmod +x /usr/bin/kubens 32 | 33 | ### MySQL client ### 34 | RUN apt-get update && apt-get install -yq \ 35 | mysql-client \ 36 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* 37 | 38 | # yq - jq for YAML files 39 | RUN cd /usr/bin && curl -L https://github.com/mikefarah/yq/releases/download/2.4.0/yq_linux_amd64 > yq && chmod +x yq 40 | 41 | ### Certbot 42 | RUN apt-get update \ 43 | && apt-get install -yq certbot python3-certbot-dns-google \ 44 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* 45 | 46 | USER gitpod 47 | 48 | ### Google Cloud ### 49 | # not installed via repository as then 'docker-credential-gcr' is not available 50 | ARG GCS_DIR=/opt/google-cloud-sdk 51 | ENV PATH=$GCS_DIR/bin:$PATH 52 | RUN sudo chown gitpod: /opt \ 53 | && mkdir $GCS_DIR \ 54 | && curl -fsSL https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-245.0.0-linux-x86_64.tar.gz \ 55 | | tar -xzvC /opt \ 56 | && /opt/google-cloud-sdk/install.sh --quiet --usage-reporting=false --bash-completion=true \ 57 | --additional-components docker-credential-gcr alpha beta \ 58 | # needed for access to our private registries 59 | && docker-credential-gcr configure-docker 60 | -------------------------------------------------------------------------------- /utils/create-gcp-resources.go: -------------------------------------------------------------------------------- 1 | //usr/bin/env go run "$0" "$@"; exit "$?" 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/base64" 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "io/ioutil" 12 | "math/rand" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "runtime" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | var ( 22 | cwd string 23 | projectID string 24 | region string 25 | zone string 26 | ipAddress string 27 | dbName string 28 | dbRootPassword string 29 | dbGitpodPassword string 30 | ) 31 | 32 | func init() { 33 | var err error 34 | cwd, err = os.Getwd() 35 | failOnError(err) 36 | cwd, err = filepath.Abs(cwd) 37 | failOnError(err) 38 | 39 | flag.StringVar(&cwd, "cwd", cwd, "working directory") 40 | } 41 | 42 | func main() { 43 | flag.Parse() 44 | defer fmt.Println() 45 | 46 | fmt.Println("This is the GCP project setup script for Gitpod.") 47 | fmt.Println("It is re-entrant, meaning that if it fails at any point you should be able to run it again without the script failing before that point.") 48 | 49 | printStep("check environment") 50 | failOnError(checkEnvironment()) 51 | 52 | printStep("create VPC network") 53 | failOnError(createVPCNetwork()) 54 | 55 | printStep("create service accounts") 56 | failOnError(createServiceAccounts()) 57 | 58 | printStep("create cluster (this takes a while)") 59 | failOnError(createCluster()) 60 | 61 | printStep("set up bucket storage") 62 | failOnError(setupBucketStorage()) 63 | 64 | printStep("set up container registry") 65 | failOnError(setupContainerRegistry()) 66 | 67 | printStep("create database (this takes a while)") 68 | failOnError(createDatabase()) 69 | 70 | printStep("initialize database") 71 | failOnError(initializeDatabase()) 72 | 73 | printStep("setup helm") 74 | failOnError(setupHelm()) 75 | 76 | printStep("create static IP address") 77 | failOnError(createIPAddress()) 78 | 79 | printNextSteps() 80 | } 81 | 82 | func checkEnvironment() error { 83 | // make sure required tools are installed 84 | requiredTools := map[string]string{ 85 | "gcloud": "Google Cloud SDK is not installed - head over to https://cloud.google.com/sdk/install and install it", 86 | "mysql": "MySQL client is not installed - make sure `mysql` is available in the PATH", 87 | "wget": "wget is required but not available", 88 | } 89 | for cmd, errmsg := range requiredTools { 90 | if _, err := exec.LookPath(cmd); err != nil { 91 | return fmt.Errorf(errmsg) 92 | } 93 | } 94 | 95 | // make sure we're logged in 96 | out, _ := run("gcloud", "auth", "list") 97 | if strings.Contains(out, "No credentialed accounts") { 98 | runLoud("gcloud", "auth", "login") 99 | } 100 | 101 | // install the gcloud beta components required for this setup 102 | failOnError(runLoud("gcloud", "components", "install", "beta")) 103 | 104 | // ensure gcloud is configured properly and extract that config 105 | configSettings := []struct { 106 | V *string 107 | GCPName string 108 | Name string 109 | Link string 110 | }{ 111 | {&projectID, "core/project", "project", ""}, 112 | {®ion, "compute/region", "compute region", "https://cloud.google.com/compute/docs/regions-zones/"}, 113 | {&zone, "compute/zone", "compute zone", "https://cloud.google.com/compute/docs/regions-zones/"}, 114 | } 115 | for _, v := range configSettings { 116 | out, err := run("gcloud", "config", "get-value", v.GCPName) 117 | if err != nil { 118 | return fmt.Errorf(errPrjNotConfigured) 119 | } 120 | 121 | val := strings.TrimSpace(string(out)) 122 | if strings.Contains(val, "(unset)") { 123 | var desc string 124 | if v.Link != "" { 125 | desc = " (see " + v.Link + ")" 126 | } 127 | fmt.Printf("\n \033[36mNo %s configured. \033[mPlease enter the %s%s:\n > ", v.GCPName, v.Name, desc) 128 | fmt.Scanln(&val) 129 | 130 | val = strings.TrimSpace(val) 131 | if val == "" { 132 | return fmt.Errorf(errPrjNotConfigured) 133 | } 134 | 135 | out, err := run("gcloud", "config", "set", v.GCPName, val) 136 | if err != nil { 137 | return fmt.Errorf(out) 138 | } 139 | } 140 | 141 | *v.V = val 142 | fmt.Printf(" %s: %s\n", v.GCPName, val) 143 | } 144 | 145 | var choice string 146 | fmt.Print("\n\033[32mBeware: \033[mthis script is about to create resources in your GCP project that will cost you money.\nDo you want to continue? [Y/n] ") 147 | fmt.Scanln(&choice) 148 | if !(choice == "" || choice == "y" || choice == "Y") { 149 | return fmt.Errorf("aborting") 150 | } 151 | 152 | out, err := run("gcloud", "projects", "describe", projectID) 153 | if err != nil { 154 | fmt.Print("\n\033[33mProject could not be accessed. \033[mCould not access project. It may not exist (or you do not have the permissions). Do you want to try to create the project? [Y/n] ") 155 | fmt.Scanln(&choice) 156 | if !(choice == "" || choice == "y" || choice == "Y") { 157 | return fmt.Errorf(string(out)) 158 | } 159 | failOnError(runLoud("gcloud", "projects", "create", projectID)) 160 | } 161 | 162 | requiredServices := []string{ 163 | "compute.googleapis.com", 164 | "iam.googleapis.com", 165 | "container.googleapis.com", 166 | "sqladmin.googleapis.com", 167 | } 168 | for _, s := range requiredServices { 169 | out, err := run("gcloud", "services", "enable", s) 170 | if err != nil && strings.Contains(string(out), "Billing") { 171 | return fmt.Errorf("billing must be enabled for this project\n head over to https://console.cloud.google.com/billing/linkedaccount?project=" + projectID + "&folder&organizationId to set it up") 172 | } 173 | if err != nil { 174 | return fmt.Errorf(string(out)) 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func createIPAddress() error { 182 | ipAddrName := "gitpod-inbound-ip" 183 | 184 | // create IP 185 | _, err := run("gcloud", "compute", "addresses", "create", ipAddrName, "--region="+region) 186 | if err != nil && !isAlreadyExistsErr(err) { 187 | return err 188 | } 189 | 190 | // find out which IP we've just created 191 | out, err := run("gcloud", "compute", "addresses", "describe", ipAddrName, "--region="+region) 192 | if err != nil { 193 | return err 194 | } 195 | for _, line := range strings.Split(string(out), "\n") { 196 | p := "address: " 197 | if !strings.HasPrefix(line, p) { 198 | continue 199 | } 200 | ipAddress = strings.TrimSpace(strings.TrimPrefix(line, p)) 201 | } 202 | if ipAddress == "" { 203 | return fmt.Errorf("unable to get IP address we just created") 204 | } 205 | 206 | // try and find configured hostname - if that fails default to your-domain.com 207 | var domain string 208 | fc, err := ioutil.ReadFile(filepath.Join(cwd, "values.yaml")) 209 | if err != nil { 210 | return fmt.Errorf("cannot set load balancer IP: %v", err) 211 | } 212 | lines := strings.Split(string(fc), "\n") 213 | for i, l := range lines { 214 | if strings.Contains(l, "hostname: ") { 215 | segs := strings.Split(l, "hostname: ") 216 | segs = strings.Fields(segs[1]) 217 | domain = segs[0] 218 | continue 219 | } 220 | if strings.Contains(l, "loadBalancerIP:") { 221 | segs := strings.Split(l, ":") 222 | lines[i] = segs[0] + ": " + ipAddress 223 | } 224 | } 225 | err = ioutil.WriteFile(filepath.Join(cwd, "values.yaml"), []byte(strings.Join(lines, "\n")), 0644) 226 | if err != nil { 227 | return fmt.Errorf("cannot write values.yaml: %v", err) 228 | } 229 | 230 | fmt.Printf(" IP address: %s\n", ipAddress) 231 | fmt.Println("\n\033[36mManual step: \033[m" + `Please set up the following DNS entries for your domain 232 | ` + domain + ` ` + ipAddress + ` 233 | *.` + domain + ` ` + ipAddress + ` 234 | *.ws.` + domain + ` ` + ipAddress + ` 235 | 236 | You don't have to do this right away; this installation script does not depend on it. 237 | Your installation will not be complete before you have set up those DNS entries, however. 238 | 239 | Press [RETURN] to continue. 240 | `) 241 | var ignoredInput string 242 | fmt.Scanln(&ignoredInput) 243 | 244 | return nil 245 | } 246 | 247 | func createVPCNetwork() error { 248 | out, err := run("gcloud", "compute", "networks", "create", "gitpod-vpc", "--bgp-routing-mode=regional", "--subnet-mode=auto") 249 | if err != nil && !strings.Contains(string(out), "already exists") { 250 | return err 251 | } 252 | return nil 253 | } 254 | 255 | func createServiceAccounts() error { 256 | serviceAccounts := []string{ 257 | "gitpod-nodes-meta", 258 | "gitpod-nodes-workspace", 259 | } 260 | roles := []string{ 261 | "roles/clouddebugger.agent", 262 | "roles/cloudtrace.agent", 263 | "roles/errorreporting.writer", 264 | "roles/logging.viewer", 265 | "roles/logging.logWriter", 266 | "roles/monitoring.metricWriter", 267 | "roles/monitoring.viewer", 268 | } 269 | 270 | for _, sa := range serviceAccounts { 271 | // create service account - don't fail if it exists already 272 | _, err := run("gcloud", "iam", "service-accounts", "create", sa) 273 | if err != nil && !isAlreadyExistsErr(err) { 274 | return err 275 | } 276 | 277 | // assign all roles to service account 278 | for _, role := range roles { 279 | out, err := run("gcloud", "projects", "add-iam-policy-binding", projectID, "--member=serviceAccount:gitpod-nodes-meta@"+projectID+".iam.gserviceaccount.com", "--role="+role) 280 | if err != nil { 281 | return fmt.Errorf("%v\n%s", err, string(out)) 282 | } 283 | } 284 | } 285 | 286 | return nil 287 | } 288 | 289 | func findAvailableGKEVersion() (version string, err error) { 290 | out, err := run("gcloud", "container", "get-server-config", "--zone="+zone, "--format=json") 291 | if err != nil { 292 | return 293 | } 294 | // trim first line which reads something like "Fetching server config for ...", hence is not valid JSON 295 | out = strings.Join(strings.Split(out, "\n")[1:], "\n") 296 | 297 | var info struct { 298 | DefaultClusterVersion string `json:"defaultClusterVersion"` 299 | ValidImageTypes []string `json:"validImageTypes"` 300 | } 301 | err = json.Unmarshal([]byte(out), &info) 302 | if err != nil { 303 | return 304 | } 305 | 306 | // while we're at it we'll make sure we have cos_containerd available 307 | var found bool 308 | for _, tpe := range info.ValidImageTypes { 309 | if tpe == "COS_CONTAINERD" { 310 | found = true 311 | break 312 | } 313 | } 314 | if !found { 315 | return "", fmt.Errorf("zone does not support cos_containerd GKE nodes: please use a different zone") 316 | } 317 | 318 | return info.DefaultClusterVersion, nil 319 | } 320 | 321 | func createCluster() error { 322 | version, err := findAvailableGKEVersion() 323 | if err != nil { 324 | return err 325 | } 326 | 327 | metapoolArgs := map[string]string{ 328 | "region": region, 329 | "node-locations": zone, 330 | "cluster-version": version, 331 | "addons=NetworkPolicy": "", 332 | "no-enable-basic-auth": "", 333 | "no-issue-client-certificate": "", 334 | "enable-ip-alias": "", 335 | "cluster-ipv4-cidr": "10.8.0.0/14", 336 | "services-ipv4-cidr": "10.0.0.0/20", 337 | "network=gitpod-vpc": "", 338 | "enable-network-policy": "", 339 | "enable-pod-security-policy": "", 340 | "metadata": "disable-legacy-endpoints=true", 341 | "num-nodes": "1", 342 | "enable-autoscaling": "", 343 | "min-nodes": "1", 344 | "max-nodes": "3", 345 | "service-account": "gitpod-nodes-meta@" + projectID + ".iam.gserviceaccount.com", 346 | "node-labels": "gitpod.io/workload_meta=true", 347 | "machine-type": "n1-standard-4", 348 | "image-type": "cos", 349 | "disk-size": "100", 350 | "disk-type": "pd-ssd", 351 | "enable-autorepair": "", 352 | "local-ssd-count": "0", 353 | "workload-metadata-from-node": "SECURE", 354 | "no-enable-autoupgrade": "", 355 | } 356 | _, err = run("gcloud", buildArgs([]string{"beta", "container", "clusters", "create", "gitpod-cluster"}, metapoolArgs)...) 357 | if err != nil && !isAlreadyExistsErr(err) { 358 | return err 359 | } 360 | 361 | wspoolArgs := map[string]string{ 362 | "region": region, 363 | "cluster": "gitpod-cluster", 364 | "metadata": "disable-legacy-endpoints=true", 365 | "num-nodes": "0", 366 | "enable-autoscaling": "", 367 | "min-nodes": "0", 368 | "max-nodes": "10", 369 | "service-account": "gitpod-nodes-workspace@" + projectID + ".iam.gserviceaccount.com", 370 | "node-labels": "gitpod.io/workload_workspace=true", 371 | "machine-type": "n1-standard-16", 372 | "image-type": "cos_containerd", 373 | "disk-size": "200", 374 | "disk-type": "pd-ssd", 375 | "enable-autorepair": "", 376 | "local-ssd-count": "1", 377 | "no-enable-autoupgrade": "", 378 | } 379 | _, err = run("gcloud", buildArgs([]string{"beta", "container", "node-pools", "create", "workspace-pool-1"}, wspoolArgs)...) 380 | if err != nil && !isAlreadyExistsErr(err) { 381 | return err 382 | } 383 | 384 | return nil 385 | } 386 | 387 | func setupBucketStorage() error { 388 | _, err := run("gcloud", "iam", "service-accounts", "create", "gitpod-workspace-syncer") 389 | if err != nil && !isAlreadyExistsErr(err) { 390 | return err 391 | } 392 | _, err = run("gcloud", "projects", "add-iam-policy-binding", projectID, "--member=serviceAccount:gitpod-workspace-syncer@"+projectID+".iam.gserviceaccount.com", "--role=roles/storage.admin") 393 | if err != nil { 394 | return err 395 | } 396 | _, err = run("gcloud", "projects", "add-iam-policy-binding", projectID, "--member=serviceAccount:gitpod-workspace-syncer@"+projectID+".iam.gserviceaccount.com", "--role=roles/storage.objectAdmin") 397 | if err != nil { 398 | return err 399 | } 400 | _, err = run("gcloud", "projects", "add-iam-policy-binding", projectID, "--member=serviceAccount:gitpod-workspace-syncer@"+projectID+".iam.gserviceaccount.com", "--role=roles/storage.objectViewer") 401 | if err != nil { 402 | return err 403 | } 404 | _, err = run("gcloud", "iam", "service-accounts", "keys", "create", "secrets/gitpod-workspace-syncer-key.json", "--iam-account=gitpod-workspace-syncer@"+projectID+".iam.gserviceaccount.com") 405 | if err != nil { 406 | return err 407 | } 408 | 409 | // write bucket config yaml file 410 | bucketsYamlFN := filepath.Join(cwd, "values", "gcp", "buckets.yaml") 411 | fc, err := ioutil.ReadFile(bucketsYamlFN) 412 | if err != nil { 413 | return err 414 | } 415 | fc = bytes.ReplaceAll(fc, []byte("some-gcp-project-id"), []byte(projectID)) 416 | fc = bytes.ReplaceAll(fc, []byte("some-gcp-region"), []byte(region)) 417 | err = ioutil.WriteFile(bucketsYamlFN, fc, 0644) 418 | if err != nil { 419 | return err 420 | } 421 | 422 | return nil 423 | } 424 | 425 | func setupContainerRegistry() error { 426 | _, err := run("gcloud", "iam", "service-accounts", "create", "gitpod-registry-full") 427 | if err != nil && !isAlreadyExistsErr(err) { 428 | return err 429 | } 430 | 431 | _, err = run("gcloud", "projects", "add-iam-policy-binding", projectID, "--member=serviceAccount:gitpod-registry-full@"+projectID+".iam.gserviceaccount.com", "--role=roles/storage.admin") 432 | if err != nil { 433 | return err 434 | } 435 | _, err = run("gcloud", "iam", "service-accounts", "keys", "create", "secrets/gitpod-registry-full-key.json", "--iam-account=gitpod-registry-full@"+projectID+".iam.gserviceaccount.com") 436 | if err != nil { 437 | return err 438 | } 439 | 440 | sakey, err := ioutil.ReadFile(filepath.Join(cwd, "secrets", "gitpod-registry-full-key.json")) 441 | if err != nil { 442 | return err 443 | } 444 | 445 | auth := base64.StdEncoding.EncodeToString(append([]byte("_json_key:"), sakey...)) 446 | ioutil.WriteFile(filepath.Join(cwd, "secrets", "registry-auth.json"), []byte(` 447 | { 448 | "auths": { 449 | "gcr.io": { 450 | "auth": "`+auth+`" 451 | } 452 | } 453 | } 454 | `), 0644) 455 | 456 | err = ioutil.WriteFile(filepath.Join(cwd, "values", "registry.yaml"), []byte(` 457 | gitpod: 458 | components: 459 | imageBuilder: 460 | registryCerts: [] 461 | registry: 462 | # name must not end with a "/" 463 | name: gcr.io/`+projectID+` 464 | secretName: image-builder-registry-secret 465 | path: secrets/registry-auth.json 466 | 467 | workspace: 468 | pullSecret: 469 | secretName: image-builder-registry-secret 470 | 471 | docker-registry: 472 | enabled: false 473 | 474 | gitpod_selfhosted: 475 | variants: 476 | customRegistry: true 477 | `), 0644) 478 | if err != nil { 479 | return err 480 | } 481 | 482 | return nil 483 | } 484 | 485 | func createDatabase() error { 486 | dbName = "gitpod-db" 487 | 488 | dbRootPassword = generateRandomPassword() 489 | fmt.Printf(" root database user password: %s\n", dbRootPassword) 490 | 491 | args := map[string]string{ 492 | "database-version": "MYSQL_5_7", 493 | "storage-size": "100", 494 | "storage-auto-increase": "", 495 | "tier": "db-n1-standard-4", 496 | "region": region, 497 | "backup-start-time": "04:00", 498 | "failover-replica-name": dbName + "-failover", 499 | "replica-type": "FAILOVER", 500 | "enable-bin-log": "", 501 | } 502 | _, err := run("gcloud", buildArgs([]string{"sql", "instances", "create", dbName}, args)...) 503 | if err != nil && !isAlreadyExistsErr(err) { 504 | return err 505 | } 506 | 507 | var pwdSet bool 508 | for i := 0; i < 5; i++ { 509 | out, err := run("gcloud", "sql", "users", "set-password", "root", "--host", "%", "--instance", dbName, "--password", dbRootPassword) 510 | if err != nil && strings.Contains(string(out), "HTTPError 409") { 511 | var waittime = (i + 1) * 15 512 | fmt.Printf(" unable to set password - retrying in %d seconds\n \033[2m%s\033[m\n", waittime, string(out)) 513 | time.Sleep(time.Duration(waittime) * time.Second) 514 | continue 515 | } 516 | if err != nil { 517 | return err 518 | } 519 | 520 | pwdSet = true 521 | break 522 | } 523 | if !pwdSet { 524 | return fmt.Errorf("unable to database password") 525 | } 526 | 527 | _, err = run("gcloud", "iam", "service-accounts", "create", "gitpod-cloudsql-client") 528 | if err != nil && !isAlreadyExistsErr(err) { 529 | return err 530 | } 531 | _, err = run("gcloud", "projects", "add-iam-policy-binding", projectID, "--member=serviceAccount:gitpod-cloudsql-client@"+projectID+".iam.gserviceaccount.com", "--role=roles/cloudsql.client") 532 | if err != nil { 533 | return err 534 | } 535 | _, err = run("gcloud", "iam", "service-accounts", "keys", "create", "secrets/gitpod-cloudsql-client-key.json", "--iam-account=gitpod-cloudsql-client@"+projectID+".iam.gserviceaccount.com") 536 | if err != nil { 537 | return err 538 | } 539 | 540 | return nil 541 | } 542 | 543 | func initializeDatabase() error { 544 | // make sure the cloudSQLProxy is available 545 | cloudSQLProxy := filepath.Join(cwd, "utils", "cloud_sql_proxy") 546 | if _, err := os.Stat(cloudSQLProxy); os.IsNotExist(err) { 547 | failOnError(runLoud("wget", "https://dl.google.com/cloudsql/cloud_sql_proxy."+runtime.GOOS+"."+runtime.GOARCH, "-O", cloudSQLProxy)) 548 | failOnError(runLoud("chmod", "+x", cloudSQLProxy)) 549 | } 550 | 551 | dbGitpodPassword = generateRandomPassword() 552 | fmt.Printf(" gitpod database user password: %s\n", dbGitpodPassword) 553 | 554 | // create DB init script 555 | initScript := []byte(` 556 | set @gitpodDbPassword = "` + dbGitpodPassword + `"; 557 | 558 | source database/01-create-user.sql 559 | source database/02-create-and-init-sessions-db.sql 560 | source database/03-recreate-gitpod-db.sql 561 | 562 | ALTER USER "gitpod"@"%" IDENTIFIED BY '` + dbGitpodPassword + `'; 563 | FLUSH PRIVILEGES; 564 | `) 565 | 566 | // start cloudSqlProxy 567 | cloudSQLProxyCmd := runC(cloudSQLProxy, "-instances="+projectID+":"+region+":"+dbName+"=tcp:0.0.0.0:3306", "-credential_file=secrets/gitpod-cloudsql-client-key.json") 568 | cloudSQLProxyCmd.Stderr = os.Stderr 569 | var sqlProxyMayFail bool 570 | go func() { 571 | err := cloudSQLProxyCmd.Start() 572 | if sqlProxyMayFail { 573 | return 574 | } 575 | failOnError(err) 576 | }() 577 | defer func() { 578 | sqlProxyMayFail = true 579 | cloudSQLProxyCmd.Process.Kill() 580 | }() 581 | 582 | // wait for some time to give the cloud_sql_proxy some time to start up 583 | time.Sleep(5 * time.Second) 584 | 585 | // run mysql to initialize the database 586 | mysqlCmd := runC("mysql", "-u", "root", "-P", "3306", "-h", "127.0.0.1", "-p"+dbRootPassword) 587 | mysqlCmd.Stdin = bytes.NewReader(initScript) 588 | out, err := mysqlCmd.CombinedOutput() 589 | if err != nil { 590 | if len(out) > 0 { 591 | return fmt.Errorf(string(out)) 592 | } 593 | 594 | return err 595 | } 596 | 597 | // write datbase yaml file 598 | err = ioutil.WriteFile(filepath.Join(cwd, "values", "gcp", "database.yaml"), []byte(` 599 | gitpod: 600 | db: 601 | password: "`+dbGitpodPassword+`" 602 | 603 | components: 604 | db: 605 | gcloudSqlProxy: 606 | enabled: true 607 | instance: `+projectID+":"+region+":"+dbName+` 608 | credentials: secrets/gitpod-cloudsql-client-key.json 609 | 610 | mysql: 611 | enabled: false 612 | `), 0644) 613 | if err != nil { 614 | return err 615 | } 616 | 617 | return nil 618 | } 619 | 620 | func setupHelm() error { 621 | valueFiles := []string{ 622 | "values.yaml", 623 | "values/gcp/database.yaml", 624 | "values/gcp/buckets.yaml", 625 | "values/registry.yaml", 626 | "values/node-affinity.yaml", 627 | "values/node-layout.yaml", 628 | "values/workspace-sizing.yaml", 629 | } 630 | err := ioutil.WriteFile(filepath.Join(cwd, "configuration.txt"), []byte(strings.Join(valueFiles, "\n")+"\n"), 0644) 631 | if err != nil { 632 | return err 633 | } 634 | 635 | _, err = run("gcloud", "container", "clusters", "get-credentials", "--region", region, "gitpod-cluster") 636 | if err != nil { 637 | return err 638 | } 639 | helmfn := filepath.Join(cwd, "utils", "helm") 640 | if _, err := os.Stat(helmfn); os.IsNotExist(err) { 641 | tgtdir := filepath.Join(cwd, runtime.GOOS+"-"+runtime.GOARCH) 642 | 643 | failOnError(runLoud("sh", "-c", "wget -O- https://get.helm.sh/helm-v3.0.1-"+runtime.GOOS+"-"+runtime.GOARCH+".tar.gz | tar xz")) 644 | failOnError(runLoud("mv", filepath.Join(tgtdir, "helm"), helmfn)) 645 | err = os.RemoveAll(tgtdir) 646 | if err != nil { 647 | return err 648 | } 649 | } 650 | return nil 651 | } 652 | 653 | func printNextSteps() { 654 | fmt.Println("\n\n\033[32mCongratulations.\033[m" + ` 655 | 656 | Your GCP project and this Helm chart are (almost) ready for installation. 657 | The steps left to do are: 658 | - [optional] set up HTTPs certificates (see https://www.gitpod.io/docs/self-hosted/latest/install/https-certs/) 659 | - [required] set your domain (see values.yaml) 660 | - [required] set up OAuth (see https://www.gitpod.io/docs/self-hosted/latest/install/oauth/) (see values.yaml) 661 | - use helm to install Gitpod: 662 | 663 | export PATH=` + filepath.Join(cwd, "utils") + `:$PATH 664 | helm repo add charts.gitpod.io https://charts.gitpod.io 665 | helm dep update 666 | helm upgrade --install $(for i in $(cat configuration.txt); do echo -e "-f $i"; done) gitpod . 667 | `) 668 | } 669 | 670 | const ( 671 | // error printed when gcloud isn't configured properly 672 | errPrjNotConfigured = `GCP project unconfigured. Use 673 | gcloud config set core/project 674 | gcloud config set compute/region 675 | gcloud config set compute/zone 676 | 677 | to set up your environment. 678 | ` 679 | ) 680 | 681 | // printStep prints a script step in a fancy dressing 682 | func printStep(m string) { 683 | fmt.Printf("\n\033[33m- %s\033[m\n", m) 684 | } 685 | 686 | // failOnError fails this script if an error occured 687 | func failOnError(err error) { 688 | if err == nil { 689 | return 690 | } 691 | 692 | fmt.Fprintf(os.Stderr, "\n\n\033[31mfailure:\033[m %v\n", err) 693 | os.Exit(1) 694 | } 695 | 696 | // isAlreadyExistsErr returns true if the error was produced because a gcloud resource already exists 697 | func isAlreadyExistsErr(err error) bool { 698 | return strings.Contains(strings.ToLower(err.Error()), "already exists") 699 | } 700 | 701 | // run executes a command end returns its output 702 | func run(command string, args ...string) (output string, err error) { 703 | cmd := runC(command, args...) 704 | buf, err := cmd.CombinedOutput() 705 | if err != nil && strings.Contains(err.Error(), "exit status") { 706 | return string(buf), fmt.Errorf(string(buf)) 707 | } 708 | 709 | return string(buf), err 710 | } 711 | 712 | // run executes a command and forwards the output to stdout/stderr 713 | func runLoud(command string, args ...string) error { 714 | cmd := runC(command, args...) 715 | cmd.Stdout = os.Stdout 716 | cmd.Stderr = os.Stderr 717 | cmd.Stdin = os.Stdin 718 | return cmd.Run() 719 | } 720 | 721 | // runC prepares the execution of a command 722 | func runC(command string, args ...string) *exec.Cmd { 723 | fmt.Printf(" \033[2mrunning: %s %s\033[m\n", command, strings.Join(args, " ")) 724 | cmd := exec.Command(command, args...) 725 | cmd.Dir = cwd 726 | cmd.Env = os.Environ() 727 | return cmd 728 | } 729 | 730 | // buildArgs turns a map into arguments in the format gcloud expects 731 | func buildArgs(prefix []string, argm map[string]string) []string { 732 | var args []string 733 | args = append(args, prefix...) 734 | for k, v := range argm { 735 | if v == "" { 736 | args = append(args, fmt.Sprintf("--%s", k)) 737 | continue 738 | } 739 | 740 | args = append(args, fmt.Sprintf("--%s=%s", k, v)) 741 | } 742 | return args 743 | } 744 | 745 | func generateRandomPassword() string { 746 | const charset = "abcdefghijklmnopqrstuvwxyz" + 747 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 748 | var password string 749 | 750 | rand.Seed(time.Now().UnixNano()) 751 | for i := 0; i < 40; i++ { 752 | p := charset[rand.Intn(len(charset))] 753 | password += string(p) 754 | } 755 | return password 756 | } 757 | --------------------------------------------------------------------------------