├── .cargo
└── config
├── .editorconfig
├── .env
├── .github
├── playground-icon.svg
└── workflows
│ ├── ci-playground.yml
│ ├── ci-templates.yml
│ └── ct-playground.yml
├── .gitignore
├── Makefile
├── README.md
├── backend
├── .dockerignore
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── README.md
└── src
│ ├── api.rs
│ ├── error.rs
│ ├── github.rs
│ ├── kubernetes.rs
│ ├── main.rs
│ ├── manager.rs
│ ├── metrics.rs
│ ├── prometheus.rs
│ └── types.rs
├── client
├── README.md
├── package.json
├── src
│ ├── index.ts
│ ├── login.ts
│ ├── rpc.ts
│ ├── session.ts
│ ├── types.ts
│ └── utils.ts
├── tsconfig.browser.json
├── tsconfig.json
└── yarn.lock
├── conf
├── k8s
│ ├── base
│ │ ├── backend-api-deployment.yaml
│ │ ├── backend-api-service.yaml
│ │ ├── backend-ui-deployment.yaml
│ │ ├── backend-ui-service.yaml
│ │ ├── cluster-role-binding.yaml
│ │ ├── grafana
│ │ │ ├── dashboards
│ │ │ │ └── home.json
│ │ │ ├── datasources.yaml
│ │ │ ├── deployment.yaml
│ │ │ ├── kustomization.yaml
│ │ │ ├── providers.yaml
│ │ │ ├── service.yaml
│ │ │ └── volume.yaml
│ │ ├── ingress.yaml
│ │ ├── kustomization.yaml
│ │ ├── nginx.yaml
│ │ ├── node-conf-daemon-set.yaml
│ │ ├── prepull-templates.yaml
│ │ ├── prometheus
│ │ │ ├── deployment.yaml
│ │ │ ├── kustomization.yaml
│ │ │ ├── prometheus.yaml
│ │ │ ├── rbac.yaml
│ │ │ └── service.yaml
│ │ └── service-account.yaml
│ ├── dashboard-user.yaml
│ ├── overlays
│ │ ├── dev
│ │ │ ├── kustomization.yaml
│ │ │ ├── templates
│ │ │ │ └── kustomization.yaml
│ │ │ └── users
│ │ │ │ └── jeluard
│ │ └── production
│ │ │ ├── kustomization.yaml
│ │ │ ├── templates
│ │ │ └── kustomization.yaml
│ │ │ └── users
│ │ │ ├── bjornwgnr
│ │ │ └── jeluard
│ └── skaffold.yaml
└── templates
│ ├── .env
│ ├── front-end-template
│ ├── ink
│ ├── node-template
│ ├── node-template-openvscode
│ ├── parachain-template
│ └── recipes
├── docs
├── building
│ └── cicd.md
├── extending
│ └── custom-template.md
├── operating
│ └── deployment.md
└── using
│ ├── 00-demo.gif
│ └── overview.md
├── e2e
├── README.md
├── ava.config.js
├── package.json
├── test
│ ├── test.ts
│ └── website.ts
└── yarn.lock
├── frontend
├── .dockerignore
├── .eslintrc.cjs
├── Dockerfile
├── README.md
├── ava.config.js
├── conf
│ └── nginx.conf
├── package.json
├── public
│ ├── assets
│ │ ├── favicon.png
│ │ └── images
│ │ │ ├── logo.png
│ │ │ ├── logo_substrate.svg
│ │ │ └── logo_substrate_onDark.svg
│ ├── index.html
│ └── robots.txt
├── src
│ ├── LogoSubstrate.tsx
│ ├── components.tsx
│ ├── hooks.tsx
│ ├── index.tsx
│ ├── lifecycle.tsx
│ ├── panels
│ │ ├── admin.tsx
│ │ ├── login.tsx
│ │ ├── session.tsx
│ │ ├── stats.tsx
│ │ ├── terms.tsx
│ │ └── theia.tsx
│ ├── terms.md
│ ├── terms.tsx
│ ├── themes
│ │ ├── index.ts
│ │ └── substrate
│ │ │ ├── colors.ts
│ │ │ ├── dark.ts
│ │ │ ├── light.ts
│ │ │ ├── shadows.ts
│ │ │ └── typography.ts
│ └── utils.tsx
├── test
│ └── connect.ts
├── tsconfig.json
└── yarn.lock
└── templates
├── .dockerignore
├── Dockerfile.base
├── Dockerfile.template
├── Dockerfile.theia-base
├── Dockerfile.theia-template
├── README.md
├── conf
└── .vscode
│ └── settings.json
├── lerna.json
├── package.json
├── theia-playground-extension
├── assets
│ └── substrate-logo.png
├── package.json
├── src
│ ├── browser
│ │ ├── http-location-mapper.ts
│ │ ├── initial-files-open.ts
│ │ ├── style
│ │ │ └── index.css
│ │ ├── theia-playground-extension-contribution.ts
│ │ └── theia-playground-extension-frontend-module.ts
│ └── node
│ │ ├── file-download-handler.ts
│ │ └── theia-playground-extension-backend-module.ts
└── tsconfig.json
├── theia-playground
├── .vscode
│ └── settings.json
├── package.json
└── webpack.config.js
└── yarn.lock
/.cargo/config:
--------------------------------------------------------------------------------
1 | [alias]
2 | check-fmt = "fmt --all -- --check"
3 | lint = "clippy -- -D warnings -A deprecated"
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 4
10 |
11 | [*.rs]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.toml]
16 | indent_style = space
17 | indent_size = 4
18 |
19 | [*.yml]
20 | indent_style = space
21 | indent_size = 2
22 |
23 | [*.md]
24 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NAMESPACE=playground
2 | GKE_PROJECT=aerobic-factor-306517
3 | GKE_ZONE=us-central1-a
4 | DOCKER_ORG=paritytech
5 |
--------------------------------------------------------------------------------
/.github/playground-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/ci-playground.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration playground
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '.github/workflows/ci-playground.yml'
7 | - 'backend/**'
8 | - 'frontend/**'
9 |
10 | jobs:
11 | frontend:
12 | runs-on: ubuntu-latest
13 | if: "! contains(github.event.head_commit.message, '[CI Skip]')"
14 | defaults:
15 | run:
16 | working-directory: ./frontend
17 | steps:
18 | - uses: actions/checkout@v2
19 |
20 | - name: Setup node
21 | uses: actions/setup-node@v2
22 | with:
23 | node-version: "14"
24 |
25 | - name: Install
26 | run: yarn install --check-files
27 |
28 | - name: Lint
29 | run: yarn lint
30 |
31 | - name: Audit
32 | run: yarn audit
33 |
34 | - name: Build
35 | run: yarn build
36 |
37 | - name: Test
38 | run: yarn test
39 |
40 | backend:
41 | runs-on: ubuntu-latest
42 | if: "! contains(github.event.head_commit.message, '[CI Skip]')"
43 | defaults:
44 | run:
45 | working-directory: ./backend
46 | steps:
47 |
48 | - uses: actions/checkout@v2
49 |
50 | - uses: actions-rs/toolchain@v1
51 | with:
52 | toolchain: nightly
53 | profile: minimal
54 | override: true
55 | components: rustfmt, clippy
56 |
57 | - name: Check Formatting
58 | run: cargo check-fmt
59 |
60 | - name: Lint
61 | run: cargo lint
62 |
63 | - name: Build
64 | run: cargo build --verbose
65 |
66 | - name: Run tests
67 | run: cargo test --verbose
68 |
69 | docker:
70 | runs-on: ubuntu-latest
71 | if: "! contains(github.event.head_commit.message, '[CI Skip]')"
72 | steps:
73 |
74 | - uses: actions/checkout@v2
75 |
76 | - uses: docker/setup-buildx-action@v1
77 | with:
78 | install: true
79 |
80 | - name: Build frontend Dockerfile
81 | run: cd frontend && docker build -f Dockerfile .
82 |
83 | - name: Build backend Dockerfile
84 | run: cd backend && docker build -f Dockerfile .
85 |
86 | - uses: AbsaOSS/k3d-action@b176c2a6dcae72e3e64e3e4d61751904ec314002 # v2.3.0
87 | with:
88 | cluster-name: "pg-cluster"
89 | args: >-
90 | --k3s-arg '--tls-san=127.0.0.1@server:*'
91 | --k3s-arg '--no-deploy=traefik@server:*'
92 | --k3s-node-label "cloud.google.com/gke-nodepool=default-workspace@server:0"
93 | --port 80:80@loadbalancer
94 | --port 443:443@loadbalancer
95 |
96 | - name: Set environment
97 | id: env
98 | run: |
99 | echo "sha=sha-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
100 |
101 | - name: Set up Kustomize
102 | run: |-
103 | curl -sfLo kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv3.6.1/kustomize_v3.6.1_linux_amd64.tar.gz
104 | tar zxvf kustomize*
105 | chmod u+x ./kustomize
106 | working-directory: resources/k8s/overlays/dev
107 |
108 | - name: Update version
109 | run: |-
110 | ./kustomize edit set image paritytech/substrate-playground-backend-api:${{ steps.env.outputs.sha }}
111 | ./kustomize edit set image paritytech/substrate-playground-backend-ui:${{ steps.env.outputs.sha }}
112 | working-directory: resources/k8s/overlays/dev
113 |
114 | - name: Load docker images
115 | run: k3d image import paritytech/substrate-playground-backend-api:${{ steps.env.outputs.sha }} paritytech/substrate-playground-backend-ui:${{ steps.env.outputs.sha }} -c pg-cluster
116 |
117 | - name: Configure playground
118 | run: |-
119 | make k8s-setup-env
120 | make dev-create-certificate
121 | make k8s-update-certificate
122 | env:
123 | GH_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
124 | GH_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
125 |
126 | - name: Deploy playground
127 | run: make k8s-deploy
128 |
129 | - name: Wait for deployment
130 | run: sleep 300
131 |
132 | # Will not run authenticated tests when triggered by PR
133 | # See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
134 | - name: Run e2e tests
135 | run: yarn && yarn test
136 | working-directory: ./e2e
137 | timeout-minutes: 3
138 | env:
139 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
140 |
--------------------------------------------------------------------------------
/.github/workflows/ci-templates.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration templates
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '.github/workflows/ci-templates.yml'
7 | - 'templates/**'
8 |
9 | jobs:
10 | templates:
11 | runs-on: ubuntu-latest
12 | if: "! contains(github.event.head_commit.message, '[CI Skip]')"
13 | defaults:
14 | run:
15 | working-directory: ./templates
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Setup node
20 | uses: actions/setup-node@v2
21 | with:
22 | node-version: "14"
23 |
24 | - name: Install
25 | run: yarn install --check-files
26 |
27 | - name: Build
28 | run: yarn workspace @parity/theia-playground theia build
29 |
30 | - uses: docker/setup-buildx-action@v1
31 | with:
32 | install: true
33 |
34 | - name: Build Dockerfile.base
35 | run: docker build -f Dockerfile.base .
36 |
37 | - name: Build Dockerfile.theia-base
38 | run: docker build -f Dockerfile.theia-base .
39 |
--------------------------------------------------------------------------------
/.github/workflows/ct-playground.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Testing playground
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | # * is a special character in YAML so you have to quote this string
7 | - cron: '*/30 * * * *' # Every 30 minutes
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | with:
15 | ref: prod
16 |
17 | - name: Setup node
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: "16"
21 |
22 | - name: Audit URLs using Lighthouse
23 | uses: treosh/lighthouse-ci-action@7.0.0
24 | with:
25 | urls: https://playground.substrate.dev/
26 |
27 | - uses: microsoft/playwright-github-action@v1
28 |
29 | - name: Run tests
30 | run: yarn && yarn test
31 | working-directory: ./e2e
32 | timeout-minutes: 10
33 | env:
34 | ENV: production
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | node_modules/
3 | **/dist/
4 | frontend/.parcel-cache/
5 | # Generated by theia itself
6 | templates/.yarnclean
7 | templates/**/lib/**
8 | templates/webpack.config.js
9 | templates/**/src-gen/
10 | templates/**/out/
11 | templates/**/lerna-debug.log
12 | templates/theia-playground/plugins
13 | templates/theia-playground/gen-webpack.config.js
14 | TODO.md
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Substrate playground
4 |
5 | A hosted website that enables the user to navigate [Substrate](https://github.com/paritytech/substrate) runtimes, modify them and run remotely accessible chain. In a couple seconds!
6 |
7 |
8 |
9 |
10 |
11 | Playground allows end-user to spin up a substrate based development environment in seconds. A full machine with terminal is then available from a web browser, ready to launch a chain and remotely access it.
12 | Playground templates can be [created and maintained](docs/extending/custom-template.md) by 3rd parties.
13 |
14 | ## Deployment
15 |
16 | Playground is a set of containerized apps deployed on a kubernetes cluster. Fear not, it's quite simple to [deploy](docs/operating/deployment.md) it!
17 |
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | node-client/
2 | target/
3 | Dockerfile
--------------------------------------------------------------------------------
/backend/Cargo.toml:
--------------------------------------------------------------------------------
1 | [[bin]]
2 | name = "playground"
3 |
4 | [package]
5 | name = "playground"
6 | version = "0.1.0"
7 | authors = ["jeluard "]
8 | edition = "2018"
9 |
10 | [dependencies]
11 | log = "0.4.14"
12 | env_logger = "0.8.3"
13 | prometheus = "0.12.0"
14 | hyper = "0.14.12"
15 | hyper-tls = "0.5.0"
16 | json-patch = "0.2.6"
17 | rocket = "0.4.11"
18 | rocket_contrib = { version = "0.4.10", features = ["json"] }
19 | rocket_cors = "0.5.2"
20 | rocket_oauth2 = { version = "0.4.1", features = ["hyper_sync_rustls_adapter"] }
21 | serde = { version = "1.0.125", features = ["derive"] }
22 | serde_json = "1.0.64"
23 | serde_yaml = "0.8.17"
24 | kube = { version = "0.60.0", default-features = true, features = ["jsonpatch"] }
25 | k8s-openapi = { version = "0.13.0", default-features = false, features = ["v1_22"] }
26 | tokio = {version = "1.13.1", features = ["macros", "rt-multi-thread"] }
27 | thiserror = "1.0"
28 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | # The server Dockerfile.
2 | #
3 | # A multi-stage docker image (https://docs.docker.com/develop/develop-images/multistage-build/)
4 | # Based on https://github.com/bjornmolin/rust-minimal-docker
5 |
6 | FROM clux/muslrust:nightly-2021-08-17 AS builder
7 |
8 | WORKDIR /opt
9 |
10 | ENV BINARY_NAME=playground
11 |
12 | # Build the project with target x86_64-unknown-linux-musl
13 |
14 | # Build dummy main with the project's Cargo lock and toml
15 | # This is a docker trick in order to avoid downloading and building
16 | # dependencies when lock and toml not is modified.
17 |
18 | COPY Cargo.* ./
19 |
20 | RUN mkdir src \
21 | && echo "fn main() {print!(\"Dummy main\");} // dummy file" > src/main.rs \
22 | && set -x && cargo build --locked --target x86_64-unknown-linux-musl --release \
23 | && set -x && rm target/x86_64-unknown-linux-musl/release/deps/$BINARY_NAME*
24 |
25 | # Now add the rest of the project and build the real main
26 |
27 | COPY src src
28 |
29 | RUN set -x && cargo build --frozen --release --out-dir=/opt/bin -Z unstable-options --target x86_64-unknown-linux-musl
30 |
31 | LABEL stage=builder
32 |
33 | ##########################
34 | # Runtime #
35 | ##########################
36 |
37 | FROM scratch
38 |
39 | ARG GITHUB_SHA
40 |
41 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
42 |
43 | ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\
44 | SSL_CERT_DIR=/etc/ssl/certs\
45 | GITHUB_SHA=$GITHUB_SHA\
46 | RUST_BACKTRACE=full\
47 | RUST_LOG="warn,playground=info"
48 |
49 | COPY --from=builder /opt/bin/$BINARY_NAME /
50 |
51 | CMD ["/playground"]
52 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # Quick start
2 |
3 | ## Configuration
4 |
5 | ### ConfigMaps
6 |
7 | ## Development server
8 |
9 | ```bash
10 | cargo run
11 | ```
--------------------------------------------------------------------------------
/backend/src/error.rs:
--------------------------------------------------------------------------------
1 | ///! Error type for the whole project
2 | ///
3 | use std::result;
4 | use thiserror::Error;
5 |
6 | /// A specialized [`Result`] type.
7 | pub type Result = result::Result;
8 |
9 | #[derive(Error, Debug)]
10 | pub enum Error {
11 | #[error("Unauthorized")]
12 | Unauthorized(/*Permission*/),
13 | #[error("Missing data {0}")]
14 | MissingData(&'static str),
15 | #[error("Failure: {0}")]
16 | Failure(#[from] Box),
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/github.rs:
--------------------------------------------------------------------------------
1 | //! GitHub utility functions
2 |
3 | use body::aggregate;
4 | use core::fmt;
5 | use hyper::{
6 | body::{self, Buf},
7 | client::HttpConnector,
8 | header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT},
9 | http::request::Builder,
10 | Body, Client, Request,
11 | };
12 | use hyper_tls::HttpsConnector;
13 | use serde::de::DeserializeOwned;
14 | use serde_json::from_reader;
15 | use std::error::Error as StdError;
16 |
17 | // Custom Error type
18 | #[derive(Debug)]
19 | struct Error {
20 | pub cause: GitHubError,
21 | }
22 |
23 | impl fmt::Display for Error {
24 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25 | write!(f, "{}", self.cause.message)
26 | }
27 | }
28 |
29 | impl StdError for Error {
30 | fn description(&self) -> &str {
31 | &self.cause.message
32 | }
33 | }
34 |
35 | /// User information to be retrieved from the GitHub API.
36 | #[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
37 | pub struct GitHubUser {
38 | pub login: String,
39 | pub organizations_url: String,
40 | }
41 |
42 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
43 | pub struct GitHubOrg {
44 | pub login: String,
45 | }
46 |
47 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
48 | pub struct GitHubError {
49 | pub message: String,
50 | pub documentation_url: Option,
51 | pub errors: Option>,
52 | }
53 |
54 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
55 | pub struct GitHubClientError {
56 | pub resource: String,
57 | pub field: String,
58 | pub code: String,
59 | }
60 |
61 | /// Create a new `Client`
62 | fn create_client() -> Client> {
63 | Client::builder().build(HttpsConnector::new())
64 | }
65 |
66 | /// Create a `Request` `Builder` with necessary headers
67 | fn create_request_builder(token: &str) -> Builder {
68 | Request::builder()
69 | .header(CONTENT_TYPE, "application/vnd.github.v3+json")
70 | .header(USER_AGENT, "Substrate Playground")
71 | .header(AUTHORIZATION, format!("token {}", token))
72 | }
73 |
74 | // Send a fresh `Request` created from a `Builder`, sends it and return the object `T` parsed from JSON.
75 | async fn send(builder: Builder) -> Result>
76 | where
77 | T: DeserializeOwned,
78 | {
79 | let client = create_client();
80 | let req = builder.body(Body::default())?;
81 | let res = client.request(req).await?;
82 | let status = res.status();
83 | let whole_body = aggregate(res).await?;
84 | if status.is_success() {
85 | from_reader(whole_body.reader()).map_err(Into::into)
86 | } else {
87 | let cause: GitHubError = from_reader(whole_body.reader())?;
88 | Err(Error { cause }.into())
89 | }
90 | }
91 |
92 | ///
93 | /// Returns current GitHubUser represented by a `token`.
94 | ///
95 | /// # Arguments
96 | ///
97 | /// * `token` - a github token
98 | ///
99 | pub async fn current_user(token: &str) -> Result> {
100 | let builder = create_request_builder(token).uri("https://api.github.com/user");
101 | send(builder).await
102 | }
103 |
104 | ///
105 | /// Returns a Vec associated to a GitHubUser.
106 | ///
107 | /// # Arguments
108 | ///
109 | /// * `token` - a github token
110 | /// * `user` - a GitHubUser
111 | ///
112 | pub async fn orgs(token: &str, user: &GitHubUser) -> Result, Box> {
113 | let builder = create_request_builder(token).uri(user.organizations_url.as_str());
114 | send(builder).await
115 | }
116 |
--------------------------------------------------------------------------------
/backend/src/main.rs:
--------------------------------------------------------------------------------
1 | #![feature(async_closure, proc_macro_hygiene, decl_macro)]
2 |
3 | mod api;
4 | mod error;
5 | mod github;
6 | mod kubernetes;
7 | mod manager;
8 | mod metrics;
9 | mod prometheus;
10 | mod types;
11 |
12 | use crate::manager::Manager;
13 | use crate::prometheus::PrometheusMetrics;
14 | use ::prometheus::Registry;
15 | use github::GitHubUser;
16 | use rocket::fairing::AdHoc;
17 | use rocket::{catchers, config::Environment, http::Method, routes};
18 | use rocket_cors::{AllowedOrigins, CorsOptions};
19 | use rocket_oauth2::{HyperSyncRustlsAdapter, OAuth2, OAuthConfig, StaticProvider};
20 | use std::{env, error::Error};
21 |
22 | pub struct Context {
23 | manager: Manager,
24 | }
25 |
26 | #[tokio::main]
27 | async fn main() -> Result<(), Box> {
28 | // Initialize log configuration. Reads `RUST_LOG` if any, otherwise fallsback to `default`
29 | if env::var("RUST_LOG").is_err() {
30 | env::set_var("RUST_LOG", "info");
31 | }
32 | env_logger::init();
33 |
34 | // Prints basic details
35 | log::info!("Running ROCKET in {:?} mode", Environment::active()?);
36 |
37 | match env::var("GITHUB_SHA") {
38 | Ok(version) => log::info!("Version {}", version),
39 | Err(_) => log::warn!("Unknown version"),
40 | }
41 |
42 | let manager = Manager::new().await?;
43 | let engine = manager.clone().engine;
44 | manager.clone().spawn_background_thread();
45 |
46 | // Configure CORS
47 | let cors = CorsOptions {
48 | allowed_origins: AllowedOrigins::all(),
49 | allowed_methods: vec![Method::Get, Method::Post, Method::Put, Method::Delete]
50 | .into_iter()
51 | .map(From::from)
52 | .collect(),
53 | allow_credentials: true,
54 | ..Default::default()
55 | }
56 | .to_cors()?;
57 |
58 | let registry = Registry::new_custom(Some("playground".to_string()), None)?;
59 | manager.clone().metrics.register(registry.clone())?;
60 | let prometheus = PrometheusMetrics::with_registry(registry);
61 | let error = rocket::ignite()
62 | .register(catchers![api::bad_request_catcher])
63 | .attach(cors)
64 | .attach(AdHoc::on_attach("github", |rocket| {
65 | let config = OAuthConfig::new(
66 | StaticProvider {
67 | auth_uri: "https://github.com/login/oauth/authorize".into(),
68 | token_uri: "https://github.com/login/oauth/access_token".into(),
69 | },
70 | engine.configuration.github_client_id,
71 | engine.secrets.github_client_secret,
72 | None,
73 | );
74 | Ok(rocket.attach(OAuth2::::custom(
75 | HyperSyncRustlsAdapter::default().basic_auth(false),
76 | config,
77 | )))
78 | }))
79 | .mount(
80 | "/api",
81 | routes![
82 | api::get,
83 | api::get_unlogged,
84 | // Users
85 | api::get_user,
86 | api::list_users,
87 | api::create_user,
88 | api::update_user,
89 | api::delete_user,
90 | // Current Session
91 | api::get_current_session,
92 | api::get_current_session_unlogged,
93 | api::create_current_session,
94 | api::create_current_session_unlogged,
95 | api::update_current_session,
96 | api::update_current_session_unlogged,
97 | api::delete_current_session,
98 | api::delete_current_session_unlogged,
99 | // Sessions
100 | api::get_session,
101 | api::list_sessions,
102 | api::create_session,
103 | api::update_session,
104 | api::delete_session,
105 | // Pools
106 | api::get_pool,
107 | api::list_pools,
108 | // Login
109 | api::github_login,
110 | api::post_install_callback,
111 | api::login,
112 | api::logout,
113 | ],
114 | )
115 | .mount("/metrics", prometheus)
116 | .manage(Context { manager })
117 | .launch();
118 |
119 | // Launch blocks unless an error is returned
120 | Err(error.into())
121 | }
122 |
--------------------------------------------------------------------------------
/backend/src/metrics.rs:
--------------------------------------------------------------------------------
1 | use prometheus::{
2 | exponential_buckets, histogram_opts, opts, Error, HistogramVec, IntCounterVec, Registry,
3 | };
4 |
5 | #[derive(Debug, Clone)]
6 | pub struct Metrics {
7 | deploy_counter: IntCounterVec,
8 | deploy_failures_counter: IntCounterVec,
9 | undeploy_counter: IntCounterVec,
10 | undeploy_failures_counter: IntCounterVec,
11 | deploy_duration: HistogramVec,
12 | }
13 |
14 | impl Metrics {
15 | const TEMPLATE_LABEL: &'static str = "template";
16 |
17 | pub fn new() -> Result {
18 | let opts = histogram_opts!(
19 | "deploy_duration",
20 | "Deployment duration in seconds",
21 | exponential_buckets(1.0, 2.0, 8).unwrap()
22 | );
23 | Ok(Metrics {
24 | deploy_counter: IntCounterVec::new(
25 | opts!("deploy_counter", "Count of deployments"),
26 | &[Self::TEMPLATE_LABEL],
27 | )?,
28 | deploy_failures_counter: IntCounterVec::new(
29 | opts!("deploy_failures_counter", "Count of deployment failures"),
30 | &[Self::TEMPLATE_LABEL],
31 | )?,
32 | undeploy_counter: IntCounterVec::new(
33 | opts!("undeploy_counter", "Count of undeployment"),
34 | &[],
35 | )?,
36 | undeploy_failures_counter: IntCounterVec::new(
37 | opts!(
38 | "undeploy_failures_counter",
39 | "Count of undeployment failures"
40 | ),
41 | &[],
42 | )?,
43 | deploy_duration: HistogramVec::new(opts, &[])?,
44 | })
45 | }
46 |
47 | /// Register all metrics in provided `Registry`
48 | pub fn register(self, registry: Registry) -> Result<(), Error> {
49 | registry.register(Box::new(self.deploy_counter))?;
50 | registry.register(Box::new(self.deploy_failures_counter))?;
51 | registry.register(Box::new(self.undeploy_counter))?;
52 | registry.register(Box::new(self.undeploy_failures_counter))?;
53 | registry.register(Box::new(self.deploy_duration))?;
54 | Ok(())
55 | }
56 | }
57 |
58 | // Helper functions
59 | impl Metrics {
60 | pub fn inc_deploy_counter(&self, template: &str) {
61 | self.deploy_counter.with_label_values(&[template]).inc();
62 | }
63 |
64 | pub fn inc_deploy_failures_counter(&self, template: &str) {
65 | self.deploy_failures_counter
66 | .with_label_values(&[template])
67 | .inc();
68 | }
69 |
70 | pub fn inc_undeploy_counter(&self) {
71 | self.undeploy_counter.with_label_values(&[]).inc();
72 | }
73 |
74 | pub fn inc_undeploy_failures_counter(&self) {
75 | self.undeploy_failures_counter.with_label_values(&[]).inc();
76 | }
77 |
78 | pub fn observe_deploy_duration(&self, duration: f64) {
79 | self.deploy_duration
80 | .with_label_values(&[])
81 | .observe(duration);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/backend/src/prometheus.rs:
--------------------------------------------------------------------------------
1 | //! Adapted from https://github.com/sd2k/rocket_prometheus/blob/master/src/lib.rs
2 |
3 | use prometheus::{Encoder, Registry, TextEncoder};
4 | use rocket::{
5 | handler::Outcome,
6 | http::{ContentType, Method},
7 | response::Content,
8 | Data, Handler, Request, Route,
9 | };
10 |
11 | #[derive(Clone)]
12 | pub struct PrometheusMetrics {
13 | pub registry: Registry,
14 | }
15 |
16 | impl PrometheusMetrics {
17 | /// Create a new `PrometheusMetrics` with a custom `Registry`.
18 | pub fn with_registry(registry: Registry) -> Self {
19 | PrometheusMetrics { registry }
20 | }
21 | }
22 |
23 | impl Handler for PrometheusMetrics {
24 | fn handle<'r>(&self, req: &'r Request, _: Data) -> Outcome<'r> {
25 | // Gather the metrics.
26 | let mut buffer = vec![];
27 | let encoder = TextEncoder::new();
28 | encoder
29 | .encode(&self.registry.gather(), &mut buffer)
30 | .unwrap();
31 | let body = String::from_utf8(buffer).unwrap();
32 | Outcome::from(
33 | req,
34 | Content(
35 | ContentType::with_params(
36 | "text",
37 | "plain",
38 | &[("version", "0.0.4"), ("charset", "utf-8")],
39 | ),
40 | body,
41 | ),
42 | )
43 | }
44 | }
45 |
46 | impl From for Vec {
47 | fn from(other: PrometheusMetrics) -> Self {
48 | vec![Route::new(Method::Get, "/", other)]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/backend/src/types.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use std::{
3 | collections::BTreeMap,
4 | str::FromStr,
5 | time::{Duration, SystemTime},
6 | };
7 |
8 | #[derive(Serialize, Clone, Debug)]
9 | pub struct Session {
10 | pub user_id: String,
11 | pub template: Template,
12 | pub url: String,
13 | pub pod: Pod,
14 | #[serde(with = "duration")]
15 | pub duration: Duration,
16 | pub node: String,
17 | }
18 |
19 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
20 | pub enum Phase {
21 | Pending,
22 | Running,
23 | Succeeded,
24 | Failed,
25 | Unknown,
26 | }
27 |
28 | impl FromStr for Phase {
29 | type Err = String;
30 | fn from_str(s: &str) -> Result {
31 | match s {
32 | "Pending" => Ok(Phase::Pending),
33 | "Running" => Ok(Phase::Running),
34 | "Succeeded" => Ok(Phase::Succeeded),
35 | "Failed" => Ok(Phase::Failed),
36 | "Unknown" => Ok(Phase::Unknown),
37 | _ => Err(format!("'{}' is not a valid value for Phase", s)),
38 | }
39 | }
40 | }
41 |
42 | #[derive(Serialize, Clone, Debug)]
43 | #[serde(rename_all = "camelCase")]
44 | pub struct Pod {
45 | pub phase: Phase,
46 | pub reason: String,
47 | pub message: String,
48 | #[serde(with = "system_time")]
49 | pub start_time: Option,
50 | pub container: Option,
51 | }
52 |
53 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
54 | pub enum ContainerPhase {
55 | Running,
56 | Terminated,
57 | Waiting,
58 | Unknown,
59 | }
60 |
61 | #[derive(Serialize, Clone, Debug)]
62 | pub struct ContainerStatus {
63 | pub phase: ContainerPhase,
64 | pub reason: Option,
65 | pub message: Option,
66 | }
67 |
68 | #[derive(Serialize, Clone, Debug)]
69 | #[serde(rename_all = "camelCase")]
70 | pub struct Pool {
71 | pub name: String,
72 | pub instance_type: Option,
73 | pub nodes: Vec,
74 | }
75 |
76 | #[derive(Serialize, Clone, Debug)]
77 | #[serde(rename_all = "camelCase")]
78 | pub struct Node {
79 | pub hostname: String,
80 | }
81 |
82 | #[derive(Deserialize, Clone, Debug)]
83 | #[serde(rename_all = "camelCase")]
84 | pub struct SessionConfiguration {
85 | pub template: String,
86 | #[serde(default)]
87 | #[serde(with = "option_duration")]
88 | pub duration: Option,
89 | pub pool_affinity: Option,
90 | }
91 |
92 | #[derive(Deserialize, Clone, Debug)]
93 | pub struct SessionUpdateConfiguration {
94 | #[serde(default)]
95 | #[serde(with = "option_duration")]
96 | pub duration: Option,
97 | }
98 |
99 | #[derive(Serialize, Debug, Clone)]
100 | #[serde(rename_all = "camelCase")]
101 | pub struct SessionDefaults {
102 | #[serde(with = "duration")]
103 | pub duration: Duration,
104 | #[serde(with = "duration")]
105 | pub max_duration: Duration,
106 | pub pool_affinity: String,
107 | pub max_sessions_per_pod: usize,
108 | }
109 |
110 | #[derive(Serialize, Deserialize, Clone, Debug)]
111 | #[serde(rename_all = "camelCase")]
112 | pub struct User {
113 | pub admin: bool,
114 | #[serde(default = "default_as_false")]
115 | pub can_customize_duration: bool,
116 | #[serde(default = "default_as_false")]
117 | pub can_customize_pool_affinity: bool,
118 | pub pool_affinity: Option,
119 | }
120 |
121 | #[derive(Serialize, Deserialize, Clone, Debug)]
122 | #[serde(rename_all = "camelCase")]
123 | pub struct UserConfiguration {
124 | pub admin: bool,
125 | #[serde(default = "default_as_false")]
126 | pub can_customize_duration: bool,
127 | #[serde(default = "default_as_false")]
128 | pub can_customize_pool_affinity: bool,
129 | pub pool_affinity: Option,
130 | }
131 |
132 | #[derive(Serialize, Deserialize, Clone, Debug)]
133 | #[serde(rename_all = "camelCase")]
134 | pub struct UserUpdateConfiguration {
135 | pub admin: bool,
136 | #[serde(default = "default_as_false")]
137 | pub can_customize_duration: bool,
138 | #[serde(default = "default_as_false")]
139 | pub can_customize_pool_affinity: bool,
140 | pub pool_affinity: Option,
141 | }
142 | #[derive(Serialize, Deserialize, Clone, Debug)]
143 | pub struct LoggedUser {
144 | pub id: String,
145 | pub admin: bool,
146 | pub organizations: Vec,
147 | pub pool_affinity: Option,
148 | pub can_customize_duration: bool,
149 | pub can_customize_pool_affinity: bool,
150 | }
151 |
152 | impl LoggedUser {
153 | pub fn is_paritytech_member(&self) -> bool {
154 | self.organizations.contains(&"paritytech".to_string())
155 | }
156 | pub fn can_customize_duration(&self) -> bool {
157 | self.admin || self.can_customize_duration || self.is_paritytech_member()
158 | }
159 |
160 | pub fn can_customize_pool_affinity(&self) -> bool {
161 | self.admin || self.can_customize_pool_affinity || self.is_paritytech_member()
162 | }
163 |
164 | pub fn has_admin_read_rights(&self) -> bool {
165 | self.admin || self.is_paritytech_member()
166 | }
167 |
168 | pub fn has_admin_edit_rights(&self) -> bool {
169 | self.admin
170 | }
171 | }
172 |
173 | #[derive(Serialize, Deserialize, Clone, Debug)]
174 | pub struct Template {
175 | pub name: String,
176 | pub image: String,
177 | pub description: String,
178 | pub tags: Option>,
179 | pub runtime: Option,
180 | }
181 |
182 | #[derive(Serialize, Deserialize, Clone, Debug)]
183 | pub struct RuntimeConfiguration {
184 | pub env: Option>,
185 | pub ports: Option>,
186 | }
187 |
188 | #[derive(Serialize, Deserialize, Clone, Debug)]
189 | pub struct NameValuePair {
190 | pub name: String,
191 | pub value: String,
192 | }
193 |
194 | #[derive(Serialize, Deserialize, Clone, Debug)]
195 | pub struct Port {
196 | pub name: String,
197 | pub protocol: Option,
198 | pub path: String,
199 | pub port: i32,
200 | pub target: Option,
201 | }
202 |
203 | #[derive(Serialize, Deserialize, Clone, Debug)]
204 | #[serde(rename_all = "camelCase")]
205 | pub struct Command {
206 | pub name: String,
207 | pub run: String,
208 | pub working_directory: String,
209 | }
210 |
211 | /// Utils
212 |
213 | mod system_time {
214 | use serde::{self, Serializer};
215 | use std::time::SystemTime;
216 |
217 | pub fn serialize(date: &Option, serializer: S) -> Result
218 | where
219 | S: Serializer,
220 | {
221 | match date.and_then(|v| v.elapsed().ok()) {
222 | Some(value) => serializer.serialize_some(&value.as_secs()),
223 | None => serializer.serialize_none(),
224 | }
225 | }
226 | }
227 |
228 | mod option_duration {
229 | use serde::{self, Deserialize, Deserializer};
230 | use std::time::Duration;
231 |
232 | pub fn deserialize<'de, D>(deserializer: D) -> Result