mirror of
https://github.com/alexhopeoconnor/binarylane-dotnet.git
synced 2026-10-04 02:18:11 +10:00
Initial BinaryLane v2 API client
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{cs,csx}]
|
||||
indent_size = 4
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
csharp_style_var_for_built_in_types = false:suggestion
|
||||
csharp_style_var_when_type_is_apparent = false:suggestion
|
||||
csharp_style_var_elsewhere = false:suggestion
|
||||
csharp_prefer_braces = true:suggestion
|
||||
csharp_style_namespace_declarations = file_scoped:suggestion
|
||||
|
||||
[*.{yml,yaml,json}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,16 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.cs text diff=csharp
|
||||
*.csproj text
|
||||
*.props text
|
||||
*.targets text
|
||||
*.sln text
|
||||
*.yml text
|
||||
*.yaml text
|
||||
*.json text
|
||||
*.md text
|
||||
*.sh text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.nupkg binary
|
||||
*.snupkg binary
|
||||
@@ -0,0 +1,12 @@
|
||||
## Summary
|
||||
|
||||
<!-- What changes for consumers, and why? -->
|
||||
|
||||
## Validation
|
||||
|
||||
- [ ] Unit tests added or updated
|
||||
- [ ] Demo builds locally
|
||||
- [ ] Documentation and XML comments updated where needed
|
||||
- [ ] API coverage and OpenAPI snapshot reviewed where applicable
|
||||
- [ ] Changelog updated for a user-visible change
|
||||
- [ ] No credentials, personal data, or sensitive infrastructure details added
|
||||
@@ -0,0 +1,23 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: nuget
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
dotnet:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -0,0 +1,91 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_NOLOGO: 1
|
||||
NUGET_XMLDOC_MODE: skip
|
||||
|
||||
jobs:
|
||||
build-test-pack:
|
||||
name: Build, test, and pack
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
global-json-file: global.json
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
|
||||
- name: Restore locked dependencies
|
||||
run: |
|
||||
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
|
||||
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
|
||||
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
|
||||
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
|
||||
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
|
||||
|
||||
- name: Test
|
||||
run: >-
|
||||
dotnet test tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-build
|
||||
-m:1
|
||||
--logger "trx;LogFileName=test-results.trx"
|
||||
--results-directory TestResults
|
||||
|
||||
- name: Verify formatting
|
||||
run: |
|
||||
dotnet format src/BinaryLane.Api/BinaryLane.Api.csproj --verify-no-changes --no-restore
|
||||
dotnet format tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --verify-no-changes --no-restore
|
||||
dotnet format examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --verify-no-changes --no-restore
|
||||
|
||||
- name: Pack
|
||||
run: >-
|
||||
dotnet pack src/BinaryLane.Api/BinaryLane.Api.csproj
|
||||
--configuration Release --no-build --output artifacts
|
||||
-p:BuildInParallel=false
|
||||
|
||||
- name: Compile the demo against the packed package
|
||||
run: ./eng/test-demo-package.sh artifacts
|
||||
|
||||
- name: Upload NuGet packages
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: test-results
|
||||
path: TestResults/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,56 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '31 4 * * 4'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze C#
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
global-json-file: global.json
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
with:
|
||||
languages: csharp
|
||||
build-mode: manual
|
||||
|
||||
- name: Restore locked dependencies
|
||||
run: |
|
||||
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
|
||||
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
|
||||
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
|
||||
|
||||
- name: Build for analysis
|
||||
run: |
|
||||
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj --configuration Release --no-restore -p:BuildInParallel=false
|
||||
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-restore -p:BuildInParallel=false
|
||||
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --configuration Release --no-restore -p:BuildInParallel=false
|
||||
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Monitor upstream OpenAPI contract
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 3 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: contract-monitor
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check BinaryLane OpenAPI contract
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Compare live contract with the reviewed snapshot
|
||||
id: contract
|
||||
shell: bash
|
||||
run: |
|
||||
set +e
|
||||
./eng/verify-openapi-contract.sh > contract-check.txt 2>&1
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
cat contract-check.txt
|
||||
case "$status" in
|
||||
0)
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
1)
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "The contract check could not complete (exit $status)." >&2
|
||||
exit "$status"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Upload contract-check report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: contract-check
|
||||
path: contract-check.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
- name: Open one review issue for contract drift
|
||||
if: steps.contract.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
existing_count="$(gh issue list \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--state open \
|
||||
--search 'in:title "BinaryLane OpenAPI contract changed"' \
|
||||
--json number \
|
||||
--jq 'length')"
|
||||
|
||||
if [[ "$existing_count" -gt 0 ]]; then
|
||||
echo "An upstream-contract review issue is already open."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo "BinaryLane's public OpenAPI contract no longer matches the reviewed snapshot."
|
||||
echo
|
||||
echo "Review the upstream change before updating generated code or SDK behavior."
|
||||
echo
|
||||
echo "```text"
|
||||
cat contract-check.txt
|
||||
echo "```"
|
||||
echo
|
||||
echo "Suggested next step: run `./eng/refresh-openapi-contract.sh`, review the diff,"
|
||||
echo "then update models, fixtures, API coverage, docs, and tests in one pull request."
|
||||
} > issue-body.md
|
||||
|
||||
gh issue create \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "BinaryLane OpenAPI contract changed" \
|
||||
--body-file issue-body.md
|
||||
@@ -0,0 +1,186 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_NOLOGO: 1
|
||||
NUGET_XMLDOC_MODE: skip
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Validate, test, and pack
|
||||
if: github.repository == 'alexhopeoconnor/binarylane-dotnet'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
package_version: ${{ steps.package_version.outputs.value }}
|
||||
|
||||
steps:
|
||||
- name: Check out tagged source
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
global-json-file: global.json
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
|
||||
- name: Verify the tagged commit is reachable from main
|
||||
run: |
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
||||
echo "Release tags must point to a commit reachable from main." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Assert tag, package version, and changelog agree
|
||||
run: ./eng/assert-release-version.sh "${{ github.ref_name }}"
|
||||
|
||||
- name: Read package version
|
||||
id: package_version
|
||||
run: echo "value=$(./eng/read-package-version.sh)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore locked dependencies
|
||||
run: |
|
||||
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
|
||||
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
|
||||
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
|
||||
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
|
||||
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --configuration Release --no-restore /p:ContinuousIntegrationBuild=true -p:BuildInParallel=false
|
||||
|
||||
- name: Test
|
||||
run: dotnet test tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --configuration Release --no-build -m:1
|
||||
|
||||
- name: Pack
|
||||
run: >-
|
||||
dotnet pack src/BinaryLane.Api/BinaryLane.Api.csproj
|
||||
--configuration Release --no-build --output artifacts
|
||||
-p:BuildInParallel=false
|
||||
|
||||
- name: Compile the demo against the packed package
|
||||
run: ./eng/test-demo-package.sh artifacts
|
||||
|
||||
- name: Upload NuGet packages
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: artifacts/
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish to NuGet.org
|
||||
needs: build
|
||||
if: github.repository == 'alexhopeoconnor/binarylane-dotnet'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download NuGet packages
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: artifacts
|
||||
|
||||
- name: Check trusted-publishing configuration
|
||||
run: |
|
||||
if [[ -z "${{ secrets.NUGET_USER }}" ]]; then
|
||||
echo "Set the protected release-environment secret NUGET_USER to the NuGet.org username." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Authenticate to NuGet.org through GitHub OIDC
|
||||
id: nuget_login
|
||||
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1
|
||||
with:
|
||||
user: ${{ secrets.NUGET_USER }}
|
||||
|
||||
- name: Publish package
|
||||
env:
|
||||
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
|
||||
run: >-
|
||||
dotnet nuget push "artifacts/*.nupkg"
|
||||
--source https://api.nuget.org/v3/index.json
|
||||
--api-key "$NUGET_API_KEY"
|
||||
--skip-duplicate
|
||||
|
||||
- name: Publish symbols
|
||||
env:
|
||||
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
for package in artifacts/*.snupkg; do
|
||||
dotnet nuget push "$package" \
|
||||
--source https://api.nuget.org/v3/index.json \
|
||||
--api-key "$NUGET_API_KEY" \
|
||||
--skip-duplicate
|
||||
done
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- build
|
||||
- publish
|
||||
if: github.repository == 'alexhopeoconnor/binarylane-dotnet'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Check out tagged source
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Download NuGet packages
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: nuget-packages
|
||||
path: artifacts
|
||||
|
||||
- name: Extract release notes
|
||||
run: ./eng/extract-release-notes.sh "${{ needs.build.outputs.package_version }}" > release-notes.md
|
||||
|
||||
- name: Create or update GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PACKAGE_VERSION: ${{ needs.build.outputs.package_version }}
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
shell: bash
|
||||
run: |
|
||||
if gh release view "$TAG_NAME" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
|
||||
gh release upload "$TAG_NAME" artifacts/* eng/openapi/contract.json --clobber --repo "$GITHUB_REPOSITORY"
|
||||
gh release edit "$TAG_NAME" \
|
||||
--title "BinaryLane.Api $PACKAGE_VERSION" \
|
||||
--notes-file release-notes.md \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
else
|
||||
gh release create "$TAG_NAME" artifacts/* eng/openapi/contract.json \
|
||||
--title "BinaryLane.Api $PACKAGE_VERSION" \
|
||||
--notes-file release-notes.md \
|
||||
--verify-tag \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
fi
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Build output
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Aa]rtifacts/
|
||||
[Tt]est[Rr]esult*/
|
||||
coverage/
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# IDE and user-specific files
|
||||
.vs/
|
||||
.vscode/
|
||||
*.user
|
||||
*.suo
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Local configuration and credentials
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
appsettings.Development.json
|
||||
secrets.json
|
||||
|
||||
# Operating-system files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Contract-monitor working files
|
||||
eng/openapi/*.downloaded.yaml
|
||||
eng/openapi/*.normalized.yaml
|
||||
@@ -0,0 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project are documented here. This project follows
|
||||
[Semantic Versioning](https://semver.org/). The SDK version describes the
|
||||
public .NET API; it is deliberately independent from BinaryLane's preview API
|
||||
version.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
## [0.1.0-beta.1] - 2026-08-16
|
||||
|
||||
### Added
|
||||
|
||||
- Initial preview release of the unofficial community .NET client for the
|
||||
BinaryLane v2 API.
|
||||
- Typed HTTP client setup, pluggable bearer-token authentication, pagination,
|
||||
API error mapping, and a maintained read-only demo application.
|
||||
- HTTPS-only bearer-token transport, protected request authority and
|
||||
authorization headers, and a 16 MiB successful-response buffer limit.
|
||||
- OpenAPI contract snapshot and change-monitoring automation for BinaryLane
|
||||
API reference version `0.39.1`.
|
||||
|
||||
[Unreleased]: https://github.com/alexhopeoconnor/binarylane-dotnet/compare/v0.1.0-beta.1...HEAD
|
||||
[0.1.0-beta.1]: https://github.com/alexhopeoconnor/binarylane-dotnet/releases/tag/v0.1.0-beta.1
|
||||
@@ -0,0 +1,71 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for improving the SDK. Keep changes focused, tested, documented, and
|
||||
backwards compatible unless a breaking change is explicitly planned.
|
||||
|
||||
## Local setup
|
||||
|
||||
Install the SDK pinned in `global.json`, then run:
|
||||
|
||||
```bash
|
||||
dotnet restore src/BinaryLane.Api/BinaryLane.Api.csproj --locked-mode
|
||||
dotnet restore tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --locked-mode
|
||||
dotnet restore examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --locked-mode
|
||||
dotnet build src/BinaryLane.Api/BinaryLane.Api.csproj -c Release --no-restore
|
||||
dotnet build tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj -c Release --no-restore
|
||||
dotnet build examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj -c Release --no-restore
|
||||
dotnet test tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj -c Release --no-build
|
||||
dotnet format src/BinaryLane.Api/BinaryLane.Api.csproj --verify-no-changes --no-restore
|
||||
dotnet format tests/BinaryLane.Api.Tests/BinaryLane.Api.Tests.csproj --verify-no-changes --no-restore
|
||||
dotnet format examples/BinaryLane.Api.Demo/BinaryLane.Api.Demo.csproj --verify-no-changes --no-restore
|
||||
```
|
||||
|
||||
The tracked demo application lives in
|
||||
`examples/BinaryLane.Api.Demo`. Configure a personal API token with user
|
||||
secrets; never put it in a file, test fixture, issue, pull request, or commit.
|
||||
|
||||
```bash
|
||||
dotnet user-secrets set \
|
||||
"BinaryLane:ApiToken" "your-token" \
|
||||
--project examples/BinaryLane.Api.Demo
|
||||
```
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Keep each pull request narrowly scoped and explain the user-visible effect.
|
||||
- Add or update unit tests for every changed HTTP request, response, error, or
|
||||
public API contract.
|
||||
- Use scrubbed fixtures only. Never record a real bearer token, password,
|
||||
user-data payload, IP address, server name, or account email unless it is
|
||||
demonstrably non-sensitive and intended for publication.
|
||||
- Update XML documentation, the README, and the appropriate page in `docs/`
|
||||
when a public behavior changes.
|
||||
- Update `docs/api-coverage.md` when support for a documented BinaryLane route
|
||||
changes.
|
||||
- Add a changelog entry under **Unreleased** for a user-visible change.
|
||||
|
||||
## API design rules
|
||||
|
||||
- Keep public APIs in `BinaryLane.Api.V2.*`.
|
||||
- Prefer focused resource interfaces over a monolithic service interface.
|
||||
- Accept a `CancellationToken` on every asynchronous operation.
|
||||
- Keep provider fields forward compatible: do not turn preview response values
|
||||
into closed C# enums unless the API contract guarantees them.
|
||||
- Do not add automatic retries for state-changing requests. BinaryLane does not
|
||||
document idempotency keys, so retries can provision or mutate resources twice.
|
||||
- Avoid breaking changes in a minor or patch SDK version. Follow
|
||||
[Semantic Versioning](https://semver.org/).
|
||||
|
||||
## Upstream OpenAPI changes
|
||||
|
||||
The provider's OpenAPI document is a preview contract and can change without a
|
||||
version increment. See [the contract workflow](docs/upstream-contract.md)
|
||||
before updating `eng/openapi/`. Review any change by hand; the raw contract
|
||||
contains virtual `#ActionName` documentation paths that must not become real
|
||||
HTTP endpoints in the SDK.
|
||||
|
||||
## Releases
|
||||
|
||||
Only maintainers may release packages. See [the release guide](docs/releasing.md).
|
||||
The release workflow uses NuGet trusted publishing through GitHub OIDC; no
|
||||
long-lived NuGet API key belongs in this repository or its GitHub secrets.
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AnalysisLevel>latest-recommended</AnalysisLevel>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Deterministic>true</Deterministic>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageVersion Include="xunit" Version="2.9.2" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Alex Hope-O'Connor
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<!-- Keep restores reproducible and prevent inherited workstation feeds from
|
||||
becoming accidental package dependencies. -->
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -0,0 +1,94 @@
|
||||
# BinaryLane.Api
|
||||
|
||||
[](https://github.com/alexhopeoconnor/binarylane-dotnet/actions/workflows/ci.yml)
|
||||
[](https://www.nuget.org/packages/BinaryLane.Api)
|
||||
|
||||
`BinaryLane.Api` is an unofficial .NET client for the BinaryLane v2 API. It
|
||||
provides typed clients for account, billing, servers, images, DNS, load
|
||||
balancers, VPCs, and other v2 resources.
|
||||
|
||||
See the [BinaryLane API reference](https://api.binarylane.com.au/reference/) for
|
||||
provider-specific API behaviour.
|
||||
|
||||
BinaryLane describes its API as a developer preview. Pin the package version
|
||||
used by production applications and review release notes before upgrading.
|
||||
|
||||
> This project is not affiliated with, endorsed by, or supported by BinaryLane.
|
||||
> For account or infrastructure support, use
|
||||
> [BinaryLane Support](https://support.binarylane.com.au/support/home).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
dotnet add package BinaryLane.Api --prerelease
|
||||
```
|
||||
|
||||
Remove `--prerelease` once a stable package version is available.
|
||||
|
||||
## Quick start
|
||||
|
||||
Keep the API token in a secret store, user secrets, or an environment variable.
|
||||
Do not commit it to configuration or source control.
|
||||
|
||||
```csharp
|
||||
using BinaryLane.Api.V2;
|
||||
using BinaryLane.Api.V2.DependencyInjection;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddBinaryLaneApi(options =>
|
||||
{
|
||||
options.ApiToken = builder.Configuration["BinaryLane:ApiToken"]
|
||||
?? Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN")
|
||||
?? throw new InvalidOperationException("A BinaryLane API token is required.");
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/account", async (IBinaryLaneClient binaryLane, CancellationToken cancellationToken) =>
|
||||
await binaryLane.Account.GetAsync(cancellationToken));
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
Use the resource clients from `IBinaryLaneClient`:
|
||||
|
||||
```csharp
|
||||
static async Task ListServersAsync(
|
||||
IBinaryLaneClient binaryLane,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var server in binaryLane.Servers.ListAllAsync(cancellationToken: cancellationToken))
|
||||
{
|
||||
Console.WriteLine($"{server.Id}: {server.Name}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
List methods support `CancellationToken`. Mutating calls are sent once; add a
|
||||
retry policy only when the operation is known to be safe to repeat.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Guide | Use it for |
|
||||
| --- | --- |
|
||||
| [Getting started](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/getting-started.md) | Your first request and user-secrets setup. |
|
||||
| [Configuration](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/configuration.md) | Tokens, timeouts, direct construction, and HTTP configuration. |
|
||||
| [Pagination](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/pagination.md) | Working with pages or async enumeration. |
|
||||
| [Actions](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/actions-and-polling.md) | Submitting and optionally waiting for server actions. |
|
||||
| [Error handling](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/docs/errors.md) | Handling API failures safely. |
|
||||
| [Demo](https://github.com/alexhopeoconnor/binarylane-dotnet/tree/main/examples/BinaryLane.Api.Demo) | Running the included read-only console app. |
|
||||
|
||||
## Compatibility
|
||||
|
||||
The package targets `net8.0` and `netstandard2.0`; .NET 10 applications use
|
||||
the `net8.0` asset. Package versions follow [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## Contributing and security
|
||||
|
||||
See [CONTRIBUTING.md](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/CONTRIBUTING.md) for development guidance and
|
||||
[SECURITY.md](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/SECURITY.md) for private vulnerability reporting.
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the [MIT License](https://github.com/alexhopeoconnor/binarylane-dotnet/blob/main/LICENSE).
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Security policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
Security fixes are made for the latest released package version. While the SDK
|
||||
is pre-1.0, upgrade to the latest beta before reporting a suspected issue.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please report vulnerabilities privately through
|
||||
[GitHub Security Advisories](https://github.com/alexhopeoconnor/binarylane-dotnet/security/advisories/new).
|
||||
If private reporting is unavailable, open a minimal public issue that asks for
|
||||
a private contact channel and does not disclose exploit details.
|
||||
|
||||
Do not include any of the following in a report, issue, pull request, log, or
|
||||
sample:
|
||||
|
||||
- BinaryLane bearer tokens or authorization headers;
|
||||
- account email addresses, invoices, or billing data;
|
||||
- passwords, password reset payloads, SSH private keys, or `user_data`;
|
||||
- private IP addresses, server names, DNS records, or raw response bodies that
|
||||
are not essential to demonstrate the problem.
|
||||
|
||||
Maintainers aim to acknowledge a report within seven days and will coordinate
|
||||
a fix and disclosure timeline with the reporter.
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" role="img" aria-labelledby="title">
|
||||
<title id="title">Abstract network route icon</title>
|
||||
<rect width="256" height="256" rx="52" fill="#172554"/>
|
||||
<path d="M66 82h55c19 0 35 16 35 35v57" fill="none" stroke="#67E8F9" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/>
|
||||
<path d="M100 174h35c19 0 35-16 35-35V82" fill="none" stroke="#A7F3D0" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/>
|
||||
<circle cx="66" cy="82" r="17" fill="#F8FAFC"/>
|
||||
<circle cx="100" cy="174" r="17" fill="#F8FAFC"/>
|
||||
<circle cx="156" cy="174" r="17" fill="#F8FAFC"/>
|
||||
<circle cx="170" cy="82" r="17" fill="#F8FAFC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 727 B |
@@ -0,0 +1,48 @@
|
||||
# Server actions
|
||||
|
||||
Use a typed `ServerAction` to submit a server action. For example, this powers
|
||||
on a server:
|
||||
|
||||
```csharp
|
||||
using BinaryLane.Api.V2.Models;
|
||||
|
||||
var submission = await client.Servers.SubmitActionAsync(
|
||||
serverId,
|
||||
new PowerOnServerAction(),
|
||||
cancellationToken);
|
||||
```
|
||||
|
||||
Common action types include `PowerOnServerAction`, `PowerOffServerAction`,
|
||||
`RebootServerAction`, `ResizeServerAction`, and `RebuildServerAction`.
|
||||
|
||||
## Waiting for completion
|
||||
|
||||
BinaryLane can accept an action before it has finished. When the response
|
||||
contains an action, use its identifier to wait for the final status:
|
||||
|
||||
```csharp
|
||||
if (submission.Action is { } action)
|
||||
{
|
||||
var completed = await client.Actions.WaitForCompletionAsync(
|
||||
action.Id,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
Pass `ActionWaitOptions` when you need a different timeout or polling interval.
|
||||
The default is a 15-minute timeout with a two-second polling interval.
|
||||
|
||||
## Status values
|
||||
|
||||
`BinaryLaneAction.Status` and `BinaryLaneAction.Type` are strings. Compare a
|
||||
documented value with `BinaryLaneValues`:
|
||||
|
||||
```csharp
|
||||
if (action.Status == BinaryLaneValues.ActionStatus.Completed)
|
||||
{
|
||||
// Continue with the next step.
|
||||
}
|
||||
```
|
||||
|
||||
If an action request has an uncertain outcome, retrieve the action or server
|
||||
state before submitting it again.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Maintainer guide: API coverage
|
||||
|
||||
This is a maintainer reference. Package consumers can start with the
|
||||
[README](../README.md).
|
||||
|
||||
The SDK's scope is the BinaryLane v2 OpenAPI contract committed at
|
||||
`eng/openapi/binarylane-v2.openapi.yaml`. The raw contract currently declares
|
||||
version `0.39.1`; it is a developer-preview contract and may change without a
|
||||
version change.
|
||||
|
||||
The raw document contains virtual paths such as
|
||||
`/v2/servers/{server_id}/actions#PowerOn`. These are API-reference aliases for
|
||||
action payload variants, not real HTTP routes. Coverage and generated-code
|
||||
checks must use the normalized contract and count only the real
|
||||
`POST /v2/servers/{server_id}/actions` route.
|
||||
|
||||
| API area | Public resource boundary |
|
||||
| --- | --- |
|
||||
| Account | `IAccountApi` |
|
||||
| Balances, invoices, and unpaid invoices | `IBillingApi` |
|
||||
| Actions | `IActionsApi` |
|
||||
| Servers and server subresources | `IServersApi` |
|
||||
| Images | `IImagesApi` |
|
||||
| SSH keys | `ISshKeysApi` |
|
||||
| DNS domains, nameservers, and records | `IDomainsApi` |
|
||||
| Load balancers and forwarding rules | `ILoadBalancersApi` |
|
||||
| VPCs and members | `IVpcsApi` |
|
||||
| Regions, sizes, and software catalogues | `IRegionsApi`, `ISizesApi`, `ISoftwareApi` |
|
||||
| Reverse names | `IReverseNamesApi` |
|
||||
| Data usage | `IDataUsageApi` |
|
||||
| Sample sets | `ISampleSetsApi` |
|
||||
|
||||
Keep this table and the related tests up to date when API support changes.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Configuration
|
||||
|
||||
## Standard registration
|
||||
|
||||
In an ASP.NET Core or generic-host application, `AddBinaryLaneApi` configures
|
||||
`IBinaryLaneClient` and the individual resource interfaces.
|
||||
|
||||
```csharp
|
||||
builder.Services.AddBinaryLaneApi(options =>
|
||||
{
|
||||
options.ApiToken = builder.Configuration["BinaryLane:ApiToken"];
|
||||
options.RequestTimeoutSeconds = 100;
|
||||
});
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `BaseUrl` | `https://api.binarylane.com.au/` | API base address. Leave unchanged for BinaryLane's public API. |
|
||||
| `ApiToken` | none | Bearer token used by the default provider. |
|
||||
| `RequestTimeoutSeconds` | `100` | Timeout for an individual HTTP request. |
|
||||
|
||||
The base URL must be an HTTPS URL without credentials, a query string, or a
|
||||
fragment. Options are validated when the application starts.
|
||||
|
||||
## Rotating tokens
|
||||
|
||||
Register `IBinaryLaneTokenProvider` before calling `AddBinaryLaneApi` when the
|
||||
token comes from a vault or changes during the lifetime of the application.
|
||||
`VaultTokenProvider` below is your implementation of that interface.
|
||||
|
||||
```csharp
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
using BinaryLane.Api.V2.DependencyInjection;
|
||||
|
||||
builder.Services.AddSingleton<IBinaryLaneTokenProvider, VaultTokenProvider>();
|
||||
builder.Services.AddBinaryLaneApi(options => options.RequestTimeoutSeconds = 100);
|
||||
```
|
||||
|
||||
The provider is called for every request. Do not log the returned token.
|
||||
|
||||
## Use without dependency injection
|
||||
|
||||
```csharp
|
||||
using BinaryLane.Api.V2;
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
|
||||
using var httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri("https://api.binarylane.com.au/"),
|
||||
};
|
||||
|
||||
var token = Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN")
|
||||
?? throw new InvalidOperationException("Set BINARYLANE_API_TOKEN.");
|
||||
|
||||
var client = new BinaryLaneClient(
|
||||
httpClient,
|
||||
new StaticBinaryLaneTokenProvider(token));
|
||||
```
|
||||
|
||||
## HTTP configuration
|
||||
|
||||
`AddBinaryLaneApi` returns an `IHttpClientBuilder`, so you can configure proxy,
|
||||
telemetry, or other application-specific handlers.
|
||||
|
||||
```csharp
|
||||
builder.Services.AddBinaryLaneApi(options => options.ApiToken = token)
|
||||
.ConfigureHttpClient(client => client.DefaultRequestHeaders.Add("X-App", "my-service"));
|
||||
```
|
||||
|
||||
Only add automatic retries when the request is safe to repeat. In particular,
|
||||
POST, PUT, PATCH, and DELETE requests may have already been applied when a
|
||||
network failure is reported.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Error handling
|
||||
|
||||
All API failures derive from `BinaryLaneApiException`.
|
||||
|
||||
| Exception | When it is used |
|
||||
| --- | --- |
|
||||
| `BinaryLaneUnauthorizedException` | The token is missing, invalid, or expired. |
|
||||
| `BinaryLaneForbiddenException` | The token does not have access to the resource. |
|
||||
| `BinaryLaneNotFoundException` | The requested resource does not exist or is not visible to the token. |
|
||||
| `BinaryLaneValidationException` | BinaryLane rejected the request payload. |
|
||||
| `BinaryLaneApiException` | Any other non-success HTTP response. |
|
||||
|
||||
```csharp
|
||||
using BinaryLane.Api.V2.Errors;
|
||||
|
||||
try
|
||||
{
|
||||
var server = await client.Servers.GetAsync(serverId, cancellationToken);
|
||||
}
|
||||
catch (BinaryLaneNotFoundException)
|
||||
{
|
||||
// Handle a missing server.
|
||||
}
|
||||
catch (BinaryLaneValidationException)
|
||||
{
|
||||
// Show an appropriate validation message to the caller.
|
||||
}
|
||||
catch (BinaryLaneApiException exception)
|
||||
{
|
||||
// Use exception.StatusCode to choose an application-specific response.
|
||||
}
|
||||
```
|
||||
|
||||
Each exception provides `StatusCode`, `RequestUri`, `Headers`, and, when
|
||||
available, `Problem` and `ResponseBody`. Provider detail text can contain user
|
||||
data. Do not write `Problem`, `Headers`, or `ResponseBody` to application logs
|
||||
without appropriate redaction.
|
||||
|
||||
Successful JSON responses are limited to 16 MiB. A larger response throws
|
||||
`HttpRequestException` before the client buffers it in memory.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Getting started
|
||||
|
||||
`BinaryLane.Api` connects to BinaryLane's bearer-token v2 API at
|
||||
`https://api.binarylane.com.au/v2/`.
|
||||
|
||||
## 1. Install the package
|
||||
|
||||
```bash
|
||||
dotnet add package BinaryLane.Api --prerelease
|
||||
```
|
||||
|
||||
## 2. Store the token safely
|
||||
|
||||
For local development, initialise user secrets for your application and add
|
||||
the token:
|
||||
|
||||
```bash
|
||||
dotnet user-secrets init
|
||||
dotnet user-secrets set "BinaryLane:ApiToken" "your-token"
|
||||
```
|
||||
|
||||
For hosted applications, use the platform's secret store or supply
|
||||
`BINARYLANE_API_TOKEN` at runtime.
|
||||
|
||||
## 3. Register the client
|
||||
|
||||
```csharp
|
||||
using BinaryLane.Api.V2;
|
||||
using BinaryLane.Api.V2.DependencyInjection;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
|
||||
builder.Services.AddBinaryLaneApi(options =>
|
||||
{
|
||||
options.ApiToken = builder.Configuration["BinaryLane:ApiToken"]
|
||||
?? Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN")
|
||||
?? throw new InvalidOperationException("A BinaryLane API token is required.");
|
||||
});
|
||||
```
|
||||
|
||||
## 4. Make a request
|
||||
|
||||
Inject `IBinaryLaneClient`, then select the resource you need:
|
||||
|
||||
```csharp
|
||||
public sealed class ServerReader(IBinaryLaneClient binaryLane)
|
||||
{
|
||||
public IAsyncEnumerable<Server> ListAsync(CancellationToken cancellationToken) =>
|
||||
binaryLane.Servers.ListAllAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
See [Configuration](configuration.md) for custom token providers and
|
||||
[Pagination](pagination.md) for page-by-page access.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Pagination
|
||||
|
||||
List endpoints accept one-based `page` and `per_page` values. `per_page` can be
|
||||
between 1 and 200.
|
||||
|
||||
Use `ListAsync` when you need a specific page or the provider's page links:
|
||||
|
||||
```csharp
|
||||
using BinaryLane.Api.V2.Pagination;
|
||||
|
||||
var page = await client.Servers.ListAsync(
|
||||
new PageRequest { Page = 1, PerPage = 50 },
|
||||
cancellationToken);
|
||||
|
||||
foreach (var server in page.Items)
|
||||
{
|
||||
await ProcessAsync(server, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
Use `ListAllAsync` to process every page as it is needed:
|
||||
|
||||
```csharp
|
||||
await foreach (var server in client.Servers.ListAllAsync(cancellationToken: cancellationToken))
|
||||
{
|
||||
await ProcessAsync(server, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
Pass a cancellation token from the calling request, worker, or command so a
|
||||
stopped operation does not continue fetching pages.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Maintainer guide: releases
|
||||
|
||||
This page is for package maintainers.
|
||||
|
||||
This repository publishes packages with NuGet trusted publishing and GitHub
|
||||
OpenID Connect (OIDC). It must not use a long-lived NuGet API key.
|
||||
|
||||
## One-time setup
|
||||
|
||||
1. On NuGet.org, create or verify the trusted-publishing policy:
|
||||
- publisher: `GitHubActions`;
|
||||
- GitHub owner: `alexhopeoconnor`;
|
||||
- repository: `binarylane-dotnet`;
|
||||
- workflow: `publish.yml`;
|
||||
- environment: `release`.
|
||||
2. In GitHub, create a protected environment named `release` and require a
|
||||
maintainer approval.
|
||||
3. Create a protected `release` environment secret named `NUGET_USER` with
|
||||
the NuGet.org username `alex.hope.oconnor`. It is an identifier, not an
|
||||
API key.
|
||||
4. Protect `main` with CI and restrict creation of `v*` tags.
|
||||
|
||||
The first successful OIDC publication must occur before NuGet's policy
|
||||
activation window expires. After success, NuGet permanently activates the
|
||||
policy for this exact repository/workflow/environment identity.
|
||||
|
||||
## Release checklist
|
||||
|
||||
1. Review BinaryLane contract changes and update the committed snapshot if
|
||||
necessary.
|
||||
2. Update code, tests, docs, API coverage, and `CHANGELOG.md`.
|
||||
3. Set the package `<Version>` to the intended SemVer release.
|
||||
4. Open and merge the release pull request after CI succeeds.
|
||||
5. Tag the exact merge commit. The tag must equal the package version with a
|
||||
leading `v`.
|
||||
|
||||
```bash
|
||||
git tag -a v0.1.0-beta.1 -m "BinaryLane.Api 0.1.0-beta.1"
|
||||
git push origin v0.1.0-beta.1
|
||||
```
|
||||
|
||||
6. Approve the protected `release` environment in GitHub Actions.
|
||||
7. Verify the package page, rendered README, icon, license, repository link,
|
||||
symbols, package ownership, and GitHub Release.
|
||||
|
||||
## What the release workflow does
|
||||
|
||||
`publish.yml` validates the tag, package version, and changelog heading; it
|
||||
then restores, builds, tests, packs, and compiles the maintained demo against
|
||||
the resulting local `.nupkg`. Only then does `NuGet/login@v1` exchange the
|
||||
GitHub OIDC identity for a short-lived one-time publish key. The key is used
|
||||
only in memory by the job.
|
||||
|
||||
The GitHub Release job runs after NuGet publishing and receives no OIDC
|
||||
permission. It attaches the package files and uses the matching changelog
|
||||
section for release notes.
|
||||
|
||||
## Recovery
|
||||
|
||||
NuGet packages cannot be overwritten. If a release has a packaging defect,
|
||||
unlist it if appropriate, publish a new version, and document the correction
|
||||
in the changelog. Do not retag a version or attempt to reuse a published
|
||||
package version.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Maintainer guide: upstream API contract
|
||||
|
||||
This page describes SDK maintenance. Package consumers can start with the
|
||||
[README](../README.md).
|
||||
|
||||
The source of truth for BinaryLane's API is its published OpenAPI document:
|
||||
|
||||
<https://api.binarylane.com.au/reference/openapi.yaml>
|
||||
|
||||
The committed raw snapshot is intentionally reviewable:
|
||||
|
||||
```text
|
||||
eng/openapi/binarylane-v2.openapi.yaml
|
||||
eng/openapi/contract.json
|
||||
```
|
||||
|
||||
`contract.json` records the source URL, provider-declared version, SHA-256,
|
||||
and retrieval time. It describes an upstream artifact; it does not set the
|
||||
NuGet package version.
|
||||
|
||||
## Snapshot purpose
|
||||
|
||||
BinaryLane's preview API can change without a version change. The committed
|
||||
snapshot makes those changes visible during SDK maintenance.
|
||||
|
||||
## Normalization
|
||||
|
||||
The raw document includes virtual `#ActionName` paths for individual server
|
||||
action variants. They exist to improve the provider's reference UI but are not
|
||||
HTTP paths. `eng/normalize-openapi.sh` strips them when producing a normalized
|
||||
document for code generation or coverage checks. Do not issue requests to
|
||||
those virtual paths.
|
||||
|
||||
## Monitoring and updating
|
||||
|
||||
The scheduled `contract-monitor.yml` workflow downloads the live document and
|
||||
runs `eng/verify-openapi-contract.sh`. If the SHA-256 or provider-declared
|
||||
version differs, it opens one issue rather than silently updating the SDK.
|
||||
|
||||
To intentionally refresh the snapshot after review:
|
||||
|
||||
```bash
|
||||
./eng/refresh-openapi-contract.sh
|
||||
git diff -- eng/openapi docs/api-coverage.md
|
||||
```
|
||||
|
||||
Then update affected models, coverage documentation, changelog, and tests in
|
||||
the same pull request. Review the change before merging it.
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Enforces the release identity before any package is published. The version is
|
||||
# intentionally read from the package project so one source of truth controls
|
||||
# both the .nupkg and its corresponding Git tag.
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
printf 'Usage: %s v<package-version>\n' "${0##*/}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
tag_name="$1"
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
|
||||
changelog_file="$repository_root/CHANGELOG.md"
|
||||
|
||||
if [[ ! -f "$changelog_file" ]]; then
|
||||
printf 'Expected changelog does not exist.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
package_version="$("$script_dir/read-package-version.sh")"
|
||||
|
||||
expected_tag="v${package_version}"
|
||||
if [[ "$tag_name" != "$expected_tag" ]]; then
|
||||
printf 'Tag %s does not match package version %s (expected %s).\n' \
|
||||
"$tag_name" "$package_version" "$expected_tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fqx "## [$package_version]" "$changelog_file" && \
|
||||
! grep -Fq "## [$package_version] -" "$changelog_file" && \
|
||||
! grep -Fqx "## $package_version" "$changelog_file" && \
|
||||
! grep -Fq "## $package_version -" "$changelog_file"; then
|
||||
printf 'CHANGELOG.md needs a level-two heading for %s.\n' "$package_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Release version is consistent: %s.\n' "$package_version"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
printf 'Usage: %s <package-version>\n' "${0##*/}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
package_version="$1"
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
|
||||
changelog_file="$repository_root/CHANGELOG.md"
|
||||
|
||||
notes="$(awk -v bracket_heading="## [$package_version]" -v bare_heading="## $package_version" '
|
||||
$0 == bracket_heading || index($0, bracket_heading " -") == 1 ||
|
||||
$0 == bare_heading || index($0, bare_heading " -") == 1 {
|
||||
capture = 1
|
||||
next
|
||||
}
|
||||
|
||||
capture && /^## / {
|
||||
exit
|
||||
}
|
||||
|
||||
capture && /^\[/ {
|
||||
exit
|
||||
}
|
||||
|
||||
capture {
|
||||
print
|
||||
}
|
||||
' "$changelog_file")"
|
||||
|
||||
if [[ -z "${notes//[[:space:]]/}" ]]; then
|
||||
printf 'No release notes found for %s in CHANGELOG.md.\n' "$package_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$notes"
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Normalize BinaryLane's raw OpenAPI document for code-generation or coverage
|
||||
# tooling. The reference includes virtual `#ActionName` paths for individual
|
||||
# server action payloads. They are documentation aliases, not HTTP endpoints.
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
default_input="$script_dir/openapi/binarylane-v2.openapi.yaml"
|
||||
input_file="${1:-$default_input}"
|
||||
output_file="${2:-}"
|
||||
|
||||
if [[ ! -f "$input_file" ]]; then
|
||||
printf 'OpenAPI input does not exist: %s\n' "$input_file" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
normalize() {
|
||||
awk '
|
||||
/^paths:$/ {
|
||||
in_paths = 1
|
||||
}
|
||||
|
||||
in_paths && /^components:$/ {
|
||||
in_paths = 0
|
||||
skip_virtual_path = 0
|
||||
}
|
||||
|
||||
in_paths && /^ ['"'"'][^'"'"']*#[^'"'"']*['"'"']:[[:space:]]*$/ {
|
||||
skip_virtual_path = 1
|
||||
removed += 1
|
||||
next
|
||||
}
|
||||
|
||||
in_paths && /^ [^[:space:]][^:]*:[[:space:]]*$/ {
|
||||
skip_virtual_path = 0
|
||||
}
|
||||
|
||||
!skip_virtual_path {
|
||||
print
|
||||
}
|
||||
|
||||
END {
|
||||
printf "Removed %d virtual action path(s).\n", removed > "/dev/stderr"
|
||||
}
|
||||
' "$input_file"
|
||||
}
|
||||
|
||||
if [[ -n "$output_file" ]]; then
|
||||
mkdir -p "$(dirname -- "$output_file")"
|
||||
normalize > "$output_file"
|
||||
else
|
||||
normalize
|
||||
fi
|
||||
@@ -0,0 +1,21 @@
|
||||
# BinaryLane API contract
|
||||
|
||||
`binarylane-v2.openapi.yaml` is an unmodified snapshot of BinaryLane's published
|
||||
OpenAPI document. Its provenance and digest are recorded in `contract.json`.
|
||||
|
||||
The provider currently describes the API as a developer preview, so an upstream
|
||||
version number alone must not be used as a compatibility guarantee. The contract
|
||||
monitor compares the document digest as well as its declared version.
|
||||
|
||||
## Normalization rule
|
||||
|
||||
The document has 52 routable paths and 42 additional paths such as
|
||||
`/v2/servers/{server_id}/actions#PowerOn`. The latter are documentation aliases
|
||||
for payload variants of `POST /v2/servers/{server_id}/actions`; URI fragments do
|
||||
not reach an HTTP server. They must be removed before an OpenAPI generator is
|
||||
asked to create a route surface. `normalization.json` records the exact rule and
|
||||
each affected path.
|
||||
|
||||
Public SDK DTOs deliberately model all provider enum values as strings. This is
|
||||
forward-compatible with a preview API that may add a value before a new SDK
|
||||
release is available.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "https://api.binarylane.com.au/reference/openapi.yaml",
|
||||
"upstreamVersion": "0.39.1",
|
||||
"sha256": "947b62c1649f68e8b0688d113f4fa3b5037b7c514b7e9cd33613e40574f854b1",
|
||||
"retrievedUtc": "2026-08-16T09:22:01Z"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"input": "binarylane-v2.openapi.yaml",
|
||||
"normalizationRules": [
|
||||
{
|
||||
"kind": "exclude-paths-containing",
|
||||
"value": "#",
|
||||
"reason": "The upstream document uses fragment-bearing paths to document variants of the one real server actions endpoint. A URI fragment is never sent in an HTTP request."
|
||||
},
|
||||
{
|
||||
"kind": "preserve-discriminator",
|
||||
"schema": "ServerAction",
|
||||
"property": "type",
|
||||
"reason": "The action body is a discriminated union. Do not flatten or silently discard the discriminator mapping when generating test fixtures or internal wire code."
|
||||
},
|
||||
{
|
||||
"kind": "open-enum",
|
||||
"reason": "Provider enum values are represented as strings in public DTOs so new upstream values remain readable by existing SDK versions."
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
"/v2/servers/{server_id}/actions#AddDisk",
|
||||
"/v2/servers/{server_id}/actions#AttachBackup",
|
||||
"/v2/servers/{server_id}/actions#ChangeAdvancedFeatures",
|
||||
"/v2/servers/{server_id}/actions#ChangeAdvancedFirewallRules",
|
||||
"/v2/servers/{server_id}/actions#ChangeBackupSchedule",
|
||||
"/v2/servers/{server_id}/actions#ChangeIpv6",
|
||||
"/v2/servers/{server_id}/actions#ChangeIpv6ReverseNameservers",
|
||||
"/v2/servers/{server_id}/actions#ChangeKernel",
|
||||
"/v2/servers/{server_id}/actions#ChangeManageOffsiteBackupCopies",
|
||||
"/v2/servers/{server_id}/actions#ChangeNetwork",
|
||||
"/v2/servers/{server_id}/actions#ChangeOffsiteBackupLocation",
|
||||
"/v2/servers/{server_id}/actions#ChangePartner",
|
||||
"/v2/servers/{server_id}/actions#ChangePortBlocking",
|
||||
"/v2/servers/{server_id}/actions#ChangeRegion",
|
||||
"/v2/servers/{server_id}/actions#ChangeReverseName",
|
||||
"/v2/servers/{server_id}/actions#ChangeSeparatePrivateNetworkInterface",
|
||||
"/v2/servers/{server_id}/actions#ChangeSourceAndDestinationCheck",
|
||||
"/v2/servers/{server_id}/actions#ChangeThresholdAlerts",
|
||||
"/v2/servers/{server_id}/actions#ChangeVpcIpv4",
|
||||
"/v2/servers/{server_id}/actions#CloneUsingBackup",
|
||||
"/v2/servers/{server_id}/actions#DeleteDisk",
|
||||
"/v2/servers/{server_id}/actions#DetachBackup",
|
||||
"/v2/servers/{server_id}/actions#DisableBackups",
|
||||
"/v2/servers/{server_id}/actions#DisableSelinux",
|
||||
"/v2/servers/{server_id}/actions#EnableBackups",
|
||||
"/v2/servers/{server_id}/actions#EnableIpv6",
|
||||
"/v2/servers/{server_id}/actions#IsRunning",
|
||||
"/v2/servers/{server_id}/actions#PasswordReset",
|
||||
"/v2/servers/{server_id}/actions#Ping",
|
||||
"/v2/servers/{server_id}/actions#PowerCycle",
|
||||
"/v2/servers/{server_id}/actions#PowerOff",
|
||||
"/v2/servers/{server_id}/actions#PowerOn",
|
||||
"/v2/servers/{server_id}/actions#Reboot",
|
||||
"/v2/servers/{server_id}/actions#Rebuild",
|
||||
"/v2/servers/{server_id}/actions#Rename",
|
||||
"/v2/servers/{server_id}/actions#Resize",
|
||||
"/v2/servers/{server_id}/actions#ResizeDisk",
|
||||
"/v2/servers/{server_id}/actions#Restore",
|
||||
"/v2/servers/{server_id}/actions#Shutdown",
|
||||
"/v2/servers/{server_id}/actions#TakeBackup",
|
||||
"/v2/servers/{server_id}/actions#Uncancel",
|
||||
"/v2/servers/{server_id}/actions#Uptime"
|
||||
]
|
||||
}
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
|
||||
output_directory="${1:-$repository_root/artifacts}"
|
||||
|
||||
dotnet pack "$repository_root/src/BinaryLane.Api/BinaryLane.Api.csproj" \
|
||||
--configuration Release \
|
||||
--no-restore \
|
||||
-m:1 \
|
||||
-nodeReuse:false \
|
||||
-p:BuildInParallel=false \
|
||||
--output "$output_directory"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
project_file="$(CDPATH= cd -- "$script_dir/.." && pwd)/src/BinaryLane.Api/BinaryLane.Api.csproj"
|
||||
|
||||
if [[ ! -f "$project_file" ]]; then
|
||||
printf 'Package project does not exist: %s\n' "$project_file" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
read_project_property() {
|
||||
local property_name="$1"
|
||||
sed -n "s@^[[:space:]]*<${property_name}>[[:space:]]*\\([^<]*\\)[[:space:]]*</${property_name}>.*@\\1@p" \
|
||||
"$project_file" | head -n 1
|
||||
}
|
||||
|
||||
package_version="$(read_project_property PackageVersion)"
|
||||
if [[ -z "$package_version" ]]; then
|
||||
package_version="$(read_project_property Version)"
|
||||
fi
|
||||
|
||||
if [[ -z "$package_version" ]]; then
|
||||
printf 'Package project must contain a literal <PackageVersion> or <Version>.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf '%s\n' "$package_version"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Downloads the public upstream contract into the reviewed repository snapshot.
|
||||
# Run this only as part of a human-reviewed change; CI intentionally verifies
|
||||
# and reports drift instead of changing the snapshot by itself.
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
|
||||
contract_directory="$repository_root/eng/openapi"
|
||||
contract_file="$contract_directory/binarylane-v2.openapi.yaml"
|
||||
metadata_file="$contract_directory/contract.json"
|
||||
source_url="${BINARYLANE_OPENAPI_URL:-https://api.binarylane.com.au/reference/openapi.yaml}"
|
||||
temporary_file="$(mktemp)"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$temporary_file"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$contract_directory"
|
||||
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--connect-timeout 15 --max-time 90 --retry 3 --retry-all-errors \
|
||||
--output "$temporary_file" \
|
||||
"$source_url"
|
||||
|
||||
if [[ ! -s "$temporary_file" ]]; then
|
||||
printf 'Downloaded OpenAPI contract is empty.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
upstream_version="$({
|
||||
awk '
|
||||
/^info:$/ { in_info = 1; next }
|
||||
in_info && /^ version: / {
|
||||
sub(/^ version: /, "")
|
||||
gsub(/[[:space:]]+$/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$temporary_file"
|
||||
} || true)"
|
||||
|
||||
if [[ -z "$upstream_version" ]]; then
|
||||
printf 'Unable to read info.version from the downloaded OpenAPI contract.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sha256="$(sha256sum "$temporary_file" | awk '{print $1}')"
|
||||
retrieved_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
cp "$temporary_file" "$contract_file"
|
||||
printf '{\n "source": "%s",\n "upstreamVersion": "%s",\n "sha256": "%s",\n "retrievedUtc": "%s"\n}\n' \
|
||||
"$source_url" "$upstream_version" "$sha256" "$retrieved_utc" > "$metadata_file"
|
||||
|
||||
printf 'Updated %s (upstream %s, sha256 %s).\n' \
|
||||
"$contract_file" "$upstream_version" "$sha256"
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Compiles the tracked demo against the nupkg just produced by this repository,
|
||||
# rather than against the local source project. This catches bad package
|
||||
# metadata, missing dependencies, and public API regressions.
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
printf 'Usage: %s <package-directory>\n' "${0##*/}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
package_directory="$(CDPATH= cd -- "$1" && pwd)"
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
|
||||
demo_source="$repository_root/examples/BinaryLane.Api.Demo"
|
||||
temporary_directory="$(mktemp -d)"
|
||||
export NUGET_PACKAGES="$temporary_directory/nuget-packages"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$temporary_directory"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
package_file="$(find "$package_directory" -maxdepth 1 -type f -name 'BinaryLane.Api.*.nupkg' -print -quit)"
|
||||
if [[ -z "$package_file" ]]; then
|
||||
printf 'No BinaryLane.Api .nupkg found in %s.\n' "$package_directory" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
package_name="$(basename -- "$package_file")"
|
||||
package_version="${package_name#BinaryLane.Api.}"
|
||||
package_version="${package_version%.nupkg}"
|
||||
demo_directory="$temporary_directory/BinaryLane.Api.Demo"
|
||||
|
||||
cp -R "$demo_source" "$demo_directory"
|
||||
cp "$repository_root/Directory.Build.props" "$temporary_directory/Directory.Build.props"
|
||||
cp "$repository_root/Directory.Packages.props" "$temporary_directory/Directory.Packages.props"
|
||||
demo_project="$demo_directory/BinaryLane.Api.Demo.csproj"
|
||||
|
||||
if ! grep -Fq '<ProjectReference Include="../../src/BinaryLane.Api/BinaryLane.Api.csproj" />' "$demo_project"; then
|
||||
printf 'Demo project does not contain the expected local SDK project reference.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
sed -i "/<\\/ItemGroup>/i\\ <PackageVersion Include=\"BinaryLane.Api\" Version=\"$package_version\" />" \
|
||||
"$temporary_directory/Directory.Packages.props"
|
||||
|
||||
sed -i "s#<ProjectReference Include=\"../../src/BinaryLane.Api/BinaryLane.Api.csproj\" />#<PackageReference Include=\"BinaryLane.Api\" />#" \
|
||||
"$demo_project"
|
||||
|
||||
dotnet restore "$demo_project" \
|
||||
--source "$package_directory" \
|
||||
--source https://api.nuget.org/v3/index.json \
|
||||
--no-cache \
|
||||
-p:RestoreLockedMode=false
|
||||
dotnet build "$demo_project" \
|
||||
--configuration Release \
|
||||
--no-restore \
|
||||
-m:1 \
|
||||
-nodeReuse:false \
|
||||
-p:BuildInParallel=false
|
||||
|
||||
printf 'Demo compiled against BinaryLane.Api %s from %s.\n' \
|
||||
"$package_version" "$package_directory"
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Checks the live public BinaryLane OpenAPI document against the committed
|
||||
# snapshot. It reports drift but never changes source-controlled files.
|
||||
|
||||
script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repository_root="$(CDPATH= cd -- "$script_dir/.." && pwd)"
|
||||
metadata_file="$repository_root/eng/openapi/contract.json"
|
||||
source_url="${BINARYLANE_OPENAPI_URL:-https://api.binarylane.com.au/reference/openapi.yaml}"
|
||||
temporary_file="$(mktemp)"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$temporary_file"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ ! -f "$metadata_file" ]]; then
|
||||
printf 'Committed contract metadata does not exist: %s\n' "$metadata_file" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
expected_sha256="$(sed -n 's/^[[:space:]]*"sha256"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file")"
|
||||
expected_version="$(sed -n 's/^[[:space:]]*"upstreamVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$metadata_file")"
|
||||
|
||||
if [[ -z "$expected_sha256" || -z "$expected_version" ]]; then
|
||||
printf 'Committed contract metadata is missing a SHA-256 or upstream version.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \
|
||||
--connect-timeout 15 --max-time 90 --retry 3 --retry-all-errors \
|
||||
--output "$temporary_file" \
|
||||
"$source_url"
|
||||
|
||||
if [[ ! -s "$temporary_file" ]]; then
|
||||
printf 'Downloaded OpenAPI contract is empty.\n' >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
actual_sha256="$(sha256sum "$temporary_file" | awk '{print $1}')"
|
||||
actual_version="$({
|
||||
awk '
|
||||
/^info:$/ { in_info = 1; next }
|
||||
in_info && /^ version: / {
|
||||
sub(/^ version: /, "")
|
||||
gsub(/[[:space:]]+$/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$temporary_file"
|
||||
} || true)"
|
||||
|
||||
if [[ "$expected_sha256" == "$actual_sha256" && "$expected_version" == "$actual_version" ]]; then
|
||||
printf 'BinaryLane OpenAPI contract matches committed snapshot (%s, %s).\n' \
|
||||
"$actual_version" "$actual_sha256"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'BinaryLane OpenAPI contract changed.\n' >&2
|
||||
printf ' expected version: %s\n' "$expected_version" >&2
|
||||
printf ' actual version: %s\n' "${actual_version:-<unreadable>}" >&2
|
||||
printf ' expected sha256: %s\n' "$expected_sha256" >&2
|
||||
printf ' actual sha256: %s\n' "$actual_sha256" >&2
|
||||
printf 'Run ./eng/refresh-openapi-contract.sh, review the diff, then update SDK coverage and tests.\n' >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<UserSecretsId>binarylane-api-demo</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<ProjectReference Include="../../src/BinaryLane.Api/BinaryLane.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Text.Json;
|
||||
using BinaryLane.Api.V2;
|
||||
using BinaryLane.Api.V2.DependencyInjection;
|
||||
using BinaryLane.Api.V2.Errors;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
return await DemoProgram.RunAsync(args);
|
||||
|
||||
internal static class DemoProgram
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
public static async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
var command = args.FirstOrDefault()?.ToLowerInvariant();
|
||||
if (command is null or "--help" or "-h" or "help")
|
||||
{
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var cancellationSource = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (_, eventArgs) =>
|
||||
{
|
||||
eventArgs.Cancel = true;
|
||||
cancellationSource.Cancel();
|
||||
};
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: true)
|
||||
.AddUserSecrets(typeof(DemoProgram).Assembly, optional: true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
var token = builder.Configuration["BinaryLane:ApiToken"]
|
||||
?? Environment.GetEnvironmentVariable("BINARYLANE_API_TOKEN");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"No API token is configured. Use user secrets or set BINARYLANE_API_TOKEN. "
|
||||
+ "Run with --help for a safe setup command.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
builder.Services.AddBinaryLaneApi(options =>
|
||||
{
|
||||
options.ApiToken = token;
|
||||
|
||||
var baseUrl = builder.Configuration["BinaryLane:BaseUrl"];
|
||||
if (!string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
options.BaseUrl = baseUrl;
|
||||
}
|
||||
});
|
||||
|
||||
using var host = builder.Build();
|
||||
var client = host.Services.GetRequiredService<IBinaryLaneClient>();
|
||||
|
||||
try
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case "account":
|
||||
await WriteJsonAsync(
|
||||
await client.Account.GetAsync(cancellationSource.Token));
|
||||
return 0;
|
||||
|
||||
case "servers":
|
||||
await WriteItemsAsync(
|
||||
client.Servers.ListAllAsync(cancellationToken: cancellationSource.Token));
|
||||
return 0;
|
||||
|
||||
case "server":
|
||||
if (!TryReadServerId(args, out var serverId))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
await WriteJsonAsync(
|
||||
await client.Servers.GetAsync(serverId, cancellationSource.Token));
|
||||
return 0;
|
||||
|
||||
case "regions":
|
||||
await WriteItemsAsync(
|
||||
client.Regions.ListAllAsync(cancellationToken: cancellationSource.Token));
|
||||
return 0;
|
||||
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown command: {command}");
|
||||
PrintUsage();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationSource.IsCancellationRequested)
|
||||
{
|
||||
Console.Error.WriteLine("Cancelled.");
|
||||
return 130;
|
||||
}
|
||||
catch (BinaryLaneApiException exception)
|
||||
{
|
||||
// Provider details may contain user data. Do not print Problem, headers,
|
||||
// or ResponseBody in a sample application.
|
||||
Console.Error.WriteLine(
|
||||
$"BinaryLane request failed with HTTP {(int)exception.StatusCode} "
|
||||
+ $"({exception.StatusCode}) at {exception.RequestUri.AbsolutePath}.");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"Request failed ({exception.GetType().Name}).");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadServerId(string[] args, out long serverId)
|
||||
{
|
||||
if (args.Length > 1 && long.TryParse(args[1], out serverId) && serverId > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("Usage: server <positive-server-id>");
|
||||
serverId = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task WriteItemsAsync<T>(IAsyncEnumerable<T> items)
|
||||
{
|
||||
await foreach (var item in items)
|
||||
{
|
||||
await WriteJsonAsync(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task WriteJsonAsync<T>(T value)
|
||||
{
|
||||
Console.WriteLine(JsonSerializer.Serialize(value, JsonOptions));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine(
|
||||
"""
|
||||
BinaryLane.Api demo — read-only operations only
|
||||
|
||||
Setup:
|
||||
dotnet user-secrets set "BinaryLane:ApiToken" "your-token" \
|
||||
--project examples/BinaryLane.Api.Demo
|
||||
|
||||
Commands:
|
||||
account
|
||||
servers
|
||||
server <positive-server-id>
|
||||
regions
|
||||
|
||||
You can also set BINARYLANE_API_TOKEN for one process. Never commit
|
||||
a token or add it to appsettings.json.
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# BinaryLane.Api demo
|
||||
|
||||
This console app demonstrates read-only account, server, and region requests.
|
||||
It does not create, change, or delete infrastructure.
|
||||
|
||||
## Configure a token
|
||||
|
||||
From the repository root, store a token with .NET user secrets:
|
||||
|
||||
```bash
|
||||
dotnet user-secrets set \
|
||||
"BinaryLane:ApiToken" "your-token" \
|
||||
--project examples/BinaryLane.Api.Demo
|
||||
```
|
||||
|
||||
Alternatively, set `BINARYLANE_API_TOKEN` for one process. Do not add a token
|
||||
to `appsettings.json`.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
dotnet run --project examples/BinaryLane.Api.Demo -- account
|
||||
dotnet run --project examples/BinaryLane.Api.Demo -- servers
|
||||
dotnet run --project examples/BinaryLane.Api.Demo -- server 12345
|
||||
dotnet run --project examples/BinaryLane.Api.Demo -- regions
|
||||
```
|
||||
|
||||
Press Ctrl+C to cancel a request or listing.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"BinaryLane": {
|
||||
"BaseUrl": "https://api.binarylane.com.au/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"Microsoft.Extensions.Configuration.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "L89DLNuimOghjV3tLx0ArFDwVEJD6+uGB3BMCMX01kaLzXkaXHb2021xOMl2QOxUxbdePKUZsUY7n2UUkycjRg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": "8.0.1",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.UserSecrets": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "7tYqdPPpAK+3jO9d5LTuCK2VxrEdf85Ol4trUr6ds4jclBecadWZ/RyPCbNjfbN5iGTfUnD/h65TOQuqQv2c+A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Json": "8.0.1",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.FileProviders.Physical": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Hosting": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "bP9EEkHBEfjgYiG8nUaXqMk/ujwJrffOkNPP7onpRMO8R+OUSESSP4xHkCAXgYZ1COP2Q9lXlU5gkMFh20gRuw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.2",
|
||||
"Microsoft.Extensions.Configuration.CommandLine": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": "8.0.1",
|
||||
"Microsoft.Extensions.Configuration.Json": "8.0.1",
|
||||
"Microsoft.Extensions.Configuration.UserSecrets": "8.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection": "8.0.1",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Diagnostics": "8.0.1",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.FileProviders.Physical": "8.0.0",
|
||||
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging.Configuration": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Console": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Debug": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.EventLog": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.EventSource": "8.0.1",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.CommandLine": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "EJzSNO9oaAXnTdtdNO6npPRsIIeZCBSNmdQ091VDO7fBiOtJAAeEq6dtrVXIi3ZyjC5XRSAtVvF8SzcneRHqKQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.FileProviders.Physical": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "doVPCUUCY7c6LhBsEfiy3W1bvS7Mi6LkfQMS8nlC22jZWNxBv8VO8bdfeyvpYFst6Kxqk7HBC6lytmEoBssvSQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Physical": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.FileSystemGlobbing": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.FileSystemGlobbing": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ=="
|
||||
},
|
||||
"Microsoft.Extensions.Hosting.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
|
||||
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "QWwTrsgOnJMmn+XUslm8D2H1n3PkP/u/v52FODtyBc/k4W9r3i2vcXXeeX/upnzllJYRRbrzVzT0OclfNJtBJA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.2",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "uzcg/5U2eLyn5LIKlERkdSxw6VPC1yydnOSQiRRWGBGN3kphq3iL4emORzrojScDmxRhv49gp5BI8U3Dz7y4iA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging.Configuration": "8.0.1",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Debug": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "B8hqNuYudC2RB+L/DI33uO4rf5by41fZVdcVL2oZj0UyoAZqnwTwYHp1KafoH4nkl1/23piNeybFFASaV2HkFg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "ZD1m4GXoxcZeDJIq8qePKj+QAWeQNO/OG8skvrOG8RQfxLp9MAKRoliTc27xanoNUzeqvX5HhS/I7c0BvwAYUg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2",
|
||||
"System.Diagnostics.EventLog": "8.0.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.EventSource": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "YMXMAla6B6sEf/SnfZYTty633Ool3AH7KOw2LOaaEqwSo2piK4f7HMtzyc3CNiipDnq1fsUSuG5Oc7ZzpVy8WQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
|
||||
},
|
||||
"System.Diagnostics.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "n1ZP7NM2Gkn/MgD8+eOT5MulMj6wfeQMNS2Pizvq5GHCZfjlFMXV2irQlQmJhwA2VABC57M0auudO89Iu2uRLg=="
|
||||
},
|
||||
"binarylane.api": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.2, )",
|
||||
"Microsoft.Extensions.Http": "[8.0.1, )",
|
||||
"Microsoft.Extensions.Options": "[8.0.2, )"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "7IQhGK+wjyGrNsPBjJcZwWAr+Wf6D4+TwOptUt77bWtgNkiV8tDEbhFS+dDamtQFZ2X7kWG9m71iZQRj2x3zgQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
|
||||
},
|
||||
"Microsoft.Extensions.Http": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Diagnostics": "8.0.1",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.302",
|
||||
"rollForward": "disable",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BinaryLane.Api.V2.Authentication;
|
||||
|
||||
/// <summary>Supplies a BinaryLane bearer token for an outgoing request.</summary>
|
||||
public interface IBinaryLaneTokenProvider
|
||||
{
|
||||
/// <summary>Gets the current bearer token.</summary>
|
||||
ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BinaryLane.Api.V2.Authentication;
|
||||
|
||||
/// <summary>Uses the token configured in <see cref="BinaryLaneOptions"/>.</summary>
|
||||
public sealed class OptionsBinaryLaneTokenProvider : IBinaryLaneTokenProvider
|
||||
{
|
||||
private readonly IOptionsMonitor<BinaryLaneOptions> _options;
|
||||
|
||||
/// <summary>Initializes the provider.</summary>
|
||||
public OptionsBinaryLaneTokenProvider(IOptionsMonitor<BinaryLaneOptions> options) =>
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = _options.CurrentValue.ApiToken;
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No BinaryLane API token is configured. Set BinaryLaneOptions.ApiToken or register a custom IBinaryLaneTokenProvider.");
|
||||
}
|
||||
|
||||
return new ValueTask<string>(token!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BinaryLane.Api.V2.Authentication;
|
||||
|
||||
/// <summary>Provides a fixed token. Prefer a custom provider when tokens rotate.</summary>
|
||||
public sealed class StaticBinaryLaneTokenProvider : IBinaryLaneTokenProvider
|
||||
{
|
||||
private readonly string _token;
|
||||
|
||||
/// <summary>Initializes the provider with a non-empty bearer token.</summary>
|
||||
public StaticBinaryLaneTokenProvider(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
throw new ArgumentException("A BinaryLane API token is required.", nameof(token));
|
||||
}
|
||||
|
||||
_token = token;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default) =>
|
||||
new(_token);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
|
||||
<PackageId>BinaryLane.Api</PackageId>
|
||||
<AssemblyName>BinaryLane.Api</AssemblyName>
|
||||
<RootNamespace>BinaryLane.Api.V2</RootNamespace>
|
||||
<Version>0.1.0-beta.1</Version>
|
||||
<Authors>Alex Hope-O'Connor</Authors>
|
||||
<Owners>alex.hope.oconnor</Owners>
|
||||
<Copyright>Copyright © 2026 Alex Hope-O'Connor</Copyright>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Description>Unofficial, typed .NET client for BinaryLane's v2 API.</Description>
|
||||
<PackageTags>binarylane;api;cloud;vps;dns</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/alexhopeoconnor/binarylane-dotnet</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/alexhopeoconnor/binarylane-dotnet</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<!-- Pack must serialize multi-target nuspec and symbols generation. -->
|
||||
<BuildInParallel>false</BuildInParallel>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Versions are centrally managed by Directory.Packages.props. -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../../README.md" Pack="true" PackagePath="/" Link="README.md" />
|
||||
<None Include="../../LICENSE" Pack="true" PackagePath="/" Link="LICENSE" />
|
||||
<None Include="icon.png" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using BinaryLane.Api.V2.Resources;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BinaryLane.Api.V2;
|
||||
|
||||
/// <summary>Default composable BinaryLane v2 client.</summary>
|
||||
public sealed class BinaryLaneClient : IBinaryLaneClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a client using the supplied HTTP client and token provider. The HTTP client should
|
||||
/// normally be obtained through <c>IHttpClientFactory</c> or registered with
|
||||
/// <c>AddBinaryLaneApi</c>.
|
||||
/// </summary>
|
||||
public BinaryLaneClient(HttpClient httpClient, IBinaryLaneTokenProvider tokenProvider)
|
||||
: this(httpClient, tokenProvider, new BinaryLaneJsonSerializerOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Creates a client using supplied HTTP, authentication, and JSON services.</summary>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public BinaryLaneClient(
|
||||
HttpClient httpClient,
|
||||
IBinaryLaneTokenProvider tokenProvider,
|
||||
BinaryLaneJsonSerializerOptions json)
|
||||
: this(new BinaryLaneHttpExecutor(httpClient, tokenProvider, json), json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Creates a client over a custom request executor.</summary>
|
||||
public BinaryLaneClient(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(executor);
|
||||
ArgumentNullException.ThrowIfNull(json);
|
||||
#else
|
||||
if (executor is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(executor));
|
||||
}
|
||||
|
||||
if (json is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(json));
|
||||
}
|
||||
#endif
|
||||
|
||||
Executor = executor;
|
||||
|
||||
Account = new AccountApi(executor, json);
|
||||
Actions = new ActionsApi(executor, json);
|
||||
Billing = new BillingApi(executor, json);
|
||||
DataUsage = new DataUsageApi(executor, json);
|
||||
Domains = new DomainsApi(executor, json);
|
||||
Images = new ImagesApi(executor, json);
|
||||
SshKeys = new SshKeysApi(executor, json);
|
||||
LoadBalancers = new LoadBalancersApi(executor, json);
|
||||
Regions = new RegionsApi(executor, json);
|
||||
ReverseNames = new ReverseNamesApi(executor, json);
|
||||
SampleSets = new SampleSetsApi(executor, json);
|
||||
Servers = new ServersApi(executor, json);
|
||||
Sizes = new SizesApi(executor, json);
|
||||
Software = new SoftwareApi(executor, json);
|
||||
Vpcs = new VpcsApi(executor, json);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBinaryLaneApiExecutor Executor { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAccountApi Account { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IActionsApi Actions { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IBillingApi Billing { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDataUsageApi DataUsage { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDomainsApi Domains { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IImagesApi Images { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISshKeysApi SshKeys { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILoadBalancersApi LoadBalancers { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRegionsApi Regions { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReverseNamesApi ReverseNames { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISampleSetsApi SampleSets { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServersApi Servers { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISizesApi Sizes { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISoftwareApi Software { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IVpcsApi Vpcs { get; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace BinaryLane.Api.V2.Configuration;
|
||||
|
||||
/// <summary>Configures a BinaryLane API client.</summary>
|
||||
public sealed class BinaryLaneOptions
|
||||
{
|
||||
/// <summary>The configuration section convention used by consuming applications.</summary>
|
||||
public const string SectionName = "BinaryLane";
|
||||
|
||||
/// <summary>The BinaryLane API root URL.</summary>
|
||||
public string BaseUrl { get; set; } = "https://api.binarylane.com.au/";
|
||||
|
||||
/// <summary>
|
||||
/// The bearer token to use when the default token provider is registered. Prefer a secret store,
|
||||
/// user secrets, or environment-variable based configuration rather than committing this value.
|
||||
/// </summary>
|
||||
public string? ApiToken { get; set; }
|
||||
|
||||
/// <summary>Timeout applied to individual HTTP requests.</summary>
|
||||
public int RequestTimeoutSeconds { get; set; } = 100;
|
||||
|
||||
internal static bool IsValid(BinaryLaneOptions options)
|
||||
{
|
||||
if (options.RequestTimeoutSeconds < 1 || options.RequestTimeoutSeconds > 300)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out var uri)
|
||||
&& uri.Scheme == Uri.UriSchemeHttps
|
||||
&& string.IsNullOrEmpty(uri.UserInfo)
|
||||
&& string.IsNullOrEmpty(uri.Query)
|
||||
&& string.IsNullOrEmpty(uri.Fragment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BinaryLane.Api.V2.Configuration;
|
||||
|
||||
/// <summary>Validates client options at application startup.</summary>
|
||||
public sealed class BinaryLaneOptionsValidator : IValidateOptions<BinaryLaneOptions>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ValidateOptionsResult Validate(string? name, BinaryLaneOptions options)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
return ValidateOptionsResult.Fail("BinaryLane options are required.");
|
||||
}
|
||||
|
||||
if (!BinaryLaneOptions.IsValid(options))
|
||||
{
|
||||
return ValidateOptionsResult.Fail(
|
||||
"BinaryLane BaseUrl must be an absolute HTTPS URL and RequestTimeoutSeconds must be between 1 and 300.");
|
||||
}
|
||||
|
||||
return ValidateOptionsResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
using BinaryLane.Api.V2.Configuration;
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using BinaryLane.Api.V2.Resources;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BinaryLane.Api.V2.DependencyInjection;
|
||||
|
||||
/// <summary>Dependency-injection registration for the BinaryLane API client.</summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a typed BinaryLane client. The returned builder lets an application compose its
|
||||
/// own proxy, telemetry, and safe GET-only resilience handlers.
|
||||
/// </summary>
|
||||
public static IHttpClientBuilder AddBinaryLaneApi(
|
||||
this IServiceCollection services,
|
||||
Action<BinaryLaneOptions> configure)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
#else
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configure is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
#endif
|
||||
|
||||
services.AddOptions<BinaryLaneOptions>()
|
||||
.Configure(configure)
|
||||
.ValidateOnStart();
|
||||
services.TryAddSingleton<IValidateOptions<BinaryLaneOptions>, BinaryLaneOptionsValidator>();
|
||||
services.TryAddSingleton<IBinaryLaneTokenProvider, OptionsBinaryLaneTokenProvider>();
|
||||
services.TryAddSingleton<BinaryLaneJsonSerializerOptions>();
|
||||
|
||||
var clientBuilder = services.AddHttpClient<IBinaryLaneClient, BinaryLaneClient>((serviceProvider, client) =>
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value;
|
||||
client.BaseAddress = EnsureTrailingSlash(options.BaseUrl);
|
||||
client.Timeout = TimeSpan.FromSeconds(options.RequestTimeoutSeconds);
|
||||
});
|
||||
|
||||
// Register focused resources as well as the façade so applications can depend on precisely
|
||||
// the API area they use. TryAdd preserves a consumer's explicit test double or custom resource.
|
||||
services.TryAddTransient<IAccountApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Account);
|
||||
services.TryAddTransient<IActionsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Actions);
|
||||
services.TryAddTransient<IBillingApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Billing);
|
||||
services.TryAddTransient<IDataUsageApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().DataUsage);
|
||||
services.TryAddTransient<IDomainsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Domains);
|
||||
services.TryAddTransient<IImagesApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Images);
|
||||
services.TryAddTransient<ISshKeysApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().SshKeys);
|
||||
services.TryAddTransient<ILoadBalancersApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().LoadBalancers);
|
||||
services.TryAddTransient<IRegionsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Regions);
|
||||
services.TryAddTransient<IReverseNamesApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().ReverseNames);
|
||||
services.TryAddTransient<ISampleSetsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().SampleSets);
|
||||
services.TryAddTransient<IServersApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Servers);
|
||||
services.TryAddTransient<ISizesApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Sizes);
|
||||
services.TryAddTransient<ISoftwareApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Software);
|
||||
services.TryAddTransient<IVpcsApi>(serviceProvider => serviceProvider.GetRequiredService<IBinaryLaneClient>().Vpcs);
|
||||
|
||||
return clientBuilder;
|
||||
}
|
||||
|
||||
private static Uri EnsureTrailingSlash(string baseUrl)
|
||||
{
|
||||
var uri = new Uri(baseUrl, UriKind.Absolute);
|
||||
var absoluteUri = uri.AbsoluteUri;
|
||||
return absoluteUri.Length > 0 && absoluteUri[absoluteUri.Length - 1] == '/'
|
||||
? uri
|
||||
: new Uri(absoluteUri + "/", UriKind.Absolute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace BinaryLane.Api.V2.Errors;
|
||||
|
||||
/// <summary>Base exception for an unsuccessful BinaryLane API response.</summary>
|
||||
public class BinaryLaneApiException : Exception
|
||||
{
|
||||
internal BinaryLaneApiException(
|
||||
string message,
|
||||
HttpStatusCode statusCode,
|
||||
Uri requestUri,
|
||||
BinaryLaneApiProblem? problem,
|
||||
string? responseBody,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
RequestUri = requestUri;
|
||||
Problem = problem;
|
||||
ResponseBody = responseBody;
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
/// <summary>HTTP status code returned by BinaryLane.</summary>
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
|
||||
/// <summary>The request URI which produced the response.</summary>
|
||||
public Uri RequestUri { get; }
|
||||
|
||||
/// <summary>Structured problem details, when BinaryLane supplied them.</summary>
|
||||
public BinaryLaneApiProblem? Problem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Bounded raw response text for diagnostics. It omits request headers, but can contain
|
||||
/// provider-supplied or user-provided data; avoid logging it indiscriminately.
|
||||
/// </summary>
|
||||
public string? ResponseBody { get; }
|
||||
|
||||
/// <summary>Response and content headers.</summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> Headers { get; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace BinaryLane.Api.V2.Errors;
|
||||
|
||||
/// <summary>A structured problem response returned by BinaryLane.</summary>
|
||||
public sealed class BinaryLaneApiProblem
|
||||
{
|
||||
/// <summary>Problem type URI, if supplied.</summary>
|
||||
public string? Type { get; internal set; }
|
||||
|
||||
/// <summary>Short problem title, if supplied.</summary>
|
||||
public string? Title { get; internal set; }
|
||||
|
||||
/// <summary>Human-readable problem detail, if supplied.</summary>
|
||||
public string? Detail { get; internal set; }
|
||||
|
||||
/// <summary>Provider-supplied status code, if supplied.</summary>
|
||||
public int? Status { get; internal set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace BinaryLane.Api.V2.Errors;
|
||||
|
||||
/// <summary>Thrown when the current account is not authorized for an operation.</summary>
|
||||
public sealed class BinaryLaneForbiddenException : BinaryLaneApiException
|
||||
{
|
||||
internal BinaryLaneForbiddenException(string message, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
: base(message, HttpStatusCode.Forbidden, requestUri, problem, responseBody, headers)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace BinaryLane.Api.V2.Errors;
|
||||
|
||||
/// <summary>Thrown when BinaryLane cannot find a requested resource.</summary>
|
||||
public sealed class BinaryLaneNotFoundException : BinaryLaneApiException
|
||||
{
|
||||
internal BinaryLaneNotFoundException(string message, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
: base(message, HttpStatusCode.NotFound, requestUri, problem, responseBody, headers)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace BinaryLane.Api.V2.Errors;
|
||||
|
||||
/// <summary>Thrown when BinaryLane rejects credentials.</summary>
|
||||
public sealed class BinaryLaneUnauthorizedException : BinaryLaneApiException
|
||||
{
|
||||
internal BinaryLaneUnauthorizedException(string message, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
: base(message, HttpStatusCode.Unauthorized, requestUri, problem, responseBody, headers)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace BinaryLane.Api.V2.Errors;
|
||||
|
||||
/// <summary>Thrown when BinaryLane rejects a malformed or invalid request.</summary>
|
||||
public sealed class BinaryLaneValidationException : BinaryLaneApiException
|
||||
{
|
||||
internal BinaryLaneValidationException(string message, HttpStatusCode statusCode, Uri requestUri, BinaryLaneApiProblem? problem, string? responseBody, IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
: base(message, statusCode, requestUri, problem, responseBody, headers)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
using BinaryLane.Api.V2.Errors;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>Default HTTP implementation for the BinaryLane API.</summary>
|
||||
public sealed class BinaryLaneHttpExecutor : IBinaryLaneApiExecutor
|
||||
{
|
||||
private const int MaximumDiagnosticBodyLength = 32 * 1024;
|
||||
private const long MaximumSuccessBodyLength = 16L * 1024 * 1024;
|
||||
private static readonly Uri DefaultBaseUri = new("https://api.binarylane.com.au/");
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IBinaryLaneTokenProvider _tokenProvider;
|
||||
private readonly BinaryLaneJsonSerializerOptions _json;
|
||||
|
||||
/// <summary>Initializes the executor.</summary>
|
||||
public BinaryLaneHttpExecutor(
|
||||
HttpClient httpClient,
|
||||
IBinaryLaneTokenProvider tokenProvider,
|
||||
BinaryLaneJsonSerializerOptions? json = null)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
|
||||
_json = json ?? new BinaryLaneJsonSerializerOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryLaneResponse<TResponse>> SendAsync<TResponse>(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
#else
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
#endif
|
||||
|
||||
using var message = await CreateMessageAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
using var response = await _httpClient
|
||||
.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var headers = CollectHeaders(response);
|
||||
var body = response.Content is null
|
||||
? string.Empty
|
||||
: response.IsSuccessStatusCode
|
||||
? await ReadBodyAsync(response.Content, cancellationToken).ConfigureAwait(false)
|
||||
: await ReadDiagnosticBodyAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw CreateException(response.StatusCode, message.RequestUri!, body, headers);
|
||||
}
|
||||
|
||||
var result = Deserialize<TResponse>(body);
|
||||
return new BinaryLaneResponse<TResponse>(result, response.StatusCode, message.RequestUri!, headers);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryLaneResponse<object?>> SendAsync(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
#else
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
#endif
|
||||
|
||||
using var message = await CreateMessageAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
using var response = await _httpClient
|
||||
.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var headers = CollectHeaders(response);
|
||||
var body = response.Content is null
|
||||
? string.Empty
|
||||
: response.IsSuccessStatusCode
|
||||
? await ReadBodyAsync(response.Content, cancellationToken).ConfigureAwait(false)
|
||||
: await ReadDiagnosticBodyAsync(response.Content, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw CreateException(response.StatusCode, message.RequestUri!, body, headers);
|
||||
}
|
||||
|
||||
return new BinaryLaneResponse<object?>(null, response.StatusCode, message.RequestUri!, headers);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> GetAsync<TResponse>(
|
||||
string path,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Get, path) { Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> PostAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Post, path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> PutAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(HttpMethod.Put, path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResponse> PatchAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
(await SendAsync<TResponse>(new BinaryLaneRequest(new HttpMethod("PATCH"), path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false)).Body;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAsync(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await SendAsync(new BinaryLaneRequest(HttpMethod.Delete, path) { Body = body, Query = query }, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<HttpRequestMessage> CreateMessageAsync(BinaryLaneRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestUri = CreateUri(request.Path, request.Query);
|
||||
var token = await _tokenProvider.GetTokenAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
throw new InvalidOperationException("The configured BinaryLane token provider returned an empty token.");
|
||||
}
|
||||
|
||||
var message = new HttpRequestMessage(request.Method, requestUri);
|
||||
try
|
||||
{
|
||||
if (request.Headers is not null)
|
||||
{
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
if (string.Equals(header.Key, "Authorization", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(header.Key, "Host", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The '{header.Key}' header is managed by the BinaryLane client and cannot be overridden.",
|
||||
nameof(request));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
message.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
catch (Exception exception) when (exception is FormatException or InvalidOperationException)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The '{header.Key}' header is not a valid request header for BinaryLane API requests.",
|
||||
nameof(request),
|
||||
exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
if (request.Body is not null)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(request.Body, _json.SerializerOptions);
|
||||
message.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
catch
|
||||
{
|
||||
message.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private Uri CreateUri(string path, IReadOnlyDictionary<string, object?>? query)
|
||||
{
|
||||
var baseUri = _httpClient.BaseAddress ?? DefaultBaseUri;
|
||||
if (!baseUri.IsAbsoluteUri ||
|
||||
baseUri.Scheme != Uri.UriSchemeHttps ||
|
||||
!string.IsNullOrEmpty(baseUri.UserInfo) ||
|
||||
!string.IsNullOrEmpty(baseUri.Query) ||
|
||||
!string.IsNullOrEmpty(baseUri.Fragment))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"HttpClient.BaseAddress must be an absolute HTTPS URL without credentials, a query string, or a fragment.");
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
if (path[0] != '/' &&
|
||||
Uri.TryCreate(path, UriKind.Absolute, out var absoluteUri))
|
||||
{
|
||||
if (absoluteUri.Scheme != Uri.UriSchemeHttps)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Absolute BinaryLane API URLs must use the HTTPS scheme.",
|
||||
nameof(path));
|
||||
}
|
||||
|
||||
if (!HasSameOrigin(baseUri, absoluteUri))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Absolute BinaryLane API URLs must use the configured API origin.",
|
||||
nameof(path));
|
||||
}
|
||||
|
||||
uri = absoluteUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
uri = new Uri(baseUri, path.TrimStart('/'));
|
||||
if (!HasSameOrigin(baseUri, uri))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Relative BinaryLane API paths must resolve on the configured API origin.",
|
||||
nameof(path));
|
||||
}
|
||||
}
|
||||
|
||||
if (query is null || query.Count == 0)
|
||||
{
|
||||
return uri;
|
||||
}
|
||||
|
||||
var builder = new UriBuilder(uri);
|
||||
var queryBuilder = new StringBuilder();
|
||||
if (!string.IsNullOrWhiteSpace(builder.Query))
|
||||
{
|
||||
queryBuilder.Append(builder.Query.TrimStart('?'));
|
||||
}
|
||||
|
||||
foreach (var pair in query)
|
||||
{
|
||||
if (pair.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (queryBuilder.Length > 0)
|
||||
{
|
||||
queryBuilder.Append('&');
|
||||
}
|
||||
|
||||
queryBuilder.Append(Uri.EscapeDataString(pair.Key));
|
||||
queryBuilder.Append('=');
|
||||
queryBuilder.Append(Uri.EscapeDataString(FormatQueryValue(pair.Value)));
|
||||
}
|
||||
|
||||
builder.Query = queryBuilder.ToString();
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
private static bool HasSameOrigin(Uri first, Uri second) =>
|
||||
string.Equals(first.Scheme, second.Scheme, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(first.Host, second.Host, StringComparison.OrdinalIgnoreCase) &&
|
||||
first.Port == second.Port;
|
||||
|
||||
private static string FormatQueryValue(object value)
|
||||
{
|
||||
if (value is bool boolean)
|
||||
{
|
||||
return boolean ? "true" : "false";
|
||||
}
|
||||
|
||||
if (value is DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
return dateTimeOffset.ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (value is DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToString("O", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (value is IFormattable formattable)
|
||||
{
|
||||
return formattable.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
}
|
||||
|
||||
return value.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private TResponse Deserialize<TResponse>(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
|
||||
if (typeof(TResponse) == typeof(JsonElement))
|
||||
{
|
||||
using var document = JsonDocument.Parse(body);
|
||||
return (TResponse)(object)document.RootElement.Clone();
|
||||
}
|
||||
|
||||
var value = JsonSerializer.Deserialize<TResponse>(body, _json.SerializerOptions);
|
||||
if (value is null && typeof(TResponse).IsValueType)
|
||||
{
|
||||
throw new JsonException("BinaryLane returned a JSON null for a non-nullable response value.");
|
||||
}
|
||||
|
||||
return value!;
|
||||
}
|
||||
|
||||
private static ReadOnlyDictionary<string, IReadOnlyList<string>> CollectHeaders(HttpResponseMessage response)
|
||||
{
|
||||
var result = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var header in response.Headers)
|
||||
{
|
||||
result[header.Key] = new List<string>(header.Value).AsReadOnly();
|
||||
}
|
||||
|
||||
if (response.Content is not null)
|
||||
{
|
||||
foreach (var header in response.Content.Headers)
|
||||
{
|
||||
result[header.Key] = new List<string>(header.Value).AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
return new ReadOnlyDictionary<string, IReadOnlyList<string>>(result);
|
||||
}
|
||||
|
||||
private static BinaryLaneApiException CreateException(
|
||||
HttpStatusCode statusCode,
|
||||
Uri requestUri,
|
||||
string body,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
{
|
||||
var problem = ParseProblem(body);
|
||||
var message = $"BinaryLane API request failed with HTTP {(int)statusCode} ({statusCode}).";
|
||||
var diagnosticBody = SanitizeAndTruncate(body);
|
||||
|
||||
if (statusCode == HttpStatusCode.BadRequest || statusCode == (HttpStatusCode)422)
|
||||
{
|
||||
return new BinaryLaneValidationException(message, statusCode, requestUri, problem, diagnosticBody, headers);
|
||||
}
|
||||
|
||||
return statusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => new BinaryLaneUnauthorizedException(message, requestUri, problem, diagnosticBody, headers),
|
||||
HttpStatusCode.Forbidden => new BinaryLaneForbiddenException(message, requestUri, problem, diagnosticBody, headers),
|
||||
HttpStatusCode.NotFound => new BinaryLaneNotFoundException(message, requestUri, problem, diagnosticBody, headers),
|
||||
_ => new BinaryLaneApiException(message, statusCode, requestUri, problem, diagnosticBody, headers),
|
||||
};
|
||||
}
|
||||
|
||||
private static BinaryLaneApiProblem? ParseProblem(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(body);
|
||||
var root = document.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BinaryLaneApiProblem
|
||||
{
|
||||
Type = ReadString(root, "type"),
|
||||
Title = ReadString(root, "title"),
|
||||
Detail = ReadString(root, "detail"),
|
||||
Status = ReadInt32(root, "status"),
|
||||
};
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement element, string name) =>
|
||||
element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
|
||||
private static int? ReadInt32(JsonElement element, string name) =>
|
||||
element.TryGetProperty(name, out var value) && value.TryGetInt32(out var number)
|
||||
? number
|
||||
: null;
|
||||
|
||||
private static string? SanitizeAndTruncate(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Do not expose an unbounded provider response in exception telemetry. Consumers should still
|
||||
// treat this diagnostic field as potentially sensitive and avoid logging it indiscriminately.
|
||||
if (body.Length <= MaximumDiagnosticBodyLength)
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
return string.Concat(body.AsSpan(0, MaximumDiagnosticBodyLength), "…".AsSpan());
|
||||
#else
|
||||
return body.Substring(0, MaximumDiagnosticBodyLength) + "…";
|
||||
#endif
|
||||
}
|
||||
|
||||
private static async Task<string> ReadBodyAsync(HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
if (content.Headers.ContentLength is long contentLength && contentLength > MaximumSuccessBodyLength)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"BinaryLane response content exceeded the {MaximumSuccessBodyLength} byte limit.");
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
await content.LoadIntoBufferAsync(MaximumSuccessBodyLength).ConfigureAwait(false);
|
||||
return await content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await content.LoadIntoBufferAsync(MaximumSuccessBodyLength).ConfigureAwait(false);
|
||||
return await content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static async Task<string> ReadDiagnosticBodyAsync(HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
using var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
using var stream = await content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, leaveOpen: false);
|
||||
var buffer = new char[MaximumDiagnosticBodyLength + 1];
|
||||
var count = 0;
|
||||
|
||||
while (count < buffer.Length)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
var read = await reader
|
||||
.ReadAsync(buffer.AsMemory(count, buffer.Length - count), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
var read = await reader
|
||||
.ReadAsync(buffer, count, buffer.Length - count)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
if (read == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
count += read;
|
||||
}
|
||||
|
||||
return new string(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>JSON settings used by the BinaryLane API client.</summary>
|
||||
public sealed class BinaryLaneJsonSerializerOptions
|
||||
{
|
||||
/// <summary>Creates the default BinaryLane JSON settings.</summary>
|
||||
public BinaryLaneJsonSerializerOptions()
|
||||
{
|
||||
SerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
SerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
SerializerOptions.Converters.Add(new ServerActionJsonConverter());
|
||||
}
|
||||
|
||||
/// <summary>The serializer settings used for request and response bodies.</summary>
|
||||
public JsonSerializerOptions SerializerOptions { get; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>Represents one request to the BinaryLane v2 API.</summary>
|
||||
public sealed class BinaryLaneRequest
|
||||
{
|
||||
/// <summary>Initializes a request.</summary>
|
||||
public BinaryLaneRequest(HttpMethod method, string path)
|
||||
{
|
||||
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("An API path is required.", nameof(path));
|
||||
}
|
||||
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <summary>HTTP method.</summary>
|
||||
public HttpMethod Method { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Relative v2 path (for example <c>v2/servers</c>) or an absolute HTTPS URL on the
|
||||
/// configured API origin. Relative paths are resolved against the configured
|
||||
/// <see cref="System.Net.Http.HttpClient.BaseAddress"/>. Other origins are rejected so a
|
||||
/// provider link cannot receive the configured bearer token.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>Optional JSON request body.</summary>
|
||||
public object? Body { get; set; }
|
||||
|
||||
/// <summary>Optional query-string values. Null values are omitted.</summary>
|
||||
public IReadOnlyDictionary<string, object?>? Query { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional request headers. Authorization is supplied by the configured token provider and
|
||||
/// cannot be overridden here.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Headers { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>Response metadata and a deserialized BinaryLane API body.</summary>
|
||||
/// <typeparam name="T">The deserialized response type.</typeparam>
|
||||
public sealed class BinaryLaneResponse<T>
|
||||
{
|
||||
internal BinaryLaneResponse(
|
||||
T body,
|
||||
HttpStatusCode statusCode,
|
||||
Uri requestUri,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> headers)
|
||||
{
|
||||
Body = body;
|
||||
StatusCode = statusCode;
|
||||
RequestUri = requestUri;
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
/// <summary>Deserialized response body.</summary>
|
||||
public T Body { get; }
|
||||
|
||||
/// <summary>HTTP status code returned by BinaryLane.</summary>
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
|
||||
/// <summary>Final request URI.</summary>
|
||||
public Uri RequestUri { get; }
|
||||
|
||||
/// <summary>Response and content headers.</summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> Headers { get; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BinaryLane.Api.V2.Http;
|
||||
|
||||
/// <summary>
|
||||
/// Low-level request executor used by resource APIs. It is public so consuming applications can
|
||||
/// compose support for a newly introduced BinaryLane endpoint before a typed resource is released.
|
||||
/// </summary>
|
||||
public interface IBinaryLaneApiExecutor
|
||||
{
|
||||
/// <summary>Sends a request and returns response metadata and the deserialized body.</summary>
|
||||
Task<BinaryLaneResponse<TResponse>> SendAsync<TResponse>(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a request which is expected to have no response body.</summary>
|
||||
Task<BinaryLaneResponse<object?>> SendAsync(
|
||||
BinaryLaneRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a GET request.</summary>
|
||||
Task<TResponse> GetAsync<TResponse>(
|
||||
string path,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a POST request.</summary>
|
||||
Task<TResponse> PostAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a PUT request.</summary>
|
||||
Task<TResponse> PutAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a PATCH request.</summary>
|
||||
Task<TResponse> PatchAsync<TResponse>(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends a DELETE request.</summary>
|
||||
Task DeleteAsync(
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using BinaryLane.Api.V2.Resources;
|
||||
|
||||
namespace BinaryLane.Api.V2;
|
||||
|
||||
/// <summary>
|
||||
/// Composable entry point for the BinaryLane v2 API. Each resource is independently represented by
|
||||
/// an interface so applications can depend on and mock only the portion they use.
|
||||
/// </summary>
|
||||
public interface IBinaryLaneClient
|
||||
{
|
||||
/// <summary>Low-level executor for a newly introduced endpoint not yet represented by a resource API.</summary>
|
||||
IBinaryLaneApiExecutor Executor { get; }
|
||||
|
||||
/// <summary>Current account information.</summary>
|
||||
IAccountApi Account { get; }
|
||||
|
||||
/// <summary>Global asynchronous actions.</summary>
|
||||
IActionsApi Actions { get; }
|
||||
|
||||
/// <summary>Billing balance and invoices.</summary>
|
||||
IBillingApi Billing { get; }
|
||||
|
||||
/// <summary>Data-transfer usage.</summary>
|
||||
IDataUsageApi DataUsage { get; }
|
||||
|
||||
/// <summary>DNS domains and DNS records.</summary>
|
||||
IDomainsApi Domains { get; }
|
||||
|
||||
/// <summary>Images and image downloads.</summary>
|
||||
IImagesApi Images { get; }
|
||||
|
||||
/// <summary>SSH keys.</summary>
|
||||
ISshKeysApi SshKeys { get; }
|
||||
|
||||
/// <summary>Load balancers, their members, and forwarding rules.</summary>
|
||||
ILoadBalancersApi LoadBalancers { get; }
|
||||
|
||||
/// <summary>Available regions.</summary>
|
||||
IRegionsApi Regions { get; }
|
||||
|
||||
/// <summary>IPv6 reverse nameservers.</summary>
|
||||
IReverseNamesApi ReverseNames { get; }
|
||||
|
||||
/// <summary>Monitoring sample sets.</summary>
|
||||
ISampleSetsApi SampleSets { get; }
|
||||
|
||||
/// <summary>Servers and server-scoped resources.</summary>
|
||||
IServersApi Servers { get; }
|
||||
|
||||
/// <summary>Available server sizes.</summary>
|
||||
ISizesApi Sizes { get; }
|
||||
|
||||
/// <summary>Licensed and operating-system software.</summary>
|
||||
ISoftwareApi Software { get; }
|
||||
|
||||
/// <summary>Private virtual networks.</summary>
|
||||
IVpcsApi Vpcs { get; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for JSON objects returned by the BinaryLane API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// BinaryLane's v2 API is a developer preview and may add properties before this
|
||||
/// SDK is updated. Unknown properties are retained instead of causing a
|
||||
/// deserialization failure, so applications that need an early provider field can
|
||||
/// opt in to reading it from <see cref="AdditionalProperties"/>.
|
||||
/// </remarks>
|
||||
public abstract class BinaryLaneDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets properties supplied by the service that this version of the SDK does
|
||||
/// not yet model explicitly.
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JsonElement>? AdditionalProperties { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for request payloads accepted by the BinaryLane API.
|
||||
/// </summary>
|
||||
public abstract class BinaryLaneRequestModel : BinaryLaneDto
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Known string values in the BinaryLane v2 contract.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The API remains a developer preview. Public DTOs use strings rather than
|
||||
/// closed C# enums so an upstream value added after an SDK release remains
|
||||
/// readable. These constants make documented values discoverable without
|
||||
/// removing that forward compatibility.
|
||||
/// </remarks>
|
||||
public static class BinaryLaneValues
|
||||
{
|
||||
public static class AccountStatus
|
||||
{
|
||||
public const string Incomplete = "incomplete";
|
||||
public const string Active = "active";
|
||||
public const string Warning = "warning";
|
||||
public const string Locked = "locked";
|
||||
}
|
||||
|
||||
public static class ActionStatus
|
||||
{
|
||||
public const string InProgress = "in-progress";
|
||||
public const string Completed = "completed";
|
||||
public const string Errored = "errored";
|
||||
}
|
||||
|
||||
public static class ServerStatus
|
||||
{
|
||||
public const string New = "new";
|
||||
public const string Active = "active";
|
||||
public const string Archive = "archive";
|
||||
|
||||
// This unusual provider value is present in the 0.39.1 OpenAPI contract.
|
||||
public const string ProviderFalse = "False";
|
||||
}
|
||||
|
||||
public static class ResourceType
|
||||
{
|
||||
public const string Server = "server";
|
||||
public const string LoadBalancer = "load-balancer";
|
||||
public const string SshKey = "ssh-key";
|
||||
public const string Vpc = "vpc";
|
||||
public const string Image = "image";
|
||||
public const string RegisteredDomainName = "registered-domain-name";
|
||||
}
|
||||
|
||||
public static class ImageType
|
||||
{
|
||||
public const string Custom = "custom";
|
||||
public const string Snapshot = "snapshot";
|
||||
public const string Backup = "backup";
|
||||
}
|
||||
|
||||
public static class ImageStatus
|
||||
{
|
||||
public const string New = "NEW";
|
||||
public const string Available = "available";
|
||||
public const string Pending = "pending";
|
||||
public const string Deleted = "deleted";
|
||||
}
|
||||
|
||||
public static class ImageQueryType
|
||||
{
|
||||
public const string Distribution = "distribution";
|
||||
public const string Backup = "backup";
|
||||
}
|
||||
|
||||
public static class BackupSlot
|
||||
{
|
||||
public const string Daily = "daily";
|
||||
public const string Weekly = "weekly";
|
||||
public const string Monthly = "monthly";
|
||||
public const string Temporary = "temporary";
|
||||
}
|
||||
|
||||
public static class BackupReplacementStrategy
|
||||
{
|
||||
public const string None = "none";
|
||||
public const string Specified = "specified";
|
||||
public const string Oldest = "oldest";
|
||||
public const string Newest = "newest";
|
||||
}
|
||||
|
||||
public static class NetworkType
|
||||
{
|
||||
public const string Private = "private";
|
||||
public const string Public = "public";
|
||||
}
|
||||
|
||||
public static class DomainRecordType
|
||||
{
|
||||
public const string A = "A";
|
||||
public const string Aaaa = "AAAA";
|
||||
public const string Caa = "CAA";
|
||||
public const string Cname = "CNAME";
|
||||
public const string Mx = "MX";
|
||||
public const string Ns = "NS";
|
||||
public const string Soa = "SOA";
|
||||
public const string Srv = "SRV";
|
||||
public const string Txt = "TXT";
|
||||
}
|
||||
|
||||
public static class LoadBalancerStatus
|
||||
{
|
||||
public const string New = "new";
|
||||
public const string Active = "active";
|
||||
public const string Errored = "errored";
|
||||
}
|
||||
|
||||
public static class LoadBalancerRuleProtocol
|
||||
{
|
||||
public const string Http = "http";
|
||||
public const string Https = "https";
|
||||
}
|
||||
|
||||
public static class HealthCheckProtocol
|
||||
{
|
||||
public const string Http = "http";
|
||||
public const string Https = "https";
|
||||
public const string Both = "both";
|
||||
}
|
||||
|
||||
public static class AdvancedFirewallRuleProtocol
|
||||
{
|
||||
public const string All = "all";
|
||||
public const string Icmp = "icmp";
|
||||
public const string Tcp = "tcp";
|
||||
public const string Udp = "udp";
|
||||
}
|
||||
|
||||
public static class AdvancedFirewallRuleAction
|
||||
{
|
||||
public const string Drop = "drop";
|
||||
public const string Accept = "accept";
|
||||
}
|
||||
|
||||
public static class PasswordRecoveryType
|
||||
{
|
||||
public const string Manual = "manual";
|
||||
public const string OfflineClear = "offline-clear";
|
||||
public const string OfflineChange = "offline-change";
|
||||
public const string OnlineChange = "online-change";
|
||||
}
|
||||
|
||||
public static class DistributionFeature
|
||||
{
|
||||
public const string Ssh = "ssh";
|
||||
public const string RemoteDesktop = "remote-desktop";
|
||||
public const string UserData = "user-data";
|
||||
}
|
||||
|
||||
public static class DataInterval
|
||||
{
|
||||
public const string FiveMinute = "five-minute";
|
||||
public const string HalfHour = "half-hour";
|
||||
public const string FourHour = "four-hour";
|
||||
public const string Day = "day";
|
||||
public const string Week = "week";
|
||||
public const string Month = "month";
|
||||
}
|
||||
|
||||
public static class ThresholdAlertType
|
||||
{
|
||||
public const string Cpu = "cpu";
|
||||
public const string StorageRequests = "storage-requests";
|
||||
public const string NetworkIncoming = "network-incoming";
|
||||
public const string NetworkOutgoing = "network-outgoing";
|
||||
public const string DataTransferUsed = "data-transfer-used";
|
||||
public const string StorageUsed = "storage-used";
|
||||
public const string MemoryUsed = "memory-used";
|
||||
public const string LockedBackupSlots = "locked-backup-slots";
|
||||
}
|
||||
|
||||
public static class PaymentMethod
|
||||
{
|
||||
public const string CreditCard = "credit-card";
|
||||
public const string Paypal = "paypal";
|
||||
}
|
||||
|
||||
public static class TaxCodeType
|
||||
{
|
||||
public const string None = "none";
|
||||
public const string Scalar = "scalar";
|
||||
}
|
||||
|
||||
public static class UserInteractionType
|
||||
{
|
||||
public const string ContinueAfterPingFailure = "continue-after-ping-failure";
|
||||
public const string AllowUncleanPowerOff = "allow-unclean-power-off";
|
||||
}
|
||||
|
||||
public static class AdvancedFeature
|
||||
{
|
||||
public const string EmulatedHyperV = "emulated-hyperv";
|
||||
public const string EmulatedDevices = "emulated-devices";
|
||||
public const string NestedVirt = "nested-virt";
|
||||
public const string DriverDisk = "driver-disk";
|
||||
public const string UnsetUuid = "unset-uuid";
|
||||
public const string LocalRtc = "local-rtc";
|
||||
public const string EmulatedTpm = "emulated-tpm";
|
||||
public const string CloudInit = "cloud-init";
|
||||
public const string QemuGuestAgent = "qemu-guest-agent";
|
||||
public const string UefiBoot = "uefi-boot";
|
||||
}
|
||||
|
||||
public static class VideoDevice
|
||||
{
|
||||
public const string CirrusLogic = "cirrus-logic";
|
||||
public const string Standard = "standard";
|
||||
public const string Virtio = "virtio";
|
||||
public const string VirtioWide = "virtio-wide";
|
||||
}
|
||||
|
||||
public static class VmMachineType
|
||||
{
|
||||
public const string PcI440Fx1Point5 = "pc_i440fx_1point5";
|
||||
public const string PcI440Fx2Point11 = "pc_i440fx_2point11";
|
||||
public const string PcI440Fx4Point1 = "pc_i440fx_4point1";
|
||||
public const string PcI440Fx4Point2 = "pc_i440fx_4point2";
|
||||
public const string PcI440Fx5Point0 = "pc_i440fx_5point0";
|
||||
public const string PcI440Fx5Point1 = "pc_i440fx_5point1";
|
||||
public const string PcI440Fx7Point2 = "pc_i440fx_7point2";
|
||||
public const string PcI440Fx7Point2Point1 = "pc_i440fx_7point2point1";
|
||||
public const string PcI440Fx8Point2 = "pc_i440fx_8point2";
|
||||
}
|
||||
|
||||
/// <summary>Discriminator values for <see cref="ServerAction"/> payloads.</summary>
|
||||
public static class ServerActionType
|
||||
{
|
||||
public const string AddDisk = "add_disk";
|
||||
public const string AttachBackup = "attach_backup";
|
||||
public const string ChangeAdvancedFeatures = "change_advanced_features";
|
||||
public const string ChangeAdvancedFirewallRules = "change_advanced_firewall_rules";
|
||||
public const string ChangeBackupSchedule = "change_backup_schedule";
|
||||
public const string ChangeIpv6 = "change_ipv6";
|
||||
public const string ChangeIpv6ReverseNameservers = "change_ipv6_reverse_nameservers";
|
||||
public const string ChangeKernel = "change_kernel";
|
||||
public const string ChangeManageOffsiteBackupCopies = "change_manage_offsite_backup_copies";
|
||||
public const string ChangeNetwork = "change_network";
|
||||
public const string ChangeOffsiteBackupLocation = "change_offsite_backup_location";
|
||||
public const string ChangePartner = "change_partner";
|
||||
public const string ChangePortBlocking = "change_port_blocking";
|
||||
public const string ChangeRegion = "change_region";
|
||||
public const string ChangeReverseName = "change_reverse_name";
|
||||
public const string ChangeSeparatePrivateNetworkInterface = "change_separate_private_network_interface";
|
||||
public const string ChangeSourceAndDestinationCheck = "change_source_and_destination_check";
|
||||
public const string ChangeThresholdAlerts = "change_threshold_alerts";
|
||||
public const string ChangeVpcIpv4 = "change_vpc_ipv4";
|
||||
public const string CloneUsingBackup = "clone_using_backup";
|
||||
public const string DeleteDisk = "delete_disk";
|
||||
public const string DetachBackup = "detach_backup";
|
||||
public const string DisableBackups = "disable_backups";
|
||||
public const string DisableSelinux = "disable_selinux";
|
||||
public const string EnableBackups = "enable_backups";
|
||||
public const string EnableIpv6 = "enable_ipv6";
|
||||
public const string IsRunning = "is_running";
|
||||
public const string PasswordReset = "password_reset";
|
||||
public const string Ping = "ping";
|
||||
public const string PowerCycle = "power_cycle";
|
||||
public const string PowerOff = "power_off";
|
||||
public const string PowerOn = "power_on";
|
||||
public const string Reboot = "reboot";
|
||||
public const string Rebuild = "rebuild";
|
||||
public const string Rename = "rename";
|
||||
public const string Resize = "resize";
|
||||
public const string ResizeDisk = "resize_disk";
|
||||
public const string Restore = "restore";
|
||||
public const string Shutdown = "shutdown";
|
||||
public const string TakeBackup = "take_backup";
|
||||
public const string Uncancel = "uncancel";
|
||||
public const string Uptime = "uptime";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>Information about the authenticated BinaryLane account.</summary>
|
||||
public sealed class Account : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("email_verified")]
|
||||
public bool EmailVerified { get; init; }
|
||||
|
||||
[JsonPropertyName("two_factor_authentication_enabled")]
|
||||
public bool TwoFactorAuthenticationEnabled { get; init; }
|
||||
|
||||
/// <summary>Provider status, for example <c>active</c>.</summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("tax_code")]
|
||||
public TaxCode TaxCode { get; init; } = new();
|
||||
|
||||
/// <summary>Provider payment-method values, for example <c>credit-card</c>.</summary>
|
||||
[JsonPropertyName("configured_payment_methods")]
|
||||
public IReadOnlyList<string> ConfiguredPaymentMethods { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("additional_ipv4_limit")]
|
||||
public int AdditionalIpv4Limit { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A tax code currently applicable to an account or invoice.</summary>
|
||||
public sealed class TaxCode : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Provider tax-code type, for example <c>none</c> or <c>scalar</c>.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("fixed_percent")]
|
||||
public double? FixedPercent { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A provider action, which may still be in progress.</summary>
|
||||
public sealed class BinaryLaneAction : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>Provider status, for example <c>in-progress</c>, <c>completed</c>, or <c>errored</c>.</summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("started_at")]
|
||||
public DateTimeOffset StartedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("completed_at")]
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
|
||||
/// <summary>Provider resource type, when the action is associated with one.</summary>
|
||||
[JsonPropertyName("resource_type")]
|
||||
public string? ResourceType { get; init; }
|
||||
|
||||
[JsonPropertyName("resource_id")]
|
||||
public long? ResourceId { get; init; }
|
||||
|
||||
[JsonPropertyName("region")]
|
||||
public Region? Region { get; init; }
|
||||
|
||||
[JsonPropertyName("region_slug")]
|
||||
public string? RegionSlug { get; init; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("progress")]
|
||||
public ActionProgress Progress { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("result_data")]
|
||||
public string? ResultData { get; init; }
|
||||
|
||||
[JsonPropertyName("blocking_invoice_id")]
|
||||
public long? BlockingInvoiceId { get; init; }
|
||||
|
||||
[JsonPropertyName("user_interaction_required")]
|
||||
public UserInteractionRequired? UserInteractionRequired { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Progress information for a long-running provider action.</summary>
|
||||
public sealed class ActionProgress : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("current_step_detail")]
|
||||
public string? CurrentStepDetail { get; init; }
|
||||
|
||||
[JsonPropertyName("percent_complete")]
|
||||
public int PercentComplete { get; init; }
|
||||
|
||||
[JsonPropertyName("current_step")]
|
||||
public string? CurrentStep { get; init; }
|
||||
|
||||
[JsonPropertyName("completed_steps")]
|
||||
public IReadOnlyList<string> CompletedSteps { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>A link to an action related to a completed request.</summary>
|
||||
public sealed class ActionLink : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("rel")]
|
||||
public string Rel { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("href")]
|
||||
public string Href { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that an action needs an explicit user decision.</summary>
|
||||
public sealed class UserInteractionRequired : BinaryLaneDto
|
||||
{
|
||||
/// <summary>Provider interaction type, such as <c>continue-after-ping-failure</c>.</summary>
|
||||
[JsonPropertyName("interaction_type")]
|
||||
public string InteractionType { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>A BinaryLane region in which a resource can be created.</summary>
|
||||
public sealed class Region : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("slug")]
|
||||
public string Slug { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("sizes")]
|
||||
public IReadOnlyList<string> Sizes { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("available")]
|
||||
public bool Available { get; init; }
|
||||
|
||||
[JsonPropertyName("features")]
|
||||
public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("name_servers")]
|
||||
public IReadOnlyList<string> NameServers { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>A BinaryLane server.</summary>
|
||||
public sealed class Server : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("memory")]
|
||||
public int MemoryMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("vcpus")]
|
||||
public int Vcpus { get; init; }
|
||||
|
||||
[JsonPropertyName("disk")]
|
||||
public int DiskGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("vpc_id")]
|
||||
public long? VpcId { get; init; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>Provider server status, for example <c>active</c>.</summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("backup_ids")]
|
||||
public IReadOnlyList<long> BackupIds { get; init; } = Array.Empty<long>();
|
||||
|
||||
[JsonPropertyName("features")]
|
||||
public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("region")]
|
||||
public Region Region { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("image")]
|
||||
public Image Image { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public Size Size { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("size_slug")]
|
||||
public string SizeSlug { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("selected_size_options")]
|
||||
public SelectedSizeOptions? SelectedSizeOptions { get; init; }
|
||||
|
||||
[JsonPropertyName("networks")]
|
||||
public ServerNetworks Networks { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("kernel")]
|
||||
public Kernel? Kernel { get; init; }
|
||||
|
||||
[JsonPropertyName("next_backup_window")]
|
||||
public BackupWindow? NextBackupWindow { get; init; }
|
||||
|
||||
[JsonPropertyName("disks")]
|
||||
public IReadOnlyList<Disk> Disks { get; init; } = Array.Empty<Disk>();
|
||||
|
||||
[JsonPropertyName("backup_settings")]
|
||||
public BackupSettings BackupSettings { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("cancelled_at")]
|
||||
public DateTimeOffset? CancelledAt { get; init; }
|
||||
|
||||
[JsonPropertyName("failover_ips")]
|
||||
public IReadOnlyList<string> FailoverIps { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("host")]
|
||||
public Host Host { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("partner_id")]
|
||||
public long? PartnerId { get; init; }
|
||||
|
||||
[JsonPropertyName("password_change_supported")]
|
||||
public bool PasswordChangeSupported { get; init; }
|
||||
|
||||
[JsonPropertyName("permalink")]
|
||||
public string? Permalink { get; init; }
|
||||
|
||||
[JsonPropertyName("attached_backup")]
|
||||
public AttachedBackup? AttachedBackup { get; init; }
|
||||
|
||||
[JsonPropertyName("advanced_features")]
|
||||
public AdvancedServerFeatures AdvancedFeatures { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("is_under_maintenance")]
|
||||
public bool? IsUnderMaintenance { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A server disk.</summary>
|
||||
public sealed class Disk : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("size_gigabytes")]
|
||||
public double SizeGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonPropertyName("primary")]
|
||||
public bool Primary { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A server network assignment.</summary>
|
||||
public sealed class ServerNetwork : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("ip_address")]
|
||||
public string IpAddress { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>May be represented by the provider as either a number or a string.</summary>
|
||||
[JsonPropertyName("netmask")]
|
||||
public object? Netmask { get; init; }
|
||||
|
||||
[JsonPropertyName("gateway")]
|
||||
public string? Gateway { get; init; }
|
||||
|
||||
/// <summary>Provider network type, either <c>public</c> or <c>private</c>.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("reverse_name")]
|
||||
public string? ReverseName { get; init; }
|
||||
|
||||
[JsonPropertyName("nat_target")]
|
||||
public string? NatTarget { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>All IPv4/IPv6 network information associated with a server.</summary>
|
||||
public sealed class ServerNetworks : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("v4")]
|
||||
public IReadOnlyList<ServerNetwork> V4 { get; init; } = Array.Empty<ServerNetwork>();
|
||||
|
||||
[JsonPropertyName("v6")]
|
||||
public IReadOnlyList<ServerNetwork> V6 { get; init; } = Array.Empty<ServerNetwork>();
|
||||
|
||||
[JsonPropertyName("port_blocking")]
|
||||
public bool PortBlocking { get; init; }
|
||||
|
||||
[JsonPropertyName("separate_private_network_interface")]
|
||||
public bool? SeparatePrivateNetworkInterface { get; init; }
|
||||
|
||||
[JsonPropertyName("source_and_destination_check")]
|
||||
public bool? SourceAndDestinationCheck { get; init; }
|
||||
|
||||
[JsonPropertyName("recent_ddos")]
|
||||
public bool RecentDdos { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv6_reverse_nameservers")]
|
||||
public IReadOnlyList<string>? Ipv6ReverseNameservers { get; init; }
|
||||
|
||||
[JsonPropertyName("mac_address")]
|
||||
public string MacAddress { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>A BinaryLane image, including operating-system and backup images.</summary>
|
||||
public sealed class Image : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Provider image type, for example <c>custom</c>, <c>snapshot</c>, or <c>backup</c>.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("distribution")]
|
||||
public string? Distribution { get; init; }
|
||||
|
||||
[JsonPropertyName("full_name")]
|
||||
public string? FullName { get; init; }
|
||||
|
||||
[JsonPropertyName("slug")]
|
||||
public string? Slug { get; init; }
|
||||
|
||||
[JsonPropertyName("public")]
|
||||
public bool IsPublic { get; init; }
|
||||
|
||||
[JsonPropertyName("regions")]
|
||||
public IReadOnlyList<string> Regions { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTimeOffset? CreatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("min_disk_size")]
|
||||
public int MinDiskSizeGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("size_gigabytes")]
|
||||
public double SizeGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>Provider image status, for example <c>available</c>.</summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("error_message")]
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
[JsonPropertyName("min_memory_megabytes")]
|
||||
public int? MinMemoryMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("distribution_surcharges")]
|
||||
public DistributionSurcharges? DistributionSurcharges { get; init; }
|
||||
|
||||
[JsonPropertyName("distribution_info")]
|
||||
public DistributionInfo DistributionInfo { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("backup_info")]
|
||||
public BackupInfo? BackupInfo { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Distribution-specific image installation capabilities.</summary>
|
||||
public sealed class DistributionInfo : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("image_id")]
|
||||
public long ImageId { get; init; }
|
||||
|
||||
/// <summary>Provider password recovery mode.</summary>
|
||||
[JsonPropertyName("password_recovery")]
|
||||
public string PasswordRecovery { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("remote_access_user")]
|
||||
public string? RemoteAccessUser { get; init; }
|
||||
|
||||
[JsonPropertyName("features")]
|
||||
public IReadOnlyList<string> Features { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Additional provider charges for a distribution image.</summary>
|
||||
public sealed class DistributionSurcharges : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("surcharge_base_cost")]
|
||||
public double? SurchargeBaseCost { get; init; }
|
||||
|
||||
[JsonPropertyName("surcharge_per_memory_megabyte")]
|
||||
public double? SurchargePerMemoryMegabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("surcharge_per_memory_max_megabytes")]
|
||||
public int? SurchargePerMemoryMaxMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("surcharge_per_vcpu")]
|
||||
public double? SurchargePerVcpu { get; init; }
|
||||
|
||||
[JsonPropertyName("surcharge_min_vcpu")]
|
||||
public int? SurchargeMinVcpu { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Backup-specific metadata for an image.</summary>
|
||||
public sealed class BackupInfo : BinaryLaneDto
|
||||
{
|
||||
/// <summary>Provider backup slot, such as <c>daily</c> or <c>temporary</c>.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("server_id")]
|
||||
public long ServerId { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite")]
|
||||
public bool Offsite { get; init; }
|
||||
|
||||
[JsonPropertyName("locked")]
|
||||
public bool Locked { get; init; }
|
||||
|
||||
[JsonPropertyName("iso")]
|
||||
public bool Iso { get; init; }
|
||||
|
||||
[JsonPropertyName("backup_disks")]
|
||||
public IReadOnlyList<BackupDisk> BackupDisks { get; init; } = Array.Empty<BackupDisk>();
|
||||
}
|
||||
|
||||
/// <summary>A disk contained in a backup image.</summary>
|
||||
public sealed class BackupDisk : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("size_gigabytes")]
|
||||
public double SizeGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("min_disk_size")]
|
||||
public int MinDiskSizeGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A provider server size and its available customisation options.</summary>
|
||||
public sealed class Size : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("slug")]
|
||||
public string Slug { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
[JsonPropertyName("cpu_description")]
|
||||
public string? CpuDescription { get; init; }
|
||||
|
||||
[JsonPropertyName("storage_description")]
|
||||
public string? StorageDescription { get; init; }
|
||||
|
||||
[JsonPropertyName("size_type")]
|
||||
public SizeType SizeType { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("available")]
|
||||
public bool Available { get; init; }
|
||||
|
||||
[JsonPropertyName("regions")]
|
||||
public IReadOnlyList<string> Regions { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("regions_out_of_stock")]
|
||||
public IReadOnlyList<string>? RegionsOutOfStock { get; init; }
|
||||
|
||||
[JsonPropertyName("price_monthly")]
|
||||
public double PriceMonthly { get; init; }
|
||||
|
||||
[JsonPropertyName("price_hourly")]
|
||||
public double PriceHourly { get; init; }
|
||||
|
||||
[JsonPropertyName("disk")]
|
||||
public int DiskGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("memory")]
|
||||
public int MemoryMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer")]
|
||||
public double TransferGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("excess_transfer_cost_per_gigabyte")]
|
||||
public double ExcessTransferCostPerGigabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("vcpus")]
|
||||
public int Vcpus { get; init; }
|
||||
|
||||
[JsonPropertyName("vcpu_units")]
|
||||
public string VcpuUnits { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public SizeOptions Options { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>The provider's category for a server size.</summary>
|
||||
public sealed class SizeType : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("slug")]
|
||||
public string Slug { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Configurable bounds and costs associated with a server size.</summary>
|
||||
public sealed class SizeOptions : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("disk_min")]
|
||||
public int DiskMinGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("disk_max")]
|
||||
public int DiskMaxGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("disk_cost_per_additional_gigabyte")]
|
||||
public double DiskCostPerAdditionalGigabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("restricted_disk_values")]
|
||||
public IReadOnlyList<int>? RestrictedDiskValues { get; init; }
|
||||
|
||||
[JsonPropertyName("memory_max")]
|
||||
public int MemoryMaxMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("memory_cost_per_additional_megabyte")]
|
||||
public double MemoryCostPerAdditionalMegabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer_max")]
|
||||
public double TransferMaxGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer_cost_per_additional_gigabyte")]
|
||||
public double TransferCostPerAdditionalGigabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv4_addresses_max")]
|
||||
public int Ipv4AddressesMax { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv4_addresses_cost_per_address")]
|
||||
public double Ipv4AddressesCostPerAddress { get; init; }
|
||||
|
||||
[JsonPropertyName("discount_for_no_public_ipv4")]
|
||||
public double DiscountForNoPublicIpv4 { get; init; }
|
||||
|
||||
[JsonPropertyName("daily_backups")]
|
||||
public int DailyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("weekly_backups")]
|
||||
public int WeeklyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("monthly_backups")]
|
||||
public int MonthlyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("backups_cost_per_backup_per_gigabyte")]
|
||||
public double BackupsCostPerBackupPerGigabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite_backups_cost_per_gigabyte")]
|
||||
public double OffsiteBackupsCostPerGigabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite_backup_frequency_cost")]
|
||||
public OffsiteBackupFrequencyCost OffsiteBackupFrequencyCost { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>The backup frequency costs published for a size.</summary>
|
||||
public sealed class OffsiteBackupFrequencyCost : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("daily_per_gigabyte")]
|
||||
public double DailyPerGigabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("weekly_per_gigabyte")]
|
||||
public double WeeklyPerGigabyte { get; init; }
|
||||
|
||||
[JsonPropertyName("monthly_per_gigabyte")]
|
||||
public double MonthlyPerGigabyte { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>The options selected for an individual server.</summary>
|
||||
public sealed class SelectedSizeOptions : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("daily_backups")]
|
||||
public int DailyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("weekly_backups")]
|
||||
public int WeeklyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("monthly_backups")]
|
||||
public int MonthlyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite_backups")]
|
||||
public bool OffsiteBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv4_addresses")]
|
||||
public int Ipv4Addresses { get; init; }
|
||||
|
||||
[JsonPropertyName("memory")]
|
||||
public int MemoryMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("disk")]
|
||||
public int DiskGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer")]
|
||||
public double TransferGigabytes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#if NETSTANDARD2_0
|
||||
// The compiler emits this marker for init-only properties. netstandard2.0 does
|
||||
// not provide it, so the package carries the conventional compatibility shim.
|
||||
namespace System.Runtime.CompilerServices;
|
||||
|
||||
internal static class IsExternalInit
|
||||
{
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,390 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>Options selected while creating or rebuilding a server image.</summary>
|
||||
public sealed class ImageOptions : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>Each identifier may be either an SSH key ID or a public-key fingerprint.</summary>
|
||||
[JsonPropertyName("ssh_keys")]
|
||||
public IReadOnlyList<object>? SshKeys { get; init; }
|
||||
|
||||
/// <remarks>Do not log user-data; it can contain secrets.</remarks>
|
||||
[JsonPropertyName("user_data")]
|
||||
public string? UserData { get; init; }
|
||||
|
||||
/// <remarks>Do not log this value.</remarks>
|
||||
[JsonPropertyName("password")]
|
||||
public string? Password { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Customisable options for a server size.</summary>
|
||||
public sealed class SizeOptionsRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("daily_backups")]
|
||||
public int? DailyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("weekly_backups")]
|
||||
public int? WeeklyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("monthly_backups")]
|
||||
public int? MonthlyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite_backups")]
|
||||
public bool? OffsiteBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv4_addresses")]
|
||||
public int? Ipv4Addresses { get; init; }
|
||||
|
||||
[JsonPropertyName("memory")]
|
||||
public int? MemoryMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("disk")]
|
||||
public int? DiskGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer")]
|
||||
public double? TransferGigabytes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A requested software licence allocation.</summary>
|
||||
public sealed class License : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("software_id")]
|
||||
public long SoftwareId { get; init; }
|
||||
|
||||
[JsonPropertyName("count")]
|
||||
public int Count { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Creates a new BinaryLane server.</summary>
|
||||
public sealed class CreateServerRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("backups")]
|
||||
public bool? Backups { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv6")]
|
||||
public bool? Ipv6 { get; init; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public string Size { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>An image ID or provider image slug.</summary>
|
||||
[JsonPropertyName("image")]
|
||||
public object Image { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("region")]
|
||||
public string Region { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vpc_id")]
|
||||
public long? VpcId { get; init; }
|
||||
|
||||
[JsonPropertyName("vpc_ipv4_address")]
|
||||
public string? VpcIpv4Address { get; init; }
|
||||
|
||||
/// <summary>Each entry may be an SSH key ID or fingerprint.</summary>
|
||||
[JsonPropertyName("ssh_keys")]
|
||||
public IReadOnlyList<object>? SshKeys { get; init; }
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public SizeOptionsRequest? Options { get; init; }
|
||||
|
||||
[JsonPropertyName("licenses")]
|
||||
public IReadOnlyList<License>? Licenses { get; init; }
|
||||
|
||||
/// <remarks>Do not log user-data; it may contain secrets.</remarks>
|
||||
[JsonPropertyName("user_data")]
|
||||
public string? UserData { get; init; }
|
||||
|
||||
[JsonPropertyName("port_blocking")]
|
||||
public bool? PortBlocking { get; init; }
|
||||
|
||||
[JsonPropertyName("separate_private_network_interface")]
|
||||
public bool? SeparatePrivateNetworkInterface { get; init; }
|
||||
|
||||
/// <remarks>Do not log this value.</remarks>
|
||||
[JsonPropertyName("password")]
|
||||
public string? Password { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Creates a new VPC.</summary>
|
||||
public sealed class CreateVpcRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("route_entries")]
|
||||
public IReadOnlyList<RouteEntryRequest>? RouteEntries { get; init; }
|
||||
|
||||
[JsonPropertyName("ip_range")]
|
||||
public string? IpRange { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Replaces VPC configuration. Omitted properties are cleared by the provider.</summary>
|
||||
public sealed class UpdateVpcRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("route_entries")]
|
||||
public IReadOnlyList<RouteEntryRequest>? RouteEntries { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Partially updates VPC configuration.</summary>
|
||||
public sealed class PatchVpcRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("route_entries")]
|
||||
public IReadOnlyList<RouteEntryRequest>? RouteEntries { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A VPC route entry supplied in a create or update request.</summary>
|
||||
public sealed class RouteEntryRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("router")]
|
||||
public string Router { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("destination")]
|
||||
public string Destination { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Creates a new load balancer.</summary>
|
||||
public sealed class CreateLoadBalancerRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("forwarding_rules")]
|
||||
public IReadOnlyList<ForwardingRuleRequest>? ForwardingRules { get; init; }
|
||||
|
||||
[JsonPropertyName("health_check")]
|
||||
public HealthCheckRequest? HealthCheck { get; init; }
|
||||
|
||||
[JsonPropertyName("server_ids")]
|
||||
public IReadOnlyList<long>? ServerIds { get; init; }
|
||||
|
||||
[JsonPropertyName("region")]
|
||||
public string? Region { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Updates a load balancer.</summary>
|
||||
public sealed class UpdateLoadBalancerRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("forwarding_rules")]
|
||||
public IReadOnlyList<ForwardingRuleRequest>? ForwardingRules { get; init; }
|
||||
|
||||
[JsonPropertyName("health_check")]
|
||||
public HealthCheckRequest? HealthCheck { get; init; }
|
||||
|
||||
[JsonPropertyName("server_ids")]
|
||||
public IReadOnlyList<long>? ServerIds { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A load-balancer forwarding rule supplied in a request.</summary>
|
||||
public sealed class ForwardingRuleRequest : BinaryLaneRequestModel
|
||||
{
|
||||
/// <summary>Either <c>http</c> or <c>https</c>.</summary>
|
||||
[JsonPropertyName("entry_protocol")]
|
||||
public string EntryProtocol { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>A collection of load-balancer forwarding rules.</summary>
|
||||
public sealed class ForwardingRulesRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("forwarding_rules")]
|
||||
public IReadOnlyList<ForwardingRuleRequest> ForwardingRules { get; init; } = Array.Empty<ForwardingRuleRequest>();
|
||||
}
|
||||
|
||||
/// <summary>A load-balancer health check supplied in a request.</summary>
|
||||
public sealed class HealthCheckRequest : BinaryLaneRequestModel
|
||||
{
|
||||
/// <summary>One of <c>http</c>, <c>https</c>, or <c>both</c>.</summary>
|
||||
[JsonPropertyName("protocol")]
|
||||
public string? Protocol { get; init; }
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public string? Path { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A collection of server IDs supplied to a load-balancer operation.</summary>
|
||||
public sealed class ServerIdsRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("server_ids")]
|
||||
public IReadOnlyList<long> ServerIds { get; init; } = Array.Empty<long>();
|
||||
}
|
||||
|
||||
/// <summary>Creates a DNS domain.</summary>
|
||||
public sealed class DomainRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ip_address")]
|
||||
public string? IpAddress { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Requests refresh of the nameserver cache for domains.</summary>
|
||||
public sealed class DomainRefreshRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("domain_names")]
|
||||
public IReadOnlyList<string> DomainNames { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Creates a DNS record.</summary>
|
||||
public sealed class DomainRecordRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string Data { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public int? Priority { get; init; }
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public int? Port { get; init; }
|
||||
|
||||
[JsonPropertyName("ttl")]
|
||||
public int? Ttl { get; init; }
|
||||
|
||||
[JsonPropertyName("weight")]
|
||||
public int? Weight { get; init; }
|
||||
|
||||
[JsonPropertyName("flags")]
|
||||
public int? Flags { get; init; }
|
||||
|
||||
[JsonPropertyName("tag")]
|
||||
public string? Tag { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Updates only the supplied parts of a DNS record.</summary>
|
||||
public sealed class UpdateDomainRecordRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string? Data { get; init; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public int? Priority { get; init; }
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public int? Port { get; init; }
|
||||
|
||||
[JsonPropertyName("ttl")]
|
||||
public int? Ttl { get; init; }
|
||||
|
||||
[JsonPropertyName("weight")]
|
||||
public int? Weight { get; init; }
|
||||
|
||||
[JsonPropertyName("flags")]
|
||||
public int? Flags { get; init; }
|
||||
|
||||
[JsonPropertyName("tag")]
|
||||
public string? Tag { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Creates an SSH key.</summary>
|
||||
public sealed class SshKeyRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("public_key")]
|
||||
public string PublicKey { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("default")]
|
||||
public bool? IsDefault { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Updates an SSH key.</summary>
|
||||
public sealed class UpdateSshKeyRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("default")]
|
||||
public bool? IsDefault { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes mutable metadata on an image.</summary>
|
||||
public sealed class ImageRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("locked")]
|
||||
public bool? Locked { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Uploads an image into a backup slot.</summary>
|
||||
public sealed class UploadImageRequest : BinaryLaneRequestModel
|
||||
{
|
||||
/// <summary>Provider backup slot, required unless the replacement strategy is <c>specified</c>.</summary>
|
||||
[JsonPropertyName("backup_type")]
|
||||
public string? BackupType { get; init; }
|
||||
|
||||
/// <summary>Provider replacement strategy: <c>none</c>, <c>specified</c>, <c>oldest</c>, or <c>newest</c>.</summary>
|
||||
[JsonPropertyName("replacement_strategy")]
|
||||
public string ReplacementStrategy { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("backup_id_to_replace")]
|
||||
public long? BackupIdToReplace { get; init; }
|
||||
|
||||
[JsonPropertyName("label")]
|
||||
public string? Label { get; init; }
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Updates the IPv6 reverse nameservers configured for the account.</summary>
|
||||
public sealed class ReverseNameserversRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("reverse_nameservers")]
|
||||
public IReadOnlyList<string> ReverseNameservers { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Allows or declines the interaction requested by an action.</summary>
|
||||
public sealed class ProceedRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("proceed")]
|
||||
public bool Proceed { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Updates threshold-alert configuration.</summary>
|
||||
public sealed class ThresholdAlertRequest : BinaryLaneRequestModel
|
||||
{
|
||||
/// <summary>Provider alert type.</summary>
|
||||
[JsonPropertyName("alert_type")]
|
||||
public string AlertType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool? Enabled { get; init; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public int? Value { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>The current billing balance for the authenticated account.</summary>
|
||||
public sealed class Balance : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("unbilled_total")]
|
||||
public double UnbilledTotal { get; init; }
|
||||
|
||||
[JsonPropertyName("available_credit")]
|
||||
public double AvailableCredit { get; init; }
|
||||
|
||||
[JsonPropertyName("charges")]
|
||||
public IReadOnlyList<ChargeInformation> Charges { get; init; } = Array.Empty<ChargeInformation>();
|
||||
|
||||
[JsonPropertyName("generated_at")]
|
||||
public DateTimeOffset? GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A charge included in a balance response.</summary>
|
||||
public sealed class ChargeInformation : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("created")]
|
||||
public DateTimeOffset Created { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("total")]
|
||||
public double Total { get; init; }
|
||||
|
||||
[JsonPropertyName("ongoing")]
|
||||
public bool Ongoing { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A BinaryLane invoice.</summary>
|
||||
public sealed class Invoice : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("invoice_id")]
|
||||
public long InvoiceId { get; init; }
|
||||
|
||||
[JsonPropertyName("reference")]
|
||||
public string? Reference { get; init; }
|
||||
|
||||
[JsonPropertyName("invoice_number")]
|
||||
public string InvoiceNumber { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public double Amount { get; init; }
|
||||
|
||||
[JsonPropertyName("tax_code")]
|
||||
public TaxCode TaxCode { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("tax")]
|
||||
public double Tax { get; init; }
|
||||
|
||||
[JsonPropertyName("created")]
|
||||
public DateTimeOffset Created { get; init; }
|
||||
|
||||
[JsonPropertyName("date_due")]
|
||||
public DateTimeOffset DateDue { get; init; }
|
||||
|
||||
[JsonPropertyName("date_overdue")]
|
||||
public DateTimeOffset DateOverdue { get; init; }
|
||||
|
||||
[JsonPropertyName("paid")]
|
||||
public bool Paid { get; init; }
|
||||
|
||||
[JsonPropertyName("refunded")]
|
||||
public bool Refunded { get; init; }
|
||||
|
||||
[JsonPropertyName("payment_failure_count")]
|
||||
public int? PaymentFailureCount { get; init; }
|
||||
|
||||
[JsonPropertyName("invoice_items")]
|
||||
public IReadOnlyList<InvoiceLineItem> InvoiceItems { get; init; } = Array.Empty<InvoiceLineItem>();
|
||||
|
||||
[JsonPropertyName("invoice_download_url")]
|
||||
public string? InvoiceDownloadUrl { get; init; }
|
||||
|
||||
[JsonPropertyName("invoice_view_url")]
|
||||
public string? InvoiceViewUrl { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A line item in a BinaryLane invoice.</summary>
|
||||
public sealed class InvoiceLineItem : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("amount")]
|
||||
public double Amount { get; init; }
|
||||
|
||||
[JsonPropertyName("amount_includes_tax")]
|
||||
public bool AmountIncludesTax { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Current data-transfer use for a server.</summary>
|
||||
public sealed class DataUsage : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("server_id")]
|
||||
public long ServerId { get; init; }
|
||||
|
||||
[JsonPropertyName("expires")]
|
||||
public DateTimeOffset Expires { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer_gigabytes")]
|
||||
public long TransferGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("current_transfer_usage_gigabytes")]
|
||||
public double CurrentTransferUsageGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer_period_end")]
|
||||
public DateTimeOffset TransferPeriodEnd { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A DNS domain managed through the BinaryLane API.</summary>
|
||||
public sealed class Domain : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("current_nameservers")]
|
||||
public IReadOnlyList<string> CurrentNameservers { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("ttl")]
|
||||
public int? Ttl { get; init; }
|
||||
|
||||
[JsonPropertyName("zone_file")]
|
||||
public string ZoneFile { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>A DNS record in a BinaryLane-managed domain.</summary>
|
||||
public sealed class DomainRecord : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>Provider DNS record type, such as <c>A</c>, <c>AAAA</c>, or <c>TXT</c>.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public string? Data { get; init; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public int? Priority { get; init; }
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public int? Port { get; init; }
|
||||
|
||||
[JsonPropertyName("ttl")]
|
||||
public int Ttl { get; init; }
|
||||
|
||||
[JsonPropertyName("weight")]
|
||||
public int? Weight { get; init; }
|
||||
|
||||
[JsonPropertyName("flags")]
|
||||
public int? Flags { get; init; }
|
||||
|
||||
[JsonPropertyName("tag")]
|
||||
public string? Tag { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A temporary download link for an image.</summary>
|
||||
public sealed class ImageDownload : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("expiry")]
|
||||
public DateTimeOffset Expiry { get; init; }
|
||||
|
||||
[JsonPropertyName("disks")]
|
||||
public IReadOnlyList<ImageDiskDownload> Disks { get; init; } = Array.Empty<ImageDiskDownload>();
|
||||
}
|
||||
|
||||
/// <summary>Download URLs for one disk in an image download.</summary>
|
||||
public sealed class ImageDiskDownload : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("compressed_url")]
|
||||
public string CompressedUrl { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("raw_url")]
|
||||
public string RawUrl { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>An SSH public key owned by the authenticated account.</summary>
|
||||
public sealed class SshKey : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("fingerprint")]
|
||||
public string Fingerprint { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("public_key")]
|
||||
public string PublicKey { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("default")]
|
||||
public bool IsDefault { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A BinaryLane load balancer.</summary>
|
||||
public sealed class LoadBalancer : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ip")]
|
||||
public string Ip { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Provider load balancer status, such as <c>active</c>.</summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("forwarding_rules")]
|
||||
public IReadOnlyList<ForwardingRule> ForwardingRules { get; init; } = Array.Empty<ForwardingRule>();
|
||||
|
||||
[JsonPropertyName("health_check")]
|
||||
public HealthCheck HealthCheck { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("region")]
|
||||
public Region? Region { get; init; }
|
||||
|
||||
[JsonPropertyName("server_ids")]
|
||||
public IReadOnlyList<long> ServerIds { get; init; } = Array.Empty<long>();
|
||||
}
|
||||
|
||||
/// <summary>A load-balancer forwarding rule.</summary>
|
||||
public sealed class ForwardingRule : BinaryLaneDto
|
||||
{
|
||||
/// <summary>Provider rule protocol, either <c>http</c> or <c>https</c>.</summary>
|
||||
[JsonPropertyName("entry_protocol")]
|
||||
public string EntryProtocol { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>A load-balancer health check.</summary>
|
||||
public sealed class HealthCheck : BinaryLaneDto
|
||||
{
|
||||
/// <summary>Provider health check protocol, such as <c>http</c>, <c>https</c>, or <c>both</c>.</summary>
|
||||
[JsonPropertyName("protocol")]
|
||||
public string Protocol { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>A load-balancer configuration option available to the account.</summary>
|
||||
public sealed class LoadBalancerAvailabilityOption : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("regions")]
|
||||
public IReadOnlyList<string>? Regions { get; init; }
|
||||
|
||||
[JsonPropertyName("anycast")]
|
||||
public bool Anycast { get; init; }
|
||||
|
||||
[JsonPropertyName("price_monthly")]
|
||||
public double PriceMonthly { get; init; }
|
||||
|
||||
[JsonPropertyName("price_hourly")]
|
||||
public double PriceHourly { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A virtual private cloud.</summary>
|
||||
public sealed class Vpc : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ip_range")]
|
||||
public string IpRange { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("route_entries")]
|
||||
public IReadOnlyList<RouteEntry> RouteEntries { get; init; } = Array.Empty<RouteEntry>();
|
||||
}
|
||||
|
||||
/// <summary>A route within a VPC.</summary>
|
||||
public sealed class RouteEntry : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("router")]
|
||||
public string Router { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("destination")]
|
||||
public string Destination { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A resource connected to a VPC.</summary>
|
||||
public sealed class VpcMember : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Provider resource type.</summary>
|
||||
[JsonPropertyName("resource_type")]
|
||||
public string ResourceType { get; init; } = string.Empty;
|
||||
|
||||
/// <remarks>The provider documents this as a string, unlike action resource identifiers.</remarks>
|
||||
[JsonPropertyName("resource_id")]
|
||||
public string ResourceId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTimeOffset? CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A scheduled backup window.</summary>
|
||||
public sealed class BackupWindow : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("start")]
|
||||
public DateTimeOffset Start { get; init; }
|
||||
|
||||
[JsonPropertyName("end")]
|
||||
public DateTimeOffset End { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A server's configured backup schedule.</summary>
|
||||
public sealed class BackupSettings : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("backup_hour_of_day")]
|
||||
public int BackupHourOfDay { get; init; }
|
||||
|
||||
[JsonPropertyName("backup_day_of_week")]
|
||||
public int BackupDayOfWeek { get; init; }
|
||||
|
||||
[JsonPropertyName("backup_day_of_month")]
|
||||
public int BackupDayOfMonth { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite_backup_settings")]
|
||||
public OffsiteBackupSettings? OffsiteBackupSettings { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Offsite-backup configuration for a server.</summary>
|
||||
public sealed class OffsiteBackupSettings : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("use_custom_backup_location")]
|
||||
public bool UseCustomBackupLocation { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite_backup_location")]
|
||||
public string? OffsiteBackupLocation { get; init; }
|
||||
|
||||
[JsonPropertyName("manage_offsite_copies")]
|
||||
public bool? ManageOffsiteCopies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>The host backing a server.</summary>
|
||||
public sealed class Host : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("display_name")]
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("uptime_ms")]
|
||||
public long? UptimeMilliseconds { get; init; }
|
||||
|
||||
[JsonPropertyName("status_page")]
|
||||
public string? StatusPage { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Metadata about a backup currently attached to a server.</summary>
|
||||
public sealed class AttachedBackup : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("disk_identifiers")]
|
||||
public IReadOnlyList<string> DiskIdentifiers { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("attached_at")]
|
||||
public DateTimeOffset? AttachedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("attachment_expires")]
|
||||
public DateTimeOffset? AttachmentExpires { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Advanced virtual-machine features configured for a server.</summary>
|
||||
public sealed class AdvancedServerFeatures : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("processor_model")]
|
||||
public long? ProcessorModel { get; init; }
|
||||
|
||||
/// <summary>Provider virtual machine type.</summary>
|
||||
[JsonPropertyName("machine_type")]
|
||||
public string? MachineType { get; init; }
|
||||
|
||||
/// <summary>Provider virtual video device.</summary>
|
||||
[JsonPropertyName("video_device")]
|
||||
public string VideoDevice { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Provider advanced-feature values.</summary>
|
||||
[JsonPropertyName("enabled_advanced_features")]
|
||||
public IReadOnlyList<string> EnabledAdvancedFeatures { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Advanced server features currently available to the account.</summary>
|
||||
public sealed class AvailableAdvancedServerFeatures : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("processor_models")]
|
||||
public IReadOnlyList<ProcessorModel> ProcessorModels { get; init; } = Array.Empty<ProcessorModel>();
|
||||
|
||||
[JsonPropertyName("machine_types")]
|
||||
public IReadOnlyList<string> MachineTypes { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("advanced_features")]
|
||||
public IReadOnlyList<string> AdvancedFeatures { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>An available processor model.</summary>
|
||||
public sealed class ProcessorModel : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>An advanced firewall rule applied to a server.</summary>
|
||||
public sealed class AdvancedFirewallRule : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("source_addresses")]
|
||||
public IReadOnlyList<string> SourceAddresses { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("destination_addresses")]
|
||||
public IReadOnlyList<string> DestinationAddresses { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("destination_ports")]
|
||||
public IReadOnlyList<string>? DestinationPorts { get; init; }
|
||||
|
||||
/// <summary>Provider protocol value, such as <c>tcp</c>.</summary>
|
||||
[JsonPropertyName("protocol")]
|
||||
public string Protocol { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Provider action value, such as <c>accept</c>.</summary>
|
||||
[JsonPropertyName("action")]
|
||||
public string Action { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>An available server kernel.</summary>
|
||||
public sealed class Kernel : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A configured server threshold alert.</summary>
|
||||
public sealed class ThresholdAlert : BinaryLaneDto
|
||||
{
|
||||
/// <summary>Provider alert type.</summary>
|
||||
[JsonPropertyName("alert_type")]
|
||||
public string AlertType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("unit")]
|
||||
public string Unit { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; init; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public int Value { get; init; }
|
||||
|
||||
[JsonPropertyName("current_value")]
|
||||
public int? CurrentValue { get; init; }
|
||||
|
||||
[JsonPropertyName("last_raised")]
|
||||
public DateTimeOffset? LastRaised { get; init; }
|
||||
|
||||
[JsonPropertyName("last_cleared")]
|
||||
public DateTimeOffset? LastCleared { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Software licensed on a server.</summary>
|
||||
public sealed class LicensedSoftware : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("software")]
|
||||
public Software Software { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("licence_count")]
|
||||
public int LicenceCount { get; init; }
|
||||
|
||||
[JsonPropertyName("incompatible")]
|
||||
public bool Incompatible { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A software product that can be licensed on BinaryLane servers.</summary>
|
||||
public sealed class Software : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; init; }
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("cost_per_licence_per_month")]
|
||||
public double CostPerLicencePerMonth { get; init; }
|
||||
|
||||
[JsonPropertyName("minimum_licence_count")]
|
||||
public int MinimumLicenceCount { get; init; }
|
||||
|
||||
[JsonPropertyName("maximum_licence_count")]
|
||||
public int MaximumLicenceCount { get; init; }
|
||||
|
||||
[JsonPropertyName("licence_step_count")]
|
||||
public int LicenceStepCount { get; init; }
|
||||
|
||||
[JsonPropertyName("group")]
|
||||
public string? Group { get; init; }
|
||||
|
||||
[JsonPropertyName("supported_operating_systems")]
|
||||
public IReadOnlyList<string> SupportedOperatingSystems { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>A browser console session for a server.</summary>
|
||||
public sealed class ServerConsole : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("iframe")]
|
||||
public string Iframe { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("browser")]
|
||||
public string Browser { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
public int Width { get; init; }
|
||||
|
||||
[JsonPropertyName("height")]
|
||||
public int Height { get; init; }
|
||||
|
||||
[JsonPropertyName("expiry")]
|
||||
public DateTimeOffset Expiry { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Server user-data retained by BinaryLane.</summary>
|
||||
public sealed class UserData : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("user_data")]
|
||||
public string? Value { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A sample interval for server monitoring data.</summary>
|
||||
public sealed class Period : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("start")]
|
||||
public DateTimeOffset Start { get; init; }
|
||||
|
||||
[JsonPropertyName("end")]
|
||||
public DateTimeOffset End { get; init; }
|
||||
|
||||
/// <summary>Provider interval, such as <c>five-minute</c> or <c>day</c>.</summary>
|
||||
[JsonPropertyName("data_interval")]
|
||||
public string DataInterval { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>One monitoring aggregate sample.</summary>
|
||||
public sealed class Sample : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("cpu_usage_percent")]
|
||||
public double CpuUsagePercent { get; init; }
|
||||
|
||||
[JsonPropertyName("cpu_usage_detailed")]
|
||||
public IReadOnlyList<double> CpuUsageDetailed { get; init; } = Array.Empty<double>();
|
||||
|
||||
[JsonPropertyName("memory_usage_bytes")]
|
||||
public double MemoryUsageBytes { get; init; }
|
||||
|
||||
[JsonPropertyName("network_incoming_kbps")]
|
||||
public double NetworkIncomingKbps { get; init; }
|
||||
|
||||
[JsonPropertyName("network_outgoing_kbps")]
|
||||
public double NetworkOutgoingKbps { get; init; }
|
||||
|
||||
[JsonPropertyName("storage_usage_megabytes")]
|
||||
public double StorageUsageMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("storage_read_kbps")]
|
||||
public double StorageReadKbps { get; init; }
|
||||
|
||||
[JsonPropertyName("storage_write_kbps")]
|
||||
public double StorageWriteKbps { get; init; }
|
||||
|
||||
[JsonPropertyName("storage_read_requests_per_second")]
|
||||
public double StorageReadRequestsPerSecond { get; init; }
|
||||
|
||||
[JsonPropertyName("storage_write_requests_per_second")]
|
||||
public double StorageWriteRequestsPerSecond { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A set of monitoring samples for a server.</summary>
|
||||
public sealed class SampleSet : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("server_id")]
|
||||
public long ServerId { get; init; }
|
||||
|
||||
[JsonPropertyName("period")]
|
||||
public Period Period { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("average")]
|
||||
public Sample Average { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("maximum_memory_megabytes")]
|
||||
public double MaximumMemoryMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("maximum_storage_gigabytes")]
|
||||
public double MaximumStorageGigabytes { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>Pagination metadata returned by BinaryLane list endpoints.</summary>
|
||||
public sealed class PageMeta : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("total")]
|
||||
public int Total { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Pagination links returned by BinaryLane list endpoints.</summary>
|
||||
public sealed class PageLinks : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("pages")]
|
||||
public PageNavigation Pages { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>URLs for neighbouring pages in a BinaryLane list response.</summary>
|
||||
public sealed class PageNavigation : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("first")]
|
||||
public string? First { get; init; }
|
||||
|
||||
[JsonPropertyName("prev")]
|
||||
public string? Previous { get; init; }
|
||||
|
||||
[JsonPropertyName("next")]
|
||||
public string? Next { get; init; }
|
||||
|
||||
[JsonPropertyName("last")]
|
||||
public string? Last { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>An RFC 7807-style problem returned by BinaryLane.</summary>
|
||||
public class ProblemDetails : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; init; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public int? Status { get; init; }
|
||||
|
||||
[JsonPropertyName("detail")]
|
||||
public string? Detail { get; init; }
|
||||
|
||||
[JsonPropertyName("instance")]
|
||||
public string? Instance { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>A validation problem returned by BinaryLane.</summary>
|
||||
public sealed class ValidationProblemDetails : ProblemDetails
|
||||
{
|
||||
[JsonPropertyName("errors")]
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>>? Errors { get; init; }
|
||||
}
|
||||
|
||||
public sealed class AccountResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("account")]
|
||||
public Account Account { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class ActionResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("action")]
|
||||
public BinaryLaneAction Action { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class ActionsLinks : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("actions")]
|
||||
public IReadOnlyList<ActionLink> Actions { get; init; } = Array.Empty<ActionLink>();
|
||||
}
|
||||
|
||||
public sealed class ActionsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("actions")]
|
||||
public IReadOnlyList<BinaryLaneAction> Actions { get; init; } = Array.Empty<BinaryLaneAction>();
|
||||
}
|
||||
|
||||
public sealed class BalanceResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("balance")]
|
||||
public Balance Balance { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class InvoiceResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("invoice")]
|
||||
public Invoice Invoice { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class InvoicesResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("invoices")]
|
||||
public IReadOnlyList<Invoice> Invoices { get; init; } = Array.Empty<Invoice>();
|
||||
}
|
||||
|
||||
public sealed class UnpaidFailedInvoicesResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("invoices")]
|
||||
public IReadOnlyList<Invoice> Invoices { get; init; } = Array.Empty<Invoice>();
|
||||
}
|
||||
|
||||
public sealed class DataUsageResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("data_usage")]
|
||||
public DataUsage DataUsage { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class DataUsagesResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("data_usages")]
|
||||
public IReadOnlyList<DataUsage> DataUsages { get; init; } = Array.Empty<DataUsage>();
|
||||
}
|
||||
|
||||
public sealed class LocalNameserversResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("local_nameservers")]
|
||||
public IReadOnlyList<string> LocalNameservers { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed class DomainResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("domain")]
|
||||
public Domain Domain { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class DomainsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("domains")]
|
||||
public IReadOnlyList<Domain> Domains { get; init; } = Array.Empty<Domain>();
|
||||
}
|
||||
|
||||
public sealed class DomainRecordResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("domain_record")]
|
||||
public DomainRecord DomainRecord { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class DomainRecordsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("domain_records")]
|
||||
public IReadOnlyList<DomainRecord> DomainRecords { get; init; } = Array.Empty<DomainRecord>();
|
||||
}
|
||||
|
||||
public sealed class ImageResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("image")]
|
||||
public Image Image { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class ImagesResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("images")]
|
||||
public IReadOnlyList<Image> Images { get; init; } = Array.Empty<Image>();
|
||||
}
|
||||
|
||||
public sealed class ImageDownloadResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("link")]
|
||||
public ImageDownload Link { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class SshKeyResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("ssh_key")]
|
||||
public SshKey SshKey { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class SshKeysResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("ssh_keys")]
|
||||
public IReadOnlyList<SshKey> SshKeys { get; init; } = Array.Empty<SshKey>();
|
||||
}
|
||||
|
||||
public sealed class LoadBalancerResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("load_balancer")]
|
||||
public LoadBalancer LoadBalancer { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class LoadBalancersResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("load_balancers")]
|
||||
public IReadOnlyList<LoadBalancer> LoadBalancers { get; init; } = Array.Empty<LoadBalancer>();
|
||||
}
|
||||
|
||||
public sealed class CreateLoadBalancerResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("load_balancer")]
|
||||
public LoadBalancer LoadBalancer { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public ActionsLinks Links { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class UpdateLoadBalancerResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("load_balancer")]
|
||||
public LoadBalancer LoadBalancer { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public ActionsLinks? Links { get; init; }
|
||||
}
|
||||
|
||||
public sealed class LoadBalancerAvailabilityResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("load_balancer_availability_options")]
|
||||
public IReadOnlyList<LoadBalancerAvailabilityOption> LoadBalancerAvailabilityOptions { get; init; } = Array.Empty<LoadBalancerAvailabilityOption>();
|
||||
}
|
||||
|
||||
public sealed class RegionsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("regions")]
|
||||
public IReadOnlyList<Region> Regions { get; init; } = Array.Empty<Region>();
|
||||
}
|
||||
|
||||
public sealed class ReverseNameServersResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("reverse_nameservers")]
|
||||
public IReadOnlyList<string> ReverseNameservers { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed class SampleSetResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("sample_set")]
|
||||
public SampleSet? SampleSet { get; init; }
|
||||
}
|
||||
|
||||
public sealed class SampleSetsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("sample_sets")]
|
||||
public IReadOnlyList<SampleSet> SampleSets { get; init; } = Array.Empty<SampleSet>();
|
||||
}
|
||||
|
||||
public sealed class ServerResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("server")]
|
||||
public Server Server { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class ServersResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("servers")]
|
||||
public IReadOnlyList<Server> Servers { get; init; } = Array.Empty<Server>();
|
||||
}
|
||||
|
||||
public sealed class CreateServerResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("server")]
|
||||
public Server Server { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public ActionsLinks Links { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class AdvancedFirewallRulesResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("firewall_rules")]
|
||||
public IReadOnlyList<AdvancedFirewallRule> FirewallRules { get; init; } = Array.Empty<AdvancedFirewallRule>();
|
||||
}
|
||||
|
||||
public sealed class AvailableAdvancedServerFeaturesResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("available_advanced_server_features")]
|
||||
public AvailableAdvancedServerFeatures AvailableAdvancedServerFeatures { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class BackupsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("backups")]
|
||||
public IReadOnlyList<Image> Backups { get; init; } = Array.Empty<Image>();
|
||||
}
|
||||
|
||||
public sealed class KernelsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("kernels")]
|
||||
public IReadOnlyList<Kernel> Kernels { get; init; } = Array.Empty<Kernel>();
|
||||
}
|
||||
|
||||
public sealed class SnapshotsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("snapshots")]
|
||||
public IReadOnlyList<Image> Snapshots { get; init; } = Array.Empty<Image>();
|
||||
}
|
||||
|
||||
public sealed class ThresholdAlertsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("threshold_alerts")]
|
||||
public IReadOnlyList<ThresholdAlert> ThresholdAlerts { get; init; } = Array.Empty<ThresholdAlert>();
|
||||
}
|
||||
|
||||
public sealed class CurrentServerAlertsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("server_ids")]
|
||||
public IReadOnlyList<long> ServerIds { get; init; } = Array.Empty<long>();
|
||||
}
|
||||
|
||||
public sealed class LicensedSoftwaresResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("licensed_software")]
|
||||
public IReadOnlyList<LicensedSoftware> LicensedSoftware { get; init; } = Array.Empty<LicensedSoftware>();
|
||||
}
|
||||
|
||||
public sealed class ServerConsoleResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("console")]
|
||||
public ServerConsole Console { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class SizesResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("sizes")]
|
||||
public IReadOnlyList<Size> Sizes { get; init; } = Array.Empty<Size>();
|
||||
}
|
||||
|
||||
public sealed class SoftwareResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("software")]
|
||||
public Software Software { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class SoftwaresResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("software")]
|
||||
public IReadOnlyList<Software> Software { get; init; } = Array.Empty<Software>();
|
||||
}
|
||||
|
||||
public sealed class VpcResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("vpc")]
|
||||
public Vpc Vpc { get; init; } = new();
|
||||
}
|
||||
|
||||
public sealed class VpcsResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("vpcs")]
|
||||
public IReadOnlyList<Vpc> Vpcs { get; init; } = Array.Empty<Vpc>();
|
||||
}
|
||||
|
||||
public sealed class VpcMembersResponse : BinaryLaneDto
|
||||
{
|
||||
[JsonPropertyName("meta")]
|
||||
public PageMeta Meta { get; init; } = new();
|
||||
|
||||
[JsonPropertyName("links")]
|
||||
public PageLinks? Links { get; init; }
|
||||
|
||||
[JsonPropertyName("members")]
|
||||
public IReadOnlyList<VpcMember> Members { get; init; } = Array.Empty<VpcMember>();
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the BinaryLane server-action discriminated union when callers hold
|
||||
/// an action through its <see cref="ServerAction"/> base type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// System.Text.Json normally uses the statically declared type. That would emit
|
||||
/// only <c>type</c> for a <see cref="ServerAction"/> variable whose runtime
|
||||
/// value is, for example, <see cref="ResizeServerAction"/>. The BinaryLane API
|
||||
/// requires the concrete action properties as well, so this converter writes
|
||||
/// the runtime payload and handles all documented discriminator values. Unknown
|
||||
/// action values are preserved through <see cref="UnknownServerAction"/>.
|
||||
/// </remarks>
|
||||
public sealed class ServerActionJsonConverter : JsonConverter<ServerAction>
|
||||
{
|
||||
private static readonly PropertyInfo AdditionalPropertiesProperty =
|
||||
typeof(BinaryLaneDto).GetProperty(nameof(BinaryLaneDto.AdditionalProperties))
|
||||
?? throw new InvalidOperationException("BinaryLaneDto must expose extension data.");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert) => typeof(ServerAction).IsAssignableFrom(typeToConvert);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ServerAction? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
using JsonDocument document = JsonDocument.ParseValue(ref reader);
|
||||
if (document.RootElement.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new JsonException("A BinaryLane server action must be a JSON object.");
|
||||
}
|
||||
|
||||
string type = ReadType(document.RootElement);
|
||||
ServerAction action = CreateAction(type);
|
||||
PopulateAction(action, document.RootElement, options);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, ServerAction value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("type", value.Type);
|
||||
|
||||
var writtenProperties = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"type",
|
||||
};
|
||||
|
||||
foreach (PropertyInfo property in GetPayloadProperties(value.GetType()))
|
||||
{
|
||||
string jsonName = GetJsonName(property);
|
||||
if (!writtenProperties.Add(jsonName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
object? propertyValue = property.GetValue(value);
|
||||
if (propertyValue is null && options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
writer.WritePropertyName(jsonName);
|
||||
JsonSerializer.Serialize(writer, propertyValue, property.PropertyType, options);
|
||||
}
|
||||
|
||||
if (value.AdditionalProperties is not null)
|
||||
{
|
||||
foreach (KeyValuePair<string, JsonElement> property in value.AdditionalProperties)
|
||||
{
|
||||
if (!writtenProperties.Add(property.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
writer.WritePropertyName(property.Key);
|
||||
property.Value.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static string ReadType(JsonElement action)
|
||||
{
|
||||
if (!action.TryGetProperty("type", out JsonElement typeElement) ||
|
||||
typeElement.ValueKind != JsonValueKind.String ||
|
||||
string.IsNullOrWhiteSpace(typeElement.GetString()))
|
||||
{
|
||||
throw new JsonException("A BinaryLane server action requires a non-empty 'type' discriminator.");
|
||||
}
|
||||
|
||||
return typeElement.GetString()!;
|
||||
}
|
||||
|
||||
private static ServerAction CreateAction(string type) => type switch
|
||||
{
|
||||
BinaryLaneValues.ServerActionType.AddDisk => new AddDiskServerAction(),
|
||||
BinaryLaneValues.ServerActionType.AttachBackup => new AttachBackupServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeAdvancedFeatures => new ChangeAdvancedFeaturesServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeAdvancedFirewallRules => new ChangeAdvancedFirewallRulesServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeBackupSchedule => new ChangeBackupScheduleServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeIpv6 => new ChangeIpv6ServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeIpv6ReverseNameservers => new ChangeIpv6ReverseNameserversServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeKernel => new ChangeKernelServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeManageOffsiteBackupCopies => new ChangeManageOffsiteBackupCopiesServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeNetwork => new ChangeNetworkServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeOffsiteBackupLocation => new ChangeOffsiteBackupLocationServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangePartner => new ChangePartnerServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangePortBlocking => new ChangePortBlockingServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeRegion => new ChangeRegionServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeReverseName => new ChangeReverseNameServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeSeparatePrivateNetworkInterface => new ChangeSeparatePrivateNetworkInterfaceServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeSourceAndDestinationCheck => new ChangeSourceAndDestinationCheckServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeThresholdAlerts => new ChangeThresholdAlertsServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ChangeVpcIpv4 => new ChangeVpcIpv4ServerAction(),
|
||||
BinaryLaneValues.ServerActionType.CloneUsingBackup => new CloneUsingBackupServerAction(),
|
||||
BinaryLaneValues.ServerActionType.DeleteDisk => new DeleteDiskServerAction(),
|
||||
BinaryLaneValues.ServerActionType.DetachBackup => new DetachBackupServerAction(),
|
||||
BinaryLaneValues.ServerActionType.DisableBackups => new DisableBackupsServerAction(),
|
||||
BinaryLaneValues.ServerActionType.DisableSelinux => new DisableSelinuxServerAction(),
|
||||
BinaryLaneValues.ServerActionType.EnableBackups => new EnableBackupsServerAction(),
|
||||
BinaryLaneValues.ServerActionType.EnableIpv6 => new EnableIpv6ServerAction(),
|
||||
BinaryLaneValues.ServerActionType.IsRunning => new IsRunningServerAction(),
|
||||
BinaryLaneValues.ServerActionType.PasswordReset => new PasswordResetServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Ping => new PingServerAction(),
|
||||
BinaryLaneValues.ServerActionType.PowerCycle => new PowerCycleServerAction(),
|
||||
BinaryLaneValues.ServerActionType.PowerOff => new PowerOffServerAction(),
|
||||
BinaryLaneValues.ServerActionType.PowerOn => new PowerOnServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Reboot => new RebootServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Rebuild => new RebuildServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Rename => new RenameServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Resize => new ResizeServerAction(),
|
||||
BinaryLaneValues.ServerActionType.ResizeDisk => new ResizeDiskServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Restore => new RestoreServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Shutdown => new ShutdownServerAction(),
|
||||
BinaryLaneValues.ServerActionType.TakeBackup => new TakeBackupServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Uncancel => new UncancelServerAction(),
|
||||
BinaryLaneValues.ServerActionType.Uptime => new UptimeServerAction(),
|
||||
_ => new UnknownServerAction(type),
|
||||
};
|
||||
|
||||
private static void PopulateAction(ServerAction action, JsonElement source, JsonSerializerOptions options)
|
||||
{
|
||||
var knownProperties = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"type",
|
||||
};
|
||||
|
||||
foreach (PropertyInfo property in GetPayloadProperties(action.GetType()))
|
||||
{
|
||||
string jsonName = GetJsonName(property);
|
||||
knownProperties.Add(jsonName);
|
||||
if (!source.TryGetProperty(jsonName, out JsonElement value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
object? deserialized = JsonSerializer.Deserialize(value.GetRawText(), property.PropertyType, options);
|
||||
property.SetValue(action, deserialized);
|
||||
}
|
||||
|
||||
var additionalProperties = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
||||
foreach (JsonProperty property in source.EnumerateObject())
|
||||
{
|
||||
if (!knownProperties.Contains(property.Name))
|
||||
{
|
||||
additionalProperties[property.Name] = property.Value.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalProperties.Count > 0)
|
||||
{
|
||||
AdditionalPropertiesProperty.SetValue(action, additionalProperties);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> GetPayloadProperties(Type actionType)
|
||||
{
|
||||
foreach (PropertyInfo property in actionType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
if (!property.CanRead || !property.CanWrite ||
|
||||
property.Name == nameof(ServerAction.Type) ||
|
||||
property.Name == nameof(BinaryLaneDto.AdditionalProperties) ||
|
||||
property.GetIndexParameters().Length != 0 ||
|
||||
property.GetCustomAttribute<JsonIgnoreAttribute>()?.Condition == JsonIgnoreCondition.Always)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return property;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetJsonName(PropertyInfo property) =>
|
||||
property.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? property.Name;
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BinaryLane.Api.V2.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Base payload for <c>POST /v2/servers/{server_id}/actions</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The upstream contract is a discriminated union on <c>type</c>. Its public
|
||||
/// documentation also creates fragment-bearing pseudo-paths for each variant;
|
||||
/// those are not separate HTTP routes. Prefer a concrete derived type for a
|
||||
/// documented action. <see cref="UnknownServerAction"/> permits new provider
|
||||
/// action types without waiting for an SDK release.
|
||||
/// </remarks>
|
||||
public class ServerAction : BinaryLaneRequestModel
|
||||
{
|
||||
/// <summary>Creates an empty action for serializers and advanced callers.</summary>
|
||||
public ServerAction()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Creates a typed action with the provider discriminator value.</summary>
|
||||
protected ServerAction(string type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>An action type not yet represented by a concrete SDK class.</summary>
|
||||
public sealed class UnknownServerAction : ServerAction
|
||||
{
|
||||
public UnknownServerAction(string type)
|
||||
: base(type)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds an additional disk to a server.</summary>
|
||||
public sealed class AddDiskServerAction : ServerAction
|
||||
{
|
||||
public AddDiskServerAction() : base("add_disk") { }
|
||||
|
||||
[JsonPropertyName("size_gigabytes")]
|
||||
public int SizeGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Attaches a backup image to a server.</summary>
|
||||
public sealed class AttachBackupServerAction : ServerAction
|
||||
{
|
||||
public AttachBackupServerAction() : base("attach_backup") { }
|
||||
|
||||
[JsonPropertyName("image")]
|
||||
public long ImageId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes advanced virtual-machine features.</summary>
|
||||
public sealed class ChangeAdvancedFeaturesServerAction : ServerAction
|
||||
{
|
||||
public ChangeAdvancedFeaturesServerAction() : base("change_advanced_features") { }
|
||||
|
||||
[JsonPropertyName("enabled_advanced_features")]
|
||||
public IReadOnlyList<string>? EnabledAdvancedFeatures { get; init; }
|
||||
|
||||
[JsonPropertyName("processor_model")]
|
||||
public long? ProcessorModel { get; init; }
|
||||
|
||||
[JsonPropertyName("automatic_processor_model")]
|
||||
public bool? AutomaticProcessorModel { get; init; }
|
||||
|
||||
[JsonPropertyName("machine_type")]
|
||||
public string? MachineType { get; init; }
|
||||
|
||||
[JsonPropertyName("automatic_machine_type")]
|
||||
public bool? AutomaticMachineType { get; init; }
|
||||
|
||||
[JsonPropertyName("video_device")]
|
||||
public string? VideoDevice { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Replaces the advanced firewall rules on a server.</summary>
|
||||
public sealed class ChangeAdvancedFirewallRulesServerAction : ServerAction
|
||||
{
|
||||
public ChangeAdvancedFirewallRulesServerAction() : base("change_advanced_firewall_rules") { }
|
||||
|
||||
[JsonPropertyName("firewall_rules")]
|
||||
public IReadOnlyList<AdvancedFirewallRuleRequest> FirewallRules { get; init; } = Array.Empty<AdvancedFirewallRuleRequest>();
|
||||
}
|
||||
|
||||
/// <summary>Changes a server backup schedule.</summary>
|
||||
public sealed class ChangeBackupScheduleServerAction : ServerAction
|
||||
{
|
||||
public ChangeBackupScheduleServerAction() : base("change_backup_schedule") { }
|
||||
|
||||
[JsonPropertyName("backup_hour_of_day")]
|
||||
public int? BackupHourOfDay { get; init; }
|
||||
|
||||
[JsonPropertyName("backup_day_of_week")]
|
||||
public int? BackupDayOfWeek { get; init; }
|
||||
|
||||
[JsonPropertyName("backup_day_of_month")]
|
||||
public int? BackupDayOfMonth { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Enables or disables IPv6 for a server.</summary>
|
||||
public sealed class ChangeIpv6ServerAction : ServerAction
|
||||
{
|
||||
public ChangeIpv6ServerAction() : base("change_ipv6") { }
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes a server's IPv6 reverse nameservers.</summary>
|
||||
public sealed class ChangeIpv6ReverseNameserversServerAction : ServerAction
|
||||
{
|
||||
public ChangeIpv6ReverseNameserversServerAction() : base("change_ipv6_reverse_nameservers") { }
|
||||
|
||||
[JsonPropertyName("ipv6_reverse_nameservers")]
|
||||
public IReadOnlyList<string> Ipv6ReverseNameservers { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Changes a server kernel.</summary>
|
||||
public sealed class ChangeKernelServerAction : ServerAction
|
||||
{
|
||||
public ChangeKernelServerAction() : base("change_kernel") { }
|
||||
|
||||
[JsonPropertyName("kernel")]
|
||||
public long KernelId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes whether offsite backup copies are managed automatically.</summary>
|
||||
public sealed class ChangeManageOffsiteBackupCopiesServerAction : ServerAction
|
||||
{
|
||||
public ChangeManageOffsiteBackupCopiesServerAction() : base("change_manage_offsite_backup_copies") { }
|
||||
|
||||
[JsonPropertyName("manage_offsite_backup_copies")]
|
||||
public bool ManageOffsiteBackupCopies { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Moves a server onto or off a VPC network.</summary>
|
||||
public sealed class ChangeNetworkServerAction : ServerAction
|
||||
{
|
||||
public ChangeNetworkServerAction() : base("change_network") { }
|
||||
|
||||
[JsonPropertyName("vpc_id")]
|
||||
public long? VpcId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes the server's offsite backup location.</summary>
|
||||
public sealed class ChangeOffsiteBackupLocationServerAction : ServerAction
|
||||
{
|
||||
public ChangeOffsiteBackupLocationServerAction() : base("change_offsite_backup_location") { }
|
||||
|
||||
[JsonPropertyName("offsite_backup_location")]
|
||||
public string? OffsiteBackupLocation { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes the server's partner server relationship.</summary>
|
||||
public sealed class ChangePartnerServerAction : ServerAction
|
||||
{
|
||||
public ChangePartnerServerAction() : base("change_partner") { }
|
||||
|
||||
[JsonPropertyName("partner_server_id")]
|
||||
public long? PartnerServerId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Enables or disables port blocking.</summary>
|
||||
public sealed class ChangePortBlockingServerAction : ServerAction
|
||||
{
|
||||
public ChangePortBlockingServerAction() : base("change_port_blocking") { }
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Moves a server to a new region.</summary>
|
||||
public sealed class ChangeRegionServerAction : ServerAction
|
||||
{
|
||||
public ChangeRegionServerAction() : base("change_region") { }
|
||||
|
||||
[JsonPropertyName("region")]
|
||||
public string Region { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Changes an IPv4 reverse name.</summary>
|
||||
public sealed class ChangeReverseNameServerAction : ServerAction
|
||||
{
|
||||
public ChangeReverseNameServerAction() : base("change_reverse_name") { }
|
||||
|
||||
[JsonPropertyName("ipv4_address")]
|
||||
public string Ipv4Address { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("reverse_name")]
|
||||
public string? ReverseName { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Enables or disables a separate private network interface.</summary>
|
||||
public sealed class ChangeSeparatePrivateNetworkInterfaceServerAction : ServerAction
|
||||
{
|
||||
public ChangeSeparatePrivateNetworkInterfaceServerAction() : base("change_separate_private_network_interface") { }
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Enables or disables source-and-destination checking.</summary>
|
||||
public sealed class ChangeSourceAndDestinationCheckServerAction : ServerAction
|
||||
{
|
||||
public ChangeSourceAndDestinationCheckServerAction() : base("change_source_and_destination_check") { }
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Replaces a server's threshold-alert configuration.</summary>
|
||||
public sealed class ChangeThresholdAlertsServerAction : ServerAction
|
||||
{
|
||||
public ChangeThresholdAlertsServerAction() : base("change_threshold_alerts") { }
|
||||
|
||||
[JsonPropertyName("threshold_alerts")]
|
||||
public IReadOnlyList<ThresholdAlertRequest> ThresholdAlerts { get; init; } = Array.Empty<ThresholdAlertRequest>();
|
||||
}
|
||||
|
||||
/// <summary>Changes a VPC IPv4 address on a server.</summary>
|
||||
public sealed class ChangeVpcIpv4ServerAction : ServerAction
|
||||
{
|
||||
public ChangeVpcIpv4ServerAction() : base("change_vpc_ipv4") { }
|
||||
|
||||
[JsonPropertyName("current_ipv4_address")]
|
||||
public string CurrentIpv4Address { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("new_ipv4_address")]
|
||||
public string NewIpv4Address { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Clones a backup image onto another server.</summary>
|
||||
public sealed class CloneUsingBackupServerAction : ServerAction
|
||||
{
|
||||
public CloneUsingBackupServerAction() : base("clone_using_backup") { }
|
||||
|
||||
[JsonPropertyName("image_id")]
|
||||
public long ImageId { get; init; }
|
||||
|
||||
[JsonPropertyName("target_server_id")]
|
||||
public long TargetServerId { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Deletes a non-primary disk.</summary>
|
||||
public sealed class DeleteDiskServerAction : ServerAction
|
||||
{
|
||||
public DeleteDiskServerAction() : base("delete_disk") { }
|
||||
|
||||
[JsonPropertyName("disk_id")]
|
||||
public long DiskId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Detaches an attached backup from a server.</summary>
|
||||
public sealed class DetachBackupServerAction : ServerAction
|
||||
{
|
||||
public DetachBackupServerAction() : base("detach_backup") { }
|
||||
}
|
||||
|
||||
/// <summary>Disables automatic backups.</summary>
|
||||
public sealed class DisableBackupsServerAction : ServerAction
|
||||
{
|
||||
public DisableBackupsServerAction() : base("disable_backups") { }
|
||||
}
|
||||
|
||||
/// <summary>Disables SELinux in a compatible installed distribution.</summary>
|
||||
public sealed class DisableSelinuxServerAction : ServerAction
|
||||
{
|
||||
public DisableSelinuxServerAction() : base("disable_selinux") { }
|
||||
}
|
||||
|
||||
/// <summary>Enables automatic backups.</summary>
|
||||
public sealed class EnableBackupsServerAction : ServerAction
|
||||
{
|
||||
public EnableBackupsServerAction() : base("enable_backups") { }
|
||||
}
|
||||
|
||||
/// <summary>Enables IPv6 on a server.</summary>
|
||||
public sealed class EnableIpv6ServerAction : ServerAction
|
||||
{
|
||||
public EnableIpv6ServerAction() : base("enable_ipv6") { }
|
||||
}
|
||||
|
||||
/// <summary>Checks whether a server is running.</summary>
|
||||
public sealed class IsRunningServerAction : ServerAction
|
||||
{
|
||||
public IsRunningServerAction() : base("is_running") { }
|
||||
}
|
||||
|
||||
/// <summary>Resets a server password.</summary>
|
||||
public sealed class PasswordResetServerAction : ServerAction
|
||||
{
|
||||
public PasswordResetServerAction() : base("password_reset") { }
|
||||
|
||||
[JsonPropertyName("username")]
|
||||
public string? Username { get; init; }
|
||||
|
||||
/// <remarks>Do not log this value.</remarks>
|
||||
[JsonPropertyName("password")]
|
||||
public string? Password { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Pings a server.</summary>
|
||||
public sealed class PingServerAction : ServerAction
|
||||
{
|
||||
public PingServerAction() : base("ping") { }
|
||||
}
|
||||
|
||||
/// <summary>Power cycles a server.</summary>
|
||||
public sealed class PowerCycleServerAction : ServerAction
|
||||
{
|
||||
public PowerCycleServerAction() : base("power_cycle") { }
|
||||
}
|
||||
|
||||
/// <summary>Powers a server off.</summary>
|
||||
public sealed class PowerOffServerAction : ServerAction
|
||||
{
|
||||
public PowerOffServerAction() : base("power_off") { }
|
||||
}
|
||||
|
||||
/// <summary>Powers a server on.</summary>
|
||||
public sealed class PowerOnServerAction : ServerAction
|
||||
{
|
||||
public PowerOnServerAction() : base("power_on") { }
|
||||
}
|
||||
|
||||
/// <summary>Reboots a server.</summary>
|
||||
public sealed class RebootServerAction : ServerAction
|
||||
{
|
||||
public RebootServerAction() : base("reboot") { }
|
||||
}
|
||||
|
||||
/// <summary>Rebuilds a server from an image.</summary>
|
||||
public sealed class RebuildServerAction : ServerAction
|
||||
{
|
||||
public RebuildServerAction() : base("rebuild") { }
|
||||
|
||||
/// <summary>An image ID or image slug. Omit to reuse the existing image.</summary>
|
||||
[JsonPropertyName("image")]
|
||||
public object? Image { get; init; }
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public ImageOptions? Options { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Renames a server.</summary>
|
||||
public sealed class RenameServerAction : ServerAction
|
||||
{
|
||||
public RenameServerAction() : base("rename") { }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Changes selected size options during a resize action.</summary>
|
||||
public sealed class ChangeSizeOptionsRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("daily_backups")]
|
||||
public int? DailyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("weekly_backups")]
|
||||
public int? WeeklyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("monthly_backups")]
|
||||
public int? MonthlyBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("offsite_backups")]
|
||||
public bool? OffsiteBackups { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv4_addresses")]
|
||||
public int? Ipv4Addresses { get; init; }
|
||||
|
||||
[JsonPropertyName("memory")]
|
||||
public int? MemoryMegabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("disk")]
|
||||
public int? DiskGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("transfer")]
|
||||
public double? TransferGigabytes { get; init; }
|
||||
|
||||
[JsonPropertyName("ipv4_addresses_to_remove")]
|
||||
public IReadOnlyList<string>? Ipv4AddressesToRemove { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes the source image selected by a resize operation.</summary>
|
||||
public sealed class ChangeImage : BinaryLaneRequestModel
|
||||
{
|
||||
/// <summary>An image ID or image slug.</summary>
|
||||
[JsonPropertyName("image")]
|
||||
public object? Image { get; init; }
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public ImageOptions? Options { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Changes licences selected by a resize operation.</summary>
|
||||
public sealed class ChangeLicenses : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("licenses")]
|
||||
public IReadOnlyList<License> Licenses { get; init; } = Array.Empty<License>();
|
||||
}
|
||||
|
||||
/// <summary>Resizes a server.</summary>
|
||||
public sealed class ResizeServerAction : ServerAction
|
||||
{
|
||||
public ResizeServerAction() : base("resize") { }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public string? Size { get; init; }
|
||||
|
||||
[JsonPropertyName("options")]
|
||||
public ChangeSizeOptionsRequest? Options { get; init; }
|
||||
|
||||
[JsonPropertyName("change_image")]
|
||||
public ChangeImage? ChangeImage { get; init; }
|
||||
|
||||
[JsonPropertyName("change_licenses")]
|
||||
public ChangeLicenses? ChangeLicenses { get; init; }
|
||||
|
||||
[JsonPropertyName("pre_action_backup")]
|
||||
public TakeBackupServerAction? PreActionBackup { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Resizes an individual server disk.</summary>
|
||||
public sealed class ResizeDiskServerAction : ServerAction
|
||||
{
|
||||
public ResizeDiskServerAction() : base("resize_disk") { }
|
||||
|
||||
[JsonPropertyName("disk_id")]
|
||||
public long DiskId { get; init; }
|
||||
|
||||
[JsonPropertyName("size_gigabytes")]
|
||||
public int SizeGigabytes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Restores a server from an image.</summary>
|
||||
public sealed class RestoreServerAction : ServerAction
|
||||
{
|
||||
public RestoreServerAction() : base("restore") { }
|
||||
|
||||
/// <summary>An image ID or image slug.</summary>
|
||||
[JsonPropertyName("image")]
|
||||
public object Image { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Requests a graceful shutdown.</summary>
|
||||
public sealed class ShutdownServerAction : ServerAction
|
||||
{
|
||||
public ShutdownServerAction() : base("shutdown") { }
|
||||
}
|
||||
|
||||
/// <summary>Creates a backup of a server.</summary>
|
||||
public sealed class TakeBackupServerAction : ServerAction
|
||||
{
|
||||
public TakeBackupServerAction() : base("take_backup") { }
|
||||
|
||||
/// <summary>Provider backup slot, such as <c>daily</c> or <c>temporary</c>.</summary>
|
||||
[JsonPropertyName("backup_type")]
|
||||
public string? BackupType { get; init; }
|
||||
|
||||
/// <summary>Provider replacement strategy.</summary>
|
||||
[JsonPropertyName("replacement_strategy")]
|
||||
public string ReplacementStrategy { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("backup_id_to_replace")]
|
||||
public long? BackupIdToReplace { get; init; }
|
||||
|
||||
[JsonPropertyName("label")]
|
||||
public string? Label { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Reverses a scheduled server cancellation.</summary>
|
||||
public sealed class UncancelServerAction : ServerAction
|
||||
{
|
||||
public UncancelServerAction() : base("uncancel") { }
|
||||
}
|
||||
|
||||
/// <summary>Checks server uptime.</summary>
|
||||
public sealed class UptimeServerAction : ServerAction
|
||||
{
|
||||
public UptimeServerAction() : base("uptime") { }
|
||||
}
|
||||
|
||||
/// <summary>An advanced firewall rule supplied in a server-action request.</summary>
|
||||
public sealed class AdvancedFirewallRuleRequest : BinaryLaneRequestModel
|
||||
{
|
||||
[JsonPropertyName("source_addresses")]
|
||||
public IReadOnlyList<string> SourceAddresses { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("destination_addresses")]
|
||||
public IReadOnlyList<string> DestinationAddresses { get; init; } = Array.Empty<string>();
|
||||
|
||||
[JsonPropertyName("destination_ports")]
|
||||
public IReadOnlyList<string>? DestinationPorts { get; init; }
|
||||
|
||||
/// <summary>Provider protocol value, such as <c>tcp</c>.</summary>
|
||||
[JsonPropertyName("protocol")]
|
||||
public string Protocol { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Provider action value, such as <c>accept</c>.</summary>
|
||||
[JsonPropertyName("action")]
|
||||
public string Action { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace BinaryLane.Api.V2.Pagination;
|
||||
|
||||
/// <summary>A page returned from a BinaryLane list endpoint.</summary>
|
||||
/// <typeparam name="T">Item type.</typeparam>
|
||||
public sealed class Page<T>
|
||||
{
|
||||
internal Page(
|
||||
IReadOnlyList<T> items,
|
||||
int total,
|
||||
Uri? firstPage,
|
||||
Uri? previousPage,
|
||||
Uri? nextPage,
|
||||
Uri? lastPage)
|
||||
{
|
||||
Items = new ReadOnlyCollection<T>(new List<T>(items));
|
||||
Total = total;
|
||||
FirstPage = firstPage;
|
||||
PreviousPage = previousPage;
|
||||
NextPage = nextPage;
|
||||
LastPage = lastPage;
|
||||
}
|
||||
|
||||
/// <summary>Items returned in this response.</summary>
|
||||
public IReadOnlyList<T> Items { get; }
|
||||
|
||||
/// <summary>Total number of items available across all pages, when supplied by BinaryLane.</summary>
|
||||
public int Total { get; }
|
||||
|
||||
/// <summary>First result page, when supplied by BinaryLane.</summary>
|
||||
public Uri? FirstPage { get; }
|
||||
|
||||
/// <summary>Previous result page, when supplied by BinaryLane.</summary>
|
||||
public Uri? PreviousPage { get; }
|
||||
|
||||
/// <summary>Next result page, when supplied by BinaryLane.</summary>
|
||||
public Uri? NextPage { get; }
|
||||
|
||||
/// <summary>Last result page, when supplied by BinaryLane.</summary>
|
||||
public Uri? LastPage { get; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BinaryLane.Api.V2.Pagination;
|
||||
|
||||
/// <summary>Specifies a one-based BinaryLane API result page.</summary>
|
||||
public sealed class PageRequest
|
||||
{
|
||||
/// <summary>One-based page number.</summary>
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
/// <summary>Number of results to return, from 1 through 200.</summary>
|
||||
public int PerPage { get; set; } = 20;
|
||||
|
||||
internal IReadOnlyDictionary<string, object?> ToQuery()
|
||||
{
|
||||
if (Page < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(Page), "Page numbering starts at 1.");
|
||||
}
|
||||
|
||||
if (PerPage < 1 || PerPage > 200)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(PerPage), "PerPage must be between 1 and 200.");
|
||||
}
|
||||
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["page"] = Page,
|
||||
["per_page"] = PerPage,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
|
||||
namespace BinaryLane.Api.V2.Resources;
|
||||
|
||||
/// <summary>Result of submitting an asynchronous server action.</summary>
|
||||
public sealed class ActionSubmission
|
||||
{
|
||||
internal ActionSubmission(HttpStatusCode statusCode, BinaryLaneAction? action, JsonElement response)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
Action = action;
|
||||
Response = response;
|
||||
}
|
||||
|
||||
/// <summary>HTTP status returned by BinaryLane, commonly 200 or 202.</summary>
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
|
||||
/// <summary>The action supplied by BinaryLane, when present in the response.</summary>
|
||||
public BinaryLaneAction? Action { get; }
|
||||
|
||||
/// <summary>Raw response for forward-compatible access to provider fields.</summary>
|
||||
public JsonElement Response { get; }
|
||||
|
||||
/// <summary>Whether BinaryLane accepted the request for asynchronous completion.</summary>
|
||||
public bool IsAccepted => StatusCode == HttpStatusCode.Accepted;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace BinaryLane.Api.V2.Resources;
|
||||
|
||||
/// <summary>Controls opt-in polling of a BinaryLane asynchronous action.</summary>
|
||||
public sealed class ActionWaitOptions
|
||||
{
|
||||
/// <summary>Delay between action status checks.</summary>
|
||||
public TimeSpan PollInterval { get; set; } = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>Maximum time spent polling before a timeout is reported.</summary>
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(15);
|
||||
|
||||
internal void Validate()
|
||||
{
|
||||
if (PollInterval < TimeSpan.FromMilliseconds(100) || PollInterval > TimeSpan.FromMinutes(5))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(PollInterval), "PollInterval must be between 100 milliseconds and five minutes.");
|
||||
}
|
||||
|
||||
if (Timeout < PollInterval || Timeout > TimeSpan.FromHours(24))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(Timeout), "Timeout must be at least PollInterval and no more than 24 hours.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using BinaryLane.Api.V2.Pagination;
|
||||
|
||||
namespace BinaryLane.Api.V2.Resources;
|
||||
|
||||
/// <summary>Shared JSON-envelope and pagination behavior for resource APIs.</summary>
|
||||
public abstract class BinaryLaneResourceBase
|
||||
{
|
||||
/// <summary>Initializes a resource using the common request executor.</summary>
|
||||
protected BinaryLaneResourceBase(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
{
|
||||
Executor = executor ?? throw new ArgumentNullException(nameof(executor));
|
||||
Json = json ?? throw new ArgumentNullException(nameof(json));
|
||||
}
|
||||
|
||||
/// <summary>Common request executor.</summary>
|
||||
protected IBinaryLaneApiExecutor Executor { get; }
|
||||
|
||||
/// <summary>Common JSON options.</summary>
|
||||
protected BinaryLaneJsonSerializerOptions Json { get; }
|
||||
|
||||
/// <summary>Gets a raw JSON response, allowing forward-compatible access to a newly added API field.</summary>
|
||||
protected Task<JsonElement> GetRawAsync(
|
||||
string path,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Executor.GetAsync<JsonElement>(path, query, cancellationToken);
|
||||
|
||||
/// <summary>Sends a raw JSON mutation request.</summary>
|
||||
protected async Task<JsonElement> SendRawAsync(
|
||||
System.Net.Http.HttpMethod method,
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await Executor.SendAsync<JsonElement>(
|
||||
new BinaryLaneRequest(method, path) { Body = body, Query = query }, cancellationToken).ConfigureAwait(false);
|
||||
return response.Body;
|
||||
}
|
||||
|
||||
/// <summary>Sends a raw request and retains the HTTP response metadata.</summary>
|
||||
protected Task<BinaryLaneResponse<JsonElement>> SendResponseAsync(
|
||||
System.Net.Http.HttpMethod method,
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Executor.SendAsync<JsonElement>(
|
||||
new BinaryLaneRequest(method, path) { Body = body, Query = query }, cancellationToken);
|
||||
|
||||
/// <summary>Sends a request expected to have no response body.</summary>
|
||||
protected async Task SendNoContentAsync(
|
||||
System.Net.Http.HttpMethod method,
|
||||
string path,
|
||||
object? body = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Executor.SendAsync(
|
||||
new BinaryLaneRequest(method, path) { Body = body, Query = query }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Gets a response whose value is wrapped in a named JSON property.</summary>
|
||||
protected async Task<T> GetItemAsync<T>(
|
||||
string path,
|
||||
string propertyName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var document = await GetRawAsync(path, null, cancellationToken).ConfigureAwait(false);
|
||||
return DeserializeEnvelope<T>(document, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>Gets a response whose JSON body is the requested value rather than an envelope.</summary>
|
||||
protected async Task<T> GetDirectItemAsync<T>(
|
||||
string path,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var document = await GetRawAsync(path, null, cancellationToken).ConfigureAwait(false);
|
||||
var item = JsonSerializer.Deserialize<T>(document.GetRawText(), Json.SerializerOptions);
|
||||
if (item is null)
|
||||
{
|
||||
throw new JsonException("BinaryLane response did not contain a usable JSON value.");
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>Sends a mutation whose value is wrapped in a named JSON property.</summary>
|
||||
protected async Task<T> SendItemAsync<T>(
|
||||
System.Net.Http.HttpMethod method,
|
||||
string path,
|
||||
object? body,
|
||||
string propertyName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var document = await Executor.SendAsync<JsonElement>(
|
||||
new BinaryLaneRequest(method, path) { Body = body }, cancellationToken).ConfigureAwait(false);
|
||||
return DeserializeEnvelope<T>(document.Body, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>Gets a typed page whose items are wrapped in a named JSON property.</summary>
|
||||
protected async Task<Page<T>> GetPageAsync<T>(
|
||||
string path,
|
||||
string propertyName,
|
||||
PageRequest? page = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var parameters = MergeQuery(query, page?.ToQuery());
|
||||
var document = await GetRawAsync(path, parameters, cancellationToken).ConfigureAwait(false);
|
||||
return DeserializePage<T>(document, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>Enumerates every available page without enabling retries for mutating requests.</summary>
|
||||
protected async IAsyncEnumerable<T> GetAllPagesAsync<T>(
|
||||
string path,
|
||||
string propertyName,
|
||||
PageRequest? page = null,
|
||||
IReadOnlyDictionary<string, object?>? query = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var seenNextPageLinks = new HashSet<string>(StringComparer.Ordinal);
|
||||
var current = await GetPageAsync<T>(path, propertyName, page, query, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var item in current.Items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
|
||||
while (current.NextPage is not null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var nextPagePath = current.NextPage.IsAbsoluteUri
|
||||
? current.NextPage.AbsoluteUri
|
||||
: current.NextPage.OriginalString;
|
||||
|
||||
if (!seenNextPageLinks.Add(nextPagePath))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"BinaryLane returned a repeated pagination link: '{nextPagePath}'.");
|
||||
}
|
||||
|
||||
current = await GetPageAsync<T>(
|
||||
nextPagePath,
|
||||
propertyName,
|
||||
null,
|
||||
null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var item in current.Items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Deserializes an item from a standard BinaryLane JSON response envelope.</summary>
|
||||
protected T DeserializeEnvelope<T>(JsonElement document, string propertyName)
|
||||
{
|
||||
if (document.ValueKind != JsonValueKind.Object ||
|
||||
!document.TryGetProperty(propertyName, out var payload))
|
||||
{
|
||||
throw new JsonException($"BinaryLane response did not contain the expected '{propertyName}' envelope property.");
|
||||
}
|
||||
|
||||
var item = JsonSerializer.Deserialize<T>(payload.GetRawText(), Json.SerializerOptions);
|
||||
if (item is null)
|
||||
{
|
||||
throw new JsonException($"BinaryLane response did not contain a usable '{propertyName}' value.");
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to deserialize a value only when the named envelope property is present.</summary>
|
||||
protected bool TryDeserializeEnvelope<T>(JsonElement document, string propertyName, out T? item)
|
||||
{
|
||||
item = default;
|
||||
if (document.ValueKind != JsonValueKind.Object || !document.TryGetProperty(propertyName, out var payload))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
item = JsonSerializer.Deserialize<T>(payload.GetRawText(), Json.SerializerOptions);
|
||||
return item is not null;
|
||||
}
|
||||
|
||||
/// <summary>Deserializes a standard BinaryLane list response.</summary>
|
||||
protected Page<T> DeserializePage<T>(JsonElement document, string propertyName)
|
||||
{
|
||||
if (document.ValueKind != JsonValueKind.Object ||
|
||||
!document.TryGetProperty(propertyName, out var collection) ||
|
||||
collection.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new JsonException($"BinaryLane response did not contain the expected '{propertyName}' array.");
|
||||
}
|
||||
|
||||
var items = new List<T>();
|
||||
foreach (var value in collection.EnumerateArray())
|
||||
{
|
||||
var item = JsonSerializer.Deserialize<T>(value.GetRawText(), Json.SerializerOptions);
|
||||
if (item is null)
|
||||
{
|
||||
throw new JsonException(
|
||||
$"BinaryLane response contained an unusable item in the '{propertyName}' array.");
|
||||
}
|
||||
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
var total = items.Count;
|
||||
if (document.TryGetProperty("meta", out var meta) &&
|
||||
meta.ValueKind == JsonValueKind.Object &&
|
||||
meta.TryGetProperty("total", out var totalValue) &&
|
||||
totalValue.TryGetInt32(out var providedTotal))
|
||||
{
|
||||
total = providedTotal;
|
||||
}
|
||||
|
||||
Uri? first = null;
|
||||
Uri? previous = null;
|
||||
Uri? next = null;
|
||||
Uri? last = null;
|
||||
if (document.TryGetProperty("links", out var links) &&
|
||||
links.ValueKind == JsonValueKind.Object &&
|
||||
links.TryGetProperty("pages", out var pages) &&
|
||||
pages.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
first = ReadUri(pages, "first");
|
||||
previous = ReadUri(pages, "prev");
|
||||
next = ReadUri(pages, "next");
|
||||
last = ReadUri(pages, "last");
|
||||
}
|
||||
|
||||
return new Page<T>(items, total, first, previous, next, last);
|
||||
}
|
||||
|
||||
private static Uri? ReadUri(JsonElement element, string name)
|
||||
{
|
||||
if (!element.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Uri.TryCreate(value.GetString(), UriKind.RelativeOrAbsolute, out var uri) ? uri : null;
|
||||
}
|
||||
|
||||
/// <summary>Escapes an arbitrary API path segment.</summary>
|
||||
protected static string EscapePathSegment(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("A path segment is required.", nameof(value));
|
||||
}
|
||||
|
||||
return Uri.EscapeDataString(value);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?>? MergeQuery(
|
||||
IReadOnlyDictionary<string, object?>? primary,
|
||||
IReadOnlyDictionary<string, object?>? secondary)
|
||||
{
|
||||
if (primary is null && secondary is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, object?>();
|
||||
if (primary is not null)
|
||||
{
|
||||
foreach (var pair in primary)
|
||||
{
|
||||
result[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (secondary is not null)
|
||||
{
|
||||
foreach (var pair in secondary)
|
||||
{
|
||||
result[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
using BinaryLane.Api.V2.Pagination;
|
||||
|
||||
namespace BinaryLane.Api.V2.Resources;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class AccountApi : BinaryLaneResourceBase, IAccountApi
|
||||
{
|
||||
/// <summary>Initializes the account resource.</summary>
|
||||
public AccountApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Account> GetAsync(CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Account>("v2/account", "account", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<JsonElement> GetRawAsync(CancellationToken cancellationToken = default) =>
|
||||
base.GetRawAsync("v2/account", null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class ActionsApi : BinaryLaneResourceBase, IActionsApi
|
||||
{
|
||||
/// <summary>Initializes the actions resource.</summary>
|
||||
public ActionsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<BinaryLaneAction> GetAsync(long actionId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<BinaryLaneAction>($"v2/actions/{actionId}", "action", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<BinaryLaneAction>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<BinaryLaneAction>("v2/actions", "actions", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<BinaryLaneAction> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<BinaryLaneAction>("v2/actions", "actions", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ProceedAsync(long actionId, ProceedRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendNoContentAsync(HttpMethod.Post, $"v2/actions/{actionId}/proceed", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryLaneAction> WaitForCompletionAsync(
|
||||
long actionId,
|
||||
ActionWaitOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new ActionWaitOptions();
|
||||
options.Validate();
|
||||
var startedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var action = await GetAsync(actionId, cancellationToken).ConfigureAwait(false);
|
||||
if (IsTerminal(action.Status))
|
||||
{
|
||||
return action;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.UtcNow - startedAt >= options.Timeout)
|
||||
{
|
||||
throw new TimeoutException(
|
||||
$"Timed out waiting for BinaryLane action {actionId} after {options.Timeout}.");
|
||||
}
|
||||
|
||||
await Task.Delay(options.PollInterval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTerminal(string status) =>
|
||||
string.Equals(status, "completed", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(status, "errored", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(status, "failed", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(status, "cancelled", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(status, "canceled", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class BillingApi : BinaryLaneResourceBase, IBillingApi
|
||||
{
|
||||
/// <summary>Initializes the billing resource.</summary>
|
||||
public BillingApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Balance> GetBalanceAsync(CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Balance>("v2/customers/my/balance", "balance", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Invoice> GetInvoiceAsync(long invoiceId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Invoice>($"v2/customers/my/invoices/{invoiceId}", "invoice", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Invoice>> ListInvoicesAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Invoice>("v2/customers/my/invoices", "invoices", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Invoice>> ListUnpaidPaymentFailedInvoicesAsync(CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<IReadOnlyList<Invoice>>("v2/customers/my/unpaid-payment-failed-invoices", "invoices", cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class DataUsageApi : BinaryLaneResourceBase, IDataUsageApi
|
||||
{
|
||||
/// <summary>Initializes the data-usage resource.</summary>
|
||||
public DataUsageApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<DataUsage>> ListCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<DataUsage>("v2/data_usages/current", "data_usages", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<DataUsage> ListAllCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<DataUsage>("v2/data_usages/current", "data_usages", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DataUsage> GetCurrentAsync(long serverId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<DataUsage>($"v2/data_usages/{serverId}/current", "data_usage", cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class DomainsApi : BinaryLaneResourceBase, IDomainsApi
|
||||
{
|
||||
/// <summary>Initializes the domains resource.</summary>
|
||||
public DomainsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<string>> GetNameserversAsync(CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<IReadOnlyList<string>>("v2/domains/nameservers", "local_nameservers", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RefreshNameserverCacheAsync(DomainRefreshRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendNoContentAsync(HttpMethod.Post, "v2/domains/refresh_nameserver_cache", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Domain>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Domain>("v2/domains", "domains", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<Domain> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<Domain>("v2/domains", "domains", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Domain> CreateAsync(DomainRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<Domain>(HttpMethod.Post, "v2/domains", request ?? throw new ArgumentNullException(nameof(request)), "domain", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Domain> GetAsync(string domainName, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Domain>($"v2/domains/{EscapePathSegment(domainName)}", "domain", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteAsync(string domainName, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync($"v2/domains/{EscapePathSegment(domainName)}", null, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<DomainRecord>> ListRecordsAsync(string domainName, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<DomainRecord>($"v2/domains/{EscapePathSegment(domainName)}/records", "domain_records", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DomainRecord> CreateRecordAsync(string domainName, DomainRecordRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<DomainRecord>(HttpMethod.Post, $"v2/domains/{EscapePathSegment(domainName)}/records", request ?? throw new ArgumentNullException(nameof(request)), "domain_record", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DomainRecord> GetRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<DomainRecord>($"v2/domains/{EscapePathSegment(domainName)}/records/{recordId}", "domain_record", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DomainRecord> UpdateRecordAsync(string domainName, long recordId, UpdateDomainRecordRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<DomainRecord>(HttpMethod.Put, $"v2/domains/{EscapePathSegment(domainName)}/records/{recordId}", request ?? throw new ArgumentNullException(nameof(request)), "domain_record", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync($"v2/domains/{EscapePathSegment(domainName)}/records/{recordId}", null, null, cancellationToken);
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class ImagesApi : BinaryLaneResourceBase, IImagesApi
|
||||
{
|
||||
/// <summary>Initializes the images resource.</summary>
|
||||
public ImagesApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Image>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Image>("v2/images", "images", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<Image> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<Image>("v2/images", "images", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Image> GetAsync(string imageIdOrSlug, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Image>($"v2/images/{EscapePathSegment(imageIdOrSlug)}", "image", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Image> UpdateAsync(long imageId, ImageRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<Image>(HttpMethod.Put, $"v2/images/{imageId}", request ?? throw new ArgumentNullException(nameof(request)), "image", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ImageDownload> GetDownloadAsync(long imageId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<ImageDownload>($"v2/images/{imageId}/download", "link", cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class SshKeysApi : BinaryLaneResourceBase, ISshKeysApi
|
||||
{
|
||||
/// <summary>Initializes the SSH-key resource.</summary>
|
||||
public SshKeysApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<SshKey>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<SshKey>("v2/account/keys", "ssh_keys", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<SshKey> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<SshKey>("v2/account/keys", "ssh_keys", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SshKey> GetAsync(long keyId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<SshKey>($"v2/account/keys/{keyId}", "ssh_key", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SshKey> CreateAsync(SshKeyRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<SshKey>(HttpMethod.Post, "v2/account/keys", request ?? throw new ArgumentNullException(nameof(request)), "ssh_key", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SshKey> UpdateAsync(long keyId, UpdateSshKeyRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<SshKey>(HttpMethod.Put, $"v2/account/keys/{keyId}", request ?? throw new ArgumentNullException(nameof(request)), "ssh_key", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteAsync(long keyId, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync($"v2/account/keys/{keyId}", null, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class LoadBalancersApi : BinaryLaneResourceBase, ILoadBalancersApi
|
||||
{
|
||||
/// <summary>Initializes the load-balancer resource.</summary>
|
||||
public LoadBalancersApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<LoadBalancer>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<LoadBalancer>("v2/load_balancers", "load_balancers", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<LoadBalancer> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<LoadBalancer>("v2/load_balancers", "load_balancers", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<LoadBalancer> GetAsync(long loadBalancerId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<LoadBalancer>($"v2/load_balancers/{loadBalancerId}", "load_balancer", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<LoadBalancer> CreateAsync(CreateLoadBalancerRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<LoadBalancer>(HttpMethod.Post, "v2/load_balancers", request ?? throw new ArgumentNullException(nameof(request)), "load_balancer", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<LoadBalancer> UpdateAsync(long loadBalancerId, UpdateLoadBalancerRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<LoadBalancer>(HttpMethod.Put, $"v2/load_balancers/{loadBalancerId}", request ?? throw new ArgumentNullException(nameof(request)), "load_balancer", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteAsync(long loadBalancerId, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync($"v2/load_balancers/{loadBalancerId}", null, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<LoadBalancerAvailabilityOption>> GetAvailabilityAsync(CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<IReadOnlyList<LoadBalancerAvailabilityOption>>(
|
||||
"v2/load_balancers/availability",
|
||||
"load_balancer_availability_options",
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendNoContentAsync(HttpMethod.Post, $"v2/load_balancers/{loadBalancerId}/servers", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RemoveServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync($"v2/load_balancers/{loadBalancerId}/servers", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendNoContentAsync(HttpMethod.Post, $"v2/load_balancers/{loadBalancerId}/forwarding_rules", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RemoveForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync($"v2/load_balancers/{loadBalancerId}/forwarding_rules", request ?? throw new ArgumentNullException(nameof(request)), null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class RegionsApi : BinaryLaneResourceBase, IRegionsApi
|
||||
{
|
||||
/// <summary>Initializes the regions resource.</summary>
|
||||
public RegionsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Region>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Region>("v2/regions", "regions", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<Region> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<Region>("v2/regions", "regions", page, null, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
using BinaryLane.Api.V2.Pagination;
|
||||
|
||||
namespace BinaryLane.Api.V2.Resources;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class ReverseNamesApi : BinaryLaneResourceBase, IReverseNamesApi
|
||||
{
|
||||
/// <summary>Initializes the reverse-names resource.</summary>
|
||||
public ReverseNamesApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<string>> ListIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<string>("v2/reverse_names/ipv6", "reverse_nameservers", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<string> ListAllIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<string>("v2/reverse_names/ipv6", "reverse_nameservers", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ActionSubmission> UpdateIpv6Async(ReverseNameserversRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await SendResponseAsync(
|
||||
HttpMethod.Post,
|
||||
"v2/reverse_names/ipv6",
|
||||
request ?? throw new ArgumentNullException(nameof(request)),
|
||||
null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
TryDeserializeEnvelope<BinaryLaneAction>(response.Body, "action", out var action);
|
||||
return new ActionSubmission(response.StatusCode, action, response.Body);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class SampleSetsApi : BinaryLaneResourceBase, ISampleSetsApi
|
||||
{
|
||||
/// <summary>Initializes the monitoring sample-set resource.</summary>
|
||||
public SampleSetsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SampleSet?> GetLatestAsync(long serverId, string? dataInterval = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IReadOnlyDictionary<string, object?>? query = string.IsNullOrWhiteSpace(dataInterval)
|
||||
? null
|
||||
: new Dictionary<string, object?> { ["data_interval"] = dataInterval };
|
||||
var response = await GetRawAsync($"v2/samplesets/{serverId}/latest", query, cancellationToken).ConfigureAwait(false);
|
||||
return TryDeserializeEnvelope<SampleSet>(response, "sample_set", out var sampleSet) ? sampleSet : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<SampleSet>> ListAsync(
|
||||
long serverId,
|
||||
PageRequest? page = null,
|
||||
string? dataInterval = null,
|
||||
DateTimeOffset? start = null,
|
||||
DateTimeOffset? endAt = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new Dictionary<string, object?>
|
||||
{
|
||||
["data_interval"] = dataInterval,
|
||||
["start"] = start,
|
||||
["end"] = endAt,
|
||||
};
|
||||
return GetPageAsync<SampleSet>($"v2/samplesets/{serverId}", "sample_sets", page, query, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class ServersApi : BinaryLaneResourceBase, IServersApi
|
||||
{
|
||||
/// <summary>Initializes the servers resource.</summary>
|
||||
public ServersApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Server> GetAsync(long serverId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Server>($"v2/servers/{serverId}", "server", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Server>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Server>("v2/servers", "servers", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<Server> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<Server>("v2/servers", "servers", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Server> CreateAsync(CreateServerRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<Server>(HttpMethod.Post, "v2/servers", request ?? throw new ArgumentNullException(nameof(request)), "server", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteAsync(long serverId, string? reason = null, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync(
|
||||
$"v2/servers/{serverId}",
|
||||
null,
|
||||
string.IsNullOrWhiteSpace(reason) ? null : new Dictionary<string, object?> { ["reason"] = reason },
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<BinaryLaneAction>> ListActionsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<BinaryLaneAction>($"v2/servers/{serverId}/actions", "actions", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<BinaryLaneAction> GetActionAsync(long serverId, long actionId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<BinaryLaneAction>($"v2/servers/{serverId}/actions/{actionId}", "action", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ActionSubmission> SubmitActionAsync(long serverId, ServerAction request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
#else
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
#endif
|
||||
|
||||
var response = await SendResponseAsync(
|
||||
HttpMethod.Post,
|
||||
$"v2/servers/{serverId}/actions",
|
||||
request,
|
||||
null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
TryDeserializeEnvelope<BinaryLaneAction>(response.Body, "action", out var action);
|
||||
return new ActionSubmission(response.StatusCode, action, response.Body);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<AdvancedFirewallRule>> GetAdvancedFirewallRulesAsync(long serverId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<IReadOnlyList<AdvancedFirewallRule>>(
|
||||
$"v2/servers/{serverId}/advanced_firewall_rules",
|
||||
"firewall_rules",
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<AvailableAdvancedServerFeatures> GetAvailableAdvancedFeaturesAsync(long serverId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<AvailableAdvancedServerFeatures>(
|
||||
$"v2/servers/{serverId}/available_advanced_features",
|
||||
"available_advanced_server_features",
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Image>> ListBackupsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Image>($"v2/servers/{serverId}/backups", "backups", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<BinaryLaneAction> CreateBackupAsync(long serverId, UploadImageRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<BinaryLaneAction>(HttpMethod.Post, $"v2/servers/{serverId}/backups", request ?? throw new ArgumentNullException(nameof(request)), "action", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Kernel>> ListKernelsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Kernel>($"v2/servers/{serverId}/kernels", "kernels", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Image>> ListSnapshotsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Image>($"v2/servers/{serverId}/snapshots", "snapshots", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ThresholdAlert>> GetThresholdAlertsAsync(long serverId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<IReadOnlyList<ThresholdAlert>>($"v2/servers/{serverId}/threshold_alerts", "threshold_alerts", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<long>> GetThresholdAlertServerIdsAsync(CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<IReadOnlyList<long>>("v2/servers/threshold_alerts", "server_ids", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<LicensedSoftware>> ListSoftwareAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<LicensedSoftware>($"v2/servers/{serverId}/software", "licensed_software", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<UserData> GetUserDataAsync(long serverId, CancellationToken cancellationToken = default) =>
|
||||
GetDirectItemAsync<UserData>($"v2/servers/{serverId}/user_data", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ServerConsole> GetConsoleAsync(long serverId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<ServerConsole>($"v2/servers/{serverId}/console", "console", cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class SizesApi : BinaryLaneResourceBase, ISizesApi
|
||||
{
|
||||
/// <summary>Initializes the sizes resource.</summary>
|
||||
public SizesApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Size>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Size>("v2/sizes", "sizes", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<Size> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<Size>("v2/sizes", "sizes", page, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class SoftwareApi : BinaryLaneResourceBase, ISoftwareApi
|
||||
{
|
||||
/// <summary>Initializes the software resource.</summary>
|
||||
public SoftwareApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Software>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Software>("v2/software", "software", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<Software> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<Software>("v2/software", "software", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Software> GetAsync(long softwareId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Software>($"v2/software/{softwareId}", "software", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Software>> ListOperatingSystemAsync(string operatingSystemIdOrSlug, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Software>(
|
||||
$"v2/software/operating_system/{EscapePathSegment(operatingSystemIdOrSlug)}",
|
||||
"software",
|
||||
page,
|
||||
null,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class VpcsApi : BinaryLaneResourceBase, IVpcsApi
|
||||
{
|
||||
/// <summary>Initializes the VPC resource.</summary>
|
||||
public VpcsApi(IBinaryLaneApiExecutor executor, BinaryLaneJsonSerializerOptions json)
|
||||
: base(executor, json)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<Vpc>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<Vpc>("v2/vpcs", "vpcs", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<Vpc> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetAllPagesAsync<Vpc>("v2/vpcs", "vpcs", page, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Vpc> GetAsync(long vpcId, CancellationToken cancellationToken = default) =>
|
||||
GetItemAsync<Vpc>($"v2/vpcs/{vpcId}", "vpc", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Vpc> CreateAsync(CreateVpcRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<Vpc>(HttpMethod.Post, "v2/vpcs", request ?? throw new ArgumentNullException(nameof(request)), "vpc", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Vpc> ReplaceAsync(long vpcId, UpdateVpcRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<Vpc>(HttpMethod.Put, $"v2/vpcs/{vpcId}", request ?? throw new ArgumentNullException(nameof(request)), "vpc", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Vpc> UpdateAsync(long vpcId, PatchVpcRequest request, CancellationToken cancellationToken = default) =>
|
||||
SendItemAsync<Vpc>(new HttpMethod("PATCH"), $"v2/vpcs/{vpcId}", request ?? throw new ArgumentNullException(nameof(request)), "vpc", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteAsync(long vpcId, CancellationToken cancellationToken = default) =>
|
||||
Executor.DeleteAsync($"v2/vpcs/{vpcId}", null, null, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Page<VpcMember>> ListMembersAsync(long vpcId, PageRequest? page = null, CancellationToken cancellationToken = default) =>
|
||||
GetPageAsync<VpcMember>($"v2/vpcs/{vpcId}/members", "members", page, null, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
using BinaryLane.Api.V2.Pagination;
|
||||
|
||||
namespace BinaryLane.Api.V2.Resources;
|
||||
|
||||
/// <summary>Reads information about the authenticated account.</summary>
|
||||
public interface IAccountApi
|
||||
{
|
||||
/// <summary>Gets the current account.</summary>
|
||||
Task<Account> GetAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Gets the untyped account response for forward compatibility.</summary>
|
||||
Task<JsonElement> GetRawAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Reads and waits for asynchronous account and server actions.</summary>
|
||||
public interface IActionsApi
|
||||
{
|
||||
/// <summary>Gets an action by its global action ID.</summary>
|
||||
Task<BinaryLaneAction> GetAsync(long actionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Lists global actions.</summary>
|
||||
Task<Page<BinaryLaneAction>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Streams all global actions.</summary>
|
||||
IAsyncEnumerable<BinaryLaneAction> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Supplies a requested user interaction for an action.</summary>
|
||||
Task ProceedAsync(long actionId, ProceedRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Polls an action until BinaryLane reports a terminal state.</summary>
|
||||
Task<BinaryLaneAction> WaitForCompletionAsync(
|
||||
long actionId,
|
||||
ActionWaitOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Reads account billing and invoice information.</summary>
|
||||
public interface IBillingApi
|
||||
{
|
||||
Task<Balance> GetBalanceAsync(CancellationToken cancellationToken = default);
|
||||
Task<Invoice> GetInvoiceAsync(long invoiceId, CancellationToken cancellationToken = default);
|
||||
Task<Page<Invoice>> ListInvoicesAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Invoice>> ListUnpaidPaymentFailedInvoicesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Reads data-transfer usage data.</summary>
|
||||
public interface IDataUsageApi
|
||||
{
|
||||
Task<Page<DataUsage>> ListCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<DataUsage> ListAllCurrentAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<DataUsage> GetCurrentAsync(long serverId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Manages DNS domains and DNS records.</summary>
|
||||
public interface IDomainsApi
|
||||
{
|
||||
Task<IReadOnlyList<string>> GetNameserversAsync(CancellationToken cancellationToken = default);
|
||||
Task RefreshNameserverCacheAsync(DomainRefreshRequest request, CancellationToken cancellationToken = default);
|
||||
Task<Page<Domain>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Domain> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<Domain> CreateAsync(DomainRequest request, CancellationToken cancellationToken = default);
|
||||
Task<Domain> GetAsync(string domainName, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(string domainName, CancellationToken cancellationToken = default);
|
||||
Task<Page<DomainRecord>> ListRecordsAsync(string domainName, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<DomainRecord> CreateRecordAsync(string domainName, DomainRecordRequest request, CancellationToken cancellationToken = default);
|
||||
Task<DomainRecord> GetRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default);
|
||||
Task<DomainRecord> UpdateRecordAsync(string domainName, long recordId, UpdateDomainRecordRequest request, CancellationToken cancellationToken = default);
|
||||
Task DeleteRecordAsync(string domainName, long recordId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Reads and manages account images and backups represented as images.</summary>
|
||||
public interface IImagesApi
|
||||
{
|
||||
Task<Page<Image>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Image> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<Image> GetAsync(string imageIdOrSlug, CancellationToken cancellationToken = default);
|
||||
Task<Image> UpdateAsync(long imageId, ImageRequest request, CancellationToken cancellationToken = default);
|
||||
Task<ImageDownload> GetDownloadAsync(long imageId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Manages account SSH keys.</summary>
|
||||
public interface ISshKeysApi
|
||||
{
|
||||
Task<Page<SshKey>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<SshKey> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<SshKey> GetAsync(long keyId, CancellationToken cancellationToken = default);
|
||||
Task<SshKey> CreateAsync(SshKeyRequest request, CancellationToken cancellationToken = default);
|
||||
Task<SshKey> UpdateAsync(long keyId, UpdateSshKeyRequest request, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(long keyId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Manages load balancers, their members, and forwarding rules.</summary>
|
||||
public interface ILoadBalancersApi
|
||||
{
|
||||
Task<Page<LoadBalancer>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<LoadBalancer> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<LoadBalancer> GetAsync(long loadBalancerId, CancellationToken cancellationToken = default);
|
||||
Task<LoadBalancer> CreateAsync(CreateLoadBalancerRequest request, CancellationToken cancellationToken = default);
|
||||
Task<LoadBalancer> UpdateAsync(long loadBalancerId, UpdateLoadBalancerRequest request, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(long loadBalancerId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<LoadBalancerAvailabilityOption>> GetAvailabilityAsync(CancellationToken cancellationToken = default);
|
||||
Task AddServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default);
|
||||
Task RemoveServersAsync(long loadBalancerId, ServerIdsRequest request, CancellationToken cancellationToken = default);
|
||||
Task AddForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default);
|
||||
Task RemoveForwardingRulesAsync(long loadBalancerId, ForwardingRulesRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Lists BinaryLane regions.</summary>
|
||||
public interface IRegionsApi
|
||||
{
|
||||
Task<Page<Region>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Region> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Manages IPv6 reverse-name configuration.</summary>
|
||||
public interface IReverseNamesApi
|
||||
{
|
||||
Task<Page<string>> ListIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<string> ListAllIpv6Async(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<ActionSubmission> UpdateIpv6Async(ReverseNameserversRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Reads monitoring samples for servers.</summary>
|
||||
public interface ISampleSetsApi
|
||||
{
|
||||
Task<SampleSet?> GetLatestAsync(long serverId, string? dataInterval = null, CancellationToken cancellationToken = default);
|
||||
Task<Page<SampleSet>> ListAsync(long serverId, PageRequest? page = null, string? dataInterval = null, DateTimeOffset? start = null, DateTimeOffset? endAt = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Manages BinaryLane servers and their server-scoped resources.</summary>
|
||||
public interface IServersApi
|
||||
{
|
||||
Task<Server> GetAsync(long serverId, CancellationToken cancellationToken = default);
|
||||
Task<Page<Server>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Server> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<Server> CreateAsync(CreateServerRequest request, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(long serverId, string? reason = null, CancellationToken cancellationToken = default);
|
||||
Task<Page<BinaryLaneAction>> ListActionsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<BinaryLaneAction> GetActionAsync(long serverId, long actionId, CancellationToken cancellationToken = default);
|
||||
Task<ActionSubmission> SubmitActionAsync(long serverId, ServerAction request, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<AdvancedFirewallRule>> GetAdvancedFirewallRulesAsync(long serverId, CancellationToken cancellationToken = default);
|
||||
Task<AvailableAdvancedServerFeatures> GetAvailableAdvancedFeaturesAsync(long serverId, CancellationToken cancellationToken = default);
|
||||
Task<Page<Image>> ListBackupsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<BinaryLaneAction> CreateBackupAsync(long serverId, UploadImageRequest request, CancellationToken cancellationToken = default);
|
||||
Task<Page<Kernel>> ListKernelsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<Page<Image>> ListSnapshotsAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<ThresholdAlert>> GetThresholdAlertsAsync(long serverId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<long>> GetThresholdAlertServerIdsAsync(CancellationToken cancellationToken = default);
|
||||
Task<Page<LicensedSoftware>> ListSoftwareAsync(long serverId, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<UserData> GetUserDataAsync(long serverId, CancellationToken cancellationToken = default);
|
||||
Task<ServerConsole> GetConsoleAsync(long serverId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Lists available server sizes.</summary>
|
||||
public interface ISizesApi
|
||||
{
|
||||
Task<Page<Size>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Size> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Lists available licensed software and operating-system software.</summary>
|
||||
public interface ISoftwareApi
|
||||
{
|
||||
Task<Page<Software>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Software> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<Software> GetAsync(long softwareId, CancellationToken cancellationToken = default);
|
||||
Task<Page<Software>> ListOperatingSystemAsync(string operatingSystemIdOrSlug, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>Manages private virtual networks and their members.</summary>
|
||||
public interface IVpcsApi
|
||||
{
|
||||
Task<Page<Vpc>> ListAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
IAsyncEnumerable<Vpc> ListAllAsync(PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
Task<Vpc> GetAsync(long vpcId, CancellationToken cancellationToken = default);
|
||||
Task<Vpc> CreateAsync(CreateVpcRequest request, CancellationToken cancellationToken = default);
|
||||
Task<Vpc> ReplaceAsync(long vpcId, UpdateVpcRequest request, CancellationToken cancellationToken = default);
|
||||
Task<Vpc> UpdateAsync(long vpcId, PatchVpcRequest request, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(long vpcId, CancellationToken cancellationToken = default);
|
||||
Task<Page<VpcMember>> ListMembersAsync(long vpcId, PageRequest? page = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,348 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
|
||||
"dependencies": {
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Http": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0",
|
||||
"System.ComponentModel.Annotations": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"System.Text.Json": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.5, )",
|
||||
"resolved": "8.0.5",
|
||||
"contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
|
||||
"System.Text.Encodings.Web": "8.0.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2",
|
||||
"System.Diagnostics.DiagnosticSource": "8.0.1"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Diagnostics.DiagnosticSource": "8.0.1",
|
||||
"System.Memory": "4.5.5"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.1",
|
||||
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
|
||||
},
|
||||
"System.ComponentModel.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
|
||||
},
|
||||
"System.Diagnostics.DiagnosticSource": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "vaoWjvkG1aenR2XdjaVivlCV9fADfgyhW5bZtXT23qaEea0lWiUljdQuze4E31vKM7ZWJaSUsbYIKE3rnzfZUg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.5",
|
||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Numerics.Vectors": "4.4.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Text.Encodings.Web": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Bcl.AsyncInterfaces": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net8.0": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
|
||||
},
|
||||
"Microsoft.Extensions.Http": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Diagnostics": "8.0.1",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Tasks.Git": "8.0.0",
|
||||
"Microsoft.SourceLink.Common": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Build.Tasks.Git": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "doVPCUUCY7c6LhBsEfiy3W1bvS7Mi6LkfQMS8nlC22jZWNxBv8VO8bdfeyvpYFst6Kxqk7HBC6lytmEoBssvSQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
|
||||
},
|
||||
"Microsoft.SourceLink.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
using Xunit;
|
||||
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/BinaryLane.Api/BinaryLane.Api.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,375 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
using BinaryLane.Api.V2.Errors;
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using Xunit;
|
||||
|
||||
namespace BinaryLane.Api.Tests;
|
||||
|
||||
public sealed class BinaryLaneHttpExecutorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetAsyncAddsBearerHeaderAndSerializesQueryValues()
|
||||
{
|
||||
HttpRequestMessage? received = null;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
|
||||
{
|
||||
received = request;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{\"value\":\"ok\"}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("test-token"));
|
||||
|
||||
var response = await executor.GetAsync<JsonElement>(
|
||||
"v2/servers",
|
||||
new System.Collections.Generic.Dictionary<string, object?>
|
||||
{
|
||||
["page"] = 2,
|
||||
["include_archived"] = false,
|
||||
});
|
||||
|
||||
Assert.Equal("ok", response.GetProperty("value").GetString());
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal("Bearer", received!.Headers.Authorization!.Scheme);
|
||||
Assert.Equal("test-token", received.Headers.Authorization.Parameter);
|
||||
Assert.Equal("?page=2&include_archived=false", received.RequestUri!.Query);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsyncMapsUnauthorizedResponseWithoutLeakingBearerToken()
|
||||
{
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
Task.FromResult(JsonResponse(HttpStatusCode.Unauthorized, "{\"title\":\"Unauthorized\",\"detail\":\"Invalid token\"}"))))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("super-secret-token"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BinaryLaneUnauthorizedException>(
|
||||
() => executor.GetAsync<JsonElement>("v2/account"));
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, exception.StatusCode);
|
||||
Assert.Equal("Invalid token", exception.Problem!.Detail);
|
||||
Assert.DoesNotContain("Invalid token", exception.Message, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("super-secret-token", exception.ToString(), StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("super-secret-token", exception.ResponseBody ?? string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsyncPropagatesCancellationToTheHttpHandler()
|
||||
{
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler(async (_, token) =>
|
||||
{
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, token);
|
||||
throw new InvalidOperationException("Unreachable.");
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
cancellation.Cancel();
|
||||
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(
|
||||
() => executor.GetAsync<JsonElement>("v2/account", cancellationToken: cancellation.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TokenProviderIsEvaluatedForEveryRequestSoTokensCanRotate()
|
||||
{
|
||||
var authorizationTokens = new List<string?>();
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
|
||||
{
|
||||
authorizationTokens.Add(request.Headers.Authorization?.Parameter);
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(
|
||||
httpClient,
|
||||
new RotatingTokenProvider("first-token", "second-token"));
|
||||
|
||||
await executor.GetAsync<JsonElement>("v2/account");
|
||||
await executor.GetAsync<JsonElement>("v2/account");
|
||||
|
||||
Assert.Collection(
|
||||
authorizationTokens,
|
||||
token => Assert.Equal("first-token", token),
|
||||
token => Assert.Equal("second-token", token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryFormattingOmitsNullsAndUsesInvariantWireValues()
|
||||
{
|
||||
HttpRequestMessage? received = null;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
|
||||
{
|
||||
received = request;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
var timestamp = new DateTimeOffset(2026, 8, 16, 12, 34, 56, TimeSpan.Zero);
|
||||
|
||||
await executor.GetAsync<JsonElement>(
|
||||
"v2/actions?existing=true",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["after"] = timestamp,
|
||||
["amount"] = 10.5m,
|
||||
["omit_me"] = null,
|
||||
});
|
||||
|
||||
Assert.NotNull(received);
|
||||
var query = received!.RequestUri!.Query;
|
||||
Assert.Contains("existing=true", query, StringComparison.Ordinal);
|
||||
Assert.Contains("after=2026-08-16T12%3A34%3A56.0000000%2B00%3A00", query, StringComparison.Ordinal);
|
||||
Assert.Contains("amount=10.5", query, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("omit_me", query, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationErrorsMapToValidationExceptionAndPreserveProblemDetails()
|
||||
{
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
Task.FromResult(JsonResponse(
|
||||
(HttpStatusCode)422,
|
||||
"{\"title\":\"Validation failed\",\"detail\":\"region is required\",\"status\":422}"))))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BinaryLaneValidationException>(
|
||||
() => executor.PostAsync<JsonElement>("v2/servers", new { }));
|
||||
|
||||
Assert.Equal((HttpStatusCode)422, exception.StatusCode);
|
||||
Assert.Equal("Validation failed", exception.Problem!.Title);
|
||||
Assert.Equal("region is required", exception.Problem.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MutationsAreNotRetriedImplicitly()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.InternalServerError, "{\"title\":\"Failure\"}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
await Assert.ThrowsAsync<BinaryLaneApiException>(
|
||||
() => executor.PostAsync<JsonElement>("v2/servers", new { name = "must-not-retry" }));
|
||||
|
||||
Assert.Equal(1, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmptyTokenFailsBeforeTheRequestIsDispatched()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new RotatingTokenProvider(string.Empty));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => executor.GetAsync<JsonElement>("v2/account"));
|
||||
|
||||
Assert.Equal(0, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AbsoluteUrlsOutsideTheConfiguredOriginDoNotReceiveTheBearerToken()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => executor.GetAsync<JsonElement>("https://other.example.test/v2/servers"));
|
||||
|
||||
Assert.Equal(0, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NonHttpAbsoluteUrlsAreRejectedBeforeTheRequestIsDispatched()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => executor.GetAsync<JsonElement>("ftp://other.example.test/v2/servers"));
|
||||
|
||||
Assert.Equal(0, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallerCannotOverrideTheTokenProviderAuthorizationHeader()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
var request = new BinaryLaneRequest(HttpMethod.Get, "v2/account")
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["Authorization"] = "Bearer caller-token",
|
||||
},
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => executor.SendAsync<JsonElement>(request));
|
||||
|
||||
Assert.Equal(0, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CallerCannotOverrideTheRequestAuthorityWithAHostHeader()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
var request = new BinaryLaneRequest(HttpMethod.Get, "v2/account")
|
||||
{
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["Host"] = "other.example.test",
|
||||
},
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => executor.SendAsync<JsonElement>(request));
|
||||
|
||||
Assert.Equal(0, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlaintextBaseAddressesAreRejectedBeforeTheRequestIsDispatched()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
return Task.FromResult(JsonResponse(HttpStatusCode.OK, "{}"));
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("http://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => executor.GetAsync<JsonElement>("v2/account"));
|
||||
|
||||
Assert.Equal(0, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SuccessResponseBodiesLargerThanTheLimitAreRejectedBeforeBuffering()
|
||||
{
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
var content = new ByteArrayContent(Array.Empty<byte>());
|
||||
content.Headers.ContentLength = (16L * 1024 * 1024) + 1;
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = content });
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<HttpRequestException>(
|
||||
() => executor.GetAsync<JsonElement>("v2/account"));
|
||||
|
||||
Assert.Contains("exceeded", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ErrorResponseBodyIsBoundedBeforeItIsStoredOnTheException()
|
||||
{
|
||||
var responseBody = "{\"detail\":\"" + new string('x', 40_000) + "\"}";
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
Task.FromResult(JsonResponse(HttpStatusCode.InternalServerError, responseBody))))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var executor = new BinaryLaneHttpExecutor(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BinaryLaneApiException>(
|
||||
() => executor.GetAsync<JsonElement>("v2/account"));
|
||||
|
||||
Assert.NotNull(exception.ResponseBody);
|
||||
Assert.Equal(32_769, exception.ResponseBody!.Length);
|
||||
Assert.EndsWith("…", exception.ResponseBody, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static HttpResponseMessage JsonResponse(HttpStatusCode statusCode, string json) =>
|
||||
new(statusCode)
|
||||
{
|
||||
Content = new StringContent(json),
|
||||
};
|
||||
|
||||
private sealed class RotatingTokenProvider : IBinaryLaneTokenProvider
|
||||
{
|
||||
private readonly Queue<string> _tokens;
|
||||
|
||||
public RotatingTokenProvider(params string[] tokens)
|
||||
{
|
||||
_tokens = new Queue<string>(tokens);
|
||||
}
|
||||
|
||||
public ValueTask<string> GetTokenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new ValueTask<string>(_tokens.Dequeue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using BinaryLane.Api.V2;
|
||||
using BinaryLane.Api.V2.Authentication;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace BinaryLane.Api.Tests;
|
||||
|
||||
public sealed class ClientResourceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ServersListAllFollowsTheProviderNextPageLink()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
var json = request.RequestUri!.Query.Contains("page=2", StringComparison.Ordinal)
|
||||
? "{\"meta\":{\"total\":2},\"links\":{\"pages\":{}},\"servers\":[{\"id\":2,\"name\":\"two\",\"status\":\"active\"}]}"
|
||||
: "{\"meta\":{\"total\":2},\"links\":{\"pages\":{\"next\":\"/v2/servers?page=2\"}},\"servers\":[{\"id\":1,\"name\":\"one\",\"status\":\"active\"}]}";
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
var servers = new List<Server>();
|
||||
|
||||
await foreach (var server in client.Servers.ListAllAsync())
|
||||
{
|
||||
servers.Add(server);
|
||||
}
|
||||
|
||||
Assert.Equal(2, requestCount);
|
||||
Assert.Collection(
|
||||
servers,
|
||||
server => Assert.Equal("one", server.Name),
|
||||
server => Assert.Equal("two", server.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServersListAllStopsWhenTheProviderRepeatsANextPageLink()
|
||||
{
|
||||
var requestCount = 0;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
{
|
||||
requestCount++;
|
||||
const string json = "{\"meta\":{\"total\":1},\"links\":{\"pages\":{\"next\":\"/v2/servers?page=1\"}},\"servers\":[{\"id\":1,\"name\":\"one\",\"status\":\"active\"}]}";
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await foreach (var _ in client.Servers.ListAllAsync())
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Contains("repeated pagination link", exception.Message, StringComparison.Ordinal);
|
||||
Assert.Equal(2, requestCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitActionUsesRealActionsEndpointAndReportsAcceptedStatus()
|
||||
{
|
||||
HttpRequestMessage? received = null;
|
||||
string? body = null;
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler(async (request, cancellationToken) =>
|
||||
{
|
||||
received = request;
|
||||
body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
return new HttpResponseMessage(HttpStatusCode.Accepted)
|
||||
{
|
||||
Content = new StringContent("{\"action\":{\"id\":9,\"type\":\"power_on\",\"status\":\"in-progress\",\"started_at\":\"2026-01-01T00:00:00Z\"}}"),
|
||||
};
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var submission = await client.Servers.SubmitActionAsync(123, new PowerOnServerAction());
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal("/v2/servers/123/actions", received!.RequestUri!.AbsolutePath);
|
||||
Assert.Equal("POST", received.Method.Method);
|
||||
Assert.Contains("\"type\":\"power_on\"", body!, StringComparison.Ordinal);
|
||||
Assert.True(submission.IsAccepted);
|
||||
Assert.Equal(9, submission.Action!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListEndpointsRejectAResponseWithoutTheExpectedCollectionEnvelope()
|
||||
{
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("{\"meta\":{\"total\":0},\"wrong_collection\":[]}"),
|
||||
})))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<JsonException>(() => client.Servers.ListAsync());
|
||||
|
||||
Assert.Contains("servers", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UserDataUsesTheDocumentedDirectResponseBody()
|
||||
{
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((request, _) =>
|
||||
{
|
||||
Assert.Equal("/v2/servers/123/user_data", request.RequestUri!.AbsolutePath);
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("{\"user_data\":\"#cloud-config\"}"),
|
||||
});
|
||||
}))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var userData = await client.Servers.GetUserDataAsync(123);
|
||||
|
||||
Assert.Equal("#cloud-config", userData.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListEndpointsRejectNullItemsRatherThanSilentlyDroppingThem()
|
||||
{
|
||||
using var httpClient = new HttpClient(new HttpClientTestHandler((_, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("{\"meta\":{\"total\":1},\"servers\":[null]}"),
|
||||
})))
|
||||
{
|
||||
BaseAddress = new Uri("https://api.example.test/"),
|
||||
};
|
||||
var client = new BinaryLaneClient(httpClient, new StaticBinaryLaneTokenProvider("token"));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<JsonException>(() => client.Servers.ListAsync());
|
||||
|
||||
Assert.Contains("servers", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using BinaryLane.Api.V2;
|
||||
using BinaryLane.Api.V2.Configuration;
|
||||
using BinaryLane.Api.V2.DependencyInjection;
|
||||
using BinaryLane.Api.V2.Resources;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace BinaryLane.Api.Tests;
|
||||
|
||||
public sealed class DependencyInjectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void AddBinaryLaneApiRegistersAComposableClient()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddBinaryLaneApi(options => options.ApiToken = "test-token");
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
var client = provider.GetRequiredService<IBinaryLaneClient>();
|
||||
|
||||
Assert.NotNull(client.Account);
|
||||
Assert.NotNull(client.Actions);
|
||||
Assert.NotNull(client.Servers);
|
||||
Assert.NotNull(client.Regions);
|
||||
Assert.NotNull(client.Vpcs);
|
||||
Assert.NotNull(provider.GetRequiredService<IServersApi>());
|
||||
Assert.NotNull(provider.GetRequiredService<IDomainsApi>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidOptionsAreRejectedWhenResolved()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddBinaryLaneApi(options =>
|
||||
{
|
||||
options.BaseUrl = "not-a-url";
|
||||
options.ApiToken = "test-token";
|
||||
});
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
Assert.Throws<OptionsValidationException>(
|
||||
() => provider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(301)]
|
||||
public void OutOfRangeTimeoutIsRejectedWhenResolved(int timeoutSeconds)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddBinaryLaneApi(options =>
|
||||
{
|
||||
options.ApiToken = "test-token";
|
||||
options.RequestTimeoutSeconds = timeoutSeconds;
|
||||
});
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
Assert.Throws<OptionsValidationException>(
|
||||
() => provider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://api.example.test/")]
|
||||
[InlineData("https://user:password@api.example.test/")]
|
||||
[InlineData("https://api.example.test/?unexpected=value")]
|
||||
[InlineData("https://api.example.test/#fragment")]
|
||||
public void BaseUrlWithCredentialsQueryOrFragmentIsRejected(string baseUrl)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddBinaryLaneApi(options =>
|
||||
{
|
||||
options.BaseUrl = baseUrl;
|
||||
options.ApiToken = "test-token";
|
||||
});
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
Assert.Throws<OptionsValidationException>(
|
||||
() => provider.GetRequiredService<IOptions<BinaryLaneOptions>>().Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BinaryLane.Api.Tests;
|
||||
|
||||
internal sealed class HttpClientTestHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _send;
|
||||
|
||||
public HttpClientTestHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> send) =>
|
||||
_send = send ?? throw new ArgumentNullException(nameof(send));
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) =>
|
||||
_send(request, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Text.Json;
|
||||
using BinaryLane.Api.V2.Http;
|
||||
using BinaryLane.Api.V2.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace BinaryLane.Api.Tests;
|
||||
|
||||
public sealed class ModelSerializationTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json =
|
||||
new BinaryLaneJsonSerializerOptions().SerializerOptions;
|
||||
|
||||
[Fact]
|
||||
public void PowerOnActionSerializesTheProviderDiscriminator()
|
||||
{
|
||||
string json = JsonSerializer.Serialize<ServerAction>(new PowerOnServerAction(), Json);
|
||||
|
||||
using JsonDocument document = JsonDocument.Parse(json);
|
||||
Assert.Equal("power_on", document.RootElement.GetProperty("type").GetString());
|
||||
Assert.Single(document.RootElement.EnumerateObject());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateServerRequestSerializesBinaryLaneWireNamesAndOmitsNulls()
|
||||
{
|
||||
CreateServerRequest request = new()
|
||||
{
|
||||
Name = "demo-server",
|
||||
Image = "ubuntu-24.04",
|
||||
Region = "melbourne",
|
||||
Size = "std-min",
|
||||
PortBlocking = false,
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize(request, Json);
|
||||
using JsonDocument document = JsonDocument.Parse(json);
|
||||
|
||||
Assert.Equal("demo-server", document.RootElement.GetProperty("name").GetString());
|
||||
Assert.Equal("ubuntu-24.04", document.RootElement.GetProperty("image").GetString());
|
||||
Assert.Equal("melbourne", document.RootElement.GetProperty("region").GetString());
|
||||
Assert.Equal("std-min", document.RootElement.GetProperty("size").GetString());
|
||||
Assert.False(document.RootElement.GetProperty("port_blocking").GetBoolean());
|
||||
Assert.False(document.RootElement.TryGetProperty("password", out _));
|
||||
Assert.False(document.RootElement.TryGetProperty("user_data", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownResponsePropertiesAreRetainedForForwardCompatibility()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"email": "alex@example.test",
|
||||
"email_verified": true,
|
||||
"two_factor_authentication_enabled": false,
|
||||
"status": "active",
|
||||
"tax_code": { "name": "GST", "type": "scalar" },
|
||||
"configured_payment_methods": ["credit-card"],
|
||||
"additional_ipv4_limit": 2,
|
||||
"future_provider_field": "kept"
|
||||
}
|
||||
""";
|
||||
|
||||
Account? account = JsonSerializer.Deserialize<Account>(json, Json);
|
||||
|
||||
Assert.NotNull(account);
|
||||
Assert.Equal("active", account.Status);
|
||||
Assert.NotNull(account.AdditionalProperties);
|
||||
Assert.Equal(
|
||||
"kept",
|
||||
account.AdditionalProperties!["future_provider_field"].GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreviewEnumValuesRemainRawStrings()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"id": 42,
|
||||
"status": "future-status",
|
||||
"type": "future-action",
|
||||
"started_at": "2026-08-16T12:34:56Z",
|
||||
"title": "Future action",
|
||||
"reason": "A future provider action",
|
||||
"progress": { "percent_complete": 0, "completed_steps": [] }
|
||||
}
|
||||
""";
|
||||
|
||||
BinaryLaneAction? action = JsonSerializer.Deserialize<BinaryLaneAction>(json, Json);
|
||||
|
||||
Assert.NotNull(action);
|
||||
Assert.Equal("future-status", action.Status);
|
||||
Assert.Equal("future-action", action.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DocumentedValuesAreAvailableWithoutClosedEnums()
|
||||
{
|
||||
Assert.Equal("completed", BinaryLaneValues.ActionStatus.Completed);
|
||||
Assert.Equal("power_on", BinaryLaneValues.ServerActionType.PowerOn);
|
||||
Assert.Equal("AAAA", BinaryLaneValues.DomainRecordType.Aaaa);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComplexServerActionUsesTheSharedEndpointDiscriminatorAndWireNames()
|
||||
{
|
||||
ServerAction action = new ChangeVpcIpv4ServerAction
|
||||
{
|
||||
CurrentIpv4Address = "10.0.0.10",
|
||||
NewIpv4Address = "10.0.0.20",
|
||||
};
|
||||
|
||||
string json = JsonSerializer.Serialize(action, Json);
|
||||
using JsonDocument document = JsonDocument.Parse(json);
|
||||
|
||||
Assert.Equal("change_vpc_ipv4", document.RootElement.GetProperty("type").GetString());
|
||||
Assert.Equal("10.0.0.10", document.RootElement.GetProperty("current_ipv4_address").GetString());
|
||||
Assert.Equal("10.0.0.20", document.RootElement.GetProperty("new_ipv4_address").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerActionConverterDeserializesKnownAndUnknownProviderActions()
|
||||
{
|
||||
const string knownJson = """
|
||||
{
|
||||
"type": "change_vpc_ipv4",
|
||||
"current_ipv4_address": "10.0.0.10",
|
||||
"new_ipv4_address": "10.0.0.20"
|
||||
}
|
||||
""";
|
||||
const string unknownJson = """
|
||||
{
|
||||
"type": "future_action",
|
||||
"provider_option": "kept"
|
||||
}
|
||||
""";
|
||||
|
||||
ServerAction? known = JsonSerializer.Deserialize<ServerAction>(knownJson, Json);
|
||||
ServerAction? unknown = JsonSerializer.Deserialize<ServerAction>(unknownJson, Json);
|
||||
|
||||
ChangeVpcIpv4ServerAction typed = Assert.IsType<ChangeVpcIpv4ServerAction>(known);
|
||||
Assert.Equal("10.0.0.10", typed.CurrentIpv4Address);
|
||||
Assert.Equal("10.0.0.20", typed.NewIpv4Address);
|
||||
|
||||
UnknownServerAction future = Assert.IsType<UnknownServerAction>(unknown);
|
||||
Assert.Equal("future_action", future.Type);
|
||||
Assert.NotNull(future.AdditionalProperties);
|
||||
Assert.Equal("kept", future.AdditionalProperties!["provider_option"].GetString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"coverlet.collector": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.0.2, )",
|
||||
"resolved": "6.0.2",
|
||||
"contentHash": "bJShQ6uWRTQ100ZeyiMqcFlhP7WJ+bCuabUs885dJiBEzMsJMSFr7BOyeCw4rgvQokteGi5rKQTlkhfQPUXg2A=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.Test.Sdk": {
|
||||
"type": "Direct",
|
||||
"requested": "[17.11.1, )",
|
||||
"resolved": "17.11.1",
|
||||
"contentHash": "U3Ty4BaGoEu+T2bwSko9tWqWUOU16WzSFkq6U8zve75oRBMSLTBdMAZrVNNz1Tq12aCdDom9fcOcM9QZaFHqFg==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeCoverage": "17.11.1",
|
||||
"Microsoft.TestPlatform.TestHost": "17.11.1"
|
||||
}
|
||||
},
|
||||
"xunit": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.9.2, )",
|
||||
"resolved": "2.9.2",
|
||||
"contentHash": "7LhFS2N9Z6Xgg8aE5lY95cneYivRMfRI8v+4PATa4S64D5Z/Plkg0qa8dTRHSiGRgVZ/CL2gEfJDE5AUhOX+2Q==",
|
||||
"dependencies": {
|
||||
"xunit.analyzers": "1.16.0",
|
||||
"xunit.assert": "2.9.2",
|
||||
"xunit.core": "[2.9.2]"
|
||||
}
|
||||
},
|
||||
"xunit.runner.visualstudio": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.8.2, )",
|
||||
"resolved": "2.8.2",
|
||||
"contentHash": "vm1tbfXhFmjFMUmS4M0J0ASXz3/U5XvXBa6DOQUL3fEz4Vt6YPhv+ESCarx6M6D+9kJkJYZKCNvJMas1+nVfmQ=="
|
||||
},
|
||||
"Microsoft.CodeCoverage": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.11.1",
|
||||
"contentHash": "nPJqrcA5iX+Y0kqoT3a+pD/8lrW/V7ayqnEJQsTonSoPz59J8bmoQhcSN4G8+UJ64Hkuf0zuxnfuj2lkHOq4cA=="
|
||||
},
|
||||
"Microsoft.Extensions.Configuration": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "doVPCUUCY7c6LhBsEfiy3W1bvS7Mi6LkfQMS8nlC22jZWNxBv8VO8bdfeyvpYFst6Kxqk7HBC6lytmEoBssvSQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration": "8.0.0",
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1",
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Diagnostics.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.11.1",
|
||||
"contentHash": "E2jZqAU6JeWEVsyOEOrSW1o1bpHLgb25ypvKNB/moBXPVsFYBPd/Jwi7OrYahG50J83LfHzezYI+GaEkpAotiA==",
|
||||
"dependencies": {
|
||||
"System.Reflection.Metadata": "1.6.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.TestPlatform.TestHost": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.11.1",
|
||||
"contentHash": "DnG+GOqJXO/CkoqlJWeDFTgPhqD/V6VqUIL3vINizCWZ3X+HshCtbbyDdSHQQEjrc2Sl/K3yaxX6s+5LFEdYuw==",
|
||||
"dependencies": {
|
||||
"Microsoft.TestPlatform.ObjectModel": "17.11.1",
|
||||
"Newtonsoft.Json": "13.0.1"
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.1",
|
||||
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
|
||||
},
|
||||
"System.Reflection.Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.6.0",
|
||||
"contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ=="
|
||||
},
|
||||
"xunit.abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
|
||||
},
|
||||
"xunit.analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.16.0",
|
||||
"contentHash": "hptYM7vGr46GUIgZt21YHO4rfuBAQS2eINbFo16CV/Dqq+24Tp+P5gDCACu1AbFfW4Sp/WRfDPSK8fmUUb8s0Q=="
|
||||
},
|
||||
"xunit.assert": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.2",
|
||||
"contentHash": "QkNBAQG4pa66cholm28AxijBjrmki98/vsEh4Sx5iplzotvPgpiotcxqJQMRC8d7RV7nIT8ozh97957hDnZwsQ=="
|
||||
},
|
||||
"xunit.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.2",
|
||||
"contentHash": "O6RrNSdmZ0xgEn5kT927PNwog5vxTtKrWMihhhrT0Sg9jQ7iBDciYOwzBgP2krBEk5/GBXI18R1lKvmnxGcb4w==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.2]",
|
||||
"xunit.extensibility.execution": "[2.9.2]"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.2",
|
||||
"contentHash": "Ol+KlBJz1x8BrdnhN2DeOuLrr1I/cTwtHCggL9BvYqFuVd/TUSzxNT5O0NxCIXth30bsKxgMfdqLTcORtM52yQ==",
|
||||
"dependencies": {
|
||||
"xunit.abstractions": "2.0.3"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.execution": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.9.2",
|
||||
"contentHash": "rKMpq4GsIUIJibXuZoZ8lYp5EpROlnYaRpwu9Zr0sRZXE7JqJfEEbCsUriZqB+ByXCLFBJyjkTRULMdC+U566g==",
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "[2.9.2]"
|
||||
}
|
||||
},
|
||||
"binarylane.api": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.2, )",
|
||||
"Microsoft.Extensions.Http": "[8.0.1, )",
|
||||
"Microsoft.Extensions.Options": "[8.0.2, )"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Binder": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
|
||||
},
|
||||
"Microsoft.Extensions.Http": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.1, )",
|
||||
"resolved": "8.0.1",
|
||||
"contentHash": "kDYeKJUzh0qeg/AI+nSr3ffthmXYQTEb0nS9qRC7YhSbbuN4M4NPbaB77AJwtkTnCV9XZ7qYj3dkZaNcyl73EA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Diagnostics": "8.0.1",
|
||||
"Microsoft.Extensions.Logging": "8.0.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
|
||||
"Microsoft.Extensions.Options": "8.0.2"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.2, )",
|
||||
"resolved": "8.0.2",
|
||||
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options.ConfigurationExtensions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||
"Microsoft.Extensions.Options": "8.0.0",
|
||||
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user