Skip to content

release: Merge latest prod-staging changes to prod-stable - #9

Merged
danielvallance merged 37 commits into
prod-stablefrom
prod-staging
Oct 6, 2026
Merged

danielvallance merged 37 commits into
prod-stablefrom
prod-staging

Conversation

@unikraft-bot

Copy link
Copy Markdown

Automated changes by create-pull-request GitHub action

The transport decoded every answer as the JSON envelope, but a few
operations, such as a file download or a stream of output, answer
with the bytes themselves, and a 206 means nothing without the
Content-Range that came with it.

_request_bytes returns the payload as a RawResponse with its status
and headers, which parse the Content-Range into byte_range and
total_size, and _stream_bytes yields it as it arrives. A failure is
raised from the envelope as for any other operation.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
An operation whose success response was octet-stream was typed as
answering nothing, so its payload was lost, and a header parameter
such as Range had no argument of its own.

Such an operation returns a RawResponse through the raw transport,
and a header parameter is a keyword argument merged over the
caller's headers. Each spec has a Makefile target of its own, and
the templates take the transport's package from a variable.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The transport methods of ApiClient were private, so a client
outside this package, such as a generated plugin client that
takes a transport rather than extending one, had nothing public
to call and would have leaned on names it does not own.

request, request_no_content, request_bytes, stream_bytes and
stream are the public names, and config the configuration a
client sends with. The template emits them, and the generated
plumbing is regenerated, which changes only the names it calls.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema that is only a scalar or an array rendered as an empty
model, so a union that named it had no type to carry and a value
of it parsed to nothing.

The template renders such a schema as a type alias, such as
`Names = list[str]`, beside the union aliases.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
Every generated model marked every field optional, request bodies
included, though the specification requires many of theirs. A
service group request without its services built without complaint
and failed at the server, where the error named no field.

A schema named as a request now keeps the specification's
required fields, so the omission fails at construction. A
response stays optional throughout: one shape serves several
answers, and a summary or a failed item carries only some fields.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A named map schema rendered as an empty model, a property name
that is no identifier rendered as the field name itself, and an
operation with several tags was rendered under its first only.

A map is now an alias of dict[str, T], such a property is spelled
in snake case with the wire name as its alias, or refused when no
spelling is an identifier or two render alike, an operation is a
method of each tagged class, and literals are rendered escaped.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A specification shape the templates could not render came out as
a method that was wrong in silence: an operation without a tag was
dropped, a non-JSON body or payload was discarded, and a parameter
that was no identifier or clashed with another broke the module.

Generation now stops with a message naming the operation or tag
for each such shape, a cookie or a tag clash included; a required
query or header parameter is a required argument, a path item's
headers reach its operations, and _text() spells a non-string one.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
An alias that named another alias came out in alphabetical order,
so one that sorted before the alias it named raised a NameError on
import.

The aliases are now written in passes, each taking those whose
named aliases are already written; one left over names itself and
is refused.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A query parameter declared with content rather than a schema made
generation stop on a nil pointer instead of a message, and a
property with a leading underscore became a pydantic private
attribute, so its value never reached the field.

Such a query parameter is refused with a message naming it, and
such a property is spelled without the underscore and keeps the
wire name as its alias, as other unsafe names already are.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A property named like a pydantic member broke its model: a model_
member failed at import, and a version 1 method such as json was
shadowed with a warning. A query parameter holding objects and a
union or enum response rendered what the transport cannot handle.

A model_ name gets a field_ prefix and a version 1 method name a
trailing underscore, both with the wire name as alias. Such a
parameter or response is refused with a message that names the
operation.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema named like a keyword or an import of the models module
rendered an invalid or shadowing class, and a required date-time
that the specification allowed to be null was typed as datetime
alone, so pydantic refused the null.

A schema name is now checked before anything is rendered, and a
nullable date-time keeps its None.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema named like a builtin the annotations use took its place
in every model defined after it, a field named like one broke the
annotations of the fields after it, and a parameter named like a
helper the method body calls took the place of that helper.

Such a schema or parameter is refused with a message naming it,
and such a field is spelled with a trailing underscore and keeps
the wire name as its alias.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
Which generated models keep their required fields was decided by
the schema's name, and nothing told whoever names the schemas, or
what follows when a name breaks the convention.

