OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

heat — Rewrite pack

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-heatclient stable/2026.2 @ 88120f11830d (5.3.0-2); htp: heat-tempest-plugin master @ ceb78843237c (3.0.0-11); skyline: skyline-console stable/2026.2 @ 806d200392ee; skyapi: skyline-apiserver stable/2026.2 @ 4af72bfd4577; tf: terraform-provider-openstack main @ 1268c8db29c9 (v3.4.0-33); gc: gophercloud @ 4aae510cb4d2 (v2.15.0); Magnum stable/2026.2 @ 1910928b7d9f (23.0.0); Octavia stable/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/

FileUse 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 Accept that is not application/vnd.openstack.orchestration-v1.0 or */*: 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's stacks.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 uses Find everywhere, 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 Allow for unimplemented methods, 204 with Allow for OPTIONS (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 **200 null; software config/deployment create **200 (not 201); preview update 200.
  • Unknown query parameters ignored, except on resource list (400).
  • links[].href built 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 of heat_template_version, AWSTemplateFormatVersion, HeatTemplateFormatVersion (else "Ambiguous versions" / "Template version was not provided"); the value, as str (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 keep 2016-10-14 as a string and not coerce on/off/yes/no differently from PyYAML 1.1.
  • Sections: HOT heat_template_version, description, parameter_groups, parameters, resources, outputs (+ conditions from 2016-10-14); CFN AWSTemplateFormatVersion, 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:line in template-versions_stable-2026.2.json):
Function2010-09-09 (CFN)2012-12-122013-05-232014-10-162015-04-302015-10-152016-04-082016-10-142017-02-242017-09-012018-03-022018-08-312021-04-16
Fn::Base64, Fn::GetAZs, Fn::Join, Fn::MemberListToMap, Fn::Replace, Fn::ResourceFacade, Fn::Split, Ref✓✓✓removedremovedremovedremovedremovedremovedremovedremovedremovedremoved
Fn::Select✓✓✓✓✓removedremovedremovedremovedremovedremovedremovedremoved
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 from get_resource/get_attr/depends_on (dependencies()), and values are resolved lazily against node data (heat/engine/function.py). get_attr of a resource not yet created resolves to None during validation.
  • Parameters: types string, number, json (map), comma_delimited_list (list), boolean; constraints length, range, modulo (2017-02-24+), allowed_values, allowed_pattern, custom_constraint (heat/engine/parameters.py, heat/engine/constraints.py); hidden parameters are encrypted when encrypt_parameters_and_properties and masked as ****** in responses; immutable parameters cannot change on update; pseudo parameters OS::stack_name, OS::stack_id, OS::project_id (and AWS::* for CFN).
  • Environment (heat/engine/environment.py): parameters, parameter_defaults, resource_registry (type mappings, globs OS::Quantum*, template resources by file/URL, per-resource hooks and restricted_actions), event_sinks, encrypted_param_names, parameter_merge_strategies (overwrite, merge, deep_merge) applied when environment_files are 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:

  1. 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).
  2. Graph. Nodes (resource_id, is_update[, node_type]); edges from the template dependencies (update nodes), the requires of existing rows (cleanup ordering, reversed), replaces, and cleanup(x) → update(x) (heat/engine/stack.py:1634-1666). Stored as current_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).
  3. 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.
  4. Resource lock: engine_id + atomic_key CAS on every write; acquire requires engine_id IS NULL; release requires engine_id = me (heat/db/api.py:536-546, heat/engine/resource.py:2159-2183). atomic_key starts NULL (treated as "first write sets 1", heat/db/api.py:519-523).
  5. 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 when predecessors ⊆ keys(input_data); the stack completes when all roots reported.
  6. 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 or null.
  7. Replacement: new row + replaced_by in 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).
  8. Stale locks: on UpdateInProgress, probe the holder with listening (2 s); dead → clear engine_id, mark FAILED, retry (heat/engine/check_resource.py:60-94).
  9. Completion: mark_complete/mark_failed + rollback + purge_db (heat/engine/stack.py:2334-2442).
  10. Timeout: checked when a node starts, from updated_at (or created_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):

ItemFormat written by PythonPitfall
stack.current_depsJSON {"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_idstr(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_byJSON lists of ints, requires sorted descending; needed_by always [] on this branch (heat/engine/resource.py:2128-2129)legacy column
resource.nova_instancephysical id (the column name is historical)
resource.engine_idUUID 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_credsFernet (cryptography_decrypt_v1) or older formatsdecrypt all three (crypt-vectors)
raw_template.templatethe parsed template as JSON (not the YAML text)GET …/template returns it as is
resource_properties_data.dataJSON, or {name: [method, value]} when encrypted
event.resource_propertiesPython 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):

VerdictCountTypes
port — Neutron47OS::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 — Nova9OS::Nova::{Server, Flavor, KeyPair, ServerGroup, HostAggregate, Quota}, AWS::EC2::{Instance, EIP, EIPAssociation}
port — Cinder9OS::Cinder::{Volume, VolumeAttachment, VolumeType, EncryptedVolumeType, QoSSpecs, QoSAssociation, Quota}, AWS::EC2::{Volume, VolumeAttachment}
port — Keystone10OS::Keystone::{Domain, Project, User, Group, Role, UserRoleAssignment, GroupRoleAssignment, Service, Endpoint, Region}
port — Barbican5OS::Barbican::{Secret, Order, GenericContainer, CertificateContainer, RSAContainer}
port — Manila4OS::Manila::{Share, ShareNetwork, ShareType, SecurityService}
port — Swift4OS::Swift::Container, OS::Heat::SwiftSignal, OS::Heat::SwiftSignalHandle, AWS::S3::Bucket
port — Designate2OS::Designate::{Zone, RecordSet}
port — Glance1OS::Glance::WebImage (OS::Glance::Image is hidden on this branch)
port — Ironic1OS::Ironic::Port
internal (Heat itself)33OS::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 — Octavia12OS::Octavia::* — only if OpenKeel exposes an Octavia API (the ovn-octavia-provider reverse-spec is a Neutron/OVN driver, not an API)
drop — absent services22Aodh (8), Zaqar (4), Mistral (3), Magnum (2), Trove (2), Blazar (2), Zun (1)
drop — hidden38already refused by Heat: LBaaS v1/v2, Sahara, Senlin, Monasca, Vitrage, Nova floating IPs, RouterGateway, Designate Domain/Record, HARestarter, …
test only1OS::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 list fwaas, 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_types truthful. Do not silently accept them.
  • AWS::CloudWatch::Alarm and AWS::RDS::DBInstance are template resources from etc/heat/templates/ (resource-aliases); the alarm maps to Aodh and is unusable without it. OS::Heat::AutoScalingGroup keeps working with webhook-signalled ScalingPolicy (no Aodh needed).
  • Properties schemas, update_allowed (in-place update vs replacement) and attribute names are user-visible: generate the Go schemas from resource-types_stable-2026.2.json rather 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 the OS::Magnum::* / OS::Octavia::* plugins is required.

4. What the clients expect

ExpectationWhere the client relies on it
Error body has error.message (and optionally error.traceback); anything else is shown as the HTTP class docheatclient: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 itheatclient:heatclient/common/http.py:191-243
GET /stacks/{id} (no name) works through the redirectgc: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 Terraformtf: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, parameterstf: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 pathsgc: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 adminsskyline:src/stores/heat/stack.js:39-47
Skyline uses stacks/{name}/{id} for show/update/delete/abandon/template/events/resources, and servicesskyline: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: Truehtp: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 stackheatclient 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), most resource ↔ raw_template relations, sync_point rows of dead traversals (deleted at the next traversal or purge).
  • Locking: no SELECT … FOR UPDATE anywhere 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 conditional UPDATE. Retries: oslo.db retry on deadlock/disconnect for decorated writers.
  • Soft delete for stack and service (deleted_at); hard delete for resources at purge_db, for stacks with heat-manage purge_deleted.
  • Startup data: none (no seed rows); service rows 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 carry auth_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 unless trusts_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 implement OS-TRUST with 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 role heat_stack_user, EC2 credentials (heat/engine/clients/os/keystone/heat_keystoneclient.py:134-590). OpenKeel's Keystone must support domain-scoped tokens, /v3/credentials of type ec2 and /v3/ec2tokens.
  • oslo.policy: 100 rules, scope:action naming, resource_types:<type> rules evaluated by the engine; deployers override them with policy.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) calls db_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:

CauseTestsWhy
no Glance/Nova image and flavor configured4 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 catalogue5 errors (HEAT-E99001 Service … is not available)same, for tests without a skip guard; this is the expected ResourceTypeUnavailable answer
SQLite database is locked5 errors, 1 timeoutthe in-process engine runs many green threads on one SQLite file; never seen on MariaDB
test-bed wiring4 errorstest_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 updates5 errorsThe 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 wording3 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 exposure1 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_traversal and current_deps first, then every resource row is inserted in INIT COMPLETE with atomic_key NULL and current_template_id = the new raw_template. One sync_point row per (resource, is_update) plus one for the stack (entity_id = stack id) is created up front with atomic_key 0 and empty input_data. A resource being processed has engine_id set and its atomic_key incremented; engine_id goes back to NULL when it completes. Each completed predecessor adds one key to its successors' sync points ("tuple:(4, True, 'resource')") and bumps their atomic_key. All sync points are deleted when the stack reaches COMPLETE.
  • **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 get current_template_id moved to the new template (d keeps CREATE COMPLETE with template 2): an engine must not require UPDATE as the action of every resource after an update.
  • Update while an update runs. The second PUT replaces current_traversal at once; the resource already locked by the old traversal (a) keeps engine_id until its check_resource ends, then is processed again under the new traversal (atomic_key 5 → 7, template 3 → 4). In this run the stack ended UPDATE FAILED with current_traversal = '' (empty string, not NULL, heat/engine/stack.py:2406-2407) after b hit database 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 got replaced_by = 5, then the old row went DELETE COMPLETE and 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 CHECK actions and creates sync points too.
  • Delete builds a cleanup-only graph (5 sync points: one per resource, all is_update 0, plus the stack); rows are deleted from the database at DELETE COMPLETE.
  • Create failure with rollback. A failed resource turns the stack to ROLLBACK IN_PROGRESS with a new traversal id; the rollback graph is a cleanup graph over the rows of the failed traversal; the end state is ROLLBACK COMPLETE with 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

  1. Run the same suites against openkeel-heat with the same Keystone setup (the adapter's _populate_keystone lists every user, role, domain and endpoint): the gabbi suite is language independent; the Python functional tests only need a tempest config.
  2. Differential testing: replay exchanges_stable-2026.2.json request by request (ids differ: compare shapes with response-schemas_stable-2026.2.json, statuses and error type/message).
  3. 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.json for the same template, and that it can pick up any intermediate snapshot as a starting database (resume test).
  4. Encryption: decrypt every vector of crypt-vectors_stable-2026.2.json.
  5. 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 calls on heat.engine.function.resolve) is the next step for the Go template package.
  • YAQL: yaql function (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:action policy never enforced.
  • validate_template returns a webob exception object over RPC in two cases (heat/engine/service.py:1337-1347).
  • resource_schema meant to return 500 InvalidGlobalResource for unknown types returns 404 (the except catches the wrong class, heat/engine/service.py:1753-1759): keep the 404.
  • CFN map_remote_error substring 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_PROGRESS until 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 and max_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_stack does 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, /waitcondition and /signal.
  • [ ] RPC: three topics, method names, versions, payloads and context fields (§ 03); listening answered with the engine id.
  • [ ] Database: Heat schema at 97b2a986f922; JSON/List column encoding (null strings); the three decrypt methods.
  • [ ] Convergence invariants 1–10 of § 3.2; tuple-repr sync-point keys; resume from convergence-state snapshots.
  • [ ] 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-manage commands.
  • [ ] 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.