Skip to content

Commit

Permalink
feat(operator): rollups operator acting on CRD
Browse files Browse the repository at this point in the history
  • Loading branch information
tuler committed Aug 16, 2023
1 parent 6ec6549 commit a7b6c34
Show file tree
Hide file tree
Showing 21 changed files with 7,975 additions and 181 deletions.
1 change: 1 addition & 0 deletions packages/rollups-operator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/dist
12 changes: 12 additions & 0 deletions packages/rollups-operator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Cartesi Rollups Operator

## Development

```shell
$ kubectl create secret generic mnemonic --from-literal=MNEMONIC="test test test test test test test test test test test junk"
$ helm install redis bitnami/redis --wait --set auth.enabled=false --set architecture=standalone --set image.tag=6.2-debian-11
redis-master.default.svc.cluster.local
$ helm install postgres oci://registry-1.docker.io/bitnamicharts/postgresql
postgres-postgresql.default.svc.cluster.local
$ export POSTGRES_PASSWORD=$(kubectl get secret --namespace default postgres-postgresql -o jsonpath="{.data.postgres-password}" | base64 -d)
```
3 changes: 3 additions & 0 deletions packages/rollups-operator/bin/dev.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off

node "%~dp0\dev" %*
25 changes: 25 additions & 0 deletions packages/rollups-operator/bin/dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env node --no-warnings=ExperimentalWarning --loader ts-node/esm

import oclif from "@oclif/core";
import path from "node:path";
import url from "node:url";
import { register } from "ts-node";

// In dev mode -> use ts-node and dev plugins
process.env.NODE_ENV = "development";

const project = path.join(
path.dirname(url.fileURLToPath(import.meta.url)),
"..",
"tsconfig.json"
);
register({ project });

// In dev mode, always show stack traces
oclif.settings.debug = true;

// Start the CLI
oclif
.run(process.argv.slice(2), import.meta.url)
.then(oclif.flush)
.catch(oclif.Errors.handle);
3 changes: 3 additions & 0 deletions packages/rollups-operator/bin/run.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off

node "%~dp0\run" %*
8 changes: 8 additions & 0 deletions packages/rollups-operator/bin/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env node

import oclif from "@oclif/core";

