Skip to content

Commit a75d7ac

Browse files
committedOct 3, 2021
Add cluster api
1 parent bf09435 commit a75d7ac

11 files changed

+315
-0
lines changed
 

‎Makefile

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
run:
2+
deno run --unstable -A main.ts
3+
test:
4+
deno test --unstable -A tests/
5+
clean:
6+
rm graphteon-proxy

‎apis/cluster.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ListOptions, LazyObject } from "../types/list.ts";
2+
import { ListClusterUpdate } from "../types/cluster.ts";
3+
import { OrcinusClient } from "../libs/orcinusClient.ts";
4+
5+
export class Cluster {
6+
private client: OrcinusClient;
7+
8+
constructor(client: OrcinusClient) {
9+
this.client = client;
10+
}
11+
12+
async list(options?: ListOptions): Promise<LazyObject[]> {
13+
const res = await this.client.get("/nodes", [
14+
{ name: "filters", value: options?.filters ?? "" },
15+
]);
16+
if (!res.body || !res.body.length) {
17+
return [];
18+
}
19+
return JSON.parse(res.body);
20+
}
21+
22+
async inspect(id: string): Promise<LazyObject> {
23+
const res = await this.client.get("/nodes/" + id);
24+
if (!res.body || !res.body.length) {
25+
return {};
26+
}
27+
return JSON.parse(res.body);
28+
}
29+
30+
async delete(id: string, force: boolean = false): Promise<LazyObject> {
31+
const res = await this.client.delete(`/nodes/${id}`, "", [{
32+
name: "force", value: force.toString()
33+
}]);
34+
if (!res.body || !res.body.length) {
35+
return {};
36+
}
37+
return JSON.parse(res.body);
38+
}
39+
40+
async update(id: string, options: ListClusterUpdate): Promise<LazyObject> {
41+
const res = await this.client.post(`/nodes/${id}/update`, JSON.stringify(options), []);
42+
if (!res.body || !res.body.length) {
43+
return {};
44+
}
45+
return JSON.parse(res.body);
46+
}
47+
}

‎apis/mod.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import * from "apis";

‎libs/http.ts

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
export interface HttpQuery {
2+
name: string;
3+
value: string;
4+
}
5+
6+
export interface HttpHeader {
7+
[key: string]: string;
8+
}
9+
10+
export interface HttpResponse {
11+
status?: number;
12+
headers?: HttpHeader;
13+
body?: string;
14+
}
15+
16+
export interface HttpRequest {
17+
method: string;
18+
path: string;
19+
query: HttpQuery[];
20+
headers: HttpHeader;
21+
body: string;
22+
}
23+
24+
export class HttpClient {
25+
buf: Uint8Array;
26+
res: string;
27+
conn: Deno.Conn;
28+
29+
constructor(conn: Deno.Conn) {
30+
this.conn = conn;
31+
this.buf = new Uint8Array(1);
32+
this.res = "";
33+
}
34+
35+
async readLine() {
36+
const dec = new TextDecoder();
37+
this.res = "";
38+
this.buf = new Uint8Array(1);
39+
40+
while (true) {
41+
if (this.res.indexOf("\n") !== -1) {
42+
return this.res.slice(0, this.res.length - 2);
43+
}
44+
await this.conn.read(this.buf);
45+
this.res += dec.decode(this.buf);
46+
}
47+
}
48+
49+
async readHead(res: HttpResponse) {
50+
const line = await this.readLine();
51+
res.status = parseInt(line.split(" ")[1]);
52+
}
53+
54+
async readHeaders(res: HttpResponse) {
55+
let isEnd = false;
56+
57+
res.headers = {};
58+
while (!isEnd) {
59+
const line = await this.readLine();
60+
if (line === "") {
61+
isEnd = true;
62+
} else {
63+
const [name, value] = line.split(":");
64+
res.headers[name.trim()] = value.trim();
65+
}
66+
}
67+
}
68+
69+
async readBody(res: HttpResponse) {
70+
const dec = new TextDecoder();
71+
let finished = false;
72+
let body = "";
73+
const headers = res!.headers;
74+
75+
if (headers!["Transfer-Encoding"] === "chunked") {
76+
while (!finished) {
77+
const bufsize = parseInt(await this.readLine(), 16);
78+
if (bufsize === 0) {
79+
finished = true;
80+
} else {
81+
const buf = new ArrayBuffer(bufsize);
82+
const arr = new Uint8Array(buf);
83+
await this.read(arr);
84+
body += dec.decode(arr);
85+
await this.readLine();
86+
}
87+
}
88+
} else {
89+
const bufsize = parseInt(res?.headers!["Content-Length"], 10);
90+
const buf = new ArrayBuffer(bufsize);
91+
const arr = new Uint8Array(buf);
92+
await this.read(arr);
93+
body += dec.decode(arr);
94+
}
95+
res.body = body;
96+
}
97+
98+
async read(buf: Uint8Array) {
99+
return this.conn.read(buf);
100+
}
101+
102+
async send(data: string) {
103+
const enc = new TextEncoder();
104+
105+
return this.conn.write(enc.encode(data));
106+
}
107+
108+
buildQueryString(query: HttpQuery[]) {
109+
return query.map((v) => `${v.name}=${v.value}`).join("&");
110+
}
111+
112+
buildHeaders(headers: HttpHeader) {
113+
return Object.keys(headers).map((v) => `${v}: ${headers[v]}`).join("\r\n");
114+
}
115+
116+
async sendRequest(request: HttpRequest) {
117+
const head = `${request.method} ${request.path}?${this.buildQueryString(request.query)
118+
} HTTP/1.1\r\n`;
119+
if (request.body.length > 0) {
120+
request.headers["Content-length"] = request.body.length.toString();
121+
request.headers["Content-type"] = "application/json";
122+
}
123+
const headers = this.buildHeaders(request.headers);
124+
const reqString = head + headers + "\r\n\r\n" + request.body;
125+
await this.send(reqString);
126+
const response: HttpResponse = {};
127+
await this.readHead(response);
128+
await this.readHeaders(response);
129+
await this.readBody(response);
130+
await this.conn.close();
131+
return response;
132+
}
133+
}

