@@ -28,6 +28,9 @@ def __init__(self, module):
2828 self .changed = False
2929 # Use this to store arguments to pass to exit_json().
3030 self .result = {}
31+ # Populated by compare_config() when config differs, so diff_config()
32+ # can surface the output without running the exec a second time.
33+ self ._config_diff = None
3134
3235 self .systemd = SystemdWorker (self .params )
3336
@@ -57,36 +60,182 @@ def check_container(self):
5760
5861 def compare_container (self ):
5962 container = self .check_container ()
60- if (not container or
61- self .check_container_differs () or
62- self .compare_config () or
63- self .systemd .check_unit_change ()):
63+ failures = []
64+
65+ if not container :
66+ failures .append ({'name' : 'container_missing' ,
67+ 'current' : None ,
68+ 'desired' : self .params .get ('name' )})
69+ else :
70+ container_info = self .get_container_info ()
71+ failures .extend (self .check_container_differs (container_info ))
72+ if self .compare_config ():
73+ failures .append ({'name' : 'config' ,
74+ 'current' : self ._config_diff ,
75+ 'desired' : 'up_to_date' })
76+ if self .systemd .check_unit_change ():
77+ failures .append ({'name' : 'systemd_unit' ,
78+ 'current' : 'out_of_date' ,
79+ 'desired' : 'up_to_date' })
80+
81+ if failures :
6482 self .changed = True
83+ self .result ['comparison_failures' ] = failures
84+
6585 return self .changed
6686
67- def check_container_differs (self ):
68- container_info = self .get_container_info ()
87+ def check_container_differs (self , container_info = None ):
88+ """Return a list of dicts describing differences from the desired state
89+
90+ Each entry is a dict with keys:
91+ - ``name``: the comparison that failed (e.g. ``'image'``)
92+ - ``current``: the value found on the running container
93+ - ``desired``: the value requested by the module parameters
94+
95+ An empty list means no differences were found.
96+ """
97+ if container_info is None :
98+ container_info = self .get_container_info ()
6999 if not container_info :
70- return True
100+ return [{'name' : 'container_info_missing' ,
101+ 'current' : None , 'desired' : None }]
102+
103+ checks = [
104+ 'cap_add' ,
105+ 'security_opt' ,
106+ 'image' ,
107+ 'ipc_mode' ,
108+ 'labels' ,
109+ 'privileged' ,
110+ 'pid_mode' ,
111+ 'cgroupns_mode' ,
112+ 'tmpfs' ,
113+ 'volumes' ,
114+ 'volumes_from' ,
115+ 'environment' ,
116+ 'container_state' ,
117+ 'dimensions' ,
118+ 'command' ,
119+ 'healthcheck' ,
120+ ]
121+
122+ failures = []
123+ for name in checks :
124+ if getattr (self , 'compare_' + name )(container_info ):
125+ diff = getattr (self , 'diff_' + name )(container_info )
126+ failures .append ({'name' : name ,
127+ 'current' : diff [0 ],
128+ 'desired' : diff [1 ]})
129+ return failures
130+
131+ # ------------------------------------------------------------------
132+ # diff_* methods: return (current, desired) for each comparison.
133+ # These are only called when the corresponding compare_* has already
134+ # returned True, so they can focus purely on extracting values.
135+ # ------------------------------------------------------------------
136+
137+ def diff_ipc_mode (self , container_info ):
138+ current = container_info ['HostConfig' ].get ('IpcMode' ) or None
139+ return current , self .params .get ('ipc_mode' )
140+
141+ def diff_cap_add (self , container_info ):
142+ try :
143+ current = container_info ['HostConfig' ].get ('CapAdd' ) or []
144+ except (KeyError , TypeError ):
145+ current = []
146+ return sorted (current ), sorted (self .params .get ('cap_add' , []))
71147
72- return (
73- self .compare_cap_add (container_info ) or
74- self .compare_security_opt (container_info ) or
75- self .compare_image (container_info ) or
76- self .compare_ipc_mode (container_info ) or
77- self .compare_labels (container_info ) or
78- self .compare_privileged (container_info ) or
79- self .compare_pid_mode (container_info ) or
80- self .compare_cgroupns_mode (container_info ) or
81- self .compare_tmpfs (container_info ) or
82- self .compare_volumes (container_info ) or
83- self .compare_volumes_from (container_info ) or
84- self .compare_environment (container_info ) or
85- self .compare_container_state (container_info ) or
86- self .compare_dimensions (container_info ) or
87- self .compare_command (container_info ) or
88- self .compare_healthcheck (container_info )
89- )
148+ def diff_security_opt (self , container_info ):
149+ try :
150+ current = container_info ['HostConfig' ].get ('SecurityOpt' ) or []
151+ except (KeyError , TypeError ):
152+ current = []
153+ return sorted (current ), sorted (self .params .get ('security_opt' , []))
154+
155+ def diff_image (self , container_info ):
156+ current = (container_info .get ('Image' ) or
157+ container_info .get ('Config' , {}).get ('Image' ))
158+ return current , self .params .get ('image' )
159+
160+ def diff_labels (self , container_info ):
161+ current = container_info ['Config' ].get ('Labels' , {})
162+ desired = self .params .get ('labels' )
163+ return current , desired
164+
165+ def diff_privileged (self , container_info ):
166+ current = container_info ['HostConfig' ].get ('Privileged' )
167+ return current , self .params .get ('privileged' )
168+
169+ def diff_pid_mode (self , container_info ):
170+ current = container_info ['HostConfig' ].get ('PidMode' ) or None
171+ desired = self .params .get ('pid_mode' )
172+ return current , desired
173+
174+ def diff_cgroupns_mode (self , container_info ):
175+ current = (container_info ['HostConfig' ].get ('CgroupnsMode' ) or
176+ container_info ['HostConfig' ].get ('CgroupMode' ) or 'host' )
177+ return current , self .params .get ('cgroupns_mode' )
178+
179+ def diff_tmpfs (self , container_info ):
180+ current = list (container_info ['HostConfig' ].get ('Tmpfs' ) or [])
181+ desired = list (self .generate_tmpfs () or [])
182+ return sorted (current ), sorted (desired )
183+
184+ def diff_volumes (self , container_info ):
185+ volumes , binds = self .generate_volumes ()
186+ current_vols = list (container_info ['Config' ].get ('Volumes' ) or [])
187+ current_binds = list (container_info ['HostConfig' ].get ('Binds' ) or [])
188+ desired_vols = list (volumes or [])
189+ desired_binds = []
190+ if binds :
191+ for k , v in binds .items ():
192+ desired_binds .append ('{}:{}:{}' .format (k ,
193+ v ['bind' ],
194+ v ['mode' ]))
195+ return {'volumes' : current_vols , 'binds' : sorted (current_binds )}, \
196+ {'volumes' : desired_vols , 'binds' : sorted (desired_binds )}
197+
198+ def diff_volumes_from (self , container_info ):
199+ current = list (container_info ['HostConfig' ].get ('VolumesFrom' ) or [])
200+ desired = list (self .params .get ('volumes_from' ) or [])
201+ return sorted (current ), sorted (desired )
202+
203+ def diff_environment (self , container_info ):
204+ current_env = {}
205+ for kv in container_info ['Config' ].get ('Env' , []):
206+ k , v = kv .split ('=' , 1 )
207+ current_env [k ] = v
208+ desired_env = self .params .get ('environment' , {})
209+ # Only show keys that are actually different
210+ return sorted (current_env ), sorted (desired_env )
211+
212+ def diff_container_state (self , container_info ):
213+ current = container_info ['State' ].get ('Status' )
214+ desired = self .params .get ('state' )
215+ return current , desired
216+
217+ def diff_dimensions (self , container_info ):
218+ new_dimensions = self .params .get ('dimensions' )
219+ current_dimensions = container_info ['HostConfig' ]
220+ current = {k2 : current_dimensions .get (k2 )
221+ for k1 , k2 in self .dimension_map .items ()
222+ if k1 in new_dimensions }
223+ desired = {self .dimension_map [k ]: v
224+ for k , v in new_dimensions .items ()
225+ if k in self .dimension_map }
226+ return current , desired
227+
228+ def diff_command (self , container_info ):
229+ current = '{} {}' .format (
230+ container_info .get ('Path' , '' ),
231+ ' ' .join (container_info .get ('Args' , []))
232+ ).strip ()
233+ return current , self .params .get ('command' )
234+
235+ def diff_healthcheck (self , container_info ):
236+ current = container_info ['Config' ].get ('Healthcheck' )
237+ desired = self .params .get ('healthcheck' )
238+ return current , desired
90239
91240 def compare_ipc_mode (self , container_info ):
92241 new_ipc_mode = self .params .get ('ipc_mode' )
@@ -172,7 +321,7 @@ def compare_labels(self, container_info):
172321 if k in new_labels :
173322 if v != new_labels [k ]:
174323 return True
175- else :
324+ elif k in current_labels :
176325 del current_labels [k ]
177326
178327 if new_labels != current_labels :
0 commit comments