Skip to content

Commit 7388951

Browse files
committed
upgrade info
1 parent a5d0b57 commit 7388951

12 files changed

+1090
-20
lines changed

Diff for: Cargo.lock

+464-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ indicatif = "0.17"
1111
zip = "0.6"
1212
unicode-width = "0.1.10"
1313
postgres = "0.19"
14-
rustyline = "10.0"
14+
rustyline = "10.0"
15+
tera = "1.19.0"

Diff for: src/commands/init.rs

+195-14
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,207 @@
1+
use clap::{Command, Arg, ArgAction, ArgMatches};
2+
use std::fs;
3+
use std::io::Write;
4+
use std::path::Path;
5+
use std::collections::HashSet;
6+
use colored::*;
7+
use tera::{Tera, Context};
18
use crate::utils::display::print_unicode_box;
2-
use clap::{Arg, ArgMatches, Command};
9+
10+
// AWS templates
11+
const AWS_RESOURCE_TEMPLATE: &str = include_str!("../../templates/aws/resources/example_vpc.iql.template");
12+
const AWS_MANIFEST_TEMPLATE: &str = include_str!("../../templates/aws/stackql_manifest.yml.template");
13+
const AWS_README_TEMPLATE: &str = include_str!("../../templates/aws/README.md.template");
14+
15+
// Azure templates
16+
const AZURE_RESOURCE_TEMPLATE: &str = include_str!("../../templates/azure/resources/example_res_grp.iql.template");
17+
const AZURE_MANIFEST_TEMPLATE: &str = include_str!("../../templates/azure/stackql_manifest.yml.template");
18+
const AZURE_README_TEMPLATE: &str = include_str!("../../templates/azure/README.md.template");
19+
20+
// Google templates
21+
const GOOGLE_RESOURCE_TEMPLATE: &str = include_str!("../../templates/google/resources/example_vpc.iql.template");
22+
const GOOGLE_MANIFEST_TEMPLATE: &str = include_str!("../../templates/google/stackql_manifest.yml.template");
23+
const GOOGLE_README_TEMPLATE: &str = include_str!("../../templates/google/README.md.template");
24+
25+
const DEFAULT_PROVIDER: &str = "azure";
26+
const SUPPORTED_PROVIDERS: [&str; 3] = ["aws", "google", "azure"];
327

428
pub fn command() -> Command {
529
Command::new("init")
6-
.about("Initialize a new project")
7-
.arg(Arg::new("stack_name").required(true))
30+
.about("Initialize a new stackql-deploy project structure")
31+
.arg(
32+
Arg::new("stack_name")
33+
.help("Name of the new stack project")
34+
.required(true)
35+
.index(1)
36+
)
837
.arg(
938
Arg::new("provider")
39+
.short('p')
1040
.long("provider")
11-
.help("Specify a provider (aws, azure, google)"),
41+
.help("Specify a provider (aws, azure, google)")
42+
.action(ArgAction::Set)
43+
)
44+
.arg(
45+
Arg::new("env")
46+
.short('e')
47+
.long("env")
48+
.help("Environment name (dev, test, prod)")
49+
.default_value("dev")
50+
.action(ArgAction::Set)
1251
)
1352
}
1453