oclif
.run(process.argv.slice(2), import.meta.url)
.then(oclif.flush)
.catch(oclif.Errors.handle);
18 changes: 18 additions & 0 deletions packages/rollups-operator/config/devnet/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: cartesi-rollups-config
data:
redis: redis://redis-master.default.svc.cluster.local:6379
rpc_url: http://host.docker.internal:8545
rpc_ws_url: ws://host.docker.internal:8545
chain_id: "31337"
epoch_duration: "3600"
contracts.json: |
{
"contracts": {
"Authority": { "address": "0x5050F233F2312B1636eb7CF6c7876D9cC6ac4785" },
"History": { "address": "0x4FF8BD9122b7D91d56Dd5c88FE6891Fb3c0b5281" },
"InputBox": { "address": "0x59b22D57D4f067708AB0c00552767405926dc768" }
}
}
11 changes: 11 additions & 0 deletions packages/rollups-operator/config/devnet/database.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: cartesi-rollups-database
type: Opaque
data:
database: cG9zdGdyZXM= # base64('postgres')
hostname: cG9zdGdyZXMtcG9zdGdyZXNxbC5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2Fs # base64('postgresql-postgresql.svc.cluster.local')
password: R0J3SlBmT1p5dA== # base64('GBwJPfOZyt')
port: NTQzMg== # base64('5432')
username: cG9zdGdyZXM= # base64('postgres')
7 changes: 7 additions & 0 deletions packages/rollups-operator/config/devnet/mnemonic.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: cartesi-rollups-mnemonic
type: Opaque
data:
MNEMONIC: dGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IGp1bms= # base64('test test test test test test test test test test test junk')
8 changes: 8 additions & 0 deletions packages/rollups-operator/config/traefik/middleware.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: cartesi-rollups-strip-address
spec:
stripPrefixRegex:
regex:
- "/0x[a-fA-F0-9]{40}/"
83 changes: 83 additions & 0 deletions packages/rollups-operator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"name": "@cartesi/rollups-operator",
"version": "0.1.0",
"description": "Cartesi Rollups Operator",
"author": "Danilo Tuler <[email protected]>",
"bin": {
"sunodo": "./bin/run.js"
},
"type": "module",
"homepage": "https://github.com/cartesi/helm-charts",
"license": "Apache-2.0",
"exports": "./dist/index.js",
"repository": "cartesi/helm-charts",
"scripts": {
"build": "run-s clean compile copy-files",
"clean": "rimraf dist",
"compile": "tsc -b",
"copy-files": "copyfiles -u 1 src/**/*.yml dist",
"dev": "bin/dev.js --help",
"lint": "eslint . --ext .ts --config .eslintrc",
"postpack": "rimraf oclif.manifest.json",
"posttest": "yarn lint",
"prepack": "yarn build && oclif manifest && oclif readme",
"test": "vitest",
"version": "run-s prepack"
},
"files": [
"/bin",
"/dist",
"/oclif.manifest.json"
],
"dependencies": {
"@inquirer/prompts": "^3.0",
"@kubernetes/client-node": "^0.18",
"@oclif/core": "^2.11",
"@oclif/plugin-help": "^5.2",
"@oclif/plugin-plugins": "^3.1",
"@oclif/plugin-update": "^3.1",
"async": "^3.2",
"gaxios": "^6.1"
},
"devDependencies": {
"@oclif/test": "^2.3",
"@types/async": "^3.2",
"@types/inquirer": "^9",
"@types/node": "^20",
"@types/node-fetch": "^2.6",
"copyfiles": "^2",
"eslint": "^8",
"eslint-config-custom": "*",
"eslint-config-oclif": "^4",
"eslint-config-oclif-typescript": "^1",
"npm-run-all": "^4",
"oclif": "^3.11",
"rimraf": "^5",
"ts-node": "^10",
"tsconfig": "*",
"tslib": "^2",
"typescript": "^5",
"vitest": "^0.34"
},
"oclif": {
"bin": "cartesi-rollups-operator",
"default": "start",
"dirname": "cartesi",
"commands": "./dist/commands",
"plugins": [
"@oclif/plugin-help"
],
"topicSeparator": " ",
"macos": {
"identifier": "io.cartesi.rollups.operator"
}
},
"engines": {
"node": ">=18.0.0"
},
"bugs": "https://github.com/cartesi/helm-charts/issues",
"keywords": [
"oclif"
],
"types": "dist/index.d.ts"
}
190 changes: 190 additions & 0 deletions packages/rollups-operator/src/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import path from "path";
import k8s, { KubeConfig, KubernetesObject } from "@kubernetes/client-node";

import Operator, { ResourceEvent, ResourceEventType } from "./operator.js";
import Synthetizer, { Resources } from "./synth.js";

export interface DAppCustomResource extends KubernetesObject {
spec: DAppCustomResourceSpec;
status: DAppCustomResourceStatus;
}

export interface DAppCustomResourceSpec {
address: string;
blockHash: string;
blockNumber: string;
transactionHash: string;
cid: string;
}

export interface DAppCustomResourceStatus {
observedGeneration?: number;
}

