Skip to content

Commit a0421a0

Browse files
authored
Merge pull request #19 from stackql-labs/claude/resolve-merge-conflicts-Y5FZg
Claude/resolve merge conflicts y5 f zg
2 parents 5c737b3 + 9427935 commit a0421a0

8 files changed

Lines changed: 462 additions & 201 deletions

File tree

docs/exports.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Exports
2+
3+
Exports allow resources to publish values (e.g. IDs, ARNs, names) so that
4+
subsequent resources in the stack can reference them.
5+
6+
## Defining exports
7+
8+
Add an `exports` field to a resource in `stackql_manifest.yml`. The exports
9+
query in the resource's `.iql` file must return columns that match the
10+
export names.
11+
12+
### Simple exports
13+
14+
```yaml
15+
resources:
16+
- name: example_vpc
17+
props:
18+
- name: cidr_block
19+
value: "10.0.0.0/16"
20+
exports:
21+
- vpc_id
22+
- vpc_cidr_block
23+
```
24+
25+
The `exports` anchor in the `.iql` file must return these columns:
26+
27+
```sql
28+
/*+ exports */
29+
SELECT vpc_id, cidr_block AS vpc_cidr_block
30+
FROM awscc.ec2.vpcs
31+
WHERE region = '{{ region }}' AND vpc_id = '{{ vpc_id }}';
32+
```
33+
34+
### Aliased exports
35+
36+
Use the mapping format to rename columns on export:
37+
38+
```yaml
39+
exports:
40+
- arn: aws_iam_cross_account_role_arn
41+
- role_name: aws_iam_role_name
42+
```
43+
44+
Here the exports query returns columns `arn` and `role_name`, but they are
45+
stored in the context under the alias names `aws_iam_cross_account_role_arn`
46+
and `aws_iam_role_name`.
47+
48+
## Referencing exported values
49+
50+
Exported values are injected into the global template context and can be
51+
referenced by any subsequent resource using `{{ variable_name }}`.
52+
53+
### Unscoped references
54+
55+
The simplest way to reference an export is by its name (or alias):
56+
57+
```yaml
58+
- name: example_subnet
59+
props:
60+
- name: vpc_id
61+
value: "{{ vpc_id }}"
62+
```
63+
64+
Unscoped names follow **last-writer-wins** semantics: if two resources both
65+
export a variable called `vpc_id`, subsequent resources will see the value
66+
from whichever resource was processed last.
67+
68+
### Resource-scoped references
69+
70+
Every export is also available under a **resource-scoped** name of the form
71+
`resource_name.variable_name`. This name is **immutable** -- once set, it
72+
cannot be overwritten by a later resource:
73+
74+
```yaml
75+
- name: example_subnet
76+
props:
77+
- name: vpc_id
78+
value: "{{ example_vpc.vpc_id }}"
79+
```
80+
81+
Resource-scoped names are useful when:
82+
83+
- Multiple resources export variables with the same name and you need to
84+
reference a specific one unambiguously.
85+
- You want to make it clear which resource a value originates from for
86+
readability and maintainability.
87+
88+
### Example
89+
90+
```yaml
91+
resources:
92+
- name: aws_cross_account_role
93+
file: aws/iam/roles.iql
94+
props:
95+
- name: role_name
96+
value: "{{ stack_name }}-{{ stack_env }}-role"
97+
# ...
98+
exports:
99+
- role_name: aws_iam_cross_account_role_name
100+
- arn: aws_iam_cross_account_role_arn
101+
102+
- name: databricks_account/credentials
103+
props:
104+
- name: credentials_name
105+
value: "{{ stack_name }}-{{ stack_env }}-credentials"
106+
- name: aws_credentials
107+
value:
108+
sts_role:
109+
# Unscoped -- works because only one resource exports this name
110+
role_arn: "{{ aws_iam_cross_account_role_arn }}"
111+
# Resource-scoped -- always unambiguous
112+
# role_arn: "{{ aws_cross_account_role.aws_iam_cross_account_role_arn }}"
113+
```
114+
115+
## Protected exports
116+
117+
Sensitive values can be masked in log output by listing them under
118+
`protected`:
119+
120+
```yaml
121+
- name: secret_resource
122+
props: []
123+
exports:
124+
- api_key
125+
protected:
126+
- api_key
127+
```
128+
129+
The actual value is still stored in the context and usable by templates;
130+
only the log messages are masked.
131+
132+
## Stack-level exports
133+
134+
The top-level `exports` field in the manifest lists variables that are
135+
written to a JSON output file (when `--output` is specified):
136+
137+
```yaml
138+
exports:
139+
- vpc_id
140+
- subnet_id
141+
```
142+
143+
The output file always includes `stack_name`, `stack_env`, and
144+
`elapsed_time` automatically.