1554
pub fn execute(matches: &ArgMatches) {
16-
let stack_name = matches.get_one::<String>("stack_name").unwrap();
17-
let provider = matches
18-
.get_one::<String>("provider")
19-
.map(|s| s.as_str())
20-
.unwrap_or("azure");
21-
22-
print_unicode_box(&format!(
23-
"🛠️ Initializing project [{}] with provider [{}]",
24-
stack_name, provider
25-
));
55+
print_unicode_box("🚀 Initializing new project...");
56+
57+
let stack_name = matches.get_one::<String>("stack_name")
58+
.expect("Stack name is required");
59+
60+
let stack_name = stack_name.replace('_', "-").to_lowercase();
61+
62+
let env = matches.get_one::<String>("env")
63+
.expect("Environment defaulted to dev")
64+
.to_string();
65+
66+
// Get the provider with validation
67+
let provider = validate_provider(matches.get_one::<String>("provider").map(|s| s.as_str()));
68+
69+
// Create project structure
70+
match create_project_structure(&stack_name, &provider, &env) {
71+
Ok(_) => {
72+
println!("{}", format!("Project {} initialized successfully.", stack_name).green());
73+
},
74+
Err(e) => {
75+
eprintln!("{}", format!("Error initializing project: {}", e).red());
76+
}
77+
}
78+
}
79+
80+
fn validate_provider(provider: Option<&str>) -> String {
81+
let supported: HashSet<&str> = SUPPORTED_PROVIDERS.iter().cloned().collect();
82+
83+
match provider {
84+
Some(p) if supported.contains(p) => p.to_string(),
85+
Some(p) => {
86+
println!("{}", format!(
87+
"Provider '{}' is not supported for `init`, supported providers are: {}, defaulting to `{}`",
88+
p, SUPPORTED_PROVIDERS.join(", "), DEFAULT_PROVIDER
89+
).yellow());
90+
DEFAULT_PROVIDER.to_string()
91+
},
92+
_none => {
93+
// Silently default to DEFAULT_PROVIDER
94+
DEFAULT_PROVIDER.to_string()
95+
}
96+
}
2697
}
98+
99+
fn create_project_structure(stack_name: &str, provider: &str, env: &str) -> Result<(), String> {
100+
let cwd = std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?;
101+
let base_path = cwd.join(stack_name);
102+
103+
// Check if directory already exists
104+
if base_path.exists() {
105+
return Err(format!("Directory '{}' already exists", stack_name));
106+
}
107+
108+
// Create necessary directories
109+
let resource_dir = base_path.join("resources");
110+
fs::create_dir_all(&resource_dir).map_err(|e| format!("Failed to create directories: {}", e))?;
111+
112+
// Determine sample resource name based on provider
113+
let sample_res_name = match provider {
114+
"google" => "example_vpc",
115+
"azure" => "example_res_grp",
116+
"aws" => "example_vpc",
117+
_ => "example_resource",
118+
};
119+
120+
// Set up template context
121+
let mut context = Context::new();
122+
context.insert("stack_name", stack_name);
123+
context.insert("stack_env", env);
124+
125+
// Create files
126+
create_manifest_file(&base_path, provider, &context)?;
127+
create_readme_file(&base_path, provider, &context)?;
128+
create_resource_file(&resource_dir, sample_res_name, provider, &context)?;
129+
130+
Ok(())
131+
}
132+
133+
fn create_resource_file(resource_dir: &Path, sample_res_name: &str, provider: &str, context: &Context) -> Result<(), String> {
134+
let template_str = match provider {
135+
"aws" => AWS_RESOURCE_TEMPLATE,
136+
"azure" => AZURE_RESOURCE_TEMPLATE,
137+
"google" => GOOGLE_RESOURCE_TEMPLATE,
138+
_ => "-- Example resource\n",
139+
};
140+
141+
// Render template with Tera
142+
let resource_content = render_template(template_str, context)
143+
.map_err(|e| format!("Template rendering error: {}", e))?;
144+
145+
let resource_path = resource_dir.join(format!("{}.iql", sample_res_name));
146+
let mut file = fs::File::create(resource_path)
147+
.map_err(|e| format!("Failed to create resource file: {}", e))?;
148+
149+
file.write_all(resource_content.as_bytes())
150+
.map_err(|e| format!("Failed to write to resource file: {}", e))?;
151+
152+
Ok(())
153+
}
154+
155+
fn create_manifest_file(base_path: &Path, provider: &str, context: &Context) -> Result<(), String> {
156+
let template_str = match provider {
157+
"aws" => AWS_MANIFEST_TEMPLATE,
158+
"azure" => AZURE_MANIFEST_TEMPLATE,
159+
"google" => GOOGLE_MANIFEST_TEMPLATE,
160+
_ => "name: {{stack_name}}\nversion: 0.1.0\ndescription: StackQL IaC project\n",
161+
};
162+
163+
// Render template with Tera
164+
let manifest_content = render_template(template_str, context)
165+
.map_err(|e| format!("Template rendering error: {}", e))?;
166+
167+
let manifest_path = base_path.join("stackql_manifest.yml");
168+
let mut file = fs::File::create(manifest_path)
169+
.map_err(|e| format!("Failed to create manifest file: {}", e))?;
170+
171+
file.write_all(manifest_content.as_bytes())
172+
.map_err(|e| format!("Failed to write to manifest file: {}", e))?;
173+
174+
Ok(())
175+
}
176+
177+
fn create_readme_file(base_path: &Path, provider: &str, context: &Context) -> Result<(), String> {
178+
let template_str = match provider {
179+
"aws" => AWS_README_TEMPLATE,
180+
"azure" => AZURE_README_TEMPLATE,
181+
"google" => GOOGLE_README_TEMPLATE,
182+
_ => "# {{stack_name}}\n\nInfrastructure as Code project\n",
183+
};
184+
185+
// Render template with Tera
186+
let readme_content = render_template(template_str, context)
187+
.map_err(|e| format!("Template rendering error: {}", e))?;
188+
189+
let readme_path = base_path.join("README.md");
190+
let mut file = fs::File::create(readme_path)
191+
.map_err(|e| format!("Failed to create README file: {}", e))?;
192+
193+
file.write_all(readme_content.as_bytes())
194+
.map_err(|e| format!("Failed to write to README file: {}", e))?;
195+
196+
Ok(())
197+
}
198+
199+
fn render_template(template_str: &str, context: &Context) -> Result<String, String> {
200+
// Create a one-off Tera instance for rendering a single template
201+
let mut tera = Tera::default();
202+
tera.add_raw_template("template", template_str)
203+
.map_err(|e| format!("Failed to add template: {}", e))?;
204+
205+
tera.render("template", context)
206+
.map_err(|e| format!("Failed to render template: {}", e))
207+
}

