5
5
import logging
6
6
from collections import Mapping , namedtuple
7
7
8
- from stacker .exceptions import HookExecutionFailed
8
+ from stacker .exceptions import HookExecutionFailed , StackDoesNotExist
9
9
from stacker .util import load_object_from_string
10
+ from stacker .status import (
11
+ COMPLETE , SKIPPED , FailedStatus , NotSubmittedStatus , SkippedStatus
12
+ )
10
13
from stacker .variables import Variable
11
14
12
15
logger = logging .getLogger (__name__ )
@@ -54,11 +57,55 @@ def __init__(self, name, path, required=True, enabled=True,
54
57
self .region = region
55
58
56
59
self ._args = {}
60
+ self ._args , deps = self .parse_args (args )
61
+ self .requires .update (deps )
62
+
63
+ self ._callable = self .resolve_path ()
64
+
65
+ def parse_args (self , args ):
66
+ arg_vars = {}
67
+ deps = set ()
68
+
57
69
if args :
58
70
for key , value in args .items ():
59
- var = self . _args [key ] = \
71
+ var = arg_vars [key ] = \
60
72
Variable ('{}.args.{}' .format (self .name , key ), value )
61
- self .requires .update (var .dependencies ())
73
+ deps .update (var .dependencies ())
74
+
75
+ return arg_vars , deps
76
+
77
+ def resolve_path (self ):
78
+ try :
79
+ return load_object_from_string (self .path )
80
+ except (AttributeError , ImportError ) as e :
81
+ raise ValueError ("Unable to load method at %s for hook %s: %s" ,
82
+ self .path , self .name , str (e ))
83
+
84
+ def check_args_dependencies (self , provider , context ):
85
+ # When running hooks for destruction, we might rely on outputs of
86
+ # stacks that we assume have been deployed. Unfortunately, since
87
+ # destruction must happen in the reverse order of creation, those stack
88
+ # dependencies will not be present on `requires`, but in `required_by`,
89
+ # meaning the execution engine won't stop the hook from running early.
90
+
91
+ # To deal with that, manually find the dependencies coming from
92
+ # lookups in the hook arguments, select those that represent stacks,
93
+ # and check if they are actually available.
94
+
95
+ dependencies = set ()
96
+ for value in self ._args .values ():
97
+ dependencies .update (value .dependencies ())
98
+
99
+ for dep in dependencies :
100
+ # We assume all dependency names are valid here. Hence, if we can't
101
+ # find a stack with that same name, it must be a target or a hook,
102
+ # and hence we don't need to check it
103
+ stack = context .get_stack (dep )
104
+ if stack is None :
105
+ continue
106
+
107
+ # This will raise if the stack is missing
108
+ provider .get_stack (stack .fqn )
62
109
63
110
def resolve_args (self , provider , context ):
64
111
for key , value in self ._args .items ():
@@ -85,29 +132,15 @@ def run(self, provider, context):
85
132
"""
86
133
87
134
logger .info ("Executing hook %s" , self )
88
-
89
- if not self .enabled :
90
- logger .debug ("Hook %s is disabled, skipping" , self .name )
91
- return
92
-
93
- try :
94
- method = load_object_from_string (self .path )
95
- except (AttributeError , ImportError ) as e :
96
- logger .exception ("Unable to load method at %s for hook %s:" ,
97
- self .path , self .name )
98
- if self .required :
99
- raise HookExecutionFailed (self , exception = e )
100
-
101
- return
102
-
103
135
kwargs = dict (self .resolve_args (provider , context ))
104
136
try :
105
- result = method (context = context , provider = provider , ** kwargs )
137
+ result = self ._callable (context = context , provider = provider ,
138
+ ** kwargs )
106
139
except Exception as e :
107
140
if self .required :
108
- raise HookExecutionFailed (self , exception = e )
141
+ raise HookExecutionFailed (self , cause = e )
109
142
110
- return
143
+ return None
111
144
112
145
if not result :
113
146
if self .required :
@@ -125,6 +158,29 @@ def run(self, provider, context):
125
158
126
159
return result
127
160
161
+ def run_step (self , provider_builder , context ):
162
+ if not self .enabled :
163
+ return NotSubmittedStatus ()
164
+
165
+ provider = provider_builder .build (profile = self .profile ,
166
+ region = self .region )
167
+
168
+ try :
169
+ self .check_args_dependencies (provider , context )
170
+ except StackDoesNotExist as e :
171
+ reason = "required stack not deployed: {}" .format (e .stack_name )
172
+ return SkippedStatus (reason = reason )
173
+
174
+ try :
175
+ result = self .run (provider , context )
176
+ except HookExecutionFailed as e :
177
+ return FailedStatus (reason = str (e ))
178
+
179
+ if not result :
180
+ return SKIPPED
181
+
182
+ return COMPLETE
183
+
128
184
def __str__ (self ):
129
185
return 'Hook(name={}, path={}, profile={}, region={})' .format (
130
186
self .name , self .path , self .profile , self .region )
0 commit comments