src/commands/base.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,15 @@ use std::process;
1313
use log::{debug, error, info};
1414
use pgwire_lite::PgwireLite;
1515

16-
use crate::core::config::{
17-
get_full_context, render_globals, render_string_value,
18-
};
16+
use crate::core::config::{get_full_context, render_globals, render_string_value};
1917
use crate::core::env::load_env_vars;
2018
use crate::core::templating::{self, ParsedQuery};
2119
use crate::core::utils::{
2220
catch_error_and_exit, check_exports_as_statecheck_proxy, export_vars, perform_retries,
2321
pull_providers, run_ext_script, run_stackql_command, run_stackql_query, show_query,
2422
};
2523
use crate::resource::manifest::{Manifest, Resource};
24+
use crate::resource::validation::validate_manifest;
2625
use crate::template::engine::TemplateEngine;
2726

2827
/// Core state for all command operations, equivalent to Python's StackQLBase.
@@ -54,6 +53,18 @@ impl CommandRunner {
5453

5554
// Load manifest
5655
let manifest = Manifest::load_from_dir_or_exit(stack_dir);
56+
57+
// Validate manifest rules
58+
if let Err(errors) = validate_manifest(&manifest) {
59+
for err in &errors {
60+
error!("{}", err);
61+
}
62+
catch_error_and_exit(&format!(
63+
"Manifest validation failed with {} error(s)",
64+
errors.len()
65+
));
66+
}
67+
5768
let stack_name = manifest.name.clone();
5869

5970
// Render globals
@@ -804,4 +815,3 @@ fn evaluate_simple_condition(condition: &str) -> Option<bool> {
804815

805816
None
806817
}
807-

src/commands/common_args.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,3 @@ pub fn on_failure() -> Arg {
9898
.value_parser(value_parser!(FailureAction))
9999
.default_value("error")
100100
}
101-

src/core/utils.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,11 @@ pub fn export_vars(
489489
global_context.entry(scoped_key.clone()).or_insert_with(|| {
490490
info!(
491491
"set {} [{}] to [{}] in exports",
492-
if is_protected { "protected variable" } else { "variable" },
492+
if is_protected {
493+
"protected variable"
494+
} else {
495+
"variable"
496+
},
493497
scoped_key,
494498
display_value,
495499
);
@@ -499,7 +503,11 @@ pub fn export_vars(
499503
// --- global (unscoped) key (can be overridden by later resources) ---
500504
info!(
501505
"set {} [{}] to [{}] in exports",
502-
if is_protected { "protected variable" } else { "variable" },
506+
if is_protected {
507+
"protected variable"
508+
} else {
509+
"variable"
510+
},
503511
key,
504512
display_value,
505513
);
@@ -618,7 +626,8 @@ mod tests {
618626
assert_eq!(ctx.get("role_name").map(|s| s.as_str()), Some("my-role"));
619627
// Resource-scoped key
620628
assert_eq!(
621-
ctx.get("aws_cross_account_role.role_name").map(|s| s.as_str()),
629+
ctx.get("aws_cross_account_role.role_name")
630+
.map(|s| s.as_str()),
622631
Some("my-role"),
623632
);
624633
}
@@ -638,7 +647,10 @@ mod tests {
638647
export_vars(&mut ctx, "resource_b", &data2, &[]);
639648

640649
// Global key reflects the most recent export
641-
assert_eq!(ctx.get("role_name").map(|s| s.as_str()), Some("second-role"));
650+
assert_eq!(
651+
ctx.get("role_name").map(|s| s.as_str()),
652+
Some("second-role")
653+
);
642654
}
643655

644656
#[test]
@@ -677,7 +689,10 @@ mod tests {
677689

678690
export_vars(&mut ctx, "vault", &data, &["secret_key".to_string()]);
679691

680-
assert_eq!(ctx.get("secret_key").map(|s| s.as_str()), Some("super-secret"));
692+
assert_eq!(
693+
ctx.get("secret_key").map(|s| s.as_str()),
694+
Some("super-secret")
695+
);
681696
assert_eq!(
682697
ctx.get("vault.secret_key").map(|s| s.as_str()),
683698
Some("super-secret"),

src/globals.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,3 @@ pub fn server_port() -> u16 {
118118
.copied()
119119
.unwrap_or(DEFAULT_SERVER_PORT)
120120
}
121-

0 commit comments

Comments
 (0)