Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions src/ansible_dev_tools/resources/server/creator_dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""Dynamic, schema-driven creator API endpoints."""

from __future__ import annotations

import json
import shutil

from typing import TYPE_CHECKING, Any

# pylint: disable-next=import-error,no-name-in-module
from ansible_creator.api import V1 # type: ignore[import-not-found]
from django.core.files.storage import FileSystemStorage
from django.http import FileResponse, HttpRequest, HttpResponse, JsonResponse

from ansible_dev_tools.resources.server.creator_v2 import create_tar_file
from ansible_dev_tools.server_utils import validate_request, validate_response


if TYPE_CHECKING:
from pathlib import Path


class CreatorDynamic:
"""Dynamic creator endpoints driven by ansible-creator's V1 API.

Provides discovery, schema inspection, and generic scaffolding
without hardcoding individual project types.
"""

def _response_from_tar(self, tar_file: Path) -> FileResponse:
"""Create a FileResponse from a tar file.

Args:
tar_file: The tar file path.

Returns:
The file response.
"""
fs = FileSystemStorage(str(tar_file.parent))
response = FileResponse(
fs.open(tar_file.name, "rb"),
content_type="application/tar",
status=201,
)
response["Content-Disposition"] = f'attachment; filename="{tar_file.name}"'
return response

def capabilities(self, request: HttpRequest) -> JsonResponse | HttpResponse:
"""Return the full ansible-creator capability tree.

Args:
request: HttpRequest object.

Returns:
JSON response with the capability schema.
"""
result = validate_request(request)
if isinstance(result, HttpResponse):
return result
api = V1()
return JsonResponse(api.schema(), status=200)

def schema(self, request: HttpRequest) -> JsonResponse | HttpResponse:
"""Return the parameter schema for a specific command path.

The command path is provided via repeated ``command_path`` query
parameters, e.g. ``?command_path=init&command_path=collection``.

Args:
request: HttpRequest object.

Returns:
JSON response with the command schema, or 400 on error.
"""
result = validate_request(request)
if isinstance(result, HttpResponse):
return result
path_segments = request.GET.getlist("command_path")
if not path_segments:
return HttpResponse(
"Missing required query parameter: command_path",
status=400,
)
try:
api = V1()
schema_result = api.schema_for(*path_segments)
except KeyError as exc:
return JsonResponse({"error": str(exc)}, status=400)
return JsonResponse(schema_result, status=200)

def scaffold(self, request: HttpRequest) -> FileResponse | HttpResponse:
"""Scaffold an ansible-creator project dynamically.

Accepts a JSON body with ``command_path`` (list of strings) and
optional ``params`` (dict). Delegates to ``V1().run()`` and returns
the scaffolded content as a tar archive.

On success, logs are included in ``X-Creator-Logs`` and
``X-Creator-Message`` response headers.

On error, returns a JSON body with ``status``, ``message``,
and ``logs``.

Args:
request: HttpRequest object.

Returns:
Tar file response on success, or JSON/HTTP error response.
"""
result = validate_request(request)
if isinstance(result, HttpResponse):
return result

body: dict[str, Any] = result.body # type: ignore[assignment]
command_path: list[str] = body.get("command_path", [])
params: dict[str, Any] = body.get("params", {})

if not command_path:
return JsonResponse(
{"status": "error", "message": "Missing command_path", "logs": []},
status=400,
)

api = V1()
creator_result = api.run(*command_path, **params)

if creator_result.status == "error":
# Clean up the temp directory on error
if creator_result.path:
shutil.rmtree(creator_result.path, ignore_errors=True)
return JsonResponse(
{
"status": "error",
"message": creator_result.message,
"logs": creator_result.logs,
},
status=400,
)

if creator_result.path is None:
return JsonResponse(
{
"status": "error",
"message": "No output path",
"logs": creator_result.logs,
},
status=400,
)

try:
tar_name = f"{'_'.join(command_path)}.tar"
tar_file = creator_result.path.parent / tar_name
create_tar_file(creator_result.path, tar_file)
response = self._response_from_tar(tar_file)
response["X-Creator-Logs"] = json.dumps(creator_result.logs)
response["X-Creator-Message"] = creator_result.message
finally:
shutil.rmtree(creator_result.path, ignore_errors=True)

return validate_response(
request=request,
response=response,
)
88 changes: 88 additions & 0 deletions src/ansible_dev_tools/resources/server/data/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,71 @@ paths:
schema:
$ref: "#/components/schemas/Error"

/v2/creator/capabilities:
get:
summary: Retrieve the full ansible-creator capability tree
responses:
"200":
description: The capability schema describing all available commands
content:
application/json:
schema:
AnyValue: {}
"400":
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v2/creator/schema:
get:
summary: Retrieve parameter schema for a specific creator command
parameters:
- name: command_path
in: query
required: true
schema:
type: array
items:
type: string
style: form
explode: true
responses:
"200":
description: The parameter schema for the specified command
content:
application/json:
schema:
AnyValue: {}
"400":
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v2/creator/scaffold:
post:
summary: Scaffold an ansible-creator project dynamically
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreatorScaffold"
required: true
responses:
"201":
description: Created
content:
application/tar:
schema:
AnyValue: {}
"400":
description: Bad Request
content:
application/json:
schema:
$ref: "#/components/schemas/CreatorScaffoldError"

components:
schemas:
Metadata:
Expand Down Expand Up @@ -185,3 +250,26 @@ components:
type: integer
message:
type: string
CreatorScaffold:
type: object
additionalProperties: false
required:
- command_path
properties:
command_path:
type: array
items:
type: string
params:
type: object
CreatorScaffoldError:
type: object
properties:
status:
type: string
message:
type: string
logs:
type: array
items:
type: string
4 changes: 4 additions & 0 deletions src/ansible_dev_tools/subcommands/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.urls import path
from gunicorn.app.base import BaseApplication

from ansible_dev_tools.resources.server.creator_dynamic import CreatorDynamic
from ansible_dev_tools.resources.server.creator_v1 import CreatorFrontendV1
from ansible_dev_tools.resources.server.creator_v2 import CreatorFrontendV2
from ansible_dev_tools.resources.server.server_info import GetMetadata
Expand All @@ -29,6 +30,9 @@
path(route="v2/creator/collection", view=CreatorFrontendV2().collection),
path(route="v2/creator/devfile", view=CreatorFrontendV2().devfile),
path(route="v2/creator/ee_project", view=CreatorFrontendV2().ee_project),
path(route="v2/creator/capabilities", view=CreatorDynamic().capabilities),
path(route="v2/creator/schema", view=CreatorDynamic().schema),
path(route="v2/creator/scaffold", view=CreatorDynamic().scaffold),
)


Expand Down
Loading
Loading