feat: Initial commit

This commit is contained in:
travisladuke
2024-02-13 13:23:46 -08:00
commit 9ae6d79914
12 changed files with 8041 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/* eslint-env node */
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
};
+54
View File
@@ -0,0 +1,54 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
permissions:
contents: read
pages: write
id-token: write
jobs:
build_lint_etc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- name: Install zerotier-one
run: curl -s https://install.zerotier.com/ | sudo bash
- run: npm ci
- run: npm run build
- name: Set authtoken
run: echo "AUTH_TOKEN=$(sudo cat /var/lib/zerotier-one/authtoken.secret)" >> $GITHUB_ENV
- name: Run tests
run: npm test
- name: Archive built docs html
uses: actions/upload-artifact@v4
with:
name: index.html
path: dist/index.html
- name: Archive built openapi spec
uses: actions/upload-artifact@v4
with:
name: openapi.yaml
path: tsp-output/@typespec/openapi3/openapi.yaml
- name: Archive built json schema
uses: actions/upload-artifact@v4
with:
name: json-schema.json
path: tsp-output/@typespec/json-schema/json-schema.json
+40
View File
@@ -0,0 +1,40 @@
name: npm version && npm publish
on:
workflow_dispatch:
inputs:
newversion:
description: 'npm version {major,minor,patch}'
required: true
permissions:
contents: write
jobs:
version_and_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# fetch full history so things like auto-changelog work properly
fetch-depth: 0
- name: Install zerotier-one
run: curl -s https://install.zerotier.com/ | sudo bash
- name: Set auth token
run: echo "AUTH_TOKEN=$(sudo cat /var/lib/zerotier-one/authtoken.secret)" >> $GITHUB_ENV
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm test
- run: git config --global user.email "travis.laduke@zerotier.com"
- run: git config --global user.name "travis laduke"
- run: npm version ${{ github.event.inputs.newversion }}
- run: npm run release
env:
GH_RELEASE_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5
View File
@@ -0,0 +1,5 @@
/node_modules/
/tsp-output/
/.DS_Store
/dist/
.DS_Store
+23
View File
@@ -0,0 +1,23 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
## [v0.2.0](https://github.com/laduke/curly-octo-guacamole/compare/v0.1.1...v0.2.0)
### Commits
- feat: add to and improve spec and tests [`0733ce4`](https://github.com/laduke/curly-octo-guacamole/commit/0733ce4cb5841681cb8cbc7536eafd70e0195656)
- feat: test some network controller endpoints [`6096483`](https://github.com/laduke/curly-octo-guacamole/commit/60964834db5da3fc715a77c38ca375665ffaf04f)
- fix: Network ID list [`9c1e82c`](https://github.com/laduke/curly-octo-guacamole/commit/9c1e82cffa6438dc77fae80c9bdb95f592538251)
## v0.1.1 - 2024-02-12
### Commits
- feat: Initial commit [`4d05a59`](https://github.com/laduke/curly-octo-guacamole/commit/4d05a59dff7e169324d4827c6a4cb25f6066d24f)
- build: update repository url [`4e4238b`](https://github.com/laduke/curly-octo-guacamole/commit/4e4238b08715aeddb4d04c08ecbe1912c6b19173)
+52
View File
@@ -0,0 +1,52 @@
# ZeroTierOne API Spec
OpenAPI and JSON-schema for the [ZeroTierOne](https://github.com/zerotier/ZeroTierOne) service API.
This is the API that lets you leave and join networks on your local machine, or configure a self-hosted network controller.
The specs are generated from a [typespec](https://typespec.io/) spec.
## dev
``` sh
npm install
npm start
```
Open http://localhost:8080
## test
This integration test requires zerotier-one to be running. It may change configuration of your node! It runs in github actions too.
``` sh
AUTH_TOKEN=$(cat /path/to/authtoken.secret) npm test
```
## missing features
Features not implemented by typespec yet
- [ openapi examples ](https://github.com/microsoft/typespec/issues/2700)
- [ tag descriptions ](https://github.com/microsoft/typespec/issues/2220)
## publishing
How to create a github [release](https://github.com/zerotier/zerotier-one-api-spec/releases/).
Releases contain the openapi and json-schema files.
Try to use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/#summary) messages if you want. They will appear in the changelog.
- Go into the repo's Github Actions
- Click "npm version && npm publish"
- Click Run Workflow
- Type "patch", "minor", or "major" and submit
On the CLI you can do:
``` sh
npm version {major,minor,patch}
npm run release
```
The url to the latest release of the openapi spec is: `https://github.com/zerotier/zerotier-one-api-spec/releases/latest/download/openapi.yaml`
+525
View File
File diff suppressed because it is too large Load Diff
+7026
View File
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
{
"name": "zerotier-one-api-spec",
"repository": "https://github.com/zerotier/zerotier-one-api-spec",
"scripts": {
"start": "run-s build watch",
"test": "tsx test/*test.ts",
"build": "run-s pre-compile compile lint build:*",
"lint": "run-p lint:*",
"pre-compile": "run-p depcheck format-spec format-code",
"build:doc-html": "redocly build-docs tsp-output/@typespec/openapi3/openapi.yaml -o ./dist/index.html",
"pretest": "openapi-typescript tsp-output/@typespec/openapi3/openapi.yaml -o /tmp/schema.ts",
"clean": "rm -rf dist tsp-output",
"compile": "tsp compile ./main.tsp",
"depcheck": "depcheck --ignores=@typespec/json-schema,@typespec/openapi,@typespec/openapi3,@typespec/prettier-plugin-typespec",
"format-spec": "tsp format **/*.tsp",
"format-code": "prettier --write **/*.ts",
"lint:openapi": "redocly lint tsp-output/@typespec/openapi3/openapi.yaml",
"lint:code": "eslint .",
"release": "git push --follow-tags && gh-release -y -a=tsp-output/@typespec/openapi3/openapi.yaml,tsp-output/@typespec/json-schema/json-schema.json",
"version": "auto-changelog -p --template keepachangelog auto-changelog --breaking-pattern 'BREAKING CHANGE:' && git add CHANGELOG.md",
"watch": "run-p watch:*",
"watch:compile": "npm run compile -- --watch",
"watch:serve": "redocly preview-docs ./tsp-output/@typespec/openapi3/openapi.yaml --use-community-edition"
},
"version": "0.2.0",
"type": "module",
"dependencies": {
"@redocly/cli": "1.7",
"@types/node": "^20.11.17",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@typespec/compiler": "^0.51.0",
"@typespec/json-schema": "^0.51.0",
"@typespec/openapi": "^0.51.0",
"@typespec/openapi3": "^0.51.1",
"@typespec/prettier-plugin-typespec": "^0.51.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"auto-changelog": "^2.4.0",
"better-ajv-errors": "^1.2.0",
"depcheck": "^1.4.7",
"eslint": "^8.56.0",
"gh-release": "^7.0.2",
"npm-run-all2": "^6.1.1",
"openapi-fetch": "^0.8.2",
"openapi-typescript": "^7.0.0-next.7",
"openapi-typescript-helpers": "^0.0.6",
"prettier": "^3.2.5",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
}
}
+2
View File
@@ -0,0 +1,2 @@
rules:
no-server-example.com: off
+247
View File
@@ -0,0 +1,247 @@
import { readFileSync } from "node:fs";
import { describe, it } from "node:test";
import assert from "node:assert";
import createClient from "openapi-fetch";
import { PathsWithMethod } from "openapi-typescript-helpers";
import type { paths } from "/tmp/schema.ts"; // generated by openapi-typescript
import Ajv, { DefinedError } from "ajv/dist/2020";
import addFormats from "ajv-formats";
import betterAjvErrors from "better-ajv-errors";
if (!process.env.AUTH_TOKEN) {
console.error("No AUTH_TOKEN env var set. Exiting.");
process.exit(1);
}
function createCreateClient() {
// AUTH_TOKEN=`cat ~/Library/Application\ Support/ZeroTier/One/authtoken.secret ` npm t
const authToken = process.env.AUTH_TOKEN;
const client = createClient<paths>({
baseUrl: "http://localhost:9993/",
headers: { "X-ZT1-AUTH": authToken },
});
return client;
}
function createValidator(schemaId: string) {
const specJson = readFileSync(
"./tsp-output/@typespec/json-schema/json-schema.json",
{ encoding: "utf8" },
);
const schema = JSON.parse(specJson);
const ajv = new Ajv({ allErrors: false });
addFormats(ajv);
ajv.addSchema(schema);
const validate = ajv.getSchema(schemaId);
assert(validate);
return { validate, schema };
}
function assertValid({ validate, schema }, data: object | undefined) {
const valid = validate(data);
if (!valid) {
const output = betterAjvErrors(
schema,
data,
validate.errors as DefinedError[],
);
console.error(JSON.stringify(validate.errors, null, 4));
console.error(JSON.stringify(data, null, 4));
assert.fail(output);
}
}
describe("GET endpoints", async function () {
const map: { path: PathsWithMethod<paths, "get">; id: string }[] = [
{ path: "/status", id: "NodeStatus" },
{ path: "/controller", id: "ControllerStatus" },
{ path: "/network", id: "JoinedNetworks" },
{ path: "/peer", id: "Peers" },
];
for (const { path, id } of map) {
it(id, async () => {
const validator = createValidator(id);
const client = createCreateClient();
const { data } = await client.GET(path, {});
assert.ok(data);
assertValid(validator, data);
});
}
});
describe("API exercise", async function () {
const client = createCreateClient();
let network_id: string;
const node_id = "1122334455";
it("Creates a valid controller network", async () => {
const { data: networkData } = await client.POST("/controller/network", {
body: {},
});
assert(networkData);
const cnValidator = createValidator("ControllerNetwork");
assertValid(cnValidator, networkData);
network_id = networkData.id;
});
it("Gets the controller network by ID", async () => {
const { data } = await client.GET("/controller/network/{network_id}", {
params: { path: { network_id } },
});
assert(data);
const networkValidator = createValidator("ControllerNetwork");
assertValid(networkValidator, data);
});
it("Lists controller networks ", async () => {
const { data } = await client.GET("/controller/network");
assert(data);
const networkValidator = createValidator("ControllerNetworkIDList");
assertValid(networkValidator, data);
assert.ok(data.includes(network_id));
});
it("Creates a controller network member", async () => {
const { data } = await client.POST(
"/controller/network/{network_id}/member/{node_id}",
{
params: { path: { network_id, node_id } },
body: { authorized: true },
},
);
assert(data);
const validator = createValidator("ControllerNetworkMember");
assertValid(validator, data);
assert.equal(data.id, "1122334455");
});
it("Lists controller network members", async () => {
const { data } = await client.GET(
"/controller/network/{network_id}/member",
{ params: { path: { network_id } } },
);
assert(data);
const validator = createValidator("ControllerNetworkMemberList");
assertValid(validator, data);
assert.ok(Object.keys(data).includes("1122334455"));
});
it("Deletes a controller network member", async () => {
const { data } = await client.DELETE(
"/controller/network/{network_id}/member/{node_id}",
{
params: { path: { network_id, node_id } },
},
);
assert(data);
const validator = createValidator("ControllerNetworkMember");
assertValid(validator, data);
});
it("Deletes the network by ID", async () => {
const { data: networkData3 } = await client.DELETE(
"/controller/network/{network_id}",
{ params: { path: { network_id } } },
);
const networkDelValidator = createValidator("ControllerNetwork");
assertValid(networkDelValidator, networkData3);
});
describe("Joining networks", async () => {
const network_id = "ff00160016000000";
it("Joins a network", async () => {
const { data } = await client.POST("/network/{network_id}", {
body: {},
params: { path: { network_id } },
});
assert(data);
const validator = createValidator("JoinedNetwork");
assertValid(validator, data);
});
it("Gets the joined networks", async () => {
const { data } = await client.GET("/network");
assert(data);
const validator = createValidator("JoinedNetworks");
assertValid(validator, data);
const ids = data.map((network) => network.id);
assert.ok(ids.includes(network_id));
});
it("Gets the joined network by ID", async () => {
const { data } = await client.GET("/network/{network_id}", {
params: { path: { network_id } },
});
assert(data);
const validator = createValidator("JoinedNetwork");
assertValid(validator, data);
assert.equal(network_id, data.id);
});
it("Leaves the joined network by ID", async () => {
const { data } = await client.DELETE("/network/{network_id}", {
params: { path: { network_id } },
});
assert(data);
const validator = createValidator("LeaveResult");
assertValid(validator, data);
});
});
/// unstable
it("Lists full networks", async () => {
const { data, response } = await client.GET("/unstable/controller/network");
if (response.status !== 404) {
assert(data);
const networksValidator = createValidator("ControllerNetworks");
assertValid(networksValidator, data);
}
});
it("Lists full network members", async () => {
const { data, response } = await client.GET(
"/unstable/controller/network/{network_id}/member",
);
if (response.status !== 404) {
assert(data);
const networksValidator = createValidator(
"ControllerNetworkMemberListFull",
);
assertValid(networksValidator, data);
}
});
});
+8
View File
@@ -0,0 +1,8 @@
emit:
- "@typespec/json-schema"
- "@typespec/openapi3"
options:
"@typespec/json-schema":
file-type: "json"
bundleId: json-schema.json