├── README.md └── kube_setup.py /README.md: -------------------------------------------------------------------------------- 1 | # ansible-kubernetes-module 2 | Module for ansible to setup kubernetes objects 3 | 4 | ## Instalation 5 | 6 | * Make sure file `kube_setup.py` is accessable as [ansible module](http://docs.ansible.com/ansible/dev_guide/developing_modules.html#module-paths) 7 | 8 | Alternatively you can put it in `library` directory with your playbook 9 | 10 | ``` 11 | |- playbook.yml 12 | |- library 13 | |- kube_setup.py 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```yaml 19 | - name: create k8s objects from file 20 | kube_setup: 21 | file: "object.yml" 22 | state: "present" # default = "present" [present|absent] 23 | strategy: "default" # default = default, means - use the most suitable [create_or_replace|create_or_apply|create_or_nothing] 24 | kubectl_opts: "--context=live" # default = "" 25 | ``` 26 | -------------------------------------------------------------------------------- /kube_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ansible.module_utils.basic import * 3 | import subprocess 4 | import yaml 5 | import tempfile 6 | 7 | DOCUMENTATION = ''' 8 | --- 9 | module: kube_setup 10 | short_description: module makes sure your kubernetes cluster has required object 11 | ''' 12 | 13 | EXAMPLES = ''' 14 | - name: create k8s objects from file 15 | kube_setup: 16 | file: "object.yml" 17 | state: "present" # default = "present" [present|absent] 18 | strategy: "default" # default = default, means - use the most suitable 19 | # [create_or_force_replace|create_or_replace|create_or_apply|create_or_nothing] 20 | kubectl_opts: "--context=live" # default = "" 21 | ''' 22 | 23 | STRATEGY_DEFAULT = 'default' 24 | STRATEGY_CREATE_OR_FORCE_REPLACE = 'create_or_force_replace' 25 | STRATEGY_CREATE_OR_REPLACE = 'create_or_replace' 26 | STRATEGY_CREATE_OR_APPLY = 'create_or_apply' 27 | STRATEGY_CREATE_OR_NOTHING = 'create_or_nothing' 28 | 29 | STRATEGIES = { 30 | 'cluster': STRATEGY_CREATE_OR_NOTHING, 31 | 'componentstatus': STRATEGY_CREATE_OR_NOTHING, 32 | 'configmap': STRATEGY_CREATE_OR_REPLACE, 33 | 'daemonset': STRATEGY_CREATE_OR_APPLY, 34 | 'deployment': STRATEGY_CREATE_OR_REPLACE, 35 | 'endpoint': STRATEGY_CREATE_OR_NOTHING, 36 | 'event': STRATEGY_CREATE_OR_NOTHING, 37 | 'horizontalpodautoscaler': STRATEGY_CREATE_OR_NOTHING, 38 | 'ingress': STRATEGY_CREATE_OR_NOTHING, 39 | 'job': STRATEGY_CREATE_OR_APPLY, 40 | 'limitrange': STRATEGY_CREATE_OR_NOTHING, 41 | 'namespace': STRATEGY_CREATE_OR_APPLY, 42 | 'networkpolicies': STRATEGY_CREATE_OR_NOTHING, 43 | 'node': STRATEGY_CREATE_OR_NOTHING, 44 | 'petset': STRATEGY_CREATE_OR_FORCE_REPLACE, 45 | 'statefulset': STRATEGY_CREATE_OR_FORCE_REPLACE, 46 | 'persistentvolumeclaim': STRATEGY_CREATE_OR_NOTHING, 47 | 'persistentvolume': STRATEGY_CREATE_OR_APPLY, 48 | 'pod': STRATEGY_CREATE_OR_NOTHING, 49 | 'podsecuritypolicy': STRATEGY_CREATE_OR_NOTHING, 50 | 'podtemplate': STRATEGY_CREATE_OR_NOTHING, 51 | 'replicaset': STRATEGY_CREATE_OR_NOTHING, 52 | 'replicationcontroller': STRATEGY_CREATE_OR_NOTHING, 53 | 'resourcequota': STRATEGY_CREATE_OR_NOTHING, 54 | 'cronjob': STRATEGY_CREATE_OR_REPLACE, 55 | 'scheduledjob': STRATEGY_CREATE_OR_REPLACE, 56 | 'secret': STRATEGY_CREATE_OR_REPLACE, 57 | 'serviceaccount': STRATEGY_CREATE_OR_NOTHING, 58 | 'service': STRATEGY_CREATE_OR_APPLY, 59 | 'storageclass': STRATEGY_CREATE_OR_NOTHING, 60 | 'thirdpartyresource': STRATEGY_CREATE_OR_NOTHING, 61 | } 62 | 63 | 64 | def kube_objects_present(file_name, strategy): 65 | """ 66 | Make sure object exists and up to date 67 | :param strategy: string 68 | :param file_name: string 69 | :returns: (error:bool, changed:bool, result:dict) 70 | """ 71 | changed = False 72 | 73 | docs, error = __get_docs(file_name) 74 | 75 | if error is not None: 76 | meta = {"status": 'failed', 'file': file_name, 'response': error} 77 | return True, False, meta 78 | 79 | metas = [] 80 | doc_num = 0 81 | any_error = False 82 | any_change = False 83 | for doc in docs: 84 | error, k_object_kind, k_object_name, k_object_namespace = __extract_object_info(doc) 85 | 86 | if error is not None: 87 | meta = { 88 | "status": 'failed', 89 | 'file': file_name, 90 | 'response': error + '[doc num: ' + str(doc_num) + ']' 91 | } 92 | return True, False, meta 93 | success = False 94 | current_strategy = STRATEGIES[k_object_kind.lower()] if strategy == STRATEGY_DEFAULT else strategy 95 | 96 | if __object_exist(k_object_kind, k_object_name, k_object_namespace)[0] == True: 97 | if current_strategy == STRATEGY_CREATE_OR_FORCE_REPLACE: 98 | success, output = __replace_object(doc, k_object_namespace, True) 99 | changed = success 100 | elif current_strategy == STRATEGY_CREATE_OR_REPLACE: 101 | success, output = __replace_object(doc, k_object_namespace) 102 | changed = success 103 | elif current_strategy == STRATEGY_CREATE_OR_APPLY: 104 | success, output = __apply_object(doc, k_object_namespace) 105 | changed = success 106 | elif current_strategy == STRATEGY_CREATE_OR_NOTHING: 107 | success = True 108 | output = "Nothing to do with object" 109 | else: 110 | success, output = __create_object(doc, k_object_namespace) 111 | 112 | if any_error == False and success == False: 113 | any_error = True 114 | 115 | if any_change == False and changed == True: 116 | any_change = True 117 | 118 | meta = { 119 | "status": success, 120 | 'file': file_name, 121 | 'response': output, 122 | 'object_kind': k_object_kind, 123 | 'object_name': k_object_name, 124 | 'object_namespace': k_object_namespace, 125 | 'strategy': current_strategy 126 | } 127 | metas.append(meta) 128 | doc_num = +1 129 | 130 | return any_error, any_change, metas 131 | 132 | 133 | def kube_objects_absent(file_name): 134 | """ 135 | Make sure object does not exist in k8s 136 | :param file_name: str 137 | :return: (error:bool,changed:bool,metas:dict) 138 | """ 139 | changed = False 140 | 141 | docs, error = __get_docs(file_name) 142 | 143 | if error is not None: 144 | meta = {"status": 'failed', 'file': file_name, 'response': error} 145 | return True, False, meta 146 | 147 | metas = [] 148 | doc_num = 0 149 | for doc in docs: 150 | error, k_object_kind, k_object_name, k_object_namespace = __extract_object_info(doc) 151 | 152 | if error is not None: 153 | meta = { 154 | "status": 'failed', 155 | 'file': file_name, 156 | 'response': error + '[doc num: ' + str(doc_num) + ']' 157 | } 158 | return True, False, meta 159 | if __object_exist(k_object_kind, k_object_name, k_object_namespace)[0] == True: 160 | success, output = __delete_object(k_object_kind, k_object_name, k_object_namespace) 161 | changed = success 162 | else: 163 | success = True 164 | output = "Object already absent" 165 | 166 | meta = { 167 | "status": success, 168 | 'file': file_name, 169 | 'response': output, 170 | 'object_kind': k_object_kind, 171 | 'object_name': k_object_name, 172 | 'object_namespace': k_object_namespace, 173 | } 174 | metas.append(meta) 175 | doc_num = +1 176 | 177 | return False if meta['status'] else True, changed, metas 178 | 179 | 180 | def __get_docs(file_name): 181 | """ 182 | Extract all documents from yaml file 183 | :param file_name: 184 | :return: (docs:dict, error:str) 185 | """ 186 | error = None 187 | if not os.path.isfile(file_name): 188 | error = 'File does not exist' 189 | stream = open(file_name, "r") 190 | docs = yaml.load_all(stream) 191 | return docs, error 192 | 193 | 194 | def __extract_object_info(doc): 195 | """ 196 | Extract information about object 197 | :param doc: dict 198 | :return: (error:str|None, kind:str, name:str, namespace:str|None) 199 | """ 200 | error = k_object_kind = k_object_name = k_object_namespace = None 201 | if 'kind' not in doc: 202 | error = 'No \'kind\' for object' 203 | else: 204 | k_object_kind = doc['kind'] 205 | if k_object_kind.lower() not in STRATEGIES: 206 | error = 'Unsupported object kind ' + k_object_kind 207 | if 'metadata' not in doc: 208 | error = 'No \'metadata\' for object' 209 | else: 210 | metadata = doc['metadata'] 211 | if 'name' not in metadata: 212 | error = 'No \'metadata.name\' for object' 213 | else: 214 | k_object_name = doc['metadata']['name'] 215 | 216 | k_object_namespace = metadata['namespace'] if 'namespace' in metadata else None 217 | return error, k_object_kind, k_object_name, k_object_namespace 218 | 219 | 220 | def __object_exist(k_object_kind, k_object_name, k_object_namespace=None): 221 | """ 222 | Check if object exists 223 | 224 | :param k_object_kind: str 225 | :param k_object_name: str 226 | :param k_object_namespace: str|None 227 | :returns bool, str 228 | """ 229 | status, result = __kube_exec( 230 | "get " + k_object_kind + " " + k_object_name + ( 231 | (" --namespace=" + k_object_namespace) if k_object_namespace is not None else "" 232 | ) 233 | ) 234 | 235 | return status, result 236 | 237 | 238 | def __create_object(doc, namespace=None): 239 | """ 240 | Create k8s object 241 | :param doc: dict 242 | :return bool 243 | """ 244 | obj_file = __get_object_file(doc) 245 | return __kube_exec("create -f " + obj_file, namespace) 246 | 247 | 248 | def __apply_object(doc, namespace=None): 249 | """ 250 | Apply k8s object 251 | :param doc: dict 252 | :return bool 253 | """ 254 | obj_file = __get_object_file(doc) 255 | return __kube_exec("apply -f " + obj_file, namespace) 256 | 257 | 258 | def __replace_object(doc, namespace=None, force=False): 259 | """ 260 | Replace k8s object 261 | :param doc: 262 | :return bool 263 | """ 264 | obj_file = __get_object_file(doc) 265 | return __kube_exec("replace " + ("--force " if force else "") + "-f " + obj_file, namespace) 266 | 267 | 268 | def __delete_object(kind, name, namespace): 269 | """ 270 | Delete object from kubernetes 271 | :param kind: str 272 | :param name: str 273 | :return: 274 | """ 275 | return __kube_exec( 276 | "delete " + kind + " " + name, namespace 277 | ) 278 | 279 | 280 | def __get_object_file(doc): 281 | """ 282 | Get name of the file with object 283 | :param doc: 284 | :return: str 285 | """ 286 | obj_file = tempfile.NamedTemporaryFile(delete=False) 287 | with open(obj_file.name, 'w') as outfile: 288 | outfile.write(yaml.dump(doc, default_style='"')) 289 | outfile.close() 290 | return obj_file.name 291 | 292 | 293 | def __kube_exec(command, namespace=None): 294 | """ 295 | Execute k8s command and return status and output 296 | :param command: 297 | :return: (status:bool, result:str) 298 | """ 299 | child = subprocess.Popen( 300 | "kubectl " + (kubectl_options if kubectl_options else "") + " " + 301 | ((" --namespace=" + namespace + " ") if namespace is not None else "") + 302 | command, 303 | shell=True, 304 | stdout=subprocess.PIPE, 305 | stderr=subprocess.PIPE 306 | ) 307 | output, errors = child.communicate() 308 | status = True if child.returncode == 0 else False 309 | result = output if child.returncode == 0 else errors 310 | return status, result 311 | 312 | 313 | kubectl_options = '' 314 | 315 | 316 | def main(): 317 | global kubectl_options 318 | 319 | fields = { 320 | "file": {"required": True, "type": "str"}, 321 | "kubectl_opts": {"type": "str", "default": ""}, 322 | "state": { 323 | "default": "present", 324 | "choices": ['present', 'absent'], 325 | "type": 'str' 326 | }, 327 | "strategy": { 328 | "default": STRATEGY_DEFAULT, 329 | "choices": [STRATEGY_DEFAULT, STRATEGY_CREATE_OR_REPLACE, STRATEGY_CREATE_OR_APPLY, STRATEGY_CREATE_OR_NOTHING, STRATEGY_CREATE_OR_FORCE_REPLACE], 330 | "type": 'str' 331 | }, 332 | } 333 | 334 | module = AnsibleModule(argument_spec=fields) 335 | 336 | kubectl_options = module.params['kubectl_opts'] 337 | 338 | if module.params['state'] == 'present': 339 | is_error, has_changed, result = kube_objects_present(module.params['file'], module.params['strategy']) 340 | else: 341 | is_error, has_changed, result = kube_objects_absent(module.params['file']) 342 | 343 | if not is_error: 344 | module.exit_json(changed=has_changed, meta=result) 345 | else: 346 | module.fail_json(msg="Error creating/updating object[s]", meta=result) 347 | 348 | 349 | if __name__ == '__main__': 350 | main() 351 | --------------------------------------------------------------------------------