The models template and the README now say that a schema whose name
contains Request keeps its required fields, every other one has each
field optional, and what a request or response named otherwise does.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A named enumeration the specification marked nullable rendered as
its Literal alone, so a required request field of that type refused
None although the specification allowed it. The other alias forms
already kept their None.

The enum alias now carries | None when the schema is nullable, as
the array, map and union aliases already do.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A query parameter in a style other than form, or one that allows
reserved characters, and a path parameter in the label or matrix
style rendered as if they were form and simple, so the request
went out with a value or a route the specification did not mean.

Such a parameter is refused with a message that names it.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A schema that put oneOf or anyOf beside allOf, or beside each
other, reached the alias path, where one union keyword is rendered
and the rest is dropped, so the type was wider than the wire.

Such a schema is refused with a message that names it.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A tag is rendered verbatim in the docstring of its class, and the
name checks only saw its snake case, so a tag holding a quote or a
backslash escape passed them and left a module that does not
parse.

The tag itself is now checked before anything is rendered: letters,
digits, spaces, dots, dashes and underscores only.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A generated module imported datetime, Any, Literal, Field and
AsyncIterator whether it used them or not, so the unused-import
check had to be switched off for the whole generated tree, where
it would also have caught a stale import of ours.

The models template derives each field's name and type first and
imports only what they need, adding | None to an optional field
once; the resources template imports AsyncIterator, Any and Literal
only where a module streams, takes Any or takes an enum.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A request model was detected by its name alone, so a schema that
only requests use, such as the image spec a plugin takes, had every
field optional when its name lacked Request, and a request missing
a field the specification requires failed at the server.

A schema that only request bodies reach, directly or through the
models they name, is a request model too, as the plugin generator
has it; one a response also reaches stays optional.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A nullable query, path or header parameter rendered an argument the
transport cannot send as null: it leaves a None value out and spells
a None list item as "None". A nullable response schema was typed as
a model the transport cannot decode a null into.

Such a parameter or response is refused with a message naming it and
what to change in the specification, as the plugin generator does.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The platform and control-plane clients were rendered from an
older specification, so a create could not be given annotations,
gpus or type: check_spec refused each as unknown, though the API
has accepted all three for some time.

Both are regenerated from the published specification:
ServiceGroupsApi is ServicesApi, PlatformApi gains the audit API,
UsersApi loses add_users, request models keep their required
fields, as the README now says, and F401 is on again.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A plugin serves an API of its own from inside an instance, which
the platform proxies under the instance on the metro running it.
The SDK had no client for that API and no way to build its route.

The plumbing is the unikraft-cloud-plugin-sandbox-api package, which
plugin-sdk renders. SandboxApi, reached as ukc.api.plugins.sandbox,
sends it through the SDK's transport, and for_instance() points it
at one instance; a plugin named v1 keeps its route.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A handle memoised its lookup as a task and kept it however it
ended, so a name not listed yet, or a metro that did not answer,
was raised again by every later await. A set kept a failed lookup
too.

A lookup that failed is made again on the next await, for the
handles that only look a resource up, for sets, and for the
lookup an operation chained onto such a handle starts from; a
create, and the chained operation itself, keep their one outcome.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The SDK had no client for the sandbox plugin. A caller had to find
the instance's UUID, build the plugin's route and drive the
plumbing by hand to run a command or move a file.

sandbox() and plugin() on an instance handle give a Sandbox or a
Plugin, which read the instance only when its UUID is not known.
Sandbox runs, follows and signals commands and moves files; output
is polled, and a timeout interrupts a command, then waits for it.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
collect() gathered a command's output for the result, so a caller
that wanted it as it arrived had to poll the command itself, and
with it re-implement the interrupt and the grace a timeout gives.
A finished command also stayed in the plugin until deleted.

An on_output sink is handed each chunk as it is read, an awaitable
it returns awaited before the next, while the result still carries
everything. forget drops the plugin's record once the command has
ended; a command still running is kept.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The plugin reads a request body of two mebibytes at most, and a
whole file came back in one payload. A caller moving anything
larger chunked its writes by hand and held a download in memory.

