Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions docs/exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Exports

Exports allow resources to publish values (e.g. IDs, ARNs, names) so that
subsequent resources in the stack can reference them.

## Defining exports

Add an `exports` field to a resource in `stackql_manifest.yml`. The exports
query in the resource's `.iql` file must return columns that match the
export names.

### Simple exports

```yaml
resources:
- name: example_vpc
props:
- name: cidr_block
value: "10.0.0.0/16"
exports:
- vpc_id
- vpc_cidr_block
```

The `exports` anchor in the `.iql` file must return these columns:

```sql
/*+ exports */
SELECT vpc_id, cidr_block AS vpc_cidr_block
FROM awscc.ec2.vpcs
WHERE region = '{{ region }}' AND vpc_id = '{{ vpc_id }}';
```

### Aliased exports

Use the mapping format to rename columns on export:

```yaml
exports:
- arn: aws_iam_cross_account_role_arn
- role_name: aws_iam_role_name
```

Here the exports query returns columns `arn` and `role_name`, but they are
stored in the context under the alias names `aws_iam_cross_account_role_arn`
and `aws_iam_role_name`.

## Referencing exported values

Exported values are injected into the global template context and can be
referenced by any subsequent resource using `{{ variable_name }}`.

### Unscoped references

The simplest way to reference an export is by its name (or alias):

```yaml
- name: example_subnet
props:
- name: vpc_id
value: "{{ vpc_id }}"
```

Unscoped names follow **last-writer-wins** semantics: if two resources both
export a variable called `vpc_id`, subsequent resources will see the value
from whichever resource was processed last.

### Resource-scoped references

Every export is also available under a **resource-scoped** name of the form
`resource_name.variable_name`. This name is **immutable** -- once set, it
cannot be overwritten by a later resource:

```yaml
- name: example_subnet
props:
- name: vpc_id
value: "{{ example_vpc.vpc_id }}"
```

Resource-scoped names are useful when:

- Multiple resources export variables with the same name and you need to
reference a specific one unambiguously.
- You want to make it clear which resource a value originates from for
readability and maintainability.

### Example

```yaml
resources:
- name: aws_cross_account_role
file: aws/iam/roles.iql
props:
- name: role_name
value: "{{ stack_name }}-{{ stack_env }}-role"
# ...
exports:
- role_name: aws_iam_cross_account_role_name
- arn: aws_iam_cross_account_role_arn

- name: databricks_account/credentials
props:
- name: credentials_name
value: "{{ stack_name }}-{{ stack_env }}-credentials"
- name: aws_credentials
value:
sts_role:
# Unscoped -- works because only one resource exports this name
role_arn: "{{ aws_iam_cross_account_role_arn }}"
# Resource-scoped -- always unambiguous
# role_arn: "{{ aws_cross_account_role.aws_iam_cross_account_role_arn }}"
```

## Protected exports

Sensitive values can be masked in log output by listing them under
`protected`:

```yaml
- name: secret_resource
props: []
exports:
- api_key
protected:
- api_key
```

The actual value is still stored in the context and usable by templates;
only the log messages are masked.

## Stack-level exports

The top-level `exports` field in the manifest lists variables that are
written to a JSON output file (when `--output` is specified):

```yaml
exports:
- vpc_id
- subnet_id
```

The output file always includes `stack_name`, `stack_env`, and
`elapsed_time` automatically.
18 changes: 14 additions & 4 deletions src/commands/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ use std::process;
use log::{debug, error, info};
use pgwire_lite::PgwireLite;

use crate::core::config::{
get_full_context, render_globals, render_string_value,
};
use crate::core::config::{get_full_context, render_globals, render_string_value};
use crate::core::env::load_env_vars;
use crate::core::templating::{self, ParsedQuery};
use crate::core::utils::{
catch_error_and_exit, check_exports_as_statecheck_proxy, export_vars, perform_retries,
pull_providers, run_ext_script, run_stackql_command, run_stackql_query, show_query,
};
use crate::resource::manifest::{Manifest, Resource};
use crate::resource::validation::validate_manifest;
use crate::template::engine::TemplateEngine;

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

// Load manifest
let manifest = Manifest::load_from_dir_or_exit(stack_dir);

// Validate manifest rules
if let Err(errors) = validate_manifest(&manifest) {
for err in &errors {
error!("{}", err);
}
catch_error_and_exit(&format!(
"Manifest validation failed with {} error(s)",
errors.len()
));
}

let stack_name = manifest.name.clone();

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

None
}

1 change: 0 additions & 1 deletion src/commands/common_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,3 @@ pub fn on_failure() -> Arg {
.value_parser(value_parser!(FailureAction))
.default_value("error")
}

25 changes: 20 additions & 5 deletions src/core/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,11 @@ pub fn export_vars(
global_context.entry(scoped_key.clone()).or_insert_with(|| {
info!(
"set {} [{}] to [{}] in exports",
if is_protected { "protected variable" } else { "variable" },
if is_protected {
"protected variable"
} else {
"variable"
},
scoped_key,
display_value,
);
Expand All @@ -499,7 +503,11 @@ pub fn export_vars(
// --- global (unscoped) key (can be overridden by later resources) ---
info!(
"set {} [{}] to [{}] in exports",
if is_protected { "protected variable" } else { "variable" },
if is_protected {
"protected variable"
} else {
"variable"
},
key,
display_value,
);
Expand Down Expand Up @@ -618,7 +626,8 @@ mod tests {
assert_eq!(ctx.get("role_name").map(|s| s.as_str()), Some("my-role"));
// Resource-scoped key
assert_eq!(
ctx.get("aws_cross_account_role.role_name").map(|s| s.as_str()),
ctx.get("aws_cross_account_role.role_name")
.map(|s| s.as_str()),
Some("my-role"),
);
}
Expand All @@ -638,7 +647,10 @@ mod tests {
export_vars(&mut ctx, "resource_b", &data2, &[]);

// Global key reflects the most recent export
assert_eq!(ctx.get("role_name").map(|s| s.as_str()), Some("second-role"));
assert_eq!(
ctx.get("role_name").map(|s| s.as_str()),
Some("second-role")
);
}

#[test]
Expand Down Expand Up @@ -677,7 +689,10 @@ mod tests {

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

assert_eq!(ctx.get("secret_key").map(|s| s.as_str()), Some("super-secret"));
assert_eq!(
ctx.get("secret_key").map(|s| s.as_str()),
Some("super-secret")
);
assert_eq!(
ctx.get("vault.secret_key").map(|s| s.as_str()),
Some("super-secret"),
Expand Down
1 change: 0 additions & 1 deletion src/globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,3 @@ pub fn server_port() -> u16 {
.copied()
.unwrap_or(DEFAULT_SERVER_PORT)
}

Loading