2121commands are output as a shell script and optionally executed.
2222
2323The output images are not pushed to the Google Container Registry.
24- Not all cloudbuild.yaml functionality is supported.
24+ Not all cloudbuild.yaml functionality is supported. In particular,
25+ substitutions are a simplified subset that doesn't include all the
26+ corner cases and error conditions.
2527
2628See https://cloud.google.com/container-builder/docs/api/build-steps
2729for more information.
4446# Exclude non-printable control characters (including newlines)
4547PRINTABLE_REGEX = re .compile (r"""^[^\x00-\x1f]*$""" )
4648
49+ # Container Builder substitutions
50+ # https://cloud.google.com/container-builder/docs/api/build-requests#substitutions
51+ SUBSTITUTION_REGEX = re .compile (r"""(?x)
52+ (?<!\\) # Don't match if backslash before dollar sign
53+ \$ # Dollar sign
54+ (
55+ [A-Z0-9_]+ # Variable name, no curly brackets
56+ |
57+ {[A-Z0-9_]+} # Variable name, with curly brackets
58+ )
59+ """ )
60+ KEY_VALUE_REGEX = re .compile (r'^([A-Z0-9_]+)=(.*)$' )
61+
62+ # Default builtin substitutions
63+ DEFAULT_SUBSTITUTIONS = {
64+ 'PROJECT_ID' : 'dummy-project-id' ,
65+ 'REPO_NAME' : '' ,
66+ 'BRANCH_NAME' : '' ,
67+ 'TAG_NAME' : '' ,
68+ 'REVISION_ID' : '' ,
69+ }
70+
4771# File template
4872BUILD_SCRIPT_HEADER = """\
4973 #!/bin/bash
77101
78102
79103# Validated cloudbuild recipe + flags
80- CloudBuild = collections .namedtuple ('CloudBuild' , 'output_script run steps' )
104+ CloudBuild = collections .namedtuple ('CloudBuild' , 'output_script run steps substitutions ' )
81105
82106# Single validated step in a cloudbuild recipe
83107Step = collections .namedtuple ('Step' , 'args dir_ env name' )
84108
85109
110+ def sub_and_quote (s , substitutions ):
111+ """Return a shell-escaped, variable substituted, version of the string s."""
112+
113+ def sub (match ):
114+ """Perform a single substitution."""
115+ variable_name = match .group (1 )
116+ if variable_name [0 ] == '{' :
117+ # Strip curly brackets
118+ variable_name = variable_name [1 :- 1 ]
119+ if variable_name not in substitutions :
120+ if variable_name .startswith ('_' ):
121+ # User variables must be set
122+ raise ValueError (
123+ 'Variable "{}" used without being defined. Try adding '
124+ 'it to the --substitutions flag' .format (
125+ variable_name ))
126+ else :
127+ # Builtin variables are silently turned into empty strings
128+ value = ''
129+ else :
130+ value = substitutions .get (variable_name )
131+ return value
132+
133+ substituted_s = re .sub (SUBSTITUTION_REGEX , sub , s )
134+ quoted_s = shlex .quote (substituted_s )
135+ return quoted_s
136+
86137def get_field_value (container , field_name , field_type ):
87138 """Fetch a field from a container with typechecking and default values.
88139
@@ -153,6 +204,7 @@ def get_cloudbuild(raw_config, args):
153204 output_script = args .output_script ,
154205 run = args .run ,
155206 steps = steps ,
207+ substitutions = args .substitutions ,
156208 )
157209
158210
@@ -185,23 +237,24 @@ def get_step(raw_step):
185237 )
186238
187239
188- def generate_command (step ):
240+ def generate_command (step , subs ):
189241 """Generate a single shell command to run for a single cloudbuild step
190242
191243 Args:
192244 step (Step): Valid build step
245+ subs (dict): Substitution map to apply
193246
194247 Returns:
195248 [str]: A single shell command, expressed as a list of quoted tokens.
196249 """
197- quoted_args = [shlex . quote (arg ) for arg in step .args ]
250+ quoted_args = [sub_and_quote (arg , subs ) for arg in step .args ]
198251 quoted_env = []
199252 for env in step .env :
200- quoted_env .extend (['--env' , shlex . quote (env )])
201- quoted_name = shlex . quote (step .name )
253+ quoted_env .extend (['--env' , sub_and_quote (env , subs )])
254+ quoted_name = sub_and_quote (step .name , subs )
202255 workdir = '/workspace'
203256 if step .dir_ :
204- workdir = os .path .join (workdir , shlex . quote (step .dir_ ))
257+ workdir = os .path .join (workdir , sub_and_quote (step .dir_ , subs ))
205258 process_args = [
206259 'docker' ,
207260 'run' ,
@@ -228,7 +281,8 @@ def generate_script(cloudbuild):
228281 """
229282 outfile = io .StringIO ()
230283 outfile .write (BUILD_SCRIPT_HEADER )
231- docker_commands = [generate_command (step ) for step in cloudbuild .steps ]
284+ docker_commands = [generate_command (step , cloudbuild .substitutions )
285+ for step in cloudbuild .steps ]
232286 for docker_command in docker_commands :
233287 line = ' ' .join (docker_command ) + '\n \n '
234288 outfile .write (line )
@@ -286,6 +340,22 @@ def validate_arg_regex(flag_value, flag_regex):
286340 return flag_value
287341
288342
343+ def validate_arg_dict (flag_value ):
344+ """Parse a command line flag as a key=val,... dict"""
345+ if not flag_value :
346+ return {}
347+ entries = flag_value .split (',' )
348+ pairs = []
349+ for entry in entries :
350+ match = re .match (KEY_VALUE_REGEX , entry )
351+ if not match :
352+ raise argparse .ArgumentTypeError (
353+ 'Value "{}" should be a list like _KEY1=value1,_KEY2=value2"' .format (
354+ flag_value ))
355+ pairs .append ((match .group (1 ), match .group (2 )))
356+ return dict (pairs )
357+
358+
289359def parse_args (argv ):
290360 """Parse and validate command line flags"""
291361 parser = argparse .ArgumentParser (
@@ -309,6 +379,12 @@ def parse_args(argv):
309379 help = 'Create shell script but don\' t execute it' ,
310380 dest = 'run' ,
311381 )
382+ parser .add_argument (
383+ '--substitutions' ,
384+ type = validate_arg_dict ,
385+ default = {},
386+ help = 'Parameters to be substituted in the build specification' ,
387+ )
312388 args = parser .parse_args (argv [1 :])
313389 if not args .output_script :
314390 args .output_script = args .config + "_local.sh"
0 commit comments