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 } ;
1
8
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" ] ;
3
27
4
28
pub fn command ( ) -> Command {
5
29
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
+ )
8
37
. arg (
9
38
Arg :: new ( "provider" )
39
+ . short ( 'p' )
10
40
. 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 )
12
51
)
13
52
}
14
53
15
54
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
+ }
26
97
}
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}}\n version: 0.1.0\n description: 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 \n Infrastructure 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
+ }
0 commit comments