diff --git a/cicd/examples/aws-nginx/README.md b/cicd/examples/aws-nginx/README.md new file mode 100644 index 00000000..b8485620 --- /dev/null +++ b/cicd/examples/aws-nginx/README.md @@ -0,0 +1,148 @@ +# AWS nginx deployment through stackql + +The [resource spec](resources.json), [provider overlay](ec2.merge.json), [stopped-state spec](stopped.json) +and [bootstrap script](user-data.sh) are included here. No Omni checkout or generated SQL is required. +The spec provisions a VPC, two subnets, routing, security group, SSH key pair and nginx VM. + + +Start with **your existing pinned canonical AWS registry** in stackql. The session merge augments +that registry; do not point stackql at Omni's test corpus or its generated effective registry. +Requires stackql's `document_merge` and `inputs_file` support, consuming Omni `v0.1.11-alpha01`. +These stackql steps have not been verified against live AWS. + +Run from the **stackql repository root**. Prepare local SSH keys and nginx bootstrap inputs +(the default AMI lookup is read-only): + +```sh +export CASE="$PWD/.stackql/aws-nginx" +export SSH_CIDR="$(curl --fail --silent --show-error https://checkip.amazonaws.com)/32" +python3 cicd/examples/aws-nginx/prepare.py \ + --case "$CASE" --prefix my-nginx-demo --region ap-southeast-2 \ + --zone-a ap-southeast-2a --zone-b ap-southeast-2b \ + --ssh-cidr "$SSH_CIDR" +``` + +Choose availability zones available to your account. The default image is Amazon Linux 2023 x86_64, +resolved once during preparation through AWS's public SSM parameter. The concrete AMI ID is +persisted in `inputs.json`; launches never use a moving image reference. This default lookup requires +the AWS CLI and `ssm:GetParameter`. `--image-id` with a concrete AMI skips it. Deployment requires +the EC2 permissions. Creates billable AWS resources. HTTP is public; SSH is +restricted to the supplied CIDR. Use `--http-cidr` to restrict HTTP. + +A unique client token is generated once per prepared deployment and persisted alongside the AMI. +Retries/reapply reuse both; preparation refuses to overwrite an existing `inputs.json`. A new case +gets a new token even with the same display prefix. Do not rotate a token while an earlier launch +has an unresolved outcome. + +Open stackql using your usual pinned registry configuration, adding the preview flag: + +```sh +./build/stackql shell --preview '{"unstable":true}' +``` + +Inside that session, merge the overlay and create the deployment: + +```sql +SET "$.document_merge" = '@.stackql/aws-nginx/merge.json'; + +SELECT * FROM stackql_preview.iac.run +WHERE collection = 'my-nginx-demo' + AND region = 'ap-southeast-2' + AND resources_file = 'cicd/examples/aws-nginx/resources.json' + AND inputs_file = '.stackql/aws-nginx/inputs.json'; +``` + +Use the collection and region chosen during preparation. AWS credentials use the normal environment. +Run the same SELECT again to verify unchanged reapply. Each execution gets a fresh run ID automatically. + +## Read managed identities + +```sql +SELECT resource_key, identity, phase +FROM stackql_preview.iac.ledger +WHERE collection = 'my-nginx-demo' AND resource_key = 'instance'; +``` + +Requires an Omni release with the new read-only ledger API (not yet in `v0.1.11-alpha01`). Reads +use the same app/state root as apply, without locks, journals or cloud calls. A pending row's +identity is the previous committed identity, not confirmation of the pending operation. +Explicit `Name` tags are display labels only; ownership and recovery come from the ledger. + +## Query the IP and connect over SSH + +Set the SQL variable `instance_id` to the `identity` returned for the collection's `/instance` row +(`SET instance_id = 'the actual returned identity';`). Then, in the same merged session: + +```sql +SELECT id, public_ip, + CONCAT('ssh -i .stackql/aws-nginx/id_ed25519 ec2-user@', public_ip) AS ssh_command +FROM stackql_unstable_aws.ec2.web_instance +WHERE region = 'ap-southeast-2' + AND "InstanceId.1" = $instance_id; +``` + +Run the returned `ssh_command` in your local terminal. Preparation generated the private key at +`.stackql/aws-nginx/id_ed25519`; its public key was imported into EC2. The private key is local, +not fetched from AWS. If preparation used `--public-key`, substitute its corresponding private key +path. The query returns `public_ip` for the HTTP `/healthz` check too. EC2 running does not guarantee +nginx bootstrap has completed. + +## Stop and destroy + +Declare the VM stopped, then destroy the collection in dependency order: + +```sql +SELECT * FROM stackql_preview.iac.run +WHERE collection = 'my-nginx-demo' + AND region = 'ap-southeast-2' + AND resources_file = 'cicd/examples/aws-nginx/stopped.json' + AND inputs_file = '.stackql/aws-nginx/inputs.json'; + +-- Execute only after the stopped-state result succeeds. +SELECT * FROM stackql_preview.iac.destroy +WHERE collection = 'my-nginx-demo' + AND region = 'ap-southeast-2'; + +-- After teardown, restore the session's canonical document view. +UNSET "$.document_merge"; +``` + +Ledger and journals default to `.stackql/iac` under the working directory (or `--approot`). Merge cache +normally defaults to `.stackql/cache/document-views`; preparation's `merge.json` explicitly selects +its case-local `registry-cache`. Keep the same app root, collection and overlay for every run. +Reopening a session requires repeating SET before continuing. Paths refer to the stackql process. + +For an existing deployment, configure `iacStateRoot` to its original ledger directory before +continuing. A different ledger can create duplicates. Inspect failed/outstanding results before +any further mutation. + +## Recover a pending create, then resume + +First establish the original operation's actual outcome. For an independently verified absent +create that is no longer in flight: + +```sql +SELECT resource_key, phase, version FROM stackql_preview.iac.ledger +WHERE collection = 'my-nginx-demo' AND resource_key = 'subnet_a'; + +SELECT * FROM stackql_preview.iac.recover +WHERE collection = 'my-nginx-demo' + AND resource_key = 'subnet_a' + AND outcome = 'absent' + AND version = '1'; +``` + +Use the version returned by the first query; `1` is only the first-create example. Then repeat the +original apply SELECT with the same collection, state, inputs and resource spec. Existing resources +are retained. Recovery updates local ledger state only, records the operator assertion durably and +checks the pending version under an exclusive lock. It refuses prior committed state or dependents +for absent recovery. If the create completed, use `outcome = 'applied'` and the verified `identity`; +verify completion/readiness, not merely existence. Tags are not ownership or recovery evidence. +Requires an Omni release after `v0.1.11-alpha03` containing recovery. Older releases return an explicit +upgrade error. There is no automatic replay of an uncertain mutation. + +## Offline preparation checks + +```sh +python3 cicd/examples/aws-nginx/test_prepare.py +``` diff --git a/cicd/examples/aws-nginx/ec2.merge.json b/cicd/examples/aws-nginx/ec2.merge.json new file mode 100644 index 00000000..ca75e390 --- /dev/null +++ b/cicd/examples/aws-nginx/ec2.merge.json @@ -0,0 +1,2084 @@ +{ + "components": { + "schemas": { + "WebHandle": { + "properties": { + "id": { + "type": "string" + }, + "ok": { + "type": "boolean" + } + }, + "type": "object" + }, + "WebObservation": { + "properties": { + "id": { + "type": "string" + }, + "public_ip": { + "type": "string" + }, + "state": { + "type": "string" + }, + "State": { + "type": "string" + } + }, + "type": "object" + } + }, + "x-stackQL-resources": { + "web_association_a": { + "id": "aws.ec2.web_association_a", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=AssociateRouteTable&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"AssociateRouteTableResponse\" \"associationId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DisassociateRouteTable&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeRouteTables&Version=2016-11-15&RouteTableId.1={RouteTableId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "items", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ define \"association\" }}{\"id\":{{ toJson (index . \"routeTableAssociationId\") }}}{{ end }}{\"items\":[{{ $r := index . \"DescribeRouteTablesResponse\" }}{{ if index $r \"routeTableSet\" }}{{ $v := index $r \"routeTableSet\" \"item\" }}{{ if index $v \"associationSet\" }}{{ $a := index $v \"associationSet\" \"item\" }}{{ if eq (kindOf $a) \"slice\" }}{{ $sep := separator \",\" }}{{ range $a }}{{ call $sep }}{{ template \"association\" . }}{{ end }}{{ else if eq (kindOf $a) \"map\" }}{{ template \"association\" $a }}{{ end }}{{ end }}{{ end }}]}" + } + } + } + }, + "name": "web_association_a", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_association_a/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_association_a/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_association_a/methods/read_self" + } + ] + } + }, + "web_association_b": { + "id": "aws.ec2.web_association_b", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=AssociateRouteTable&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"AssociateRouteTableResponse\" \"associationId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DisassociateRouteTable&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeRouteTables&Version=2016-11-15&RouteTableId.1={RouteTableId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "items", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ define \"association\" }}{\"id\":{{ toJson (index . \"routeTableAssociationId\") }}}{{ end }}{\"items\":[{{ $r := index . \"DescribeRouteTablesResponse\" }}{{ if index $r \"routeTableSet\" }}{{ $v := index $r \"routeTableSet\" \"item\" }}{{ if index $v \"associationSet\" }}{{ $a := index $v \"associationSet\" \"item\" }}{{ if eq (kindOf $a) \"slice\" }}{{ $sep := separator \",\" }}{{ range $a }}{{ call $sep }}{{ template \"association\" . }}{{ end }}{{ else if eq (kindOf $a) \"map\" }}{{ template \"association\" $a }}{{ end }}{{ end }}{{ end }}]}" + } + } + } + }, + "name": "web_association_b", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_association_b/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_association_b/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_association_b/methods/read_self" + } + ] + } + }, + "web_default_route": { + "id": "aws.ec2.web_default_route", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=CreateRoute&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteRoute&Version=2016-11-15/get" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeRouteTables&Version=2016-11-15&RouteTableId.1={RouteTableId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeRouteTablesResponse\" }}{{ if index $r \"routeTableSet\" }}{{ $v := index $r \"routeTableSet\" \"item\" }}{{ $found := false }}{{ if index $v \"routeSet\" }}{{ $routes := index $v \"routeSet\" \"item\" }}{{ if eq (kindOf $routes) \"slice\" }}{{ range $routes }}{{ if eq (index . \"destinationCidrBlock\") \"0.0.0.0/0\" }}{{ $found = true }}{{ end }}{{ end }}{{ else if eq (kindOf $routes) \"map\" }}{{ if eq (index $routes \"destinationCidrBlock\") \"0.0.0.0/0\" }}{{ $found = true }}{{ end }}{{ end }}{{ end }}{{ if $found }}{\"id\":{{ toJson (index $v \"routeTableId\") }}}{{ else }}{}{{ end }}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_default_route", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_default_route/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_default_route/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_default_route/methods/read_self" + } + ] + } + }, + "web_gateway": { + "id": "aws.ec2.web_gateway", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=CreateInternetGateway&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"CreateInternetGatewayResponse\" \"internetGateway\" \"internetGatewayId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteInternetGateway&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeInternetGateways&Version=2016-11-15&InternetGatewayId.1={InternetGatewayId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeInternetGatewaysResponse\" }}{{ if index $r \"internetGatewaySet\" }}{{ $v := index $r \"internetGatewaySet\" \"item\" }}{\"id\":{{ toJson (index $v \"internetGatewayId\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_gateway", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_gateway/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_gateway/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_gateway/methods/read_self" + } + ] + } + }, + "web_gateway_attachment": { + "id": "aws.ec2.web_gateway_attachment", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=AttachInternetGateway&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DetachInternetGateway&Version=2016-11-15/get" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeInternetGateways&Version=2016-11-15&InternetGatewayId.1={InternetGatewayId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeInternetGatewaysResponse\" }}{{ if index $r \"internetGatewaySet\" }}{{ $v := index $r \"internetGatewaySet\" \"item\" }}{{ if index $v \"attachmentSet\" }}{\"id\":{{ toJson (index $v \"internetGatewayId\") }}}{{ else }}{}{{ end }}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_gateway_attachment", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_gateway_attachment/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_gateway_attachment/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_gateway_attachment/methods/read_self" + } + ] + } + }, + "web_ingress": { + "id": "aws.ec2.web_ingress", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=AuthorizeSecurityGroupIngress&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=RevokeSecurityGroupIngress&Version=2016-11-15/get" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeSecurityGroups&Version=2016-11-15&GroupId.1={GroupId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeSecurityGroupsResponse\" }}{{ if index $r \"securityGroupInfo\" }}{{ $v := index $r \"securityGroupInfo\" \"item\" }}{{ if index $v \"ipPermissions\" }}{\"id\":{{ toJson (index $v \"groupId\") }}}{{ else }}{}{{ end }}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_ingress", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_ingress/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_ingress/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_ingress/methods/read_self" + } + ] + } + }, + "web_instance": { + "id": "aws.ec2.web_instance", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=RunInstances&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"RunInstancesResponse\" \"instancesSet\" \"item\" \"instanceId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + }, + "x-stackQL-beta": { + "await": { + "done": "running", + "interval": "2s", + "maxAttempts": 120, + "statusPath": "state" + }, + "bind": { + "InstanceId.1": "id" + }, + "method": { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/wait_running" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=TerminateInstances&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"TerminateInstancesResponse\" \"instancesSet\" \"item\" \"instanceId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + }, + "x-stackQL-beta": { + "await": { + "done": "terminated", + "interval": "2s", + "maxAttempts": 120, + "statusPath": "state" + }, + "bind": { + "InstanceId.1": "id" + }, + "method": { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/wait_terminated" + } + } + }, + "stop": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=StopInstances&Version=2016-11-15&InstanceId.1={InstanceId.1}/get" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"StopInstancesResponse\" \"instancesSet\" \"item\" \"instanceId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + }, + "x-stackQL-beta": { + "await": { + "done": "stopped", + "interval": "2s", + "maxAttempts": 120, + "statusPath": "state" + }, + "bind": { + "InstanceId.1": "id" + }, + "method": { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/wait_stopped" + } + } + }, + "wait_running": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DescribeInstances&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "transform": { + "body": "{{ $r := index . \"DescribeInstancesResponse\" }}{{ if index $r \"reservationSet\" }}{{ $v := index $r \"reservationSet\" \"item\" \"instancesSet\" \"item\" }}{\"id\":{{ toJson (index $v \"instanceId\") }},\"state\":{{ toJson (index $v \"instanceState\" \"name\") }},\"public_ip\":{{ toJson (index $v \"ipAddress\") }}}{{ else }}{\"state\":\"unknown\"}{{ end }}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "wait_stopped": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DescribeInstances&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "transform": { + "body": "{{ $r := index . \"DescribeInstancesResponse\" }}{{ if index $r \"reservationSet\" }}{{ $v := index $r \"reservationSet\" \"item\" \"instancesSet\" \"item\" }}{\"id\":{{ toJson (index $v \"instanceId\") }},\"state\":{{ toJson (index $v \"instanceState\" \"name\") }},\"public_ip\":{{ toJson (index $v \"ipAddress\") }}}{{ else }}{\"state\":\"unknown\"}{{ end }}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "wait_terminated": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DescribeInstances&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "transform": { + "body": "{{ $r := index . \"DescribeInstancesResponse\" }}{{ if index $r \"reservationSet\" }}{{ $v := index $r \"reservationSet\" \"item\" \"instancesSet\" \"item\" }}{\"id\":{{ toJson (index $v \"instanceId\") }},\"state\":{{ toJson (index $v \"instanceState\" \"name\") }},\"public_ip\":{{ toJson (index $v \"ipAddress\") }}}{{ else }}{\"state\":\"unknown\"}{{ end }}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeInstances&Version=2016-11-15&InstanceId.1={InstanceId.1}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeInstancesResponse\" }}{{ if index $r \"reservationSet\" }}{{ $v := index $r \"reservationSet\" \"item\" }}{{ $v = index $v \"instancesSet\" \"item\" }}{\"id\":{{ toJson (index $v \"instanceId\") }},\"State\":{{ toJson (index $v \"instanceState\" \"name\") }},\"public_ip\":{{ toJson (index $v \"ipAddress\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_instance", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/read_self" + } + ], + "update": [ + { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/stop" + } + ] + }, + "updates": [ + { + "priority": 100, + "requires": [ + "State" + ], + "permits": [ + "State" + ], + "target": { + "$ref": "#/components/x-stackQL-resources/web_instance/methods/stop" + } + } + ] + }, + "web_key": { + "id": "aws.ec2.web_key", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=ImportKeyPair&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"ImportKeyPairResponse\" \"keyName\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteKeyPair&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeKeyPairs&Version=2016-11-15&KeyName.1={KeyName}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeKeyPairsResponse\" }}{{ if index $r \"keySet\" }}{{ $v := index $r \"keySet\" \"item\" }}{\"id\":{{ toJson (index $v \"keyName\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_key", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_key/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_key/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_key/methods/read_self" + } + ] + } + }, + "web_route_table": { + "id": "aws.ec2.web_route_table", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=CreateRouteTable&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"CreateRouteTableResponse\" \"routeTable\" \"routeTableId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteRouteTable&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeRouteTables&Version=2016-11-15&RouteTableId.1={RouteTableId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeRouteTablesResponse\" }}{{ if index $r \"routeTableSet\" }}{{ $v := index $r \"routeTableSet\" \"item\" }}{\"id\":{{ toJson (index $v \"routeTableId\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_route_table", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_route_table/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_route_table/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_route_table/methods/read_self" + } + ] + } + }, + "web_security_group": { + "id": "aws.ec2.web_security_group", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=CreateSecurityGroup&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"CreateSecurityGroupResponse\" \"groupId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteSecurityGroup&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeSecurityGroups&Version=2016-11-15&GroupId.1={GroupId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeSecurityGroupsResponse\" }}{{ if index $r \"securityGroupInfo\" }}{{ $v := index $r \"securityGroupInfo\" \"item\" }}{\"id\":{{ toJson (index $v \"groupId\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_security_group", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_security_group/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_security_group/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_security_group/methods/read_self" + } + ] + } + }, + "web_subnet_a": { + "id": "aws.ec2.web_subnet_a", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=CreateSubnet&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"CreateSubnetResponse\" \"subnet\" \"subnetId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + }, + "x-stackQL-beta": { + "await": { + "done": "available", + "interval": "2s", + "maxAttempts": 120, + "statusPath": "state" + }, + "bind": { + "SubnetId.1": "id" + }, + "method": { + "$ref": "#/components/x-stackQL-resources/web_subnet_a/methods/wait_available" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteSubnet&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "wait_available": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DescribeSubnets&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "transform": { + "body": "{{ $v := index . \"DescribeSubnetsResponse\" \"subnetSet\" \"item\" }}{\"id\":{{ toJson (index $v \"subnetId\") }},\"state\":{{ toJson (index $v \"state\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeSubnets&Version=2016-11-15&SubnetId.1={SubnetId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeSubnetsResponse\" }}{{ if index $r \"subnetSet\" }}{{ $v := index $r \"subnetSet\" \"item\" }}{\"id\":{{ toJson (index $v \"subnetId\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_subnet_a", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_subnet_a/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_subnet_a/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_subnet_a/methods/read_self" + } + ] + } + }, + "web_subnet_b": { + "id": "aws.ec2.web_subnet_b", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=CreateSubnet&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"CreateSubnetResponse\" \"subnet\" \"subnetId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + }, + "x-stackQL-beta": { + "await": { + "done": "available", + "interval": "2s", + "maxAttempts": 120, + "statusPath": "state" + }, + "bind": { + "SubnetId.1": "id" + }, + "method": { + "$ref": "#/components/x-stackQL-resources/web_subnet_b/methods/wait_available" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteSubnet&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "wait_available": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DescribeSubnets&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "transform": { + "body": "{{ $v := index . \"DescribeSubnetsResponse\" \"subnetSet\" \"item\" }}{\"id\":{{ toJson (index $v \"subnetId\") }},\"state\":{{ toJson (index $v \"state\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeSubnets&Version=2016-11-15&SubnetId.1={SubnetId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeSubnetsResponse\" }}{{ if index $r \"subnetSet\" }}{{ $v := index $r \"subnetSet\" \"item\" }}{\"id\":{{ toJson (index $v \"subnetId\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_subnet_b", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_subnet_b/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_subnet_b/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_subnet_b/methods/read_self" + } + ] + } + }, + "web_vpc": { + "id": "aws.ec2.web_vpc", + "methods": { + "create": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=CreateVpc&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"id\":{{ toJson (index . \"CreateVpcResponse\" \"vpc\" \"vpcId\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + }, + "x-stackQL-beta": { + "await": { + "done": "available", + "interval": "2s", + "maxAttempts": 120, + "statusPath": "state" + }, + "bind": { + "VpcId.1": "id" + }, + "method": { + "$ref": "#/components/x-stackQL-resources/web_vpc/methods/wait_available" + } + } + }, + "delete": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DeleteVpc&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebHandle" + }, + "transform": { + "body": "{\"ok\":true}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "wait_available": { + "config": { + "queryParamTranspose": { + "algorithm": "AWSCanonical" + }, + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + } + }, + "operation": { + "$ref": "#/paths/~1?Action=DescribeVpcs&Version=2016-11-15/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "response": { + "mediaType": "application/xml", + "objectKey": "$", + "openAPIDocKey": "200", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "transform": { + "body": "{{ $v := index . \"DescribeVpcsResponse\" \"vpcSet\" \"item\" }}{\"id\":{{ toJson (index $v \"vpcId\") }},\"state\":{{ toJson (index $v \"state\") }}}", + "type": "golang_template_mxj_v0.1.0" + } + } + }, + "read_self": { + "operation": { + "$ref": "#/paths/~1?Action=DescribeVpcs&Version=2016-11-15&VpcId.1={VpcId}/get" + }, + "request": { + "nativeCasing": "pascal" + }, + "config": { + "requestTranslate": { + "algorithm": "get_query_to_post_form_utf_8" + }, + "queryParamTranspose": { + "algorithm": "AWSCanonical" + } + }, + "response": { + "openAPIDocKey": "200", + "mediaType": "application/xml", + "overrideMediaType": "application/json", + "schema_override": { + "$ref": "#/components/schemas/WebObservation" + }, + "objectKey": "$", + "transform": { + "type": "golang_template_mxj_v0.1.0", + "body": "{{ $r := index . \"DescribeVpcsResponse\" }}{{ if index $r \"vpcSet\" }}{{ $v := index $r \"vpcSet\" \"item\" }}{\"id\":{{ toJson (index $v \"vpcId\") }}}{{ else }}{}{{ end }}" + } + } + } + }, + "name": "web_vpc", + "sqlVerbs": { + "delete": [ + { + "$ref": "#/components/x-stackQL-resources/web_vpc/methods/delete" + } + ], + "insert": [ + { + "$ref": "#/components/x-stackQL-resources/web_vpc/methods/create" + } + ], + "select": [ + { + "$ref": "#/components/x-stackQL-resources/web_vpc/methods/read_self" + } + ] + } + } + } + }, + "paths": { + "/?Action=StopInstances&Version=2016-11-15": { + "get": { + "parameters": [ + { + "description": "The IDs of the instances.", + "in": "query", + "name": "InstanceId", + "required": false, + "schema": { + "$ref": "#/components/schemas/InstanceIdStringList" + } + }, + { + "description": "Hibernates the instance if the instance was enabled for hibernation at launch. If the instance cannot hibernate successfully, a normal shutdown occurs. For more information, see Hibernate your Amazon EC2 instance in the Amazon EC2 User Guide. Default: false", + "in": "query", + "name": "Hibernate", + "required": false, + "schema": { + "$ref": "#/components/schemas/Boolean" + } + }, + { + "description": "Specifies whether to bypass the graceful OS shutdown process when the instance is stopped. Bypassing the graceful OS shutdown might result in data loss or corruption (for example, memory contents not flushed to disk or loss of in-flight IOs) or skipped shutdown scripts. Default: false", + "in": "query", + "name": "SkipOsShutdown", + "required": false, + "schema": { + "$ref": "#/components/schemas/Boolean" + } + }, + { + "description": "Checks whether you have the required permissions for the operation, without actually making the request, and provides an error response. If you have the required permissions, the error response is DryRunOperation. Otherwise, it is UnauthorizedOperation.", + "in": "query", + "name": "DryRun", + "required": false, + "schema": { + "$ref": "#/components/schemas/Boolean" + } + }, + { + "description": "Forces the instance to stop. The instance will first attempt a graceful shutdown, which includes flushing file system caches and metadata. If the graceful shutdown fails to complete within the timeout period, the instance shuts down forcibly without flushing the file system caches and metadata. After using this option, you must perform file system check and repair procedures. This option is not recommended for Windows instances. For more information, see Troubleshoot Amazon EC2 instance stop issues in the Amazon EC2 User Guide. Default: false", + "in": "query", + "name": "Force", + "required": false, + "schema": { + "$ref": "#/components/schemas/Boolean" + } + }, + { + "in": "query", + "name": "InstanceId.1", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/?Action=TerminateInstances&Version=2016-11-15": { + "get": { + "parameters": [ + { + "description": "The IDs of the instances. Constraints: Up to 1000 instance IDs. We recommend breaking up this request into smaller batches.", + "in": "query", + "name": "InstanceId", + "required": false, + "schema": { + "$ref": "#/components/schemas/InstanceIdStringList" + } + }, + { + "description": "Forces the instances to terminate. The instance will first attempt a graceful shutdown, which includes flushing file system caches and metadata. If the graceful shutdown fails to complete within the timeout period, the instance shuts down forcibly without flushing the file system caches and metadata.", + "in": "query", + "name": "Force", + "required": false, + "schema": { + "$ref": "#/components/schemas/Boolean" + } + }, + { + "description": "Specifies whether to bypass the graceful OS shutdown process when the instance is terminated. Default: false", + "in": "query", + "name": "SkipOsShutdown", + "required": false, + "schema": { + "$ref": "#/components/schemas/Boolean" + } + }, + { + "description": "Checks whether you have the required permissions for the operation, without actually making the request, and provides an error response. If you have the required permissions, the error response is DryRunOperation. Otherwise, it is UnauthorizedOperation.", + "in": "query", + "name": "DryRun", + "required": false, + "schema": { + "$ref": "#/components/schemas/Boolean" + } + }, + { + "in": "query", + "name": "InstanceId.1", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/?Action=DescribeVpcs&Version=2016-11-15&VpcId.1={VpcId}": { + "get": { + "operationId": "WebRead_vpc", + "parameters": [ + { + "name": "VpcId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Read the addressed object" + } + } + } + }, + "/?Action=DescribeSubnets&Version=2016-11-15&SubnetId.1={SubnetId}": { + "get": { + "operationId": "WebRead_subnet_b", + "parameters": [ + { + "name": "SubnetId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Read the addressed object" + } + } + } + }, + "/?Action=DescribeInternetGateways&Version=2016-11-15&InternetGatewayId.1={InternetGatewayId}": { + "get": { + "operationId": "WebRead_gateway_attachment", + "parameters": [ + { + "name": "InternetGatewayId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Read the addressed object" + } + } + } + }, + "/?Action=DescribeRouteTables&Version=2016-11-15&RouteTableId.1={RouteTableId}": { + "get": { + "operationId": "WebListAssociations", + "parameters": [ + { + "name": "RouteTableId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Table associations" + } + } + } + }, + "/?Action=DescribeRouteTables&Version=2016-11-15&Filter.1.Value.1={AssociationId}&Filter.1.Name=association.association-id": { + "get": { + "operationId": "WebRead_association_b", + "parameters": [ + { + "name": "AssociationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Read the addressed object" + } + } + } + }, + "/?Action=DescribeSecurityGroups&Version=2016-11-15&GroupId.1={GroupId}": { + "get": { + "operationId": "WebRead_ingress", + "parameters": [ + { + "name": "GroupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Read the addressed object" + } + } + } + }, + "/?Action=DescribeKeyPairs&Version=2016-11-15&KeyName.1={KeyName}": { + "get": { + "operationId": "WebRead_key", + "parameters": [ + { + "name": "KeyName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Read the addressed object" + } + } + } + }, + "/?Action=DescribeInstances&Version=2016-11-15&InstanceId.1={InstanceId.1}": { + "get": { + "operationId": "WebRead_instance", + "parameters": [ + { + "name": "InstanceId.1", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "InstanceId", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Read the addressed object" + } + } + } + }, + "/?Action=StopInstances&Version=2016-11-15&InstanceId.1={InstanceId.1}": { + "get": { + "operationId": "WebStopInstance", + "parameters": [ + { + "name": "InstanceId.1", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "State", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "stopped" + ] + } + }, + { + "name": "InstanceId", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Stop requested" + } + } + } + } + } +} diff --git a/cicd/examples/aws-nginx/image_inputs.py b/cicd/examples/aws-nginx/image_inputs.py new file mode 100644 index 00000000..5756944e --- /dev/null +++ b/cicd/examples/aws-nginx/image_inputs.py @@ -0,0 +1,20 @@ +"""Resolve the image once during preparation, never during a launch retry.""" +import re +import subprocess + +PARAMETER = '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64' + + +def resolve_image(image_id, region, runner=subprocess.run): + if image_id is None: + try: + result = runner([ + 'aws', 'ssm', 'get-parameter', '--region', region, + '--name', PARAMETER, '--query', 'Parameter.Value', '--output', 'text', + ], check=True, capture_output=True, text=True, timeout=30) + except subprocess.CalledProcessError as error: + raise ValueError('AMI lookup failed: ' + (error.stderr or str(error)).strip()) from error + image_id = result.stdout.strip() + if not re.fullmatch(r'ami-(?:[0-9a-f]{8}|[0-9a-f]{17})', image_id): + raise ValueError('image must be a concrete AMI ID; moving resolve:ssm references are not accepted') + return image_id diff --git a/cicd/examples/aws-nginx/prepare.py b/cicd/examples/aws-nginx/prepare.py new file mode 100644 index 00000000..caa2b88a --- /dev/null +++ b/cicd/examples/aws-nginx/prepare.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Prepare local inputs and a document merge request. Resolves an AMI read-only; never provisions cloud resources.""" +import argparse +import base64 +import json +import ipaddress +from pathlib import Path +import re +import subprocess +import uuid +from image_inputs import resolve_image + +p=argparse.ArgumentParser(description=__doc__) +p.add_argument('--case',type=Path,required=True) +p.add_argument('--region', required=True) +p.add_argument('--image-id', help='Concrete AMI ID; otherwise resolve AL2023 through AWS SSM once and persist it') +p.add_argument('--zone-a',required=True) +p.add_argument('--zone-b',required=True) +p.add_argument('--ssh-cidr',required=True) +p.add_argument('--http-cidr',default='0.0.0.0/0') +p.add_argument('--prefix',default='omni-web') +p.add_argument('--instance-type',default='t3.micro') +p.add_argument('--public-key',type=Path) +p.add_argument('--poll-interval',default='2s') +p.add_argument('--poll-attempts',type=int,default=120) +a=p.parse_args() +if not re.fullmatch(r'[A-Za-z0-9-]{1,40}',a.prefix):p.error('invalid prefix') +for cidr in (a.ssh_cidr,a.http_cidr): + if ipaddress.ip_network(cidr).version!=4:p.error('IPv4 CIDRs required') +if a.poll_attempts<1:p.error('poll attempts must be positive') +a.case=a.case.resolve();a.case.mkdir(mode=0o700,parents=True,exist_ok=True) +if (a.case/'inputs.json').exists():p.error('case already prepared; use its inputs and ledger, or a new directory') +try: + a.image_id = resolve_image(a.image_id, a.region) +except (ValueError, OSError, subprocess.SubprocessError) as error: + p.error('cannot pin AMI: ' + str(error)) + +key=a.public_key +if key is None: + private=a.case/'id_ed25519' + if private.exists():p.error('refusing to overwrite local SSH key') + subprocess.run(['ssh-keygen','-q','-t','ed25519','-N','','-C',a.prefix,'-f',str(private)],check=True) + key=private.with_suffix('.pub') +public=key.read_text().strip() +if not public.startswith('ssh-'):p.error('OpenSSH public key required') +base=Path(__file__).resolve().parent +script=(base/'user-data.sh').read_text().replace('__DEPLOYMENT__',a.prefix) +inputs={'image_id':a.image_id,'instance_type':a.instance_type,'zone_a':a.zone_a,'zone_b':a.zone_b,'ssh_cidr':a.ssh_cidr,'http_cidr':a.http_cidr,'key_name':a.prefix+'-ssh','group_name':a.prefix+'-web','client_token':uuid.uuid4().hex,'public_key_material':base64.b64encode(public.encode()).decode(),'user_data':base64.b64encode(script.encode()).decode()} +# Tags are user inputs to the static deployment specification. +for resource in json.loads((base/'resources.json').read_text())['resources']: + inputs['tag_'+resource['key']]=a.prefix+'-'+resource['key'] +patch=json.loads((base/'ec2.merge.json').read_text()) +for resource in patch['components']['x-stackQL-resources'].values(): + for method in resource['methods'].values(): + waiter=method.get('x-stackQL-beta',{}).get('await') + if waiter:waiter.update(interval=a.poll_interval,maxAttempts=a.poll_attempts) +for name,data in {'inputs.json':inputs,'merge.json':{'patches':[{'service':'stackql_unstable_aws.ec2','merge':patch}],'doc_cache':{'dir':str(a.case/'registry-cache')}}}.items(): + path=a.case/name;path.write_text(json.dumps(data,indent=2)+'\n');path.chmod(0o600) +(a.case/'user-data.sh').write_text(script) diff --git a/cicd/examples/aws-nginx/resources.json b/cicd/examples/aws-nginx/resources.json new file mode 100644 index 00000000..70b0a98d --- /dev/null +++ b/cicd/examples/aws-nginx/resources.json @@ -0,0 +1,327 @@ +{ + "resources": [ + { + "key": "vpc", + "provider": "aws", + "address": "ec2.web_vpc", + "identity": "id", + "addressed_by": "VpcId", + "write_once": { + "CidrBlock": "10.77.0.0/16", + "TagSpecification.1.ResourceType": "vpc", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_vpc" + } + }, + "desired": {} + }, + { + "key": "subnet_a", + "provider": "aws", + "address": "ec2.web_subnet_a", + "identity": "id", + "addressed_by": "SubnetId", + "write_once": { + "AvailabilityZone": { + "from": "input", + "attribute": "zone_a" + }, + "CidrBlock": "10.77.1.0/24", + "TagSpecification.1.ResourceType": "subnet", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_subnet_a" + } + }, + "desired": {}, + "inbound": [ + { + "from": "vpc", + "as": "VpcId" + } + ] + }, + { + "key": "subnet_b", + "provider": "aws", + "address": "ec2.web_subnet_b", + "identity": "id", + "addressed_by": "SubnetId", + "write_once": { + "AvailabilityZone": { + "from": "input", + "attribute": "zone_b" + }, + "CidrBlock": "10.77.2.0/24", + "TagSpecification.1.ResourceType": "subnet", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_subnet_b" + } + }, + "desired": {}, + "inbound": [ + { + "from": "vpc", + "as": "VpcId" + } + ] + }, + { + "key": "gateway", + "provider": "aws", + "address": "ec2.web_gateway", + "identity": "id", + "addressed_by": "InternetGatewayId", + "write_once": { + "TagSpecification.1.ResourceType": "internet-gateway", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_gateway" + } + }, + "desired": {} + }, + { + "key": "gateway_attachment", + "provider": "aws", + "address": "ec2.web_gateway_attachment", + "identity": "id", + "addressed_by": "InternetGatewayId", + "write_once": {}, + "desired": {}, + "inbound": [ + { + "from": "gateway", + "as": "InternetGatewayId" + }, + { + "from": "vpc", + "as": "VpcId" + } + ] + }, + { + "key": "route_table", + "provider": "aws", + "address": "ec2.web_route_table", + "identity": "id", + "addressed_by": "RouteTableId", + "write_once": { + "TagSpecification.1.ResourceType": "route-table", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_route_table" + } + }, + "desired": {}, + "inbound": [ + { + "from": "vpc", + "as": "VpcId" + } + ] + }, + { + "key": "default_route", + "provider": "aws", + "address": "ec2.web_default_route", + "identity": "id", + "addressed_by": "RouteTableId", + "write_once": { + "DestinationCidrBlock": "0.0.0.0/0" + }, + "desired": {}, + "inbound": [ + { + "from": "route_table", + "as": "RouteTableId" + }, + { + "from": "gateway_attachment", + "as": "GatewayId" + } + ] + }, + { + "key": "association_a", + "provider": "aws", + "address": "ec2.web_association_a", + "identity": "id", + "addressed_by": "AssociationId", + "write_once": {}, + "desired": {}, + "inbound": [ + { + "from": "route_table", + "as": "RouteTableId" + }, + { + "from": "subnet_a", + "as": "SubnetId" + } + ] + }, + { + "key": "association_b", + "provider": "aws", + "address": "ec2.web_association_b", + "identity": "id", + "addressed_by": "AssociationId", + "write_once": {}, + "desired": {}, + "inbound": [ + { + "from": "route_table", + "as": "RouteTableId" + }, + { + "from": "subnet_b", + "as": "SubnetId" + } + ] + }, + { + "key": "security_group", + "provider": "aws", + "address": "ec2.web_security_group", + "identity": "id", + "addressed_by": "GroupId", + "write_once": { + "GroupDescription": "omnisdk nginx demo", + "GroupName": { + "from": "input", + "attribute": "group_name" + }, + "TagSpecification.1.ResourceType": "security-group", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_security_group" + } + }, + "desired": {}, + "inbound": [ + { + "from": "vpc", + "as": "VpcId" + } + ] + }, + { + "key": "ingress", + "provider": "aws", + "address": "ec2.web_ingress", + "identity": "id", + "addressed_by": "GroupId", + "write_once": { + "IpPermissions.1.FromPort": "22", + "IpPermissions.1.IpProtocol": "tcp", + "IpPermissions.1.IpRanges.1.CidrIp": { + "from": "input", + "attribute": "ssh_cidr" + }, + "IpPermissions.1.ToPort": "22", + "IpPermissions.2.FromPort": "80", + "IpPermissions.2.IpProtocol": "tcp", + "IpPermissions.2.IpRanges.1.CidrIp": { + "from": "input", + "attribute": "http_cidr" + }, + "IpPermissions.2.ToPort": "80" + }, + "desired": {}, + "inbound": [ + { + "from": "security_group", + "as": "GroupId" + } + ] + }, + { + "key": "key", + "provider": "aws", + "address": "ec2.web_key", + "identity": "id", + "addressed_by": "KeyName", + "write_once": { + "KeyName": { + "from": "input", + "attribute": "key_name" + }, + "PublicKeyMaterial": { + "from": "input", + "attribute": "public_key_material" + } + }, + "desired": {} + }, + { + "key": "instance", + "provider": "aws", + "address": "ec2.web_instance", + "identity": "id", + "addressed_by": "InstanceId.1", + "write_once": { + "ClientToken": { + "from": "input", + "attribute": "client_token" + }, + "ImageId": { + "from": "input", + "attribute": "image_id" + }, + "InstanceType": { + "from": "input", + "attribute": "instance_type" + }, + "MaxCount": "1", + "MetadataOptions.HttpTokens": "required", + "MinCount": "1", + "NetworkInterface.1.AssociatePublicIpAddress": "true", + "NetworkInterface.1.DeviceIndex": "0", + "TagSpecification.1.ResourceType": "instance", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_instance" + }, + "UserData": { + "from": "input", + "attribute": "user_data" + } + }, + "desired": {}, + "inbound": [ + { + "from": "subnet_a", + "as": "subnet_id" + }, + { + "from": "key", + "as": "key_name" + }, + { + "from": "ingress", + "as": "security_group" + }, + { + "from": "default_route", + "as": "route_ready" + }, + { + "from": "association_a", + "as": "association_ready" + } + ], + "via_type": "starlark", + "via": "{\"NetworkInterface.1.SubnetId\":inbox[\"subnet_id\"],\"NetworkInterface.1.SecurityGroupId.1\":inbox[\"security_group\"],\"KeyName\":inbox[\"key_name\"]}" + } + ] +} diff --git a/cicd/examples/aws-nginx/stopped.json b/cicd/examples/aws-nginx/stopped.json new file mode 100644 index 00000000..9821166f --- /dev/null +++ b/cicd/examples/aws-nginx/stopped.json @@ -0,0 +1,67 @@ +{ + "resources": [ + { + "key": "instance", + "provider": "aws", + "address": "ec2.web_instance", + "identity": "id", + "addressed_by": "InstanceId.1", + "write_once": { + "ClientToken": { + "from": "input", + "attribute": "client_token" + }, + "ImageId": { + "from": "input", + "attribute": "image_id" + }, + "InstanceType": { + "from": "input", + "attribute": "instance_type" + }, + "MaxCount": "1", + "MetadataOptions.HttpTokens": "required", + "MinCount": "1", + "NetworkInterface.1.AssociatePublicIpAddress": "true", + "NetworkInterface.1.DeviceIndex": "0", + "TagSpecification.1.ResourceType": "instance", + "TagSpecification.1.Tag.1.Key": "Name", + "TagSpecification.1.Tag.1.Value": { + "from": "input", + "attribute": "tag_instance" + }, + "UserData": { + "from": "input", + "attribute": "user_data" + } + }, + "desired": { + "State": "stopped" + }, + "inbound": [ + { + "from": "subnet_a", + "as": "subnet_id" + }, + { + "from": "key", + "as": "key_name" + }, + { + "from": "ingress", + "as": "security_group" + }, + { + "from": "default_route", + "as": "route_ready" + }, + { + "from": "association_a", + "as": "association_ready" + } + ], + "via_type": "starlark", + "via": "{\"NetworkInterface.1.SubnetId\":inbox[\"subnet_id\"],\"NetworkInterface.1.SecurityGroupId.1\":inbox[\"security_group\"],\"KeyName\":inbox[\"key_name\"]}" + } + ] +} diff --git a/cicd/examples/aws-nginx/test_prepare.py b/cicd/examples/aws-nginx/test_prepare.py new file mode 100644 index 00000000..1ea118af --- /dev/null +++ b/cicd/examples/aws-nginx/test_prepare.py @@ -0,0 +1,72 @@ +"""Offline checks of executable image defaults and explicit pinning.""" +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import Mock +from image_inputs import resolve_image, PARAMETER + + +class PrepareInputsTest(unittest.TestCase): + def prepare(self, *extra): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + public = root / 'existing.pub' + public.write_text('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFixture offline-test\n') + case = root / 'case' + result = subprocess.run([ + sys.executable, str(Path(__file__).with_name('prepare.py')), + '--case', str(case), '--region', 'ap-southeast-2', '--image-id', 'ami-00000000000000001', '--zone-a', 'ap-southeast-2a', '--zone-b', 'ap-southeast-2b', + '--ssh-cidr', '203.0.113.7/32', '--public-key', str(public), *extra, + ], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads((case / 'inputs.json').read_text()) + + def test_prepared_image_is_concrete(self): + self.assertEqual(self.prepare()['image_id'], 'ami-00000000000000001') + + def test_default_resolves_once_before_launch(self): + runner = Mock(return_value=Mock(stdout='ami-00000000000000002\n')) + self.assertEqual(resolve_image(None, 'ap-southeast-2', runner), 'ami-00000000000000002') + runner.assert_called_once() + self.assertIn(PARAMETER, runner.call_args.args[0]) + self.assertIn('ap-southeast-2', runner.call_args.args[0]) + + def test_pinned_image_never_looks_up_latest(self): + runner = Mock() + self.assertEqual(resolve_image('ami-00000000000000001', 'ap-southeast-2', runner), 'ami-00000000000000001') + runner.assert_not_called() + + def test_moving_reference_is_rejected(self): + with self.assertRaises(ValueError): + resolve_image('resolve:ssm:' + PARAMETER, 'ap-southeast-2') + + def test_separate_deployments_do_not_recycle_client_tokens(self): + first, second = self.prepare(), self.prepare() + self.assertNotEqual(first['client_token'], second['client_token']) + self.assertLessEqual(len(first['client_token']), 64) + + def test_existing_inputs_are_not_rotated(self): + with tempfile.TemporaryDirectory() as temporary: + case = Path(temporary) + original = '{"image_id":"ami-00000000000000001","client_token":"persisted-token"}' + (case / 'inputs.json').write_text(original) + result = subprocess.run([ + sys.executable, str(Path(__file__).with_name('prepare.py')), + '--case', str(case), '--region', 'ap-southeast-2', + '--zone-a', 'ap-southeast-2a', '--zone-b', 'ap-southeast-2b', + '--ssh-cidr', '203.0.113.7/32', + ], capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertIn('case already prepared', result.stderr) + self.assertEqual((case / 'inputs.json').read_text(), original) + + def test_explicit_image_can_be_pinned(self): + image = 'ami-00000000000000001' + self.assertEqual(self.prepare('--image-id', image)['image_id'], image) + + +if __name__ == '__main__': + unittest.main() diff --git a/cicd/examples/aws-nginx/user-data.sh b/cicd/examples/aws-nginx/user-data.sh new file mode 100644 index 00000000..e95317a6 --- /dev/null +++ b/cicd/examples/aws-nginx/user-data.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -euxo pipefail +dnf install -y nginx +cat > /etc/nginx/nginx.conf <<'NGINX' +user nginx; +worker_processes auto; +pid /run/nginx.pid; +error_log /var/log/nginx/error.log; +events { worker_connections 1024; } +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /var/log/nginx/access.log; + server { + listen 80 default_server; + server_name _; + root /usr/share/nginx/html; + add_header X-Omnisdk-Deployment "__DEPLOYMENT__" always; + location / { try_files $uri $uri/ =404; } + location = /healthz { default_type text/plain; return 200 "omnisdk __DEPLOYMENT__ ready\n"; } + } +} +NGINX +cat > /usr/share/nginx/html/index.html <<'HTML' +
nginx, provisioned through merged EC2 documents.
+HTML +nginx -t +systemctl enable --now nginx diff --git a/docs/provider_sessions.md b/docs/provider_sessions.md index a4838454..776ab9f0 100644 --- a/docs/provider_sessions.md +++ b/docs/provider_sessions.md @@ -187,3 +187,18 @@ With caller access management enabled, variable reads require `session/variables resets require `session/config:write`. Per-variable/per-secret owner and group grants are not yet implemented. Resolved SQL objects are checked before execution; variable substitution grants no additional provider/backend permission. Existing credential reload remains a separate mechanism. + +## Session document merge + +```sql +SET "$.document_merge" = '{"patches":[{"service":"stackql_unstable_google.compute","merge":{"components":{"x-stackQL-resources":{"networks":{"methods":{"list":{"response":{"objectKey":"$.items"}}}}}}}}]}'; +-- Alternatively, read the same configuration from a server-local file: +SET "$.document_merge" = '@/absolute/path/merge.json'; +UNSET "$.document_merge"; +``` + +Requires the document-driven provider/preview integration. Subsequent document descriptions, queries and IaC use the effective view; the base registry is unchanged. Each SET replaces the patch set against the base (include all desired fragments in order). Failed SET preserves the previous view; UNSET restores the base. New client sessions inherit the configured view but later changes are isolated. + +The default merge cache is `