├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── Makefile ├── README.md ├── chart ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── secret.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml └── values.yaml ├── go.mod ├── go.sum ├── main.go └── terraform ├── gke-cluster ├── .terraform.lock.hcl ├── gke.tf ├── output.tf ├── terraform.tfvars ├── versions.tf └── vpc.tf └── helm-release ├── .terraform.lock.hcl ├── helm-release.tf ├── kubernetes.tf ├── terraform.tfvars ├── variables.tf └── versions.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # code-execute generated files 2 | code/ 3 | code/code.go 4 | *.exe 5 | 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | # .tfstate files 10 | *.tfstate 11 | *.tfstate.* 12 | 13 | # Crash log files 14 | crash.log 15 | 16 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 17 | # .tfvars files are managed as part of configuration and so should be included in 18 | # version control. 19 | # 20 | # example.tfvars 21 | 22 | # Ignore override files as they are usually used to override resources locally and so 23 | # are not checked in 24 | override.tf 25 | override.tf.json 26 | *_override.tf 27 | *_override.tf.json 28 | 29 | # Include override files you do wish to add to version control using negated pattern 30 | # 31 | # !example_override.tf 32 | 33 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 34 | # example: *tfplan* 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/antonbabenko/pre-commit-terraform 8 | rev: v1.64.0 # Get the latest from: https://github.com/antonbabenko/pre-commit-terraform/releases 9 | hooks: 10 | - id: terraform_fmt 11 | - id: terraform_docs 12 | - repo: https://github.com/dnephin/pre-commit-golang 13 | rev: v0.5.0 14 | hooks: 15 | - id: go-vet 16 | - id: golangci-lint 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine 2 | 3 | ARG app 4 | ARG token 5 | 6 | WORKDIR /bot 7 | 8 | COPY go.mod . 9 | COPY go.sum . 10 | COPY main.go . 11 | 12 | RUN go mod tidy 13 | 14 | RUN go build 15 | 16 | CMD [ "./codeexecute" ] 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-build: 2 | docker build -t codeexecute:1.0.2 . 3 | 4 | helm-install: 5 | helm upgrade --install chart chart 6 | 7 | helm-uninstall: 8 | helm uninstall chart 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeExecute 2 | 3 | CodeExecute is a discord bot that enables developer collaboration through discord messages. It will allow developers to interactively execute code through discord messages while sharing and previewing the output of the code execution. 4 | 5 | # Based on Piston 6 | This bot executes code using the [Piston](https://github.com/engineer-man/piston) library which includes sandboxing and added security. 7 | You can read up on their level of security, supported languages and more: 8 | - [Supported Languages](https://github.com/engineer-man/piston#supported-languages) 9 | - [Principle of Operation](https://github.com/engineer-man/piston#principle-of-operation) 10 | - [Security](https://github.com/engineer-man/piston#security) 11 | 12 | # How to use 13 | ## Syntax 14 | ###### Basic code block execution syntax 15 | ```` 16 | run```[language] 17 | 18 | ``` 19 | ```` 20 | ###### The bot also supports messages with text before and after your code block. 21 | ````java 22 | Hello! Can anyone help me with this code 23 | run```go 24 | package main 25 | import "fmt" 26 | func main() { 27 | fmt.Printf("Hello world") 28 | } 29 | ``` 30 | Thanks in advance! 31 | ```` 32 | ###### Execute a github gist 33 | ``` 34 | run https://gist.github.com/michaelassaf/29a8eb718842c1cb91718e91b53fe200 35 | ``` 36 | ###### Execute a file attached to your message 37 | ``` 38 | run file 39 | ``` 40 | 41 | The discord bot will return a reply message with the output of the code and with a *Run* button that allows the user to execute their code as many times as they wish. This gives the user the possibility to modify their code and re-execute their code. 42 | 43 | ![Alt Text](https://media.giphy.com/media/v5kxUwov8ajcKqeNee/giphy.gif) 44 | -------------------------------------------------------------------------------- /chart/.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 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: chart 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.0.0" 25 | -------------------------------------------------------------------------------- /chart/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naruto-Leader/code-execute/320e063c6bbbff68e572423244ca8b83d4350c02/chart/templates/NOTES.txt -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "chart.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "chart.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "chart.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "chart.labels" -}} 37 | helm.sh/chart: {{ include "chart.chart" . }} 38 | {{ include "chart.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "chart.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "chart.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "chart.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "chart.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /chart/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | labels: 6 | {{- include "chart.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "chart.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "chart.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "chart.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | ports: 37 | - name: http 38 | containerPort: 80 39 | protocol: TCP 40 | resources: 41 | {{- toYaml .Values.resources | nindent 12 }} 42 | env: 43 | - name: BOT_TOKEN 44 | valueFrom: 45 | secretKeyRef: 46 | name: bot-secret 47 | key: token 48 | {{- with .Values.nodeSelector }} 49 | nodeSelector: 50 | {{- toYaml . | nindent 8 }} 51 | {{- end }} 52 | {{- with .Values.affinity }} 53 | affinity: 54 | {{- toYaml . | nindent 8 }} 55 | {{- end }} 56 | {{- with .Values.tolerations }} 57 | tolerations: 58 | {{- toYaml . | nindent 8 }} 59 | {{- end }} 60 | -------------------------------------------------------------------------------- /chart/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "chart.fullname" . }} 6 | labels: 7 | {{- include "chart.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "chart.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "chart.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "chart.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /chart/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: bot-secret 5 | type: Opaque 6 | data: 7 | token: {{ .Values.bot.token }} 8 | -------------------------------------------------------------------------------- /chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | labels: 6 | {{- include "chart.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "chart.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "chart.serviceAccountName" . }} 6 | labels: 7 | {{- include "chart.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /chart/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "chart.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "chart.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "chart.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for chart. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: xcodecraft/codeexecute 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: ClusterIP 41 | port: 80 42 | 43 | ingress: 44 | enabled: false 45 | className: "" 46 | annotations: {} 47 | # kubernetes.io/ingress.class: nginx 48 | # kubernetes.io/tls-acme: "true" 49 | hosts: 50 | - host: chart-example.local 51 | paths: 52 | - path: / 53 | pathType: ImplementationSpecific 54 | tls: [] 55 | # - secretName: chart-example-tls 56 | # hosts: 57 | # - chart-example.local 58 | 59 | resources: {} 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | # limits: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | # requests: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | 71 | autoscaling: 72 | enabled: false 73 | minReplicas: 1 74 | maxReplicas: 100 75 | targetCPUUtilizationPercentage: 80 76 | # targetMemoryUtilizationPercentage: 80 77 | 78 | nodeSelector: {} 79 | 80 | tolerations: [] 81 | 82 | affinity: {} 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module codeexecute 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.24.0 7 | github.com/google/go-github/v43 v43.0.0 8 | github.com/milindmadhukar/go-piston v0.0.0-20211122120254-64da61081d05 9 | ) 10 | 11 | require ( 12 | github.com/google/go-querystring v1.1.0 // indirect 13 | github.com/gorilla/websocket v1.4.2 // indirect 14 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect 15 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bwmarrin/discordgo v0.24.0 h1:Gw4MYxqHdvhO99A3nXnSLy97z5pmIKHZVJ1JY5ZDPqY= 2 | github.com/bwmarrin/discordgo v0.24.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 3 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 4 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 5 | github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= 6 | github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= 7 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 8 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 9 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 10 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 11 | github.com/milindmadhukar/go-piston v0.0.0-20211122120254-64da61081d05 h1:DJtDN26io353OHf60ahLS7ZmwnNmiMEoIL1Z5HT3v8I= 12 | github.com/milindmadhukar/go-piston v0.0.0-20211122120254-64da61081d05/go.mod h1:UGaEMhOv9qK6z4E663UiqUB1M7J+aXDJV716oclA8Dg= 13 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 14 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= 15 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 16 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 17 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 19 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 21 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 22 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 23 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | 16 | "github.com/bwmarrin/discordgo" 17 | "github.com/google/go-github/v43/github" 18 | piston "github.com/milindmadhukar/go-piston" 19 | ) 20 | 21 | var s *discordgo.Session 22 | var gclient *github.Client 23 | var ctx context.Context 24 | 25 | // bot parameters 26 | var ( 27 | botToken string 28 | pclient *piston.Client 29 | ) 30 | 31 | // code execution types 32 | const ( 33 | cblock int = iota 34 | cgist 35 | cfile 36 | ) 37 | 38 | // code execution ouptput 39 | var o chan string 40 | 41 | func init() { 42 | botToken = os.Getenv("BOT_TOKEN") 43 | flag.Parse() 44 | pclient = piston.CreateDefaultClient() 45 | ctx = context.Background() 46 | gclient = github.NewClient(nil) 47 | o = make(chan string) 48 | } 49 | 50 | // create discord session 51 | func init() { 52 | var err error 53 | s, err = discordgo.New("Bot " + botToken) 54 | if err != nil { 55 | log.Fatalf("Invalid bot parameters: %v", err) 56 | } 57 | } 58 | 59 | func main() { 60 | // add function handlers for code execution 61 | s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { log.Println("Bot is up!") }) 62 | s.AddHandler(executionHandler) 63 | s.AddHandler(reExecuctionHandler) 64 | 65 | err := s.Open() 66 | if err != nil { 67 | log.Fatalf("Cannot open the session: %v", err) 68 | } 69 | defer s.Close() 70 | 71 | stop := make(chan os.Signal, 1) 72 | signal.Notify(stop, os.Interrupt) 73 | <-stop 74 | log.Println("Graceful shutdown") 75 | } 76 | 77 | func executionHandler(s *discordgo.Session, m *discordgo.MessageCreate) { 78 | // avoid handling the message that the bot creates when replying to a user 79 | if m.Author.Bot { 80 | return 81 | } 82 | 83 | // extract code block from message and execute code 84 | var responseContent string 85 | ctype, lang, codeBlock := codeBlockExtractor(m.Message) 86 | if lang != "" && codeBlock != "" { 87 | go exec(m.ChannelID, codeBlock, m.Reference(), lang) 88 | responseContent = <-o 89 | } else { 90 | return 91 | } 92 | 93 | // only add run button for code block and gist execution 94 | var runButton []discordgo.MessageComponent 95 | if ctype != cfile { 96 | runButton = []discordgo.MessageComponent{discordgo.ActionsRow{ 97 | Components: []discordgo.MessageComponent{ 98 | discordgo.Button{ 99 | Label: "Run", 100 | Style: discordgo.SuccessButton, 101 | CustomID: "run", 102 | }, 103 | }, 104 | }, 105 | } 106 | } 107 | 108 | // send initial reply message containing output of code execution 109 | // "Run" button is injected in the message so the user may re run their code 110 | _, _ = s.ChannelMessageSendComplex(m.ChannelID, &discordgo.MessageSend{ 111 | Content: responseContent, 112 | Reference: m.Reference(), 113 | Components: runButton, 114 | }) 115 | } 116 | 117 | // handler for re-executing go code when the "Run" button is clicked 118 | func reExecuctionHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { 119 | // check if go button was clicked 120 | if i.MessageComponentData().CustomID == "run" { 121 | // get referenced channel message 122 | // used to fetch the code from the message that contains it 123 | m, err := s.ChannelMessage(i.ChannelID, i.Message.MessageReference.MessageID) 124 | if err != nil { 125 | log.Printf("Could not get message reference: %v", err) 126 | } 127 | 128 | // extract code block from message and execute code 129 | var responseContent string 130 | if _, lang, codeBlock := codeBlockExtractor(m); lang != "" || codeBlock != "" { 131 | go exec(i.ChannelID, codeBlock, i.Message.Reference(), lang) 132 | responseContent = <-o 133 | } else { 134 | responseContent = fmt.Sprintln("Could not find any code in message to execute") 135 | } 136 | 137 | // send interaction respond 138 | // update message reply with new code execution output 139 | err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 140 | Type: discordgo.InteractionResponseUpdateMessage, 141 | Data: &discordgo.InteractionResponseData{ 142 | Content: responseContent, 143 | Components: []discordgo.MessageComponent{ 144 | discordgo.ActionsRow{ 145 | Components: []discordgo.MessageComponent{ 146 | discordgo.Button{ 147 | Label: "Run", 148 | Style: discordgo.SuccessButton, 149 | CustomID: "run", 150 | }, 151 | }, 152 | }, 153 | }, 154 | }, 155 | }) 156 | 157 | if err != nil { 158 | log.Printf("Could not send respond interaction: %v", err) 159 | } 160 | } 161 | } 162 | 163 | // handle code execution 164 | // sends output to chan 165 | func exec(channelID string, code string, messageReference *discordgo.MessageReference, lang string) { 166 | // execute code using piston library 167 | output, err := pclient.Execute(lang, "", 168 | []piston.Code{ 169 | { 170 | Name: fmt.Sprintf("%s-code", messageReference.MessageID), 171 | Content: code, 172 | }, 173 | }, 174 | ) 175 | if err != nil { 176 | o <- fmt.Sprintf(">>> Execution Failed [%s - %s]\n```\n%s\n```\n", output.Language, output.Version, err) 177 | } 178 | 179 | o <- fmt.Sprintf(">>> Output [%s - %s]\n```\n%s\n```\n", output.Language, output.Version, output.GetOutput()) 180 | } 181 | 182 | func codeBlockExtractor(m *discordgo.Message) (int, string, string) { 183 | mc := m.Content 184 | // syntax for executing a code block 185 | // this is based on a writing standard in discord for writing code in a paragraph message block 186 | // example message: ```go ... ``` 187 | rcb, _ := regexp.Compile("run```.*") 188 | // syntax for executing a gist 189 | rg, _ := regexp.Compile("run https://gist.github.com/.*/.*") 190 | rgist, _ := regexp.Compile("run https://gist.github.com/.*/") 191 | // syntax for executing an attached file 192 | rf, _ := regexp.Compile("run *.*") 193 | 194 | c := strings.Split(mc, "\n") 195 | for bi, bb := range c { 196 | // extract code block to execute 197 | if rcb.MatchString(bb) { 198 | lang := strings.TrimPrefix(string(rcb.Find([]byte(mc))), "run```") 199 | // find end of code block 200 | var codeBlock string 201 | endBlockRegx, _ := regexp.Compile("```") 202 | 203 | sa := c[bi+1:] 204 | for ei, eb := range sa { 205 | if endBlockRegx.Match([]byte(eb)) { 206 | // create code block to execute 207 | codeBlock = strings.Join(sa[:ei], "\n") 208 | return cblock, lang, codeBlock 209 | } 210 | } 211 | } 212 | // extract gist language and code to execute 213 | if rg.MatchString(bb) { 214 | gistID := rgist.ReplaceAllString(bb, "") 215 | gist, _, err := gclient.Gists.Get(ctx, gistID) 216 | if err != nil { 217 | log.Printf("Failed to obtain gist: %v\n", err) 218 | } 219 | return cgist, strings.ToLower(*gist.Files["helloworld.go"].Language), *gist.Files["helloworld.go"].Content 220 | } 221 | // extract file language and code to execute 222 | if rf.MatchString(bb) { 223 | if len(m.Attachments) > 0 { 224 | // handle 1 file in message attachments 225 | f := m.Attachments[0] 226 | // get language from extension 227 | lang := strings.TrimLeft(filepath.Ext(f.Filename), ".") 228 | // get code from file 229 | resp, err := http.Get(f.URL) 230 | if err != nil { 231 | log.Printf("Failed GET http call to file attachment URL: %v\n", err) 232 | } 233 | defer resp.Body.Close() 234 | codeBlock, err := io.ReadAll(resp.Body) 235 | if err != nil { 236 | log.Printf("Failed to obtain code from response body: %v\n", err) 237 | } 238 | return cfile, lang, string(codeBlock) 239 | } 240 | } 241 | } 242 | 243 | return -1, "", "" 244 | } 245 | -------------------------------------------------------------------------------- /terraform/gke-cluster/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/google" { 5 | version = "4.15.0" 6 | constraints = "4.15.0" 7 | hashes = [ 8 | "h1:0cU7xDMM+ZlYQ/a40Kxpx1JQQEEdk7qwPIS90Mj7oGM=", 9 | "zh:179d039e925fe5f2f3400a1c79ee08483c60decfcb0f01f589caa3a43f1c936f", 10 | "zh:25ca1e184aa8c1dd6d453608fd27b31dc4e4febf5e3005f70a784389e182f046", 11 | "zh:46e7f7f943eece80a6989849b5b9cb99923a952912414f1a6a55357c55b93095", 12 | "zh:51833859fb961c695723e2b992d86956cce760d7a14b9c3056557252e941b582", 13 | "zh:626d41def5eb26bfd181a2a8dfb454f1fb404dbb932e487fd5fd06e35b0d689b", 14 | "zh:7ba9fb03ff4033ab47ab48d396802572e87c3bb22b76043da17bd26efdbb4283", 15 | "zh:a57abb9dbef7de9f31ca137640df0aee2308953234bb29a2fca9a19bc7996d45", 16 | "zh:b694d1881f506a43a6f0a01fd58d3ebc5fb34fc8d74a907c94c68732fa203f50", 17 | "zh:b880f21d85e9cc651fdac36474c9369d5cba5e00fa64b6f99c02c7b4a46e7171", 18 | "zh:f15a16639c7d1cfc4a39049ad48c241bb9ba1dc7b68b2b72f988679d13cc40ff", 19 | "zh:f495b4b0c9059236b0a32ebb94ae2bf7e7139a4945b698fcdb7a3f9214a1e6a6", 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /terraform/gke-cluster/gke.tf: -------------------------------------------------------------------------------- 1 | variable "gke_username" { 2 | default = "" 3 | description = "gke username" 4 | } 5 | 6 | variable "gke_password" { 7 | default = "" 8 | description = "gke password" 9 | } 10 | 11 | variable "gke_num_nodes" { 12 | default = 1 13 | description = "number of gke nodes" 14 | } 15 | 16 | # GKE cluster 17 | resource "google_container_cluster" "primary" { 18 | name = "${var.project_id}-gke" 19 | location = var.region 20 | 21 | # We can't create a cluster with no node pool defined, but we want to only use 22 | # separately managed node pools. So we create the smallest possible default 23 | # node pool and immediately delete it. 24 | remove_default_node_pool = true 25 | initial_node_count = 1 26 | 27 | network = google_compute_network.vpc.name 28 | subnetwork = google_compute_subnetwork.subnet.name 29 | } 30 | 31 | # Separately Managed Node Pool 32 | resource "google_container_node_pool" "primary_nodes" { 33 | name = "${google_container_cluster.primary.name}-node-pool" 34 | location = var.region 35 | node_locations = ["us-east1-b"] 36 | cluster = google_container_cluster.primary.name 37 | node_count = var.gke_num_nodes 38 | 39 | 40 | node_config { 41 | oauth_scopes = [ 42 | "https://www.googleapis.com/auth/logging.write", 43 | "https://www.googleapis.com/auth/monitoring", 44 | ] 45 | 46 | labels = { 47 | env = var.project_id 48 | } 49 | 50 | # preemptible = true 51 | machine_type = "e2-highcpu-2" 52 | tags = ["gke-node", "${var.project_id}-gke"] 53 | metadata = { 54 | disable-legacy-endpoints = "true" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /terraform/gke-cluster/output.tf: -------------------------------------------------------------------------------- 1 | output "region" { 2 | value = var.region 3 | description = "GCloud Region" 4 | } 5 | 6 | output "project_id" { 7 | value = var.project_id 8 | description = "GCloud Project ID" 9 | } 10 | 11 | output "kubernetes_cluster_name" { 12 | value = google_container_cluster.primary.name 13 | description = "GKE Cluster Name" 14 | } 15 | 16 | output "kubernetes_cluster_location" { 17 | value = google_container_cluster.primary.location 18 | description = "GKE Cluster Location" 19 | } 20 | 21 | output "kubernetes_cluster_host" { 22 | value = google_container_cluster.primary.endpoint 23 | description = "GKE Cluster Host" 24 | } 25 | -------------------------------------------------------------------------------- /terraform/gke-cluster/terraform.tfvars: -------------------------------------------------------------------------------- 1 | project_id = "codeexecute-345900" 2 | region = "us-east1" 3 | -------------------------------------------------------------------------------- /terraform/gke-cluster/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "4.15.0" 6 | } 7 | } 8 | 9 | backend "gcs" { 10 | bucket = "codeexecute-terraform-state-gke" 11 | prefix = "terraform/state" 12 | } 13 | 14 | required_version = ">= 0.14" 15 | } 16 | 17 | provider "google" { 18 | project = var.project_id 19 | region = var.region 20 | } 21 | -------------------------------------------------------------------------------- /terraform/gke-cluster/vpc.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "project id" 3 | } 4 | 5 | variable "region" { 6 | description = "region" 7 | } 8 | 9 | # VPC 10 | resource "google_compute_network" "vpc" { 11 | name = "${var.project_id}-vpc" 12 | auto_create_subnetworks = "false" 13 | } 14 | 15 | # Subnet 16 | resource "google_compute_subnetwork" "subnet" { 17 | name = "${var.project_id}-subnet" 18 | region = var.region 19 | network = google_compute_network.vpc.name 20 | ip_cidr_range = "10.10.0.0/24" 21 | } 22 | -------------------------------------------------------------------------------- /terraform/helm-release/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/google" { 5 | version = "4.15.0" 6 | constraints = "4.15.0" 7 | hashes = [ 8 | "h1:0cU7xDMM+ZlYQ/a40Kxpx1JQQEEdk7qwPIS90Mj7oGM=", 9 | "h1:kCR1yWEPkPv1R6qsKlAarmXtUVLsbP6ogsfJmaYyV0M=", 10 | "zh:179d039e925fe5f2f3400a1c79ee08483c60decfcb0f01f589caa3a43f1c936f", 11 | "zh:25ca1e184aa8c1dd6d453608fd27b31dc4e4febf5e3005f70a784389e182f046", 12 | "zh:46e7f7f943eece80a6989849b5b9cb99923a952912414f1a6a55357c55b93095", 13 | "zh:51833859fb961c695723e2b992d86956cce760d7a14b9c3056557252e941b582", 14 | "zh:626d41def5eb26bfd181a2a8dfb454f1fb404dbb932e487fd5fd06e35b0d689b", 15 | "zh:7ba9fb03ff4033ab47ab48d396802572e87c3bb22b76043da17bd26efdbb4283", 16 | "zh:a57abb9dbef7de9f31ca137640df0aee2308953234bb29a2fca9a19bc7996d45", 17 | "zh:b694d1881f506a43a6f0a01fd58d3ebc5fb34fc8d74a907c94c68732fa203f50", 18 | "zh:b880f21d85e9cc651fdac36474c9369d5cba5e00fa64b6f99c02c7b4a46e7171", 19 | "zh:f15a16639c7d1cfc4a39049ad48c241bb9ba1dc7b68b2b72f988679d13cc40ff", 20 | "zh:f495b4b0c9059236b0a32ebb94ae2bf7e7139a4945b698fcdb7a3f9214a1e6a6", 21 | ] 22 | } 23 | 24 | provider "registry.terraform.io/hashicorp/helm" { 25 | version = "2.0.3" 26 | constraints = "~> 2.0.1" 27 | hashes = [ 28 | "h1:FRSVqY+1/AUO/j/lVxHHsLudfSA9gDc7Dsu+YxcJSEY=", 29 | "h1:a/VTIYg/0Tkyz7XgRbEB5h1x4ftSPC0E4skxBVd6ufk=", 30 | "zh:154e0aa489e474e2eeb3de94be7666133faf6fd950712a640425b2bf3a81ee95", 31 | "zh:16a2be6c4b61d0c5205c63816148c7ab0c8f56a75c05e8d897fa4d5cac0c029a", 32 | "zh:189e47bc723f8c29bcfe2c1638d43b8148f614ea86e642f4b50b2acb4b760224", 33 | "zh:3763901d3630213002cb8c70bb24c628cd29738ff6591585250ea8636264abd6", 34 | "zh:4822f85e4700ea049384523d98de0ef7d83549844b13e94bbd544cec05557a9a", 35 | "zh:62c5b87b09e0051bab0b712e3ad465fd53e66f9619dbe76ee23519d1087d8a05", 36 | "zh:a0a6a842b11190dd1841e98bbb74961074e7ffb95984be5cc392df9f532d803e", 37 | "zh:beac4e6806e77447e1018f3404a5fbf782d20d82a0d9b4a31e9bfc7d2bbecab6", 38 | "zh:e1bbaa09bf4f4a91ec7606f84d2e0200a02e7b24d045e8b5daebd87d7a75b7ce", 39 | "zh:ed1e05c50212d4f57435ccdd68cfb98d8395927c316df76d1dd6509566d3aeaa", 40 | "zh:fdc687e16a964bb652ddb670f6832fdead25235eca551796cfed70ec07d94931", 41 | ] 42 | } 43 | 44 | provider "registry.terraform.io/hashicorp/kubernetes" { 45 | version = "2.0.3" 46 | constraints = "~> 2.0.1" 47 | hashes = [ 48 | "h1:u3QLP3ca1ZYtdx4t9LrNLzAA3HY/GAT06hznb2pqNC4=", 49 | "h1:zDdnJmYBp0zOsaHMrrbJgbSjyEbUybcRUx4DP6H4ZHw=", 50 | "zh:3847733636ed2aca8694227ee6936fecc6cec9573818ecf64acc2a01a4fb3ae4", 51 | "zh:44d3e88227174d2e51e0273e732e54bb5f5d8150bbf1adecd2be198c857a0264", 52 | "zh:7c147baeac90ddc71b8c106409dc2e06e6fe04448a0ae36c7ef919b8b27c8d5f", 53 | "zh:832ebd1eec844fdc00ecbc5e694ecf532d0c7a94f2a1697ea0c7bc159696d529", 54 | "zh:920c8f64925d346a0621aadede6a4ac1bbe650f16c2074a0a6b5eeb7a5c35cf7", 55 | "zh:a26906cd3aeb28f402972154dbb3ae1ac6d739ce3e4b00906c9d4b1e263d5a56", 56 | "zh:a7b3cbd06684f843f700ee9281c583cc593e00e3acc2c0a34f6dad4e35a1ec60", 57 | "zh:b43c05ade832995a09c40a1c2f080586f38de1edc8c46be2e6f90b96c58a2482", 58 | "zh:cba2a979889e79ffe48bb5ea7e63f88c344ae2cd7a341afb77b3c8d417658d6c", 59 | "zh:e00b2ec7357a58a435a69146da3c256178caab83a6574969d4660b69996c7955", 60 | "zh:f95f179f68aed39b8d73d7914176baf74509d74f90a1e3a600a8eb4461f8f0a3", 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /terraform/helm-release/helm-release.tf: -------------------------------------------------------------------------------- 1 | # Retrieve an access token as the Terraform runner 2 | data "google_client_config" "provider" {} 3 | 4 | provider "helm" { 5 | kubernetes { 6 | host = "https://${data.google_container_cluster.cluster.endpoint}" 7 | token = data.google_client_config.provider.access_token 8 | cluster_ca_certificate = base64decode( 9 | data.google_container_cluster.cluster.master_auth[0].cluster_ca_certificate, 10 | ) 11 | } 12 | } 13 | 14 | resource "helm_release" "codeexecute" { 15 | name = "ce-chart" 16 | chart = "../../chart/" 17 | 18 | set { 19 | name = "image.tag" 20 | value = var.image_tag 21 | } 22 | 23 | set_sensitive { 24 | name = "bot.token" 25 | value = base64encode(var.bot_token) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /terraform/helm-release/kubernetes.tf: -------------------------------------------------------------------------------- 1 | data "terraform_remote_state" "gke" { 2 | backend = "gcs" 3 | config = { 4 | bucket = "codeexecute-terraform-state-gke" 5 | prefix = "terraform/state" 6 | } 7 | } 8 | 9 | # Retrieve GKE cluster configuration 10 | data "google_container_cluster" "cluster" { 11 | name = data.terraform_remote_state.gke.outputs.kubernetes_cluster_name 12 | location = data.terraform_remote_state.gke.outputs.kubernetes_cluster_location 13 | } 14 | 15 | module "gke_auth" { 16 | source = "terraform-google-modules/kubernetes-engine/google//modules/auth" 17 | project_id = data.terraform_remote_state.gke.outputs.project_id 18 | location = data.google_container_cluster.cluster.location 19 | cluster_name = data.google_container_cluster.cluster.name 20 | } 21 | -------------------------------------------------------------------------------- /terraform/helm-release/terraform.tfvars: -------------------------------------------------------------------------------- 1 | project_id = "codeexecute-345900" 2 | region = "us-east1" 3 | -------------------------------------------------------------------------------- /terraform/helm-release/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | description = "Project ID" 3 | } 4 | 5 | variable "region" { 6 | default = "us-east1" 7 | } 8 | 9 | variable "application_name" { 10 | type = string 11 | default = "code-execute" 12 | } 13 | 14 | variable "image_tag" { 15 | description = "Docker Image Tag" 16 | } 17 | 18 | variable "bot_token" { 19 | type = string 20 | description = "Discord Bot Token" 21 | sensitive = true 22 | } 23 | -------------------------------------------------------------------------------- /terraform/helm-release/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | helm = { 4 | source = "hashicorp/helm" 5 | version = "~> 2.0.1" 6 | } 7 | kubernetes = { 8 | source = "hashicorp/kubernetes" 9 | version = "~> 2.0.1" 10 | } 11 | google = { 12 | source = "hashicorp/google" 13 | version = "4.15.0" 14 | } 15 | } 16 | 17 | backend "gcs" { 18 | bucket = "codeexecute-terraform-state-helm" 19 | prefix = "terraform/state" 20 | } 21 | 22 | required_version = ">= 1.1.7" 23 | } 24 | 25 | provider "google" { 26 | project = var.project_id 27 | region = var.region 28 | } 29 | --------------------------------------------------------------------------------