Fixing/cleaning up kubernetes submission

1) Removed kubectl functionality. We'll move that into a different
   module in the future. Also removed post/put/patch/delete options,
   as they are not Ansible best practice.
2) Expanded error handling in areas where tracebacks were most likely,
   based on bad data from users, etc.
3) Added an 'insecure' option and made the password param optional, to
   enable the use of the local insecure port.
4) Allowed the data (both inline and from the file) to support multiple
   items via a list. This is common in YAML files where mutliple docs
   are used to create/remove multiple resources in one shot.
5) General bug fixing.
This commit is contained in:
James Cammarata 2016-04-14 12:30:34 -04:00 committed by Matt Clay
parent a59cb5cae1
commit c6e8b9a0b7

View file

@ -28,9 +28,6 @@ description:
Only supports HTTP Basic Auth Only supports HTTP Basic Auth
Only supports 'strategic merge' for update, http://goo.gl/fCPYxT Only supports 'strategic merge' for update, http://goo.gl/fCPYxT
SSL certs are not working, use 'validate_certs=off' to disable SSL certs are not working, use 'validate_certs=off' to disable
This module can mimic the 'kubectl' Kubernetes client for commands
such as 'get', 'cluster-info', and 'version'. This is useful if you
want to fetch full object details for existing Kubernetes resources.
options: options:
api_endpoint: api_endpoint:
description: description:
@ -40,12 +37,14 @@ options:
aliases: ["endpoint"] aliases: ["endpoint"]
inline_data: inline_data:
description: description:
- The Kubernetes YAML data to send to the API I(endpoint). - The Kubernetes YAML data to send to the API I(endpoint). This option is
mutually exclusive with C('file_reference').
required: true required: true
default: null default: null
file_reference: file_reference:
description: description:
- Specify full path to a Kubernets YAML file to send to API I(endpoint). - Specify full path to a Kubernets YAML file to send to API I(endpoint).
This option is mutually exclusive with C('inline_data').
required: false required: false
default: null default: null
certificate_authority_data: certificate_authority_data:
@ -56,53 +55,27 @@ options:
'match_hostname' that can match the IP address against the CA data. 'match_hostname' that can match the IP address against the CA data.
required: false required: false
default: null default: null
kubectl_api_versions:
description:
- Mimic the 'kubectl api-versions' command, values are ignored.
required: false
default: null
kubectl_cluster_info:
description:
- Mimic the 'kubectl cluster-info' command, values are ignored.
required: false
default: null
kubectl_get:
description:
- Mimic the 'kubectl get' command. Specify the object(s) to fetch such
as 'pods' or 'replicationcontrollers/mycontroller'. It does not
support shortcuts (e.g. 'po', 'rc', 'svc').
required: false
default: null
kubectl_namespace:
description:
- Specify the namespace to use for 'kubectl' commands.
required: false
default: "default"
kubectl_version:
description:
- Mimic the 'kubectl version' command, values are ignored.
required: false
default: null
state: state:
description: description:
- The desired action to take on the Kubernetes data, or 'kubectl' to - The desired action to take on the Kubernetes data.
mimic some kubectl commands.
required: true required: true
default: "present" default: "present"
choices: ["present", "post", "absent", "delete", "update", "patch", choices: ["present", "absent", "update", "replace"]
"replace", "put", "kubectl"] password:
url_password:
description: description:
- The HTTP Basic Auth password for the API I(endpoint). - The HTTP Basic Auth password for the API I(endpoint). This should be set
required: true unless using the C('insecure') option.
default: null default: null
aliases: ["password", "api_password"] username:
url_username:
description: description:
- The HTTP Basic Auth username for the API I(endpoint). - The HTTP Basic Auth username for the API I(endpoint). This should be set
required: true unless using the C('insecure') option.
default: "admin" default: "admin"
aliases: ["username", "api_username"] insecure:
description:
- "Reverts the connection to using HTTP instead of HTTPS. This option should
only be used when execuing the M('kubernetes') module local to the Kubernetes
cluster using the insecure local port (locahost:8080 by default)."
validate_certs: validate_certs:
description: description:
- Enable/disable certificate validation. Note that this is set to - Enable/disable certificate validation. Note that this is set to
@ -110,7 +83,6 @@ options:
hostname matching (exists in >= python3.5.0). hostname matching (exists in >= python3.5.0).
required: false required: false
default: false default: false
choices: BOOLEANS
author: "Eric Johnson (@erjohnso) <erjohnso@google.com>" author: "Eric Johnson (@erjohnso) <erjohnso@google.com>"
''' '''
@ -120,8 +92,8 @@ EXAMPLES = '''
- name: Create a kubernetes namespace - name: Create a kubernetes namespace
kubernetes: kubernetes:
api_endpoint: 123.45.67.89 api_endpoint: 123.45.67.89
url_username: admin username: admin
url_password: redacted password: redacted
inline_data: inline_data:
kind: Namespace kind: Namespace
apiVersion: v1 apiVersion: v1
@ -139,28 +111,19 @@ EXAMPLES = '''
- name: Create a kubernetes namespace - name: Create a kubernetes namespace
kubernetes: kubernetes:
api_endpoint: 123.45.67.89 api_endpoint: 123.45.67.89
url_username: admin username: admin
url_password: redacted password: redacted
file_reference: /path/to/create_namespace.yaml file_reference: /path/to/create_namespace.yaml
state: present state: present
# Fetch info about the Kubernets cluster with a fake 'kubectl' command. # Do the same thing, but using the insecure localhost port
- name: Look up cluster info - name: Create a kubernetes namespace
kubernetes: kubernetes:
api_endpoint: 123.45.67.89 api_endpoint: 123.45.67.89
url_username: admin insecure: true
url_password: redacted file_reference: /path/to/create_namespace.yaml
kubectl_cluster_info: 1 state: present
state: kubectl
# Fetch info about the Kubernets pods with a fake 'kubectl' command.
- name: Look up pods
kubernetes:
api_endpoint: 123.45.67.89
url_username: admin
url_password: redacted
kubectl_get: pods
state: kubectl
''' '''
RETURN = ''' RETURN = '''
@ -259,6 +222,7 @@ USER_AGENT = "ansible-k8s-module/0.0.1"
# - Set 'required=true' for certificate_authority_data and ensure that # - Set 'required=true' for certificate_authority_data and ensure that
# ansible's SSLValidationHandler.get_ca_certs() can pick up this CA cert # ansible's SSLValidationHandler.get_ca_certs() can pick up this CA cert
# - Set 'required=true' for the validate_certs param. # - Set 'required=true' for the validate_certs param.
def decode_cert_data(module): def decode_cert_data(module):
return return
d = module.params.get("certificate_authority_data") d = module.params.get("certificate_authority_data")
@ -270,75 +234,70 @@ def api_request(module, url, method="GET", headers=None, data=None):
body = None body = None
if data: if data:
data = json.dumps(data) data = json.dumps(data)
response, info = fetch_url(module, url, method=method, headers=headers, response, info = fetch_url(module, url, method=method, headers=headers, data=data)
data=data) if int(info['status']) == -1:
module.fail_json(msg="Failed to execute the API request: %s" % info['msg'], url=url, method=method, headers=headers)
if response is not None: if response is not None:
body = json.loads(response.read()) body = json.loads(response.read())
return info, body return info, body
def k8s_kubectl_get(module, url):
req = module.params.get("kubectl_get")
info, body = api_request(module, url + "/" + req)
return False, body
def k8s_delete_resource(module, url, data):
name = None
if 'metadata' in data:
name = data['metadata'].get('name')
if name is None:
module.fail_json(msg="Missing a named resource in object metadata")
url = url + '/' + name
info, body = api_request(module, url, method="DELETE")
if info['status'] == 404:
return False, {}
if info['status'] == 200:
return True, body
module.fail_json(msg="%s: fetching URL '%s'" % (info['msg'], url))
def k8s_create_resource(module, url, data): def k8s_create_resource(module, url, data):
info, body = api_request(module, url, method="POST", data=data) info, body = api_request(module, url, method="POST", data=data, headers={"Content-Type": "application/json"})
if info['status'] == 409: if info['status'] == 409:
name = data["metadata"].get("name", None) name = data["metadata"].get("name", None)
info, body = api_request(module, url + "/" + name) info, body = api_request(module, url + "/" + name)
return False, body return False, body
elif info['status'] >= 400:
module.fail_json(msg="failed to create the resource: %s" % info['msg'], url=url)
return True, body return True, body
def k8s_replace_resource(module, url, data): def k8s_delete_resource(module, url, data):
name = None name = data.get('metadata', {}).get('name')
if 'metadata' in data:
name = data['metadata'].get('name')
if name is None: if name is None:
module.fail_json(msg="Missing a named resource in object metadata") module.fail_json(msg="Missing a named resource in object metadata when trying to remove a resource")
url = url + '/' + name
info, body = api_request(module, url, method="PUT", data=data) url = url + '/' + name
info, body = api_request(module, url, method="DELETE")
if info['status'] == 404:
return False, "Resource name '%s' already absent" % name
elif info['status'] >= 400:
module.fail_json(msg="failed to delete the resource '%s': %s" % (name, info['msg']), url=url)
return True, "Successfully deleted resource name '%s'" % name
def k8s_replace_resource(module, url, data):
name = data.get('metadata', {}).get('name')
if name is None:
module.fail_json(msg="Missing a named resource in object metadata when trying to replace a resource")
headers = {"Content-Type": "application/json"}
url = url + '/' + name
info, body = api_request(module, url, method="PUT", data=data, headers=headers)
if info['status'] == 409: if info['status'] == 409:
name = data["metadata"].get("name", None) name = data["metadata"].get("name", None)
info, body = api_request(module, url + "/" + name) info, body = api_request(module, url + "/" + name)
return False, body return False, body
elif info['status'] >= 400:
module.fail_json(msg="failed to replace the resource '%s': %s" % (name, info['msg']), url=url)
return True, body return True, body
def k8s_update_resource(module, url, data): def k8s_update_resource(module, url, data):
name = None name = data.get('metadata', {}).get('name')
if 'metadata' in data:
name = data['metadata'].get('name')
if name is None: if name is None:
module.fail_json(msg="Missing a named resource in object metadata") module.fail_json(msg="Missing a named resource in object metadata when trying to update a resource")
url = url + '/' + name
headers = {"Content-Type": "application/strategic-merge-patch+json"} headers = {"Content-Type": "application/strategic-merge-patch+json"}
info, body = api_request(module, url, method="PATCH", data=data, url = url + '/' + name
headers=headers) info, body = api_request(module, url, method="PATCH", data=data, headers=headers)
if info['status'] == 409: if info['status'] == 409:
name = data["metadata"].get("name", None) name = data["metadata"].get("name", None)
info, body = api_request(module, url + "/" + name) info, body = api_request(module, url + "/" + name)
return False, body return False, body
elif info['status'] >= 400:
module.fail_json(msg="failed to update the resource '%s': %s" % (name, info['msg']), url=url)
return True, body return True, body
@ -347,97 +306,81 @@ def main():
argument_spec=dict( argument_spec=dict(
http_agent=dict(default=USER_AGENT), http_agent=dict(default=USER_AGENT),
url_username=dict(default="admin"), username=dict(default="admin"),
url_password=dict(required=True, no_log=True), password=dict(default="", no_log=True),
force_basic_auth=dict(default="yes"), force_basic_auth=dict(default="yes"),
validate_certs=dict(default=False, choices=BOOLEANS), validate_certs=dict(default=False, type='bool'),
certificate_authority_data=dict(required=False), certificate_authority_data=dict(required=False),
insecure=dict(default=False, type='bool'),
# fake 'kubectl' commands
kubectl_api_versions=dict(required=False),
kubectl_cluster_info=dict(required=False),
kubectl_get=dict(required=False),
kubectl_namespace=dict(required=False, default="default"),
kubectl_version=dict(required=False),
# k8s API module variables
api_endpoint=dict(required=True), api_endpoint=dict(required=True),
file_reference=dict(required=False), file_reference=dict(required=False),
inline_data=dict(required=False), inline_data=dict(required=False),
state=dict(default="present", state=dict(default="present", choices=["present", "absent", "update", "replace"])
choices=["present", "post", ),
"absent", "delete", mutually_exclusive = (('file_reference', 'inline_data'), ('username', 'insecure'), ('password', 'insecure')),
"update", "put", required_one_of = (('file_reference', 'inline_data'),),
"replace", "patch",
"kubectl"])
)
) )
decode_cert_data(module) decode_cert_data(module)
changed = False api_endpoint = module.params.get('api_endpoint')
data = module.params.get('inline_data', {})
if not data:
dfile = module.params.get('file_reference')
if dfile:
f = open(dfile, "r")
data = yaml.load(f)
endpoint = "https://" + module.params.get('api_endpoint')
url = endpoint
namespace = "default"
if data and 'metadata' in data:
namespace = data['metadata'].get('namespace', "default")
kind = data['kind'].lower()
url = endpoint + KIND_URL[kind]
url = url.replace("{namespace}", namespace)
# check for 'kubectl' commands
kubectl_api_versions = module.params.get('kubectl_api_versions')
kubectl_cluster_info = module.params.get('kubectl_cluster_info')
kubectl_get = module.params.get('kubectl_get')
kubectl_namespace = module.params.get('kubectl_namespace')
kubectl_version = module.params.get('kubectl_version')
state = module.params.get('state') state = module.params.get('state')
if state in ['present', 'post']: insecure = module.params.get('insecure')
changed, body = k8s_create_resource(module, url, data) inline_data = module.params.get('inline_data')
module.exit_json(changed=changed, api_response=body) file_reference = module.params.get('file_reference')
if state in ['absent', 'delete']: if inline_data:
changed, body = k8s_delete_resource(module, url, data) data = inline_data
module.exit_json(changed=changed, api_response=body) else:
try:
f = open(file_reference, "r")
data = [x for x in yaml.load_all(f)]
f.close()
if not data:
module.fail_json(msg="No valid data could be found.")
except:
module.fail_json(msg="The file '%s' was not found or contained invalid YAML/JSON data" % file_reference)
if state in ['replace', 'put']: # set the transport type and build the target endpoint url
changed, body = k8s_replace_resource(module, url, data) transport = 'https'
module.exit_json(changed=changed, api_response=body) if insecure:
transport = 'http'
if state in ['update', 'patch']: target_endpoint = "%s://%s" % (transport, api_endpoint)
changed, body = k8s_update_resource(module, url, data)
module.exit_json(changed=changed, api_response=body)
if state == 'kubectl': body = []
kurl = url + "/api/v1/namespaces/" + kubectl_namespace changed = False
if kubectl_get:
if kubectl_get.startswith("namespaces"):
kurl = url + "/api/v1"
changed, body = k8s_kubectl_get(module, kurl)
module.exit_json(changed=changed, api_response=body)
if kubectl_version:
info, body = api_request(module, url + "/version")
module.exit_json(changed=False, api_response=body)
if kubectl_api_versions:
info, body = api_request(module, url + "/api")
module.exit_json(changed=False, api_response=body)
if kubectl_cluster_info:
info, body = api_request(module, url +
"/api/v1/namespaces/kube-system"
"/services?labelSelector=kubernetes"
".io/cluster-service=true")
module.exit_json(changed=False, api_response=body)
module.fail_json(msg="Invalid state: '%s'" % state) # make sure the data is a list
if not isinstance(data, list):
data = [ data ]
for item in data:
namespace = "default"
if item and 'metadata' in item:
namespace = item.get('metadata', {}).get('namespace', "default")
kind = item.get('kind', '').lower()
try:
url = target_endpoint + KIND_URL[kind]
except KeyError:
module.fail_json("invalid resource kind specified in the data: '%s'" % kind)
url = url.replace("{namespace}", namespace)
else:
url = target_endpoint
if state == 'present':
item_changed, item_body = k8s_create_resource(module, url, item)
elif state == 'absent':
item_changed, item_body = k8s_delete_resource(module, url, item)
elif state == 'replace':
item_changed, item_body = k8s_replace_resource(module, url, item)
elif state == 'update':
item_changed, item_body = k8s_update_resource(module, url, item)
changed |= item_changed
body.append(item_body)
module.exit_json(changed=changed, api_response=body)
# import module snippets # import module snippets