Diff for: templates/aws/README.md.template

+63
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 {{ stack_name }} --provider=azure` or `stackql-deploy {{ stack_name }} --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 {{ stack_name }} to an environment labeled `sit`, run the following:
33+
34+
```bash
35+
stackql-deploy build {{ stack_name }} 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 {{ stack_name }} 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 {{ stack_name }} 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 {{ stack_name }} sit \
62+
-e AWS_REGION=ap-southeast-2
63+
```

Diff for: templates/aws/resources/example_vpc.iql.template

+67
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 = '{% raw %}{{ region }}{% endraw %}'
12+
AND cidr_block = '{% raw %}{{ vpc_cidr_block }}{% endraw %}'
13+
GROUP BY vpc_id
14+
HAVING json_extract(tags, '$.Provisioner') = 'stackql'
15+
AND json_extract(tags, '$.StackName') = '{% raw %}{{ stack_name }}{% endraw %}'
16+
AND json_extract(tags, '$.StackEnv') = '{% raw %}{{ stack_env }}{% endraw %}'
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+
'{% raw %}{{ vpc_cidr_block }}{% endraw %}',
29+
'{% raw %}{{ vpc_tags }}{% endraw %}',
30+
true,
31+
true,
32+
'{% raw %}{{ region }}{% endraw %}';
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 = '{% raw %}{{ region }}{% endraw %}'
42+
AND cidr_block = '{% raw %}{{ vpc_cidr_block }}{% endraw %}'
43+
GROUP BY vpc_id
44+
HAVING json_extract(tags, '$.Provisioner') = 'stackql'
45+
AND json_extract(tags, '$.StackName') = '{% raw %}{{ stack_name }}{% endraw %}'
46+
AND json_extract(tags, '$.StackEnv') = '{% raw %}{{ stack_env }}{% endraw %}'
47+
) t
48+
WHERE cidr_block = '{% raw %}{{ vpc_cidr_block }}{% endraw %}';
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 = '{% raw %}{{ region }}{% endraw %}'
57+
AND cidr_block = '{% raw %}{{ vpc_cidr_block }}{% endraw %}'
58+
GROUP BY vpc_id
59+
HAVING json_extract(tags, '$.Provisioner') = 'stackql'
60+
AND json_extract(tags, '$.StackName') = '{% raw %}{{ stack_name }}{% endraw %}'
61+
AND json_extract(tags, '$.StackEnv') = '{% raw %}{{ stack_env }}{% endraw %}'
62+
) t;
63+
64+
/*+ delete */
65+
DELETE FROM aws.ec2.vpcs
66+
WHERE data__Identifier = '{% raw %}{{ vpc_id }}{% endraw %}'
67+
AND region = '{% raw %}{{ region }}{% endraw %}';

0 commit comments

Comments
 (0)