feat(sdk): route volume content to BYOC cluster domain (#1634)
When a team is connected to a custom (BYOC) cluster, the volume API now returns that cluster's domain in the create and get responses. The JS and Python (sync + async) SDKs use this domain as the destination for volume content requests instead of the default api.<E2B_DOMAIN> host, falling back to the configured domain when none is returned. The domain field is read defensively from the response until spec/infra-ref is bumped to the infra commit that adds it and `make codegen` regenerates the typed schema. Claude-Session: https://claude.ai/code/session_01212WCmNz1prPKrjhTv2PDj --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Matt Brockman <matt.brockman@e2b.dev>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'e2b': minor
|
||||
'@e2b/python-sdk': minor
|
||||
---
|
||||
|
||||
Route volume content requests to a team's custom (BYOC) cluster. When a team is connected to a custom cluster, the volume create and get endpoints now return that cluster's `domain`, and the SDK uses it as the destination for volume content requests instead of the default `api.<E2B_DOMAIN>` host. Teams on the default cluster are unaffected and keep their configured domain.
|
||||
Generated
+18
-3
@@ -716,6 +716,7 @@ export interface paths {
|
||||
/** @description Successfully returned snapshots */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Next-Token": components["headers"]["XNextToken"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
@@ -980,6 +981,7 @@ export interface paths {
|
||||
/** @description Successfully returned the template with its builds */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Next-Token": components["headers"]["XNextToken"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
@@ -1478,6 +1480,8 @@ export interface paths {
|
||||
/** @description Successfully returned all running sandboxes */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Next-Token": components["headers"]["XNextToken"];
|
||||
"X-Total-Running": components["headers"]["XTotalRunning"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
@@ -1581,8 +1585,7 @@ export interface paths {
|
||||
/** @description Successfully returned all templates */
|
||||
200: {
|
||||
headers: {
|
||||
/** @description Cursor to fetch the next page of results, if more exist */
|
||||
"X-Next-Token"?: string;
|
||||
"X-Next-Token": components["headers"]["XNextToken"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
@@ -3068,6 +3071,12 @@ export interface components {
|
||||
volumeID: string;
|
||||
};
|
||||
VolumeAndToken: {
|
||||
/** @description Domain to use as the destination for volume content requests,
|
||||
* replacing the default `api.<E2B_DOMAIN>`. Only returned when the
|
||||
* team is connected to a custom (BYOC) cluster; absent otherwise, in
|
||||
* which case the default domain is used.
|
||||
* */
|
||||
domain?: string;
|
||||
/** @description Name of the volume */
|
||||
name: string;
|
||||
/** @description Auth token to use for interacting with volume content */
|
||||
@@ -3161,7 +3170,13 @@ export interface components {
|
||||
volumeID: string;
|
||||
};
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
headers: {
|
||||
/** @description Cursor to fetch the next page of results, if more exist */
|
||||
XNextToken: string;
|
||||
/** @description Number of running sandboxes matching the filters, before pagination is applied. Only present when running sandboxes were requested.
|
||||
* */
|
||||
XTotalRunning: number;
|
||||
};
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
|
||||
@@ -135,7 +135,7 @@ export class Volume {
|
||||
res.data.volumeID,
|
||||
res.data.name,
|
||||
res.data.token,
|
||||
config.domain,
|
||||
res.data.domain || config.domain,
|
||||
config.debug,
|
||||
config.proxy
|
||||
)
|
||||
@@ -154,12 +154,12 @@ export class Volume {
|
||||
opts?: ConnectionOpts
|
||||
): Promise<Volume> {
|
||||
const config = new ConnectionConfig(opts)
|
||||
const { name, token } = await Volume.getInfo(volumeId, opts)
|
||||
const { name, token, domain } = await Volume.getInfo(volumeId, opts)
|
||||
return new Volume(
|
||||
volumeId,
|
||||
name,
|
||||
token,
|
||||
config.domain,
|
||||
domain ?? config.domain,
|
||||
config.debug,
|
||||
config.proxy
|
||||
)
|
||||
@@ -202,6 +202,7 @@ export class Volume {
|
||||
volumeId: res.data!.volumeID,
|
||||
name: res.data!.name,
|
||||
token: res.data!.token,
|
||||
domain: res.data!.domain || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,13 @@ export type VolumeAndToken = VolumeInfo & {
|
||||
* Volume auth token.
|
||||
*/
|
||||
token: string
|
||||
|
||||
/**
|
||||
* Domain to use as the destination for volume content requests, replacing
|
||||
* the default `api.<domain>` host. Only set for teams connected to a custom
|
||||
* (BYOC) cluster; `undefined` otherwise.
|
||||
*/
|
||||
domain?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -209,6 +209,68 @@ describe('Volume CRUD', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Volume BYOC domain', () => {
|
||||
const byocDomain = 'cluster.example.com'
|
||||
const defaultDomain = process.env.E2B_DOMAIN || 'e2b.app'
|
||||
|
||||
it('uses the domain returned by create for content requests', async () => {
|
||||
server.use(
|
||||
http.post(apiUrl('/volumes'), async ({ request }) => {
|
||||
const { name } = (await request.clone().json()) as { name: string }
|
||||
const volumeID = randomUUID()
|
||||
const token = `vol-token-${randomUUID()}`
|
||||
return HttpResponse.json(
|
||||
{ volumeID, name, token, domain: byocDomain },
|
||||
{ status: 201 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const vol = await Volume.create('byoc-volume')
|
||||
|
||||
// The BYOC domain is stored on the instance and drives the content API
|
||||
// destination (https://api.<domain>), replacing the default domain.
|
||||
expect(vol.domain).toBe(byocDomain)
|
||||
const config = new VolumeConnectionConfig(vol)
|
||||
expect(config.domain).toBe(byocDomain)
|
||||
expect(config.apiUrl).toBe(`https://api.${byocDomain}`)
|
||||
})
|
||||
|
||||
it('falls back to the default domain when create returns no domain', async () => {
|
||||
const vol = await Volume.create('default-volume')
|
||||
|
||||
expect(vol.domain).toBe(defaultDomain)
|
||||
expect(new VolumeConnectionConfig(vol).domain).toBe(defaultDomain)
|
||||
})
|
||||
|
||||
it('propagates the domain through getInfo and connect', async () => {
|
||||
const created = await Volume.create('connect-volume')
|
||||
|
||||
server.use(
|
||||
http.get<{ volumeID: string }>(
|
||||
apiUrl('/volumes/:volumeID'),
|
||||
({ params }) => {
|
||||
const vol = volumes.get(params.volumeID)
|
||||
if (!vol) {
|
||||
return HttpResponse.json(
|
||||
{ code: 404, message: 'Not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
return HttpResponse.json({ ...vol, domain: byocDomain })
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
const info = await Volume.getInfo(created.volumeId)
|
||||
expect(info.domain).toBe(byocDomain)
|
||||
|
||||
const connected = await Volume.connect(created.volumeId)
|
||||
expect(connected.domain).toBe(byocDomain)
|
||||
expect(new VolumeConnectionConfig(connected).domain).toBe(byocDomain)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Volume content readFile', () => {
|
||||
it('should return content for a non-empty file in every format', async () => {
|
||||
volumeFiles.set('hello.txt', 'hello world')
|
||||
|
||||
+15
-1
@@ -1,9 +1,11 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, TypeVar
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from attrs import define as _attrs_define
|
||||
from attrs import field as _attrs_field
|
||||
|
||||
from ..types import UNSET, Unset
|
||||
|
||||
T = TypeVar("T", bound="VolumeAndToken")
|
||||
|
||||
|
||||
@@ -14,11 +16,16 @@ class VolumeAndToken:
|
||||
volume_id (str): ID of the volume
|
||||
name (str): Name of the volume
|
||||
token (str): Auth token to use for interacting with volume content
|
||||
domain (Union[Unset, str]): Domain to use as the destination for volume content requests,
|
||||
replacing the default `api.<E2B_DOMAIN>`. Only returned when the
|
||||
team is connected to a custom (BYOC) cluster; absent otherwise, in
|
||||
which case the default domain is used.
|
||||
"""
|
||||
|
||||
volume_id: str
|
||||
name: str
|
||||
token: str
|
||||
domain: Union[Unset, str] = UNSET
|
||||
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@@ -28,6 +35,8 @@ class VolumeAndToken:
|
||||
|
||||
token = self.token
|
||||
|
||||
domain = self.domain
|
||||
|
||||
field_dict: dict[str, Any] = {}
|
||||
field_dict.update(self.additional_properties)
|
||||
field_dict.update(
|
||||
@@ -37,6 +46,8 @@ class VolumeAndToken:
|
||||
"token": token,
|
||||
}
|
||||
)
|
||||
if domain is not UNSET:
|
||||
field_dict["domain"] = domain
|
||||
|
||||
return field_dict
|
||||
|
||||
@@ -49,10 +60,13 @@ class VolumeAndToken:
|
||||
|
||||
token = d.pop("token")
|
||||
|
||||
domain = d.pop("domain", UNSET)
|
||||
|
||||
volume_and_token = cls(
|
||||
volume_id=volume_id,
|
||||
name=name,
|
||||
token=token,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
volume_and_token.additional_properties = d
|
||||
|
||||
@@ -24,6 +24,10 @@ class VolumeAndToken(VolumeInfo):
|
||||
|
||||
token: str
|
||||
"""Volume auth token."""
|
||||
domain: Optional[str] = None
|
||||
"""Domain to use as the destination for volume content requests, replacing
|
||||
the default ``api.<domain>`` host. Only set for teams connected to a custom
|
||||
(BYOC) cluster; ``None`` otherwise."""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -47,7 +47,10 @@ from e2b.volume.types import (
|
||||
VolumeEntryStat,
|
||||
)
|
||||
from e2b.io_utils import aiter_io_chunks
|
||||
from e2b.volume.utils import DualMethod, convert_volume_entry_stat
|
||||
from e2b.volume.utils import (
|
||||
DualMethod,
|
||||
convert_volume_entry_stat,
|
||||
)
|
||||
|
||||
|
||||
class AsyncVolume:
|
||||
@@ -121,11 +124,16 @@ class AsyncVolume:
|
||||
if isinstance(res.parsed, Error):
|
||||
raise Exception(f"{res.parsed.message}: Request failed")
|
||||
|
||||
domain = (
|
||||
res.parsed.domain
|
||||
if isinstance(res.parsed.domain, str) and res.parsed.domain
|
||||
else None
|
||||
)
|
||||
vol = cls(
|
||||
volume_id=res.parsed.volume_id,
|
||||
name=res.parsed.name,
|
||||
token=res.parsed.token,
|
||||
domain=config.domain,
|
||||
domain=domain or config.domain,
|
||||
debug=config.debug,
|
||||
proxy=config.proxy,
|
||||
)
|
||||
@@ -146,7 +154,7 @@ class AsyncVolume:
|
||||
volume_id=volume_id,
|
||||
name=info.name,
|
||||
token=info.token,
|
||||
domain=config.domain,
|
||||
domain=info.domain or config.domain,
|
||||
debug=config.debug,
|
||||
proxy=config.proxy,
|
||||
)
|
||||
@@ -182,10 +190,16 @@ class AsyncVolume:
|
||||
if isinstance(res.parsed, Error):
|
||||
raise Exception(f"{res.parsed.message}: Request failed")
|
||||
|
||||
domain = (
|
||||
res.parsed.domain
|
||||
if isinstance(res.parsed.domain, str) and res.parsed.domain
|
||||
else None
|
||||
)
|
||||
return VolumeAndToken(
|
||||
volume_id=res.parsed.volume_id,
|
||||
name=res.parsed.name,
|
||||
token=res.parsed.token,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -48,7 +48,10 @@ from e2b.volume.types import (
|
||||
VolumeEntryStat,
|
||||
)
|
||||
from e2b.io_utils import iter_io_chunks
|
||||
from e2b.volume.utils import DualMethod, convert_volume_entry_stat
|
||||
from e2b.volume.utils import (
|
||||
DualMethod,
|
||||
convert_volume_entry_stat,
|
||||
)
|
||||
|
||||
|
||||
class Volume:
|
||||
@@ -122,11 +125,16 @@ class Volume:
|
||||
if isinstance(res.parsed, Error):
|
||||
raise Exception(f"{res.parsed.message}: Request failed")
|
||||
|
||||
domain = (
|
||||
res.parsed.domain
|
||||
if isinstance(res.parsed.domain, str) and res.parsed.domain
|
||||
else None
|
||||
)
|
||||
vol = cls(
|
||||
volume_id=res.parsed.volume_id,
|
||||
name=res.parsed.name,
|
||||
token=res.parsed.token,
|
||||
domain=config.domain,
|
||||
domain=domain or config.domain,
|
||||
debug=config.debug,
|
||||
proxy=config.proxy,
|
||||
)
|
||||
@@ -147,7 +155,7 @@ class Volume:
|
||||
volume_id=volume_id,
|
||||
name=info.name,
|
||||
token=info.token,
|
||||
domain=config.domain,
|
||||
domain=info.domain or config.domain,
|
||||
debug=config.debug,
|
||||
proxy=config.proxy,
|
||||
)
|
||||
@@ -181,10 +189,16 @@ class Volume:
|
||||
if isinstance(res.parsed, Error):
|
||||
raise Exception(f"{res.parsed.message}: Request failed")
|
||||
|
||||
domain = (
|
||||
res.parsed.domain
|
||||
if isinstance(res.parsed.domain, str) and res.parsed.domain
|
||||
else None
|
||||
)
|
||||
return VolumeAndToken(
|
||||
volume_id=res.parsed.volume_id,
|
||||
name=res.parsed.name,
|
||||
token=res.parsed.token,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
import pytest
|
||||
|
||||
from e2b import AsyncVolume
|
||||
from e2b.connection_config import ConnectionConfig
|
||||
from e2b.exceptions import NotFoundException
|
||||
from e2b.api.client.models.volume_and_token import VolumeAndToken
|
||||
from e2b.api.client.types import Response
|
||||
@@ -154,6 +155,55 @@ async def test_volume_per_call_proxy_overrides_instance():
|
||||
assert config.proxy == "http://127.0.0.1:9090"
|
||||
|
||||
|
||||
async def test_create_volume_uses_byoc_domain(monkeypatch):
|
||||
byoc_domain = "cluster.example.com"
|
||||
|
||||
async def mock_post(*, client, body):
|
||||
vol_id = str(uuid4())
|
||||
vol = VolumeAndToken.from_dict(
|
||||
{
|
||||
"volumeID": vol_id,
|
||||
"name": body.name,
|
||||
"token": f"vol-token-{uuid4()}",
|
||||
"domain": byoc_domain,
|
||||
}
|
||||
)
|
||||
_volumes[vol_id] = vol
|
||||
return Response(
|
||||
status_code=HTTPStatus(201), content=b"", headers={}, parsed=vol
|
||||
)
|
||||
|
||||
monkeypatch.setattr(post_volumes_mod, "asyncio_detailed", mock_post)
|
||||
|
||||
vol = await AsyncVolume.create("byoc-volume")
|
||||
|
||||
# The BYOC domain drives the content API destination (https://api.<domain>),
|
||||
# replacing the default domain.
|
||||
assert vol._domain == byoc_domain
|
||||
assert vol._get_volume_config().domain == byoc_domain
|
||||
assert vol._get_volume_config().api_url == f"https://api.{byoc_domain}"
|
||||
|
||||
|
||||
async def test_create_volume_falls_back_to_default_domain():
|
||||
vol = await AsyncVolume.create("default-volume")
|
||||
|
||||
# No domain in the response -> fall back to the connection default.
|
||||
assert vol._domain == ConnectionConfig().domain
|
||||
|
||||
|
||||
async def test_connect_propagates_byoc_domain():
|
||||
byoc_domain = "cluster.example.com"
|
||||
created = await AsyncVolume.create("connect-volume")
|
||||
_volumes[created.volume_id].domain = byoc_domain
|
||||
|
||||
info = await AsyncVolume.get_info(created.volume_id)
|
||||
assert info.domain == byoc_domain
|
||||
|
||||
connected = await AsyncVolume.connect(created.volume_id)
|
||||
assert connected._domain == byoc_domain
|
||||
assert connected._get_volume_config().domain == byoc_domain
|
||||
|
||||
|
||||
async def test_volume_full_lifecycle():
|
||||
# Create
|
||||
vol = await AsyncVolume.create("lifecycle-vol")
|
||||
|
||||
@@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
import pytest
|
||||
|
||||
from e2b import Volume
|
||||
from e2b.connection_config import ConnectionConfig
|
||||
from e2b.exceptions import NotFoundException
|
||||
from e2b.api.client.models.volume_and_token import VolumeAndToken
|
||||
from e2b.api.client.types import Response
|
||||
@@ -152,6 +153,55 @@ def test_volume_per_call_proxy_overrides_instance():
|
||||
assert config.proxy == "http://127.0.0.1:9090"
|
||||
|
||||
|
||||
def test_create_volume_uses_byoc_domain(monkeypatch):
|
||||
byoc_domain = "cluster.example.com"
|
||||
|
||||
def mock_post(*, client, body):
|
||||
vol_id = str(uuid4())
|
||||
vol = VolumeAndToken.from_dict(
|
||||
{
|
||||
"volumeID": vol_id,
|
||||
"name": body.name,
|
||||
"token": f"vol-token-{uuid4()}",
|
||||
"domain": byoc_domain,
|
||||
}
|
||||
)
|
||||
_volumes[vol_id] = vol
|
||||
return Response(
|
||||
status_code=HTTPStatus(201), content=b"", headers={}, parsed=vol
|
||||
)
|
||||
|
||||
monkeypatch.setattr(post_volumes_mod, "sync_detailed", mock_post)
|
||||
|
||||
vol = Volume.create("byoc-volume")
|
||||
|
||||
# The BYOC domain drives the content API destination (https://api.<domain>),
|
||||
# replacing the default domain.
|
||||
assert vol._domain == byoc_domain
|
||||
assert vol._get_volume_config().domain == byoc_domain
|
||||
assert vol._get_volume_config().api_url == f"https://api.{byoc_domain}"
|
||||
|
||||
|
||||
def test_create_volume_falls_back_to_default_domain():
|
||||
vol = Volume.create("default-volume")
|
||||
|
||||
# No domain in the response -> fall back to the connection default.
|
||||
assert vol._domain == ConnectionConfig().domain
|
||||
|
||||
|
||||
def test_connect_propagates_byoc_domain():
|
||||
byoc_domain = "cluster.example.com"
|
||||
created = Volume.create("connect-volume")
|
||||
_volumes[created.volume_id].domain = byoc_domain
|
||||
|
||||
info = Volume.get_info(created.volume_id)
|
||||
assert info.domain == byoc_domain
|
||||
|
||||
connected = Volume.connect(created.volume_id)
|
||||
assert connected._domain == byoc_domain
|
||||
assert connected._get_volume_config().domain == byoc_domain
|
||||
|
||||
|
||||
def test_volume_full_lifecycle():
|
||||
# Create
|
||||
vol = Volume.create("lifecycle-vol")
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
97782d4d2812ffaa88fce2eb640dac645ccbdbd4
|
||||
24a054bca26ec50a6d59031d9360c1582612b3f8
|
||||
|
||||
+40
-3
@@ -123,6 +123,19 @@ components:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
headers:
|
||||
XNextToken:
|
||||
description: Cursor to fetch the next page of results, if more exist
|
||||
schema:
|
||||
type: string
|
||||
XTotalRunning:
|
||||
description: >
|
||||
Number of running sandboxes matching the filters, before pagination is applied.
|
||||
Only present when running sandboxes were requested.
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
|
||||
responses:
|
||||
"400":
|
||||
description: Bad request
|
||||
@@ -300,6 +313,10 @@ components:
|
||||
egressProxy:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/SandboxEgressProxyConfig"
|
||||
description: >-
|
||||
SOCKS5 proxy for sandbox egress. Outbound TCP is tunneled through the
|
||||
proxy after allow/deny filtering; the sandbox is unaware. Domain-matched
|
||||
flows use remote DNS (ATYP=domain).
|
||||
x-not-implemented: true
|
||||
maskRequestHost:
|
||||
type: string
|
||||
@@ -332,6 +349,10 @@ components:
|
||||
egressProxy:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/SandboxEgressProxyConfig"
|
||||
description: >-
|
||||
SOCKS5 proxy for sandbox egress. Outbound TCP is tunneled through the
|
||||
proxy after allow/deny filtering; the sandbox is unaware. Domain-matched
|
||||
flows use remote DNS (ATYP=domain).
|
||||
x-not-implemented: true
|
||||
rules:
|
||||
type: object
|
||||
@@ -2037,6 +2058,13 @@ components:
|
||||
token:
|
||||
type: string
|
||||
description: Auth token to use for interacting with volume content
|
||||
domain:
|
||||
type: string
|
||||
description: |
|
||||
Domain to use as the destination for volume content requests,
|
||||
replacing the default `api.<E2B_DOMAIN>`. Only returned when the
|
||||
team is connected to a custom (BYOC) cluster; absent otherwise, in
|
||||
which case the default domain is used.
|
||||
required:
|
||||
- volumeID
|
||||
- name
|
||||
@@ -2286,6 +2314,11 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully returned all running sandboxes
|
||||
headers:
|
||||
X-Next-Token:
|
||||
$ref: "#/components/headers/XNextToken"
|
||||
X-Total-Running:
|
||||
$ref: "#/components/headers/XTotalRunning"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -2837,6 +2870,9 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully returned snapshots
|
||||
headers:
|
||||
X-Next-Token:
|
||||
$ref: "#/components/headers/XNextToken"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -2908,9 +2944,7 @@ paths:
|
||||
description: Successfully returned all templates
|
||||
headers:
|
||||
X-Next-Token:
|
||||
description: Cursor to fetch the next page of results, if more exist
|
||||
schema:
|
||||
type: string
|
||||
$ref: "#/components/headers/XNextToken"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -3075,6 +3109,9 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully returned the template with its builds
|
||||
headers:
|
||||
X-Next-Token:
|
||||
$ref: "#/components/headers/XNextToken"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
||||
Reference in New Issue
Block a user