‎libs/mod.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import * from "libs";

‎libs/orcinusClient.ts

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as base64 from "https://deno.land/x/base64/mod.ts";
2+
import { HttpClient, HttpQuery, HttpRequest } from "./http.ts";
3+
4+
export interface RegistryAuth {
5+
username: string;
6+
password: string;
7+
email: string;
8+
serveraddress: string;
9+
}
10+
11+
export class OrcinusClient {
12+
socketAddress: string;
13+
authConfig: RegistryAuth | null;
14+
client: () => Promise<HttpClient>;
15+
16+
constructor(socketAddress: string, authConfig: RegistryAuth | null = null) {
17+
this.socketAddress = socketAddress;
18+
this.authConfig = authConfig;
19+
this.client = this.init;
20+
}
21+
22+
async init(): Promise<HttpClient> {
23+
const conn = await Deno.connect(
24+
<any>{ transport: "unix", path: this.socketAddress },
25+
);
26+
return new HttpClient(conn);
27+
}
28+
29+
async makeRequest(
30+
method: string,
31+
path: string,
32+
body: string,
33+
query: HttpQuery[],
34+
) {
35+
const client = await this.client();
36+
const enc = new TextEncoder();
37+
const headers: { [key: string]: string } = {
38+
"Host": "orcinus",
39+
"Accept": "application/json",
40+
}
41+
if (this.authConfig) {
42+
headers["X-Registry-Auth"] = base64.fromUint8Array(enc.encode(JSON.stringify(this.authConfig)));
43+
}
44+
const request: HttpRequest = {
45+
method: method,
46+
path,
47+
query,
48+
headers,
49+
body,
50+
};
51+
return client.sendRequest(request);
52+
}
53+
54+
async get(path: string, query: HttpQuery[] = []) {
55+
return this.makeRequest("GET", path, "", query);
56+
}
57+
58+
async post(path: string, body: string, query: HttpQuery[] = []) {
59+
return this.makeRequest("POST", path, body, query);
60+
}
61+
62+
async delete(path: string, body: string, query: HttpQuery[] = []) {
63+
return this.makeRequest("DELETE", path, body, query);
64+
}
65+
}

‎main.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { RegistryAuth, OrcinusClient } from "./libs/orcinusClient.ts";
2+
import { Cluster } from "./apis/cluster.ts";
3+
4+
export default class Orcinusd {
5+
cluster: Cluster;
6+
7+
constructor(socketAddress: string = "/var/run/docker.sock", auth: RegistryAuth | null = null) {
8+
const client = new OrcinusClient(socketAddress, auth);
9+
this.cluster = new Cluster(client);
10+
}
11+
}

‎tests/cluster.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
assert,
3+
} from "https://deno.land/std/testing/asserts.ts";
4+
import Orcinusd from "../main.ts";
5+
6+
const orcinus = new Orcinusd();
7+
8+
Deno.test({
9+
name: "Cluster list and inspect",
10+
fn: async () => {
11+
const cluster = await orcinus.cluster.list();
12+
if (cluster.length > 0) {
13+
const id = cluster[0].ID;
14+
assert(typeof id === "string");
15+
const clusterFilter = await orcinus.cluster.list({ filters: JSON.stringify({ id: [id] }) });
16+
assert(clusterFilter[0].ID === id);
17+
const clusterFilterFail = await orcinus.cluster.list({ filters: JSON.stringify({ id: ["failinput"] }) });
18+
assert(clusterFilterFail.length === 0);
19+
const clusterInspect = await orcinus.cluster.inspect(id);
20+
assert(clusterInspect.ID === id)
21+
}
22+
else {
23+
assert(typeof cluster === "object");
24+
}
25+
},
26+
sanitizeResources: false,
27+
sanitizeOps: false
28+
});

‎types/cluster.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { LazyObject } from "./list.ts";
2+
3+
export interface ListClusterUpdate {
4+
Name? : string;
5+
Labels? : LazyObject;
6+
Role? : string;
7+
Availability? : string;
8+
}

‎types/list.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export interface ListOptions {
2+
// Return all containers. By default, only running containers are shown
3+
all?: boolean;
4+
// Return this number of most recently created containers, including non-running ones.
5+
limit?: number;
6+
// Return the size of container as fields SizeRw and SizeRootFs.
7+
size?: number;
8+
// Filters to process on the container list, encoded as JSON (a map[string][]string). For example, {"status": ["paused"]} will only return paused containers.
9+
filters?: string;
10+
}
11+
12+
export interface LazyObject {
13+
[key: string]: any
14+
}

‎types/mod.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import * from "types";

0 commit comments

Comments
 (0)
Please sign in to comment.