From b5cade25326b30562c3700819fa1c7d4a53ccf4f Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Tue, 23 Apr 2024 15:23:10 +0200 Subject: [PATCH] refactor(sdl): separates yaml parser and factory from SDL (#77) --- .commitlintrc.json | 18 +--- package-lock.json | 22 ++++- package.json | 5 +- src/config/network.ts | 9 ++ src/sdl/SDLFactory.ts | 23 +++++ src/sdl/YamlSDLParser.ts | 192 +++++++++++++++++++++++++++++++++++++++ src/sdl/index.ts | 41 ++++++--- src/sdl/types.ts | 107 +++++++++------------- src/types/network.ts | 14 +++ 9 files changed, 335 insertions(+), 96 deletions(-) create mode 100644 src/config/network.ts create mode 100644 src/sdl/SDLFactory.ts create mode 100644 src/sdl/YamlSDLParser.ts create mode 100644 src/types/network.ts diff --git a/.commitlintrc.json b/.commitlintrc.json index 3337497..01089c7 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,18 +1,6 @@ { - "extends": [ - "@commitlint/config-conventional" - ], + "extends": ["@commitlint/config-conventional"], "rules": { - "scope-enum": [ - 2, - "always", - [ - "certificates", - "network", - "wallet", - "api", - "stargate" - ] - ] + "scope-enum": [2, "always", ["certificates", "network", "wallet", "api", "stargate", "sdl"]] } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 6e2bb7d..60a737d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,14 @@ "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.2", "keytar": "^7.7.0", + "lodash": "^4.17.21", "node-fetch": "2", "pkijs": "^3.0.0", "process": "^0.11.10", "pvutils": "^1.0.17", "simple-jsonrpc-js": "^1.2.0", - "sort-json": "^2.0.1" + "sort-json": "^2.0.1", + "zod": "^3.23.3" }, "devDependencies": { "@commitlint/cli": "^19.2.2", @@ -42,6 +44,7 @@ "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.5", "@types/json-stable-stringify": "^1.0.34", + "@types/lodash": "^4.17.0", "@types/node-fetch": "2", "@types/sinon": "^10.0.11", "@types/tap": "^15.0.5", @@ -3911,6 +3914,12 @@ "integrity": "sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "dev": true + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -10790,8 +10799,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-es": { "version": "4.17.21", @@ -21088,6 +21096,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.3.tgz", + "integrity": "sha512-tPvq1B/2Yu/dh2uAIH2/BhUlUeLIUvAjr6dpL/75I0pCYefHgjhXk1o1Kob3kTU8C7yU1j396jFHlsVWFi9ogg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 08a47c9..ca65c29 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,14 @@ "js-yaml": "^4.1.0", "json-stable-stringify": "^1.0.2", "keytar": "^7.7.0", + "lodash": "^4.17.21", "node-fetch": "2", "pkijs": "^3.0.0", "process": "^0.11.10", "pvutils": "^1.0.17", "simple-jsonrpc-js": "^1.2.0", - "sort-json": "^2.0.1" + "sort-json": "^2.0.1", + "zod": "^3.23.3" }, "description": "Akash Network JS SDK", "devDependencies": { @@ -40,6 +42,7 @@ "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.5", "@types/json-stable-stringify": "^1.0.34", + "@types/lodash": "^4.17.0", "@types/node-fetch": "2", "@types/sinon": "^10.0.11", "@types/tap": "^15.0.5", diff --git a/src/config/network.ts b/src/config/network.ts new file mode 100644 index 0000000..dc227c1 --- /dev/null +++ b/src/config/network.ts @@ -0,0 +1,9 @@ +import { NetworkId } from "../types/network"; + +export const MAINNET_ID: NetworkId = "mainnet"; +export const SANDBOX_ID: NetworkId = "sandbox"; + +export const USDC_IBC_DENOMS: Record = { + [MAINNET_ID]: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1", + [SANDBOX_ID]: "ibc/12C6A0C374171B595A0A9E18B83FA09D295FB1F2D8C6DAA3AC28683471752D84" +}; diff --git a/src/sdl/SDLFactory.ts b/src/sdl/SDLFactory.ts new file mode 100644 index 0000000..a238c11 --- /dev/null +++ b/src/sdl/SDLFactory.ts @@ -0,0 +1,23 @@ +import { NetworkId } from "../types/network"; +import { NetworkVersion, BetaSdl, NetworkBeta2, NetworkBeta3 } from "./types"; +import { SDL } from "./index"; +import { YamlSDLParser } from "./YamlSDLParser"; + +export class SDLFactory { + static create({ data, version, network }: { data: BetaSdl; version: NetworkVersion; network: NetworkId }): SDL { + return new SDL(data, version, network); + } + + static createFromYaml({ data, version, network }: { data: string; version: NetworkBeta2; network: NetworkId }): SDL; + static createFromYaml({ data, version, network }: { data: string; version: NetworkBeta3; network: NetworkId }): SDL; + static createFromYaml({ data, version, network }: { data: string; version: NetworkVersion; network: NetworkId }): SDL { + let parsed: BetaSdl; + if (version === "beta2") { + parsed = YamlSDLParser.parse(data, version); + } else { + parsed = YamlSDLParser.parse(data, version); + } + + return this.create({ data: parsed, version, network }); + } +} diff --git a/src/sdl/YamlSDLParser.ts b/src/sdl/YamlSDLParser.ts new file mode 100644 index 0000000..414c8ef --- /dev/null +++ b/src/sdl/YamlSDLParser.ts @@ -0,0 +1,192 @@ +import { z } from "zod"; +import { BetaSdl, NetworkBeta2, NetworkBeta3, NetworkVersion } from "./types"; +import YAML from "js-yaml"; + +export const v2HTTPOptions = z.object({ + max_body_size: z.number(), + read_timeout: z.number(), + send_timeout: z.number(), + next_tries: z.number(), + next_timeout: z.number(), + next_cases: z.array(z.string()) +}); + +export const v2ExposeTo = z.object({ + service: z.string().optional(), + global: z.boolean().optional(), + http_options: v2HTTPOptions, + ip: z.string() +}); + +export const v2Accept = z.object({ + items: z.array(z.string()).optional() +}); + +export const v2Expose = z.object({ + port: z.number(), + as: z.number(), + proto: z.string().optional(), + to: z.array(v2ExposeTo).optional(), + accept: v2Accept, + http_options: v2HTTPOptions +}); + +export const v2ServiceStorageParams = z.object({ + name: z.string(), + mount: z.string(), + readOnly: z.boolean() +}); + +export const v2ServiceParams = z.object({ + storage: z.record(v2ServiceStorageParams).optional() +}); + +export const v2Dependency = z.object({ + service: z.string() +}); + +export const v2Service = z.object({ + image: z.string(), + command: z.array(z.string()).nullable(), + args: z.array(z.string()).nullable(), + env: z.array(z.string()).nullable(), + expose: z.array(v2Expose), + dependencies: z.array(v2Dependency).optional(), + params: v2ServiceParams.optional() +}); + +export const v2ServiceDeployment = z.object({ + profile: z.string(), + count: z.number() +}); + +export const v2Deployment = z.record(v2ServiceDeployment); + +export const v2Endpoint = z.object({ + kind: z.string() +}); + +export const v2Profiles = z.object({ + compute: z.record( + z.object({ + resources: z.object({ + cpu: z.object({ + units: z.union([z.number(), z.string()]), + attributes: z.record(z.any()).optional() + }), + memory: z.object({ + size: z.string(), + attributes: z.record(z.any()).optional() + }), + storage: z.union([ + z.array( + z.object({ + name: z.string(), + size: z.string(), + attributes: z.record(z.any()) + }) + ), + z.object({ + name: z.string(), + size: z.string(), + attributes: z.record(z.any()) + }) + ]) + }) + }) + ), + placement: z.record( + z.object({ + attributes: z.record(z.string()), + signedBy: z.object({ + allOf: z.array(z.string()).optional(), + anyOf: z.array(z.string()).optional() + }), + pricing: z.record( + z.object({ + denom: z.string(), + value: z.number(), + amount: z.number() + }) + ) + }) + ) +}); + +const v2Storage = z.object({ + name: z.string(), + size: z.string(), + attributes: z.record(z.any()) +}); + +export const v2ProfileCompute = z.object({ + resources: z.object({ + cpu: z.object({ + units: z.union([z.number(), z.string()]), + attributes: z.record(z.any()).optional() + }), + memory: z.object({ + size: z.string(), + attributes: z.record(z.any()).optional() + }), + storage: z.union([z.array(v2Storage), v2Storage]) + }) +}); + +export const v3ProfileCompute = z.object({ + resources: z.object({ + cpu: v2ProfileCompute.shape.resources.shape.cpu, + memory: v2ProfileCompute.shape.resources.shape.memory, + storage: v2ProfileCompute.shape.resources.shape.storage, + gpu: z.object({ + units: z.union([z.number(), z.string()]), + attributes: z.object({ + vendor: z.record( + z.array( + z.object({ + model: z.string(), + ram: z.string().optional(), + interface: z.string().optional() + }) + ) + ) + }) + }), + id: z.number() + }) +}); + +export const v3Profiles = z.object({ + compute: z.record(v3ProfileCompute), + placement: v2Profiles.shape.placement +}); + +const Beta2Sdl = z.object({ + services: z.record(v2Service), + deployment: z.record(v2Deployment), + endpoints: z.record(v2Endpoint), + profiles: v2Profiles +}); + +const Beta3Sdl = z.object({ + services: z.record(v2Service), + deployment: z.record(v2Deployment), + endpoints: z.record(v2Endpoint), + profiles: v3Profiles +}); + +export class YamlSDLParser { + static parse(yaml: string, version: NetworkBeta2): BetaSdl; + static parse(yaml: string, version: NetworkBeta3): BetaSdl; + static parse(yaml: string, version: NetworkVersion): BetaSdl { + const json = YAML.load(yaml); + + if (version === "beta2") { + return Beta2Sdl.parse(json) as BetaSdl; + } else if (version === "beta3") { + return Beta3Sdl.parse(json) as BetaSdl; + } else { + throw new Error("Unsupported version"); + } + } +} diff --git a/src/sdl/index.ts b/src/sdl/index.ts index a6b9076..7468e81 100644 --- a/src/sdl/index.ts +++ b/src/sdl/index.ts @@ -1,4 +1,3 @@ -import YAML from "js-yaml"; import { v2Manifest, v3Manifest, @@ -22,16 +21,19 @@ import { v3ServiceExposeHttpOptions, v2ManifestServiceParams, v3GPUAttributes, - v3Sdl, v3ProfileCompute, v3ComputeResources, v2ServiceParams, v3DeploymentGroup, - v3ManifestServiceParams + v3ManifestServiceParams, + BetaSdl, + NetworkBeta3 } from "./types"; import { convertCpuResourceString, convertResourceString } from "./sizes"; import { default as stableStringify } from "json-stable-stringify"; import crypto from "node:crypto"; +import { NetworkId } from "../types/network"; +import YAML from "js-yaml"; const Endpoint_SHARED_HTTP = 0; const Endpoint_RANDOM_PORT = 1; @@ -49,25 +51,19 @@ function isString(str: any): str is string { type NetworkVersion = "beta2" | "beta3"; -export class SDL { - data: v2Sdl; - version: NetworkVersion; - - constructor(data: v2Sdl, version: NetworkVersion = "beta2") { - this.data = data; - this.version = version; - } - +export class SDL { static fromString(yaml: string, version: NetworkVersion = "beta2") { + console.warn("SDL.fromString is deprecated. Use SDLFactory.createFromYaml instead."); const data = SDL.validate(yaml) as v2Sdl; return new SDL(data, version); } static validate(yaml: string) { + console.warn("SDL.validate is deprecated. Use SDLFactory.createFromYaml instead."); // TODO: this should really be cast to unknown, then assigned // to v2 or v3 SDL only after being validated - const data = YAML.load(yaml) as v3Sdl; + const data = YAML.load(yaml) as BetaSdl; for (const [name, profile] of Object.entries(data.profiles.compute)) { SDL.validateGPU(name, profile.resources.gpu); @@ -141,6 +137,25 @@ export class SDL { } } + constructor( + public readonly data: BetaSdl, + public readonly version: NetworkVersion = "beta2", + public readonly network: NetworkId = "mainnet" + ) { + this.validate(); + } + + private validate() { + Object.keys(this.data.profiles.compute).forEach(name => { + const { resources } = this.data.profiles.compute[name]; + if ("gpu" in resources) { + SDL.validateGPU(name, resources.gpu); + } + + SDL.validateStorage(name, resources.storage); + }); + } + services() { if (this.data) { return this.data.services; diff --git a/src/sdl/types.ts b/src/sdl/types.ts index a12efe8..aa7520b 100644 --- a/src/sdl/types.ts +++ b/src/sdl/types.ts @@ -1,3 +1,21 @@ +import { z } from "zod"; +import { + v2Accept, + v2Dependency, + v2Deployment, + v2Expose, + v2ExposeTo, + v2HTTPOptions, + v2ProfileCompute, + v2Profiles, + v2Service, + v2ServiceDeployment, + v2ServiceParams, + v2ServiceStorageParams, + v3ProfileCompute, + v3Profiles +} from "./YamlSDLParser"; + export type v2Manifest = v2Group[]; export type v3Manifest = v3Group[]; @@ -102,69 +120,40 @@ export type v3Sdl = { endpoints: Record; }; +export type NetworkBeta2 = "beta2"; +export type NetworkBeta3 = "beta3"; +export type NetworkVersion = NetworkBeta2 | NetworkBeta3; + +export interface BetaSdl { + services: Record; + deployment: Record; + endpoints: Record; + profiles: V extends NetworkBeta2 ? v2Profiles : v3Profiles; +} + export type v2Endpoint = { kind: string; }; -export type v2ExposeTo = { - service?: string; - global?: boolean; - http_options: v2HTTPOptions; - ip: string; -}; +export type v2ExposeTo = z.infer; -export type v2HTTPOptions = { - max_body_size: number; - read_timeout: number; - send_timeout: number; - next_tries: number; - next_timeout: number; - next_cases: string[]; -}; +export type v2HTTPOptions = z.infer; -export type v2Accept = { - items?: string[]; -}; +export type v2Accept = z.infer; -export type v2Expose = { - port: number; - as: number; - proto?: string; - to?: v2ExposeTo[]; - accept: v2Accept; - http_options: v2HTTPOptions; -}; +export type v2Expose = z.infer; -export type v2Dependency = { - service: string; -}; +export type v2Dependency = z.infer; -export type v2ServiceStorageParams = { - name: string; - mount: string; - readOnly: boolean; -}; +export type v2ServiceStorageParams = z.infer; -export type v2ServiceParams = { - storage?: Record; -}; +export type v2ServiceParams = z.infer; -export type v2Service = { - image: string; - command: string[] | null; - args: string[] | null; - env: string[] | null; - expose: v2Expose[]; - dependencies?: v2Dependency[]; - params?: v2ServiceParams; -}; +export type v2Service = z.infer; -export type v2ServiceDeployment = { - profile: string; - count: number; -}; +export type v2ServiceDeployment = z.infer; -export type v2Deployment = Record; +export type v2Deployment = z.infer; export type v2CPUAttributes = Record; @@ -213,13 +202,9 @@ export type v3ComputeResources = { id: number; }; -export type v2ProfileCompute = { - resources: v2ComputeResources; -}; +export type v2ProfileCompute = z.infer; -export type v3ProfileCompute = { - resources: v3ComputeResources; -}; +export type v3ProfileCompute = z.infer; export type v2PlacementAttributes = Attributes; @@ -242,15 +227,9 @@ export type v2ProfilePlacement = { pricing: v2PlacementPricing; }; -export type v2Profiles = { - compute: Record; - placement: Record; -}; +export type v2Profiles = z.infer; -export type v3Profiles = { - compute: Record; - placement: Record; -}; +export type v3Profiles = z.infer; export type Attribute = { key: string; diff --git a/src/types/network.ts b/src/types/network.ts new file mode 100644 index 0000000..1e82759 --- /dev/null +++ b/src/types/network.ts @@ -0,0 +1,14 @@ +export type NetworkId = "mainnet" | "sandbox"; + +export type Network = { + id: string; + title: string; + description: string; + nodesUrl: string; + chainId: string; + chainRegistryName: string; + versionUrl: string; + rpcEndpoint?: string; + version: string; + enabled: boolean; +};