Rewrite pack — heat — branch stable/2026.2
Branch
stable/2026.2@5872396e548c(27.0.1) — repository https://opendev.org/openstack/heat — analysed on 2026-10-29 Client repositories cited:heatclient:python-heatclientstable/2026.2@88120f11830d(5.3.0-2);htp:heat-tempest-pluginmaster@ceb78843237c(3.0.0-11);skyline:skyline-consolestable/2026.2@ 806d200392ee;skyapi:skyline-apiserverstable/2026.2@ 4af72bfd4577;tf:terraform-provider-openstackmain@1268c8db29c9(v3.4.0-33);gc:gophercloud @4aae510cb4d2(v2.15.0); Magnumstable/2026.2@1910928b7d9f(23.0.0); Octaviastable/2026.2@2900ed67cac2(19.0.0-19).
Target: openkeel-heat (~/.claude/skills/generate-go-from-spec/references/backends/heat-orchestration.md) — native and CFN APIs, convergence engine as Go workers on RabbitMQ RPC, Heat's MariaDB schema, Keystone trusts, resource plugins for the services OpenKeel ships. Two requirements shape this pack: users' templates must keep working unchanged, and the Go engine must be able to take over stacks created by Python Heat in the same database, including stacks in the middle of a traversal.
1. Contents of _rewrite/
| File | Use in a rewrite |
|---|---|
[routes_stable-2026.2.json](_rewrite/routes_stable-2026.2.json) | native routes (40 paths, 61 operations, plus the generated 405/OPTIONS routes) with handler, decorators and the policy rule enforced; CFN routes and the Action → handler table (tools/heat_routes.py) |
[routes-list_stable-2026.2.json](_rewrite/routes-list_stable-2026.2.json) | the same paths as the plain list conformance_capture.py --routes-json expects |
[rpc-contract_stable-2026.2.json](_rewrite/rpc-contract_stable-2026.2.json) | the full API↔engine and engine↔engine RPC contract: 54 + 3 + 1 methods, mode, pinned version, arguments, client and server file:line, request_context wrapping, server methods no client calls (tools/heat_rpc.py) |
[api-ref_stable-2026.2.json](_rewrite/api-ref_stable-2026.2.json) | the 58 documented operations and parameters (api-ref) |
[policies_stable-2026.2.json](_rewrite/policies_stable-2026.2.json), [policies_stable-2026.2.yaml](_rewrite/policies_stable-2026.2.yaml) | the 100 default rules (secure RBAC), with deprecated rules and scope types |
[config_stable-2026.2.conf](_rewrite/config_stable-2026.2.conf) | oslo-config-generator sample of every option (45 sections) |
[schema-sqlite-migrated_stable-2026.2.sql](_rewrite/schema-sqlite-migrated_stable-2026.2.sql) | schema produced by heat-manage db_sync (Alembic head 97b2a986f922) — the authority |
[schema-mysql_stable-2026.2.sql](_rewrite/schema-mysql_stable-2026.2.sql), [schema-postgresql_stable-2026.2.sql](_rewrite/schema-postgresql_stable-2026.2.sql) | DDL from the ORM models (MySQL types: LONGTEXT JSON columns, index prefixes) |
[schema-diff_stable-2026.2.md](_rewrite/schema-diff_stable-2026.2.md) | migrations vs models: 0 differences (tools/schema_diff.py) |
[errors_stable-2026.2.json](_rewrite/errors_stable-2026.2.json) | 75 exceptions with message template, native HTTP status, CFN error code, safe, error code and raise sites (tools/heat_errors.py) |
[resource-types_stable-2026.2.json](_rewrite/resource-types_stable-2026.2.json) | the registry dumped from the venv: 201 resource types, full properties schemas (type, constraints, update_allowed, immutable, support status), attributes, implemented actions, signal and nested-stack flags, required service extension, OpenKeel verdict (tools/heat_registry.py) |
[resource-aliases_stable-2026.2.json](_rewrite/resource-aliases_stable-2026.2.json) | the default environment mappings (OS::Quantum* → OS::Neutron*, Ceilometer → Aodh, two template resources) |
[template-versions_stable-2026.2.json](_rewrite/template-versions_stable-2026.2.json) | the 13 template classes, aliases, sections, every intrinsic and condition function with its implementing class and file:line |
[constraints_stable-2026.2.json](_rewrite/constraints_stable-2026.2.json) | the 70 custom constraints (heat.constraints) |
[client-plugins_stable-2026.2.json](_rewrite/client-plugins_stable-2026.2.json) | the 20 client plugins and the resource types using each |
[crypt-vectors_stable-2026.2.json](_rewrite/crypt-vectors_stable-2026.2.json) | 21 decryption vectors for the three decrypt_method formats, each checked with Heat's decrypt() (tools/crypt_vectors.py) |
[convergence-state_stable-2026.2.json](_rewrite/convergence-state_stable-2026.2.json) | the convergence tables polled every 0.2 s during seven real scenarios (tools/convergence_state.py) |
[exchanges_stable-2026.2.json](_rewrite/exchanges_stable-2026.2.json), response-schemas_*, observed-errors_*, functional-run_* | the black-box suites recorded against the real APIs (§ 7) |
[rewrite-pack-index_stable-2026.2.md](_rewrite/rewrite-pack-index_stable-2026.2.md) | what was produced and how |
Library versions of the extraction (venv, Python 3.12, upper-constraints stable/2026.2): openstack-heat 27.0.1, oslo.messaging 18.3.1, oslo.db 18.1.1, oslo.policy 6.0.1, SQLAlchemy 2.0.52, Routes 2.5.1, WebOb 1.8.11, keystonemiddleware 13.0.1, python-heatclient 5.3.1.dev2, keystone 30.0.1.dev5, tempest 47.0.0, gabbi 4.2.0.
2. HTTP behaviour the contracts do not show
2.1 Pipeline
Native: cors → request_id → faultwrap → http_proxy_to_wsgi → versionnegotiation → authurl → authtoken → context → osprofiler → router (etc/heat/api-paste.ini:15). Consequences:
- unknown version in path, or no version and an
Acceptthat is notapplication/vnd.openstack.orchestration-v1.0or*/*: 300/404 before authentication (heat/api/middleware/version_negotiation.py:60-111); - 401 is keystonemiddleware's body (outside
faultwrap); every other error is Heat's JSON body; - CFN:
cors → request_id → http_proxy_to_wsgi → cfnversionnegotiation → ec2authtoken → authtoken → context → osprofiler → router, no fault wrapper, AWS XML errors (etc/heat/api-paste.ini:22).
2.2 Error body
{"code", "title", "explanation", "error": {"type", "traceback", "message"?}} (heat/api/middleware/fault.py:152-164). Clients read error.message (heatclient heatclient:heatclient/exc.py:48-73 prints ERROR: <message>), error.type (tests), and explanation for API-raised 400s (validation errors: POST /validate puts the message in explanation, heat/api/openstack/v1/stacks.py:650-651). The status is chosen by class name and bases (heat/api/middleware/fault.py:55-107): reproduce the table in errors_stable-2026.2.json (native_http) rather than re-deriving it.
2.3 Other rules
- Every path has
/{tenant_id}; mismatch with the token project → 403 unless admin (heat/api/openstack/v1/util.py:37-41). GET|POST|PUT|PATCH|DELETE /stacks/{name_or_id}[/<sub>]→ 302 to the canonical/stacks/{name}/{id}[/<sub>]keeping the query (heat/api/openstack/v1/stacks.py:426-442). Clients depend on it (gophercloud'sstacks.Find,gc:openstack/orchestration/v1/stacks/requests.go:305-306; heatclient re-issues the same method on 302,heatclient:heatclient/common/http.py:191-243; Terraform usesFindeverywhere,tf:openstack/resource_openstack_orchestration_stack_v1.go:257).- Body parsing only for JSON bodies starting with
{; missing required body → 400 "The server could not comply…" (heat/common/wsgi.py:236-354). - 405 with
Allowfor unimplemented methods, 204 withAllowforOPTIONS(heat/api/openstack/v1/__init__.py:64-80). - Status codes to keep: create 201 +
Location; update/PATCH/restore 202 empty; delete 204; signal and actions **200null; software config/deployment create **200 (not 201); preview update 200. - Unknown query parameters ignored, except on resource list (400).
links[].hrefbuilt from the request host (req.relative_url).
3. Core algorithms
3.1 Template parsing and validation
- Version selection (
heat/engine/template.py:31-89): exactly one ofheat_template_version,AWSTemplateFormatVersion,HeatTemplateFormatVersion(else "Ambiguous versions" / "Template version was not provided"); the value, asstr(YAML dates are converted), must be a registered entry point<key>.<value>(13 classes, 6 aliases:newton,ocata,pike,queens,rocky,wallaby), else the error lists the valid ones. - YAML loading: Heat's own loader (
heat/common/template_format.py): YAML 1.1 safe loader where timestamps stay strings, JSON accepted,max_template_size(524288 bytes) and depth limits ([inferred] limits not traced in detail); a Go parser must keep2016-10-14as a string and not coerceon/off/yes/nodifferently from PyYAML 1.1. - Sections: HOT
heat_template_version, description, parameter_groups, parameters, resources, outputs(+conditionsfrom 2016-10-14); CFNAWSTemplateFormatVersion, Description, Mappings, Parameters, Resources, Outputs, Conditions(template-versions_stable-2026.2.json,sections). Unknown top-level keys →InvalidTemplateSection. - Intrinsic functions per version (✓ supported, removed = the name is reserved and raises "… is not supported in this template version"; implementation classes and
file:lineintemplate-versions_stable-2026.2.json):
| Function | 2010-09-09 (CFN) | 2012-12-12 | 2013-05-23 | 2014-10-16 | 2015-04-30 | 2015-10-15 | 2016-04-08 | 2016-10-14 | 2017-02-24 | 2017-09-01 | 2018-03-02 | 2018-08-31 | 2021-04-16 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Fn::Base64, Fn::GetAZs, Fn::Join, Fn::MemberListToMap, Fn::Replace, Fn::ResourceFacade, Fn::Split, Ref | ✓ | ✓ | ✓ | removed | removed | removed | removed | removed | removed | removed | removed | removed | removed |
Fn::Select | ✓ | ✓ | ✓ | ✓ | ✓ | removed | removed | removed | removed | removed | removed | removed | removed |
Fn::FindInMap, Fn::GetAtt | ✓ | ✓ | |||||||||||
Fn::If | ✓ | ||||||||||||
get_attr, get_file, get_param, get_resource, list_join, resource_facade, str_replace | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ||
digest, repeat | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ||||
str_split | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |||||
map_merge | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ||||||
if, map_replace, yaql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |||||||
filter, str_replace_strict | ✓ | ✓ | ✓ | ✓ | ✓ | ||||||||
contains, list_concat, list_concat_unique, make_url, str_replace_vstrict | ✓ | ✓ | ✓ | ✓ |
Condition functions: CFN 2010-09-09 Fn::And, Fn::Equals, Fn::FindInMap, Fn::Not, Fn::Or, Ref; HOT ≥ 2016-10-14 and, equals, get_param, not, or, + contains, yaql from 2017-09-01.
The same name changes semantics across versions (one implementation class per range): - get_attr: GetAttThenSelect (2013-05-23, heat/engine/hot/functions.py:155) → GetAtt (2014-10-16, heat/engine/hot/functions.py:269) → GetAttAllAttributes (2015-10-15, heat/engine/hot/functions.py:300, {get_attr: [r]} returns all attributes); - list_join: Join → JoinMultiple (2015-10-15, heat/engine/hot/functions.py:633, several lists, non-string items JSON-encoded); - str_replace: Replace → ReplaceJson (2015-10-15, heat/engine/hot/functions.py:461, dict/list values JSON-encoded), str_replace_strict/vstrict (heat/engine/hot/functions.py:522, heat/engine/hot/functions.py:532); - repeat: Repeat (2015-04-30, heat/engine/hot/functions.py:893) → RepeatWithMap (2016-10-14, heat/engine/hot/functions.py:989) → RepeatWithNestedLoop (2017-09-01, heat/engine/hot/functions.py:1014, permutations: false); - if: If (2016-10-14, heat/engine/hot/functions.py:1273) → IfNullable (2021-04-16, heat/engine/hot/functions.py:1315, a missing branch removes the key).
yaql (heat/engine/hot/functions.py:1171) evaluates YAQL expressions with limits [yaql]/limit_iterators, memory_quota: a Go engine needs a YAQL implementation or must refuse templates that use it — a compatibility gap to decide (§ 8).
- Resolution model: functions are parsed into a tree, validated (
validate()per function, before any resource exists), dependencies are taken fromget_resource/get_attr/depends_on(dependencies()), and values are resolved lazily against node data (heat/engine/function.py).get_attrof a resource not yet created resolves toNoneduring validation. - Parameters: types
string, number, json (map), comma_delimited_list (list), boolean; constraintslength, range, modulo (2017-02-24+), allowed_values, allowed_pattern, custom_constraint(heat/engine/parameters.py,heat/engine/constraints.py);hiddenparameters are encrypted whenencrypt_parameters_and_propertiesand masked as******in responses;immutableparameters cannot change on update; pseudo parametersOS::stack_name,OS::stack_id,OS::project_id(andAWS::*for CFN). - Environment (
heat/engine/environment.py):parameters,parameter_defaults,resource_registry(type mappings, globsOS::Quantum*, template resources by file/URL, per-resourcehooksandrestricted_actions),event_sinks,encrypted_param_names,parameter_merge_strategies(overwrite,merge,deep_merge) applied whenenvironment_filesare combined (heat/engine/service.py:799-801). - Test data for a Go parser: Heat's unit tests
heat/tests/test_hot.py,heat/tests/test_function.py,heat/tests/test_parameters.py,heat/tests/test_template.py(not converted to vectors in this pack, § 8).
3.2 Convergence (formal specification)
Full narrative in 05 § 2. The invariants a re-implementation must keep, with their source:
- Traversal ownership. Every stack write during a traversal is
UPDATE stack … WHERE current_traversal = <mine>(heat/db/api.py:1003-1028); a traversal whose id is no longer current must not change the stack, and must not propagate (heat/engine/worker.py:238-241). - Graph. Nodes
(resource_id, is_update[, node_type]); edges from the template dependencies (update nodes), therequiresof existing rows (cleanup ordering, reversed),replaces, andcleanup(x) → update(x)(heat/engine/stack.py:1634-1666). Stored ascurrent_deps = {"edges": [[[id, bool], [id, bool] | null], …]}with 3-element nodes only for snapshots (heat/engine/stack.py:89-93,heat/engine/stack.py:1512-1516). Leaves start, update leaves after cleanup leaves (heat/engine/stack.py:1543-1564). - Row selection on update: the scoring of § 2.2 step 4 of 05 (
heat/engine/stack.py:1587-1617) decides which existing row a template resource maps to — essential for rollbacks and for re-running after failures. - Resource lock:
engine_id+atomic_keyCAS on every write; acquire requiresengine_id IS NULL; release requiresengine_id = me(heat/db/api.py:536-546,heat/engine/resource.py:2159-2183).atomic_keystarts NULL (treated as "first write sets 1",heat/db/api.py:519-523). - Sync points: one per node and one for the stack, created before the first cast; merge with CAS on
atomic_key, unbounded retries with jitter (heat/engine/sync_point.py:132-177); a node is ready whenpredecessors ⊆ keys(input_data); the stack completes when all roots reported. - Propagated data:
{"id", "name", "uuid", "reference_id", "attrs", "action", "status"}per predecessor (heat/engine/node_data.py:75-93); cleanup nodes receive a resource id ornull. - Replacement: new row +
replaced_byin one transaction conditional on the old row's lock state (heat/db/api.py:287-309); the replacement impersonates the old node in the current graph (heat/engine/check_resource.py:238-245). - Stale locks: on
UpdateInProgress, probe the holder withlistening(2 s); dead → clearengine_id, mark FAILED, retry (heat/engine/check_resource.py:60-94). - Completion:
mark_complete/mark_failed+ rollback +purge_db(heat/engine/stack.py:2334-2442). - Timeout: checked when a node starts, from
updated_at(orcreated_at) of the stack (heat/engine/stack.py:2444-2458).
What is not deterministic: the engine that processes a node (round-robin), the order in which siblings complete, and therefore the order of events and of atomic_key values; the resource ids (auto-increment) and every UUID. Tests must compare graphs and states, not ids.
3.3 Resuming stacks created by Python Heat
The Go engine shares the database with (or replaces) Python engines. What it must read exactly as Python wrote it — samples of every case are in [convergence-state_stable-2026.2.json](_rewrite/convergence-state_stable-2026.2.json):
| Item | Format written by Python | Pitfall |
|---|---|---|
stack.current_deps | JSON {"edges": [[[41, true], [40, true]], [[42, false], null], [["<snapshot uuid>", false, "snapshot"], null]]} | resource ids are integers, snapshot ids strings; 2- and 3-element nodes coexist |
sync_point.entity_id | str(resource_id) or stack/snapshot UUID (heat/db/api.py:2103, :2112) | integer ids stored as strings |
sync_point.input_data | {"input_data": {"tuple:(41, True, 'resource')": {node data}, …}} | keys are Python tuple reprs (heat/engine/sync_point.py:74-102); nested attribute-path keys too ("tuple:('show', 'value')"). Rows written before the snapshot-node change (heat/engine/stack.py:63-93) use 2-element tuples "tuple:(41, True)"; a resumed traversal mixing both forms would never match its predecessors in Python itself [inferred: ConvergenceNode always yields 3 fields, so a predecessor key written by an older engine is (41, True) and is not equal to (41, True, 'resource')]. Only an in-flight upgrade hits this; drain traversals before switching engines. |
sync_point.extra_data | {"extra_data": {"resource_failures": {...}, "skip_propagate": true}} or null (heat/engine/sync_point.py:120-129) | added by migration d2db381e3324 |
resource.requires / needed_by | JSON lists of ints, requires sorted descending; needed_by always [] on this branch (heat/engine/resource.py:2128-2129) | legacy column |
resource.nova_instance | physical id (the column name is historical) | |
resource.engine_id | UUID of a process (heat/common/service_utils.py:80-81) | a Go engine needs its own engine ids and must answer listening on heat-engine-listener with server = its engine id, otherwise Python engines will consider its locks stale and steal them |
user_creds | Fernet (cryptography_decrypt_v1) or older formats | decrypt all three (crypt-vectors) |
raw_template.template | the parsed template as JSON (not the YAML text) | GET …/template returns it as is |
resource_properties_data.data | JSON, or {name: [method, value]} when encrypted | |
event.resource_properties | Python pickle (legacy column, heat/db/models.py:222) | new events use rsrc_prop_data_id; old rows need a pickle reader or must be ignored |
Mixed fleets. Python and Go engines can share a fleet only if the Go engine implements the three RPC topics with the same method names, versions and payloads (§ 1 of 03), including cancel_check_resource and listening, and the same CAS rules. The simpler path is a drain: stop accepting operations, wait until no stack is IN_PROGRESS (SELECT … FROM stack WHERE status='IN_PROGRESS' AND deleted_at IS NULL), stop Python engines, start Go engines. Stacks with convergence = 0 (legacy) must be migrated with heat-manage migrate_convergence_1 first, or the Go engine must implement the legacy path; recommend the migration (only COMPLETE stacks can be migrated, heat/engine/service.py:2494-2507).
3.4 Resource plugins: what to port
201 resource types are registered ([resource-types_stable-2026.2.json](_rewrite/resource-types_stable-2026.2.json), field openkeel):
| Verdict | Count | Types |
|---|---|---|
| port — Neutron | 47 | OS::Neutron::{Net, Subnet, Port, Router, RouterInterface, FloatingIP, FloatingIPAssociation, SecurityGroup, SecurityGroupRule, ProviderNet, Segment, SubnetPool, AddressScope, Trunk, RBACPolicy, Quota, QoSPolicy, QoS*Rule (4), ExtraRoute, ExtraRouteSet, MeteringLabel, MeteringRule, Firewall*, VPNService, IKEPolicy, IPsecPolicy, IPsecSiteConnection, L2Gateway*, NetworkGateway, TaaS::*, FlowClassifier, PortPair*, PortChain} + AWS::EC2::{VPC, Subnet, NetworkInterface, RouteTable, SubnetRouteTableAssociation, VPCGatewayAttachment, InternetGateway, SecurityGroup}, plus OS::Neutron::FloatingIPPortForward (openstacksdk) |
| port — Nova | 9 | OS::Nova::{Server, Flavor, KeyPair, ServerGroup, HostAggregate, Quota}, AWS::EC2::{Instance, EIP, EIPAssociation} |
| port — Cinder | 9 | OS::Cinder::{Volume, VolumeAttachment, VolumeType, EncryptedVolumeType, QoSSpecs, QoSAssociation, Quota}, AWS::EC2::{Volume, VolumeAttachment} |
| port — Keystone | 10 | OS::Keystone::{Domain, Project, User, Group, Role, UserRoleAssignment, GroupRoleAssignment, Service, Endpoint, Region} |
| port — Barbican | 5 | OS::Barbican::{Secret, Order, GenericContainer, CertificateContainer, RSAContainer} |
| port — Manila | 4 | OS::Manila::{Share, ShareNetwork, ShareType, SecurityService} |
| port — Swift | 4 | OS::Swift::Container, OS::Heat::SwiftSignal, OS::Heat::SwiftSignalHandle, AWS::S3::Bucket |
| port — Designate | 2 | OS::Designate::{Zone, RecordSet} |
| port — Glance | 1 | OS::Glance::WebImage (OS::Glance::Image is hidden on this branch) |
| port — Ironic | 1 | OS::Ironic::Port |
| internal (Heat itself) | 33 | OS::Heat::{ResourceGroup, AutoScalingGroup, ScalingPolicy, InstanceGroup, ResourceChain, Stack (remote), Value, None, RandomString, Delay, WaitCondition, WaitConditionHandle, UpdateWaitConditionHandle, SoftwareConfig, SoftwareDeployment, SoftwareDeploymentGroup, StructuredConfig, StructuredDeployment, StructuredDeploymentGroup, SoftwareComponent, CloudConfig, MultipartMime, DeployedServer, AccessPolicy}, AWS::CloudFormation::{Stack, WaitCondition, WaitConditionHandle}, AWS::AutoScaling::{AutoScalingGroup, LaunchConfiguration, ScalingPolicy}, AWS::ElasticLoadBalancing::LoadBalancer, AWS::IAM::{User, AccessKey} |
| conditional — Octavia | 12 | OS::Octavia::* — only if OpenKeel exposes an Octavia API (the ovn-octavia-provider reverse-spec is a Neutron/OVN driver, not an API) |
| drop — absent services | 22 | Aodh (8), Zaqar (4), Mistral (3), Magnum (2), Trove (2), Blazar (2), Zun (1) |
| drop — hidden | 38 | already refused by Heat: LBaaS v1/v2, Sahara, Senlin, Monasca, Vitrage, Nova floating IPs, RouterGateway, Designate Domain/Record, HARestarter, … |
| test only | 1 | OS::Heat::TestResource (keep it: the conformance suites depend on it) |
Rules for the port:
- Availability is already dynamic: a type is offered only when its service is in the Keystone catalog and, for 39 Neutron/Cinder types, when the service lists the extension named by
required_service_extension(heat/engine/resource.py:785-828; values in the dump). An OpenKeel Neutron on Kube-OVN that does not listfwaas,vpnaas,sfc,taas,l2-gateway… makes those types unavailable with Heat's own error (HEAT-E99001 …, 400), which is the compatible behaviour. - Absent services: keep the class registered and refuse it the same way (or with "The Resource Type (X) could not be found." if not registered at all); both are 4xx and keep
GET /resource_typestruthful. Do not silently accept them. AWS::CloudWatch::AlarmandAWS::RDS::DBInstanceare template resources frometc/heat/templates/(resource-aliases); the alarm maps to Aodh and is unusable without it.OS::Heat::AutoScalingGroupkeeps working with webhook-signalledScalingPolicy(no Aodh needed).- Properties schemas,
update_allowed(in-place update vs replacement) and attribute names are user-visible: generate the Go schemas fromresource-types_stable-2026.2.jsonrather than re-typing them. - Magnum and Octavia templates as clients: Magnum 23.0.0 no longer ships Heat drivers (release notes
drop-heat-driver-3c45429c61a33b1c.yaml), and Octavia does not call Heat; nothing beyond theOS::Magnum::*/OS::Octavia::*plugins is required.
4. What the clients expect
| Expectation | Where the client relies on it |
|---|---|
Error body has error.message (and optionally error.traceback); anything else is shown as the HTTP class doc | heatclient:heatclient/exc.py:48-73 |
302 from stacks/{name_or_id} with an absolute Location; the client re-sends the same method (DELETE, PUT…) to it | heatclient:heatclient/common/http.py:191-243 |
GET /stacks/{id} (no name) works through the redirect | gc:openstack/orchestration/v1/stacks/requests.go:305-306, tf:openstack/resource_openstack_orchestration_stack_v1.go:257 |
Statuses CREATE_IN_PROGRESS/INIT_COMPLETE while creating, CREATE_COMPLETE, UPDATE_IN_PROGRESS→UPDATE_COMPLETE, DELETE_IN_PROGRESS→DELETE_COMPLETE (or 404 after delete); any other value is a failure for Terraform | tf:openstack/resource_openstack_orchestration_stack_v1.go:226-231, tf:openstack/resource_openstack_orchestration_stack_v1.go:386-391, tf:openstack/resource_openstack_orchestration_stack_v1.go:428-433 |
Stack fields disable_rollback, stack_status, stack_status_reason, timeout_mins, tags, outputs, parameters | tf:openstack/resource_openstack_orchestration_stack_v1.go:265-304 |
Templates and get_file / child templates are resolved client side into files (gophercloud, heatclient template_utils); the server must accept file keys that are URLs or relative paths | gc:openstack/orchestration/v1/stacks/template.go:45-70 |
Skyline lists all projects' stacks with global_tenant=true; under the default policy (stacks:global_index = !) this is 403: deploy a policy override for admins | skyline:src/stores/heat/stack.js:39-47 |
Skyline uses stacks/{name}/{id} for show/update/delete/abandon/template/events/resources, and services | skyline:src/client/heat/index.js:32-85 |
Skyline's apiserver hides the domain heat_user_domain from domain lists: name the stack-user domain accordingly in OpenKeel (devstack uses heat) | skyapi:skyline_apiserver/config/openstack.py:96-103 |
heat-tempest-plugin gabbits expect Location: //stacks/<name>/<uuid>/ patterns, content-type: application/json, and poll with redirects: True | htp:heat_tempest_plugin/tests/api/gabbits/stacks.yaml |
OSC openstack stack create --wait polls GET …/events with marker/sort_dir and stops on <ACTION>_COMPLETE|FAILED of the stack | heatclient heatclient/common/event_utils.py [inferred, not traced line by line] |
5. Database strategy
- Reuse Heat's schema unchanged (Alembic head
97b2a986f922): it is what lets a Go engine resume Python stacks (§ 3.3). The migrated schema and the models agree. - Integrity kept by code: stack-name uniqueness (
heat/db/api.py:982-998), mostresource↔raw_templaterelations,sync_pointrows of dead traversals (deleted at the next traversal or purge). - Locking: no
SELECT … FOR UPDATEanywhere in the convergence path; everything is optimistic CAS (traversal id,atomic_key,engine_id). Isolation: the default of the database (REPEATABLE READ on MariaDB) is fine because every decision is a conditionalUPDATE. Retries: oslo.db retry on deadlock/disconnect for decorated writers. - Soft delete for
stackandservice(deleted_at); hard delete for resources atpurge_db, for stacks withheat-manage purge_deleted. - Startup data: none (no seed rows);
servicerows are written by engines. - Galera: the CAS pattern is Galera-friendly (certification conflicts surface as deadlocks → retried); keep the retries.
6. Authentication, policies, configuration and CLI
- keystonemiddleware in front of the native API,
[keystone_authtoken]; EC2 signature auth for CFN (/v3/ec2tokens); the request context must carryauth_token_info(the full token body) because the engine reuses it to call other services and to create trusts with the token's role ids (heat/engine/clients/os/keystone/heat_keystoneclient.py:207-217). - Trusts: one per stack, trustor = user, trustee =
[trustee]user,impersonation=True, all roles unlesstrusts_delegated_roles; regenerated when another user updates/deletes the stack (heat/engine/stack.py:594-619,heat/engine/stack.py:780-791). OpenKeel's Keystone must implementOS-TRUSTwith impersonation and trust-scoped tokens. - Stack domain: domain (
stack_user_domain_name), domain admin (stack_domain_admin), per-stack project<project>-<stack id>, users with roleheat_stack_user, EC2 credentials (heat/engine/clients/os/keystone/heat_keystoneclient.py:134-590). OpenKeel's Keystone must support domain-scoped tokens,/v3/credentialsof typeec2and/v3/ec2tokens. - oslo.policy: 100 rules,
scope:actionnaming,resource_types:<type>rules evaluated by the engine; deployers override them withpolicy.yaml. A Go service must evaluate the same check-string language (role:,rule:,project_id:%(project_id)s,not,!,@) to accept existing overrides. - Configuration: keep option names and sections (
config_stable-2026.2.conf); the options a Go engine must honour are the limits (max_*), timeouts (stack_action_timeout,engine_life_check_timeout,rpc_response_timeout,error_wait_time,template_fetch_timeout,metadata_put_timeout), auth (deferred_auth_method,reauthentication_auth_method,trusts_delegated_roles,allow_trusts_redelegation,stack_user_domain_*,stack_domain_admin*,heat_stack_user_role), URLs (heat_metadata_server_url,heat_waitcondition_server_url,[clients_heat]/url), transports defaults (default_software_config_transport,default_deployment_signal_transport,default_user_data_format),auth_encryption_key,encrypt_parameters_and_properties,enable_stack_abandon/adopt,hidden_stack_tags,convergence_engine. - CLI
heat-manage:db_sync,db_version,purge_deleted,reset_stack_status,migrate_convergence_1,update_params(encrypt/decrypt),resource_data_list,service list|clean,migrate_properties_data(heat/cmd/manage.py:155-232). Deployment tooling (Helm jobs) callsdb_sync.
7. Conformance strategy
7.1 Black-box suites against the real service
New adapter scripts/adapters/heat.py (skill): Keystone in a subprocess (SQLite, Fernet, the users, roles and the heat domain devstack creates), heat-api and heat-api-cfn from the real etc/heat/api-paste.ini pipelines on one server (CFN under /cfn), an in-process EngineService (convergence, fake:// transport, SQLite), and the suites in a subprocess tagged with X-Test-Id:
heat_integrationtests.functional.*(35 modules, in the Heat repository);heat_tempest_plugin.tests.api.test_heat_api(the gabbi YAML API suite);heat_tempest_plugin.tests.functional.*(24 modules).
Results ([functional-run_stable-2026.2.json](_rewrite/functional-run_stable-2026.2.json), [exchanges_stable-2026.2.json](_rewrite/exchanges_stable-2026.2.json)): 322 tests in 57 modules, one hour, 6681 HTTP exchanges recorded (200 ×4238, 302 ×1836, 201 ×161, 204 ×161, 404 ×149, 202 ×114, 400 ×18, 403 ×3, 300 ×1) — 220 ok, 76 skipped, 4 failed, 22 errors. The 302s are the GET /stacks/{name} → /stacks/{name}/{id} redirects heatclient follows on every lookup by name (01); the 404s are mostly the "stack gone" polls that end each delete. Response shapes inferred from the exchanges are in [response-schemas_stable-2026.2.md](_rewrite/response-schemas_stable-2026.2.md), every distinct error body in [observed-errors_stable-2026.2.md](_rewrite/observed-errors_stable-2026.2.md).
None of the 26 failures and errors points to a Heat behaviour a rewrite should copy differently; they are limits of the test bed:
| Cause | Tests | Why |
|---|---|---|
| no Glance/Nova image and flavor configured | 4 errors (The Parameter (flavor|image) was not provided) | the bed has no Nova/Glance; tests that do not check the service catalogue first fail at validation |
| Neutron / Barbican not in the catalogue | 5 errors (HEAT-E99001 Service … is not available) | same, for tests without a skip guard; this is the expected ResourceTypeUnavailable answer |
SQLite database is locked | 5 errors, 1 timeout | the in-process engine runs many green threads on one SQLite file; never seen on MariaDB |
| test-bed wiring | 4 errors | test_notifications needs a real RabbitMQ (rabbit_userid); test_purge runs heat-manage from PATH; test_keystone_user_with_domain deletes a Keystone user Heat had already removed |
| nested template-resource updates | 5 errors | The Referenced Attribute (the_nested the_str) is incorrect and one update timeout, all in TemplateResourceUpdateTest.test_template_resource_update_template_schema; follow the SQLite errors in the same run [inferred] |
| message wording | 3 failures (test_resource_group_zero_novalidate, test_preview.test_basic_fail, test_remote_stack.test_stack_create_bad_region) | the tests expect older error texts; the bodies Heat 27.0.1 returns are in observed-errors_* and are the reference |
| service-based exposure | 1 failure (test_unavailable_resources_not_created) | the engine raised over the fake:// driver instead of the expected 400; to check on a real bus |
Skips (76) are services missing from the bed: no image (30), Glance (13), Neutron (8 + 12 for trunk and extra-route resources), Swift (7), Zaqar (3), Cinder (1), multi-cloud (1). These scenarios must be replayed on a full OpenKeel cloud.
The adapter sets [DEFAULT]/debug = True (cfg.CONF.set_override("debug", True) in the skill adapter scripts/adapters/heat.py), so 500 and engine-side error bodies carry Python tracebacks (traceback field, 02); paths in them are rewritten to <site-packages>, <venv>, <heat>. Request headers keep the Fernet tokens of the throw-away Keystone: they are expired and worthless, but a replayer must issue its own.
7.2 Convergence state samples
[convergence-state_stable-2026.2.json](_rewrite/convergence-state_stable-2026.2.json): seven scenarios run one after the other on the same stack (template chain: a → (b, c) → d with OS::Heat::TestResource and action_wait_secs; replace; fail), 33 distinct snapshots of stack, resource, sync_point and stack_lock. What they show, and what a Go engine sharing the database must reproduce:
- Create. The stack row gets
current_traversalandcurrent_depsfirst, then every resource row is inserted inINIT COMPLETEwithatomic_keyNULL andcurrent_template_id= the newraw_template. Onesync_pointrow per (resource,is_update) plus one for the stack (entity_id= stack id) is created up front withatomic_key0 and emptyinput_data. A resource being processed hasengine_idset and itsatomic_keyincremented;engine_idgoes back to NULL when it completes. Each completed predecessor adds one key to its successors' sync points ("tuple:(4, True, 'resource')") and bumps theiratomic_key. All sync points are deleted when the stack reachesCOMPLETE. - **
current_deps** is stored as{"edges": [[[2, true], [4, true]], …]}:[resource id, is_update]pairs, update nodes forward and cleanup nodes (false) backward, plus one[[id, false], [id, true]]edge per resource (05 § 2.1–2.2). - Update in place. A new traversal id, 9 sync points (update and cleanup nodes). Resources move to
UPDATE IN_PROGRESS→UPDATE COMPLETE; resources whose definition did not change only getcurrent_template_idmoved to the new template (d keepsCREATE COMPLETEwith template 2): an engine must not requireUPDATEas the action of every resource after an update. - Update while an update runs. The second
PUTreplacescurrent_traversalat once; the resource already locked by the old traversal (a) keepsengine_iduntil itscheck_resourceends, then is processed again under the new traversal (atomic_key5 → 7, template 3 → 4). In this run the stack endedUPDATE FAILEDwithcurrent_traversal=''(empty string, not NULL,heat/engine/stack.py:2406-2407) after b hitdatabase is locked(SQLite, see § 7.1). - Update of a FAILED resource. The next update replaced b, which was
FAILED: a new row (id 5,CREATE IN_PROGRESS,replaces= 3) while the old row gotreplaced_by= 5, then the old row wentDELETE COMPLETEand was removed at the end of the traversal. The cleanup node of the old row joins on the sync point of the dependent's cleanup (tuple:(1, False, 'resource')). - Check walks the same graph with
CHECKactions and creates sync points too. - Delete builds a cleanup-only graph (5 sync points: one per resource, all
is_update0, plus the stack); rows are deleted from the database atDELETE COMPLETE. - Create failure with rollback. A failed resource turns the stack to
ROLLBACK IN_PROGRESSwith a new traversal id; the rollback graph is a cleanup graph over the rows of the failed traversal; the end state isROLLBACK COMPLETEwith no resource rows left.
Stack locks (stack_lock) stay empty in every snapshot: convergence does not take the legacy stack lock (05 § 3). Ids and UUIDs are those of the run; the timings depend on action_wait_secs and the 0.2 s polling, so compare sequences of states, not timings.
7.3 How a rewrite uses them
- Run the same suites against
openkeel-heatwith the same Keystone setup (the adapter's_populate_keystonelists every user, role, domain and endpoint): the gabbi suite is language independent; the Python functional tests only need a tempest config. - Differential testing: replay
exchanges_stable-2026.2.jsonrequest by request (ids differ: compare shapes withresponse-schemas_stable-2026.2.json, statuses and errortype/message). - Convergence: assert the Go engine produces the same sequence of states (stack status, resource action/status, lock held or not, sync points present) as
convergence-state_stable-2026.2.jsonfor the same template, and that it can pick up any intermediate snapshot as a starting database (resume test). - Encryption: decrypt every vector of
crypt-vectors_stable-2026.2.json. - Template parser: port Heat's unit test tables (
heat/tests/test_hot.py,heat/tests/test_function.py,heat/tests/test_parameters.py,heat/tests/test_template_format.py).
8. Gaps found while building the pack
- No template-function vectors: the parser tests are Python unit tests building objects directly; converting them (
conformance_capture.py callsonheat.engine.function.resolve) is the next step for the Go template package. - YAQL:
yaqlfunction (and condition) needs a YAQL engine; none exists in Go. Decide: embed a subset, or refuse with "The function yaql is not supported" (a visible incompatibility). - Undocumented routes: CFN REST-style
/v1/stacks…routes (heat/api/cfn/v1/__init__.py:48-49);stacks/{name}accepting every method;actions:actionpolicy never enforced. validate_templatereturns awebobexception object over RPC in two cases (heat/engine/service.py:1337-1347).resource_schemameant to return 500InvalidGlobalResourcefor unknown types returns 404 (theexceptcatches the wrong class,heat/engine/service.py:1753-1759): keep the 404.- CFN
map_remote_errorsubstring matching (heat/api/aws/exception.py:315-317). - Public RPC methods callable by anyone on the bus (
reset_stack_status,service_manage_*,stop_traversal,stop_all_workers): the Go engine should not expose them. - 2- vs 3-element sync-point keys across an engine upgrade (§ 3.3) [inferred].
- Engine death leaves convergence stacks
IN_PROGRESSuntil timeout or user action: no automatic resume (§ 05 2.6). A Go engine may add one (re-trigger nodes whose lock holder is dead), but it changes behaviour. - Legacy (
convergence = 0) path not specified in detail; recommend migration. - Heat's own unit tests (
heat/tests/api) mock the RPC client and were not recorded: they add nothing over the black-box suites.
9. Non-functional requirements
- Stack sizes up to
max_resources_per_stack(1000) per level andmax_nested_stack_depth(5): the hot sync point of a wide fan-in must not starve (jittered CAS retries, § 05 2.4). - RPC payloads carry the user's token body and resolved attributes: size the broker's frame limits accordingly; never log contexts.
rpc_response_timeout(60 s) bounds synchronous API calls;create_stackdoes validation, trust creation and project creation inside it.- Engine heartbeats every 60 s; liveness probe 2 s.
10. Rewrite checklist
- [ ] Native API: routes, 302 lookup, 405/OPTIONS, status codes of § 2.3, error body and the class-name status table.
- [ ] CFN API: Action router, XML/JSON bodies, AWS error codes, EC2 signature auth via Keystone,
/waitconditionand/signal. - [ ] RPC: three topics, method names, versions, payloads and context fields (§ 03);
listeninganswered with the engine id. - [ ] Database: Heat schema at
97b2a986f922; JSON/List column encoding (nullstrings); the three decrypt methods. - [ ] Convergence invariants 1–10 of § 3.2; tuple-repr sync-point keys; resume from
convergence-statesnapshots. - [ ] Template versions, sections, functions per version with their per-version semantics; parameters and constraints; environment merge strategies.
- [ ] Resource plugins of § 3.4 generated from
resource-types_stable-2026.2.json; availability by catalog and extensions;OS::Heat::TestResource. - [ ] Trusts, stack domain, EC2 credentials, signal URLs (CFN pre-signed, Heat, Swift TempURL); software deployment metadata push.
- [ ] Policies (100 defaults,
resource_types:*), configuration option names,heat-managecommands. - [ ] Conformance: the recorded suites pass, differential replay of the exchanges, crypt vectors decrypt.
Source: reverse-spec-heat/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.