write() sends data longer than a chunk in appended pieces and can
create the directories above the file; upload_file() streams a
local file the same way. stream() and read_to() hand a file over
as it arrives, and examples/sandbox.py walks through the client.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The metro proxy ends a plugin request after 60s. An unbounded
wait for a command's end, and any wait longer than that, was cut
off with the proxy's 504 page, so a long command could not be
waited for or followed to its end.

A wait is sent as waits of at most WAIT_SLICE, thirty seconds,
until the command ends or the caller's timeout runs out, each
given a read timeout that outlasts it; one the proxy drops is
retried as a poll is. The README gains the sandbox section.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The SDK had no idiomatic images client, and images are reported
per metro, so an account-wide view meant asking every metro in
scope and merging the answers by hand.

Images, reached as scope.images, lists every metro in scope and
tags each image with its metro; a partial answer raises with what
did arrive. A lookup is a filtered listing, as in the CLI, and one
metro's view is that metro's client.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The images client lists what each metro's nodes have cached, which
is not whether an image can be pulled: the cache drops images the
registry keeps and keeps ones it has dropped. Callers asked the OCI
registry directly, with its token exchange, to find out.

Images.find and Images.exists ask the control plane, which answers
from the registry itself and needs only the SDK's token. A
reference may carry a tag, a digest, a registry host or a scheme,
and a bare one means the latest tag.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The API reports a stop as two integers, a reason bitmask and a
code whose meaning depends on it. The Go SDK decodes both; this
one handed them over raw, so every caller decoded the platform's
image-pull failure and the kernel's out-of-memory for itself.

Stop, StopReason and the platform and kernel codes port the Go
package; Instance.stop reads them off an instance and
describe_stop() puts them in words, as the CLI reports a stop,
e.g. "kernel crash: out of memory (ENOMEM)".

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A create that waited for its instance to run raised a bare "API
reported an error" when the instance stopped instead. The failed
item names the instance and nothing else, so the reason survived
only on the stopped instance, which every caller had to read.

Instances.create reads that instance and raises InstanceStoppedError
with its decoded stop; ResponseError carries an item's state. A
create that waits stretches its read timeout past that wait, as
wait() does, and raises a lapsed wait as WaitTimeoutError.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
A delete answered as soon as it was under way, failed on an
instance already gone, and failed at once on one the platform
reported busy, as a relay interface is for a while after its
instance is deleted. Callers waited and retried by hand.

delete() takes timeout_seconds for the API's own wait, missing_ok
and retry_busy, which backs off while the API says EBUSY; a wait
of -1 leaves the read timeout unbounded. NotFoundError.absent marks
a missing resource, and only that not-found is ever forgiven.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
An instance set could be named only by uuid or name, one match per
metro. A caller cleaning up everything a job had created had to
list by tag and then delete each match itself, or guess at names.

each(tags=[...]) locates every instance in scope carrying the tags
and hands back the usual set, so `.delete(missing_ok=True)` covers
them all. Tags that select nothing are an empty set rather than a
failure, so a cleanup can run again.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
Templates had plumbing only. Preparing one meant creating a seed
clone, waiting for the template to be listed and deleting the seed
by hand, and cloning meant building the create request yourself.

Add Templates, reached as scope.templates: prepare makes a template
from an instance specification, in the one metro the scope names,
and get(...).clone stamps instances out of it; list, each and delete
work as for every other resource. examples/templates.py shows it.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The public surface changed: a dependency on the sandbox plumbing
package, which names 0.2.0 as the oldest SDK whose transport it
fits, plumbing renamed and removed by the regeneration, and
required fields that are now required.

The package and `__version__` say 0.2.0, the lock follows, and a
changelog records what the release adds, changes and removes.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
The Makefile's help called fmt a format of all sources and
misaligned its columns, openapi-gen was passed a variable no
template reads, the README linked openapi-gen to the GitHub
organisation, and several docstrings and comments were stale.

fmt says what it formats, the columns fit the longest target and
the dead flag is gone. The README says the Makefile pins
openapi-gen, and the stale docstrings and comments are corrected.

Signed-off-by: Daniel Vallance <daniel@unikraft.com>
@danielvallance
danielvallance self-requested a review October 6, 2026 15:02
@danielvallance
danielvallance merged commit a9b156e into prod-stable Oct 6, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants