mirror of
https://github.com/zerotier/zerotier-one-api-spec.git
synced 2026-05-22 16:29:58 -07:00
feat: Initial commit
This commit is contained in:
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,5 @@
|
||||
/node_modules/
|
||||
/tsp-output/
|
||||
/.DS_Store
|
||||
/dist/
|
||||
.DS_Store
|
||||
@@ -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)
|
||||
@@ -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`
|
||||
|
||||
Generated
+7026
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
rules:
|
||||
no-server-example.com: off
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
emit:
|
||||
- "@typespec/json-schema"
|
||||
- "@typespec/openapi3"
|
||||
|
||||
options:
|
||||
"@typespec/json-schema":
|
||||
file-type: "json"
|
||||
bundleId: json-schema.json
|
||||
Reference in New Issue
Block a user