Skip to content

Commit 081d61e

Browse files
authored
Merge pull request #55 from derek10cloud/feat/conditional_resource_eval
Feat/conditional resource eval
2 parents 6828e9f + 4764f47 commit 081d61e

File tree

10 files changed

+304
-28
lines changed

10 files changed

+304
-28
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,7 @@ instance/
8383
# Sphinx documentation
8484
docs/_build/
8585

86+
.envrc
87+
88+
venv/
8689
.DS_Store

stackql_deploy/cmd/build.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
run_ext_script,
66
get_type
77
)
8-
from ..lib.config import get_full_context
8+
from ..lib.config import get_full_context, render_value
99
from ..lib.templating import get_queries
1010
from .base import StackQLBase
1111

@@ -49,6 +49,23 @@ def run(self, dry_run, show_queries, on_failure):
4949
# get full context
5050
full_context = get_full_context(self.env, self.global_context, resource, self.logger)
5151

52+
# Check if the resource has an 'if' condition and evaluate it
53+
if 'if' in resource:
54+
condition = resource['if']
55+
try:
56+
# Render the condition with the full context to resolve any template variables
57+
rendered_condition = render_value(self.env, condition, full_context, self.logger)
58+
# Evaluate the condition
59+
condition_result = eval(rendered_condition)
60+
if not condition_result:
61+
self.logger.info(f"skipping resource [{resource['name']}] due to condition: {condition}")
62+
continue
63+
except Exception as e:
64+
catch_error_and_exit(
65+
f"error evaluating condition for resource [{resource['name']}]: {e}",
66+
self.logger
67+
)
68+
5269
if type == 'script':
5370
self.process_script_resource(resource, dry_run, full_context)
5471
continue

stackql_deploy/cmd/teardown.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
catch_error_and_exit,
44
get_type
55
)
6-
from ..lib.config import get_full_context
6+
from ..lib.config import get_full_context, render_value
77
from ..lib.templating import get_queries
88
from .base import StackQLBase
99

@@ -63,6 +63,23 @@ def run(self, dry_run, show_queries, on_failure):
6363
# get full context
6464
full_context = get_full_context(self.env, self.global_context, resource, self.logger)
6565

66+
# Check if the resource has an 'if' condition and evaluate it
67+
if 'if' in resource:
68+
condition = resource['if']
69+
try:
70+
# Render the condition with the full context to resolve any template variables
71+
rendered_condition = render_value(self.env, condition, full_context, self.logger)
72+
# Evaluate the condition
73+
condition_result = eval(rendered_condition)
74+
if not condition_result:
75+
self.logger.info(f"skipping resource [{resource['name']}] due to condition: {condition}")
76+
continue
77+
except Exception as e:
78+
catch_error_and_exit(
79+
f"error evaluating condition for resource [{resource['name']}]: {e}",
80+
self.logger
81+
)
82+
6683
# add reverse export map variable to full context
6784
if 'exports' in resource:
6885
for export in resource['exports']:

test-derek-aws/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# `stackql-deploy` starter project for `aws`
2+
3+
> for starter projects using other providers, try `stackql-deploy test-derek-aws --provider=azure` or `stackql-deploy test-derek-aws --provider=google`
4+
5+
see the following links for more information on `stackql`, `stackql-deploy` and the `aws` provider:
6+
7+
- [`aws` provider docs](https://stackql.io/registry/aws)
8+
- [`stackql`](https://github.com/stackql/stackql)
9+
- [`stackql-deploy` PyPI home page](https://pypi.org/project/stackql-deploy/)
10+
- [`stackql-deploy` GitHub repo](https://github.com/stackql/stackql-deploy)
11+
12+
## Overview
13+
14+
__`stackql-deploy`__ is a stateless, declarative, SQL driven Infrastructure-as-Code (IaC) framework. There is no state file required as the current state is assessed for each resource at runtime. __`stackql-deploy`__ is capable of provisioning, deprovisioning and testing a stack which can include resources across different providers, like a stack spanning `aws` and `azure` for example.
15+
16+
## Prerequisites
17+
18+
This example requires `stackql-deploy` to be installed using __`pip install stackql-deploy`__. The host used to run `stackql-deploy` needs the necessary environment variables set to authenticate to your specific provider, in the case of the `aws` provider, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and optionally `AWS_SESSION_TOKEN` must be set, for more information on authentication to `aws` see the [`aws` provider documentation](https://aws.stackql.io/providers/aws).
19+
20+
## Usage
21+
22+
Adjust the values in the [__`stackql_manifest.yml`__](stackql_manifest.yml) file if desired. The [__`stackql_manifest.yml`__](stackql_manifest.yml) file contains resource configuration variables to support multiple deployment environments, these will be used for `stackql` queries in the `resources` folder.
23+
24+
The syntax for the `stackql-deploy` command is as follows:
25+
26+
```bash
27+
stackql-deploy { build | test | teardown } { stack-directory } { deployment environment} [ optional flags ]
28+
```
29+
30+
### Deploying a stack
31+
32+
For example, to deploy the stack named test-derek-aws to an environment labeled `sit`, run the following:
33+
34+
```bash
35+
stackql-deploy build test-derek-aws sit \
36+
-e AWS_REGION=ap-southeast-2
37+
```
38+
39+
Use the `--dry-run` flag to view the queries to be run without actually running them, for example:
40+
41+
```bash
42+
stackql-deploy build test-derek-aws sit \
43+
-e AWS_REGION=ap-southeast-2 \
44+
--dry-run
45+
```
46+
47+
### Testing a stack
48+
49+
To test a stack to ensure that all resources are present and in the desired state, run the following (in our `sit` deployment example):
50+
51+
```bash
52+
stackql-deploy test test-derek-aws sit \
53+
-e AWS_REGION=ap-southeast-2
54+
```
55+
56+
### Tearing down a stack
57+
58+
To destroy or deprovision all resources in a stack for our `sit` deployment example, run the following:
59+
60+
```bash
61+
stackql-deploy teardown test-derek-aws sit \
62+
-e AWS_REGION=ap-southeast-2
63+
```
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* defines the provisioning and deprovisioning commands
2+
used to create, update or delete the resource
3+
replace queries with your queries */
4+
5+
/*+ exists */
6+
SELECT COUNT(*) as count FROM
7+
(
8+
SELECT vpc_id,
9+
json_group_object(tag_key, tag_value) as tags
10+
FROM aws.ec2.vpc_tags
11+
WHERE region = '{{ region }}'
12+
AND cidr_block = '{{ vpc_cidr_block }}'
13+
GROUP BY vpc_id
14+
HAVING json_extract(tags, '$.Provisioner') = 'stackql'
15+
AND json_extract(tags, '$.StackName') = '{{ stack_name }}'
16+
AND json_extract(tags, '$.StackEnv') = '{{ stack_env }}'
17+
) t;
18+
19+
/*+ create */
20+
INSERT INTO aws.ec2.vpcs (
21+
CidrBlock,
22+
Tags,
23+
EnableDnsSupport,
24+
EnableDnsHostnames,
25+
region
26+
)
27+
SELECT
28+
'{{ vpc_cidr_block }}',
29+
'{{ vpc_tags }}',
30+
true,
31+
true,
32+
'{{ region }}';
33+
34+
/*+ statecheck, retries=5, retry_delay=5 */
35+
SELECT COUNT(*) as count FROM
36+
(
37+
SELECT vpc_id,
38+
cidr_block,
39+
json_group_object(tag_key, tag_value) as tags
40+
FROM aws.ec2.vpc_tags
41+
WHERE region = '{{ region }}'
42+
AND cidr_block = '{{ vpc_cidr_block }}'
43+
GROUP BY vpc_id
44+
HAVING json_extract(tags, '$.Provisioner') = 'stackql'
45+
AND json_extract(tags, '$.StackName') = '{{ stack_name }}'
46+
AND json_extract(tags, '$.StackEnv') = '{{ stack_env }}'
47+
) t
48+
WHERE cidr_block = '{{ vpc_cidr_block }}';
49+
50+
/*+ exports, retries=5, retry_delay=5 */
51+
SELECT vpc_id, vpc_cidr_block FROM
52+
(
53+
SELECT vpc_id, cidr_block as "vpc_cidr_block",
54+
json_group_object(tag_key, tag_value) as tags
55+
FROM aws.ec2.vpc_tags
56+
WHERE region = '{{ region }}'
57+
AND cidr_block = '{{ vpc_cidr_block }}'
58+
GROUP BY vpc_id
59+
HAVING json_extract(tags, '$.Provisioner') = 'stackql'
60+
AND json_extract(tags, '$.StackName') = '{{ stack_name }}'
61+
AND json_extract(tags, '$.StackEnv') = '{{ stack_env }}'
62+
) t;
63+
64+
/*+ delete */
65+
DELETE FROM aws.ec2.vpcs
66+
WHERE data__Identifier = '{{ vpc_id }}'
67+
AND region = '{{ region }}';

test-derek-aws/stackql_manifest.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#
2+
# aws starter project manifest file, add and update values as needed
3+
#
4+
version: 1
5+
name: "test-derek-aws"
6+
description: description for "test-derek-aws"
7+
providers:
8+
- aws
9+
globals:
10+
- name: region
11+
description: aws region
12+
value: "{{ AWS_REGION }}"
13+
- name: global_tags
14+
value:
15+
- Key: Provisioner
16+
Value: stackql
17+
- Key: StackName
18+
Value: "{{ stack_name }}"
19+
- Key: StackEnv
20+
Value: "{{ stack_env }}"
21+
resources:
22+
- name: example_vpc
23+
description: example vpc resource
24+
if: "'{{ stack_env }}' == 'sit'"
25+
props:
26+
- name: vpc_cidr_block
27+
values:
28+
prd:
29+
value: "10.0.0.0/16"
30+
sit:
31+
value: "10.1.0.0/16"
32+
dev:
33+
value: "10.2.0.0/16"
34+
- name: vpc_tags
35+
value:
36+
- Key: Name
37+
Value: "{{ stack_name }}-{{ stack_env }}-vpc"
38+
merge: ['global_tags']
39+
exports:
40+
- vpc_id
41+
- vpc_cidr_block
42+
- name: example_vpc_dev
43+
description: example vpc resource for dev only
44+
if: "'{{ stack_env }}' == 'dev'"
45+
file: example_vpc.iql
46+
props:
47+
- name: vpc_cidr_block
48+
value: "10.3.0.0/16"
49+
- name: vpc_tags
50+
value:
51+
- Key: Name
52+
Value: "{{ stack_name }}-{{ stack_env }}-vpc"
53+
merge: ['global_tags']
54+
exports:
55+
- vpc_id
56+
- vpc_cidr_block

website/docs/manifest-file.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ the fields within the __`stackql_manifest.yml`__ file are described in further d
117117

118118
***
119119

120+
### <span className="docFieldHeading">`resource.if`</span>
121+
122+
<ManifestFields.ResourceIf />
123+
124+
***
125+
120126
### <span className="docFieldHeading">`resource.props`</span>
121127

122128
<ManifestFields.ResourceProps />
@@ -390,4 +396,4 @@ resources:
390396
- {dest_range: "10.200.2.0/24", next_hop_ip: "10.240.0.22"}
391397
```
392398
393-
</File>
399+
</File>

website/docs/manifest_fields/index.js

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1-
export { default as Name } from './name.mdx';
2-
export { default as Description } from './description.mdx';
3-
export { default as Providers } from './providers.mdx';
4-
export { default as Globals } from './globals.mdx';
5-
export { default as GlobalName } from './globals/name.mdx';
6-
export { default as GlobalDescription } from './globals/description.mdx';
7-
export { default as GlobalValue } from './globals/value.mdx';
8-
export { default as Resources } from './resources.mdx';
9-
export { default as ResourceName } from './resources/name.mdx';
10-
export { default as ResourceType } from './resources/type.mdx';
11-
export { default as ResourceFile } from './resources/file.mdx';
12-
export { default as ResourceDescription } from './resources/description.mdx';
13-
export { default as ResourceExports } from './resources/exports.mdx';
14-
export { default as ResourceProps } from './resources/props.mdx';
15-
export { default as ResourceProtected } from './resources/protected.mdx';
16-
export { default as ResourceAuth } from './resources/auth.mdx';
17-
export { default as ResourcePropName } from './resources/props/name.mdx';
18-
export { default as ResourcePropDescription } from './resources/props/description.mdx';
19-
export { default as ResourcePropValue } from './resources/props/value.mdx';
20-
export { default as ResourcePropValues } from './resources/props/values.mdx';
21-
export { default as ResourcePropMerge } from './resources/props/merge.mdx';
22-
export { default as Version } from './version.mdx';
23-
24-
1+
export { default as Name } from "./name.mdx";
2+
export { default as Description } from "./description.mdx";
3+
export { default as Providers } from "./providers.mdx";
4+
export { default as Globals } from "./globals.mdx";
5+
export { default as GlobalName } from "./globals/name.mdx";
6+
export { default as GlobalDescription } from "./globals/description.mdx";
7+
export { default as GlobalValue } from "./globals/value.mdx";
8+
export { default as Resources } from "./resources.mdx";
9+
export { default as ResourceName } from "./resources/name.mdx";
10+
export { default as ResourceType } from "./resources/type.mdx";
11+
export { default as ResourceFile } from "./resources/file.mdx";
12+
export { default as ResourceDescription } from "./resources/description.mdx";
13+
export { default as ResourceExports } from "./resources/exports.mdx";
14+
export { default as ResourceProps } from "./resources/props.mdx";
15+
export { default as ResourceProtected } from "./resources/protected.mdx";
16+
export { default as ResourceAuth } from "./resources/auth.mdx";
17+
export { default as ResourceIf } from "./resources/if.mdx";
18+
export { default as ResourcePropName } from "./resources/props/name.mdx";
19+
export { default as ResourcePropDescription } from "./resources/props/description.mdx";
20+
export { default as ResourcePropValue } from "./resources/props/value.mdx";
21+
export { default as ResourcePropValues } from "./resources/props/values.mdx";
22+
export { default as ResourcePropMerge } from "./resources/props/merge.mdx";
23+
export { default as Version } from "./version.mdx";

website/docs/manifest_fields/resources.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import LeftAlignedTable from '@site/src/components/LeftAlignedTable';
1212
{ name: 'resource.exports', anchor: 'resourceexports' },
1313
{ name: 'resource.protected', anchor: 'resourceprotected' },
1414
{ name: 'resource.description', anchor: 'resourcedescription' },
15+
{ name: 'resource.if', anchor: 'resourceif' },
1516
]}
1617
/>
1718

@@ -52,4 +53,4 @@ A file with the name of the resource with an `.iql` extension is expected to exi
5253

5354
</File>
5455

55-
:::
56+
:::
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import File from '@site/src/components/File';
2+
import LeftAlignedTable from '@site/src/components/LeftAlignedTable';
3+
4+
<LeftAlignedTable type="string" required={false} />
5+
6+
A conditional expression that determines whether a resource should be tested, provisioned, or deprovisioned.
7+
You can use Python expressions to conditionally determine if a resource should be processed.
8+
9+
<File name='stackql_manifest.yml'>
10+
11+
```yaml {3}
12+
resources:
13+
- name: get_transfer_kms_key_id
14+
if: "environment == 'production'"
15+
...
16+
```
17+
18+
</File>
19+
20+
:::info
21+
22+
- Conditions are evaluated as Python expressions.
23+
- You can reference literals (string, boolean, integer, etc.) or runtime template variables.
24+
- If the condition evaluates to `True`, the resource is processed; if `False`, it is skipped.
25+
- Template variables can be referenced using Jinja2 template syntax (`{{ variable }}`).
26+
27+
:::
28+
29+
## Examples
30+
31+
Conditionally process a resource based on environment:
32+
33+
```yaml
34+
resources:
35+
- name: get_transfer_kms_key_id
36+
if: "environment == 'production'"
37+
...
38+
```
39+
40+
Conditionally process based on other variable values:
41+
42+
```yaml
43+
resources:
44+
- name: get_transfer_kms_key_id
45+
if: "some_var == '{{ some_other_var_value }}'"
46+
...
47+
```

0 commit comments

Comments
 (0)