export default class DAppOperator extends Operator {
protected synthetizer: Synthetizer;
protected namespace: string;

constructor(kubeConfig: KubeConfig, namespace: string) {
super(kubeConfig, console);
this.namespace = namespace;
// instantiate resources synthetizer
this.synthetizer = new Synthetizer();
}

protected async init() {
// register CRD
const crdFile = path.join(
path.dirname(new URL(import.meta.url).pathname),
"dapp.yaml",
);
const { group, versions, plural } =
await this.registerCustomResourceDefinition(crdFile);

// watch CRD resources
await this.watchResource(
group,
versions[0].name,
plural,
async (e) => {
console.log(e);
if (
e.type === ResourceEventType.Added ||
e.type === ResourceEventType.Modified
) {
if (
!(await this.handleResourceFinalizer(
e,
group,
(event) => this.delete(event),
))
) {
await this.create(e);
}
} else if (e.type === ResourceEventType.Deleted) {
await this.delete(e);
}
},
this.namespace,
);
}

protected async print(resources: Resources) {
const configMaps = resources.configMaps
.map((r) => JSON.stringify(r, undefined, 4))
.join("\n---\n");
const deployments = resources.deployments
.map((r) => JSON.stringify(r, undefined, 4))
.join("\n---\n");
const services = resources.services
.map((r) => JSON.stringify(r, undefined, 4))
.join("\n---\n");

console.log(configMaps);
console.log("---");
console.log(deployments);
console.log("---");
console.log(services);
}

protected async apply(resources: Resources) {
const core = this.k8sApi;
const apps = this.kubeConfig.makeApiClient(k8s.AppsV1Api);
const network = this.kubeConfig.makeApiClient(k8s.NetworkingV1Api);
const { namespace } = this;

const ignore409 = (err: any) => {
if ((err as any).response?.statusCode !== 409) {
throw err;
}
};

// create all configMaps
resources.configMaps.forEach((r) => {
console.log(`creating ${r.kind} ${r.metadata!.name}`);
core.createNamespacedConfigMap(namespace, r)
.then(({ body }) => {
console.log(`created ${body.kind} ${body.metadata!.name}`);
})
.catch(ignore409);
});

// create all deployments
resources.deployments.forEach((r) => {
console.log(`creating ${r.kind} ${r.metadata!.name}`);
apps.createNamespacedDeployment(namespace, r)
.then(({ body }) => {
console.log(`created ${body.kind} ${body.metadata!.name}`);
})
.catch(ignore409);
});

// create all services
resources.services.forEach((r) => {
console.log(`creating ${r.kind} ${r.metadata!.name}`);
core.createNamespacedService(namespace, r)
.then(({ body }) => {
console.log(`created ${body.kind} ${body.metadata!.name}`);
})
.catch(ignore409);
});

// create all ingresses
resources.ingresses.forEach((r) => {
console.log(`creating ${r.kind} ${r.metadata!.name}`);
network
.createNamespacedIngress(namespace, r)
.then(({ body }) => {
console.log(`created ${body.kind} ${body.metadata!.name}`);
})
.catch(ignore409);
});
}

protected async create(event: ResourceEvent) {
const resources = this.synthetizer.synth(
event.object as DAppCustomResource,
);
await this.print(resources);
await this.apply(resources);
}

protected async delete(event: ResourceEvent) {
const namespace = event.meta.namespace ?? "default";
const core = this.k8sApi;
const apps = this.kubeConfig.makeApiClient(k8s.AppsV1Api);
const network = this.kubeConfig.makeApiClient(k8s.NetworkingV1Api);

const resources = this.synthetizer.synth(
event.object as DAppCustomResource,
);

// delete ingresses
await Promise.all(
resources.ingresses.map((r) =>
network.deleteNamespacedIngress(r.metadata!.name!, namespace),
),
);

// delete services
await Promise.all(
resources.services.map((r) =>
core.deleteNamespacedService(r.metadata!.name!, namespace),
),
);

// delete deployments
await Promise.all(
resources.deployments.map((r) =>
apps.deleteNamespacedDeployment(r.metadata!.name!, namespace),
),
);

// delete configMaps
await Promise.all(
resources.configMaps.map((r) =>
core.deleteNamespacedConfigMap(r.metadata!.name!, namespace),
),
);
}
}
Loading

0 comments on commit a7b6c34

Please sign in to comment.