#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function

__metaclass__ = type


DOCUMENTATION = r"""
---
module: digital_ocean_kubernetes
short_description: Create and delete a DigitalOcean Kubernetes cluster
description:
  - Create and delete a Kubernetes cluster in DigitalOcean (and optionally wait for it to be running).
version_added: 1.3.0
author: Mark Mercado (@mamercad)
options:
  oauth_token:
    description:
      - DigitalOcean OAuth token; can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
    type: str
    aliases: ['API_TOKEN']
    required: true
  state:
    description:
      - The usual, C(present) to create, C(absent) to destroy
    type: str
    choices: ['present', 'absent']
    default: present
  name:
    description:
      - A human-readable name for a Kubernetes cluster.
    type: str
    required: true
  region:
    description:
      - The slug identifier for the region where the Kubernetes cluster will be created.
    type: str
    aliases: ['region_id']
    default: nyc1
  version:
    description:
      - The slug identifier for the version of Kubernetes used for the cluster. See the /v2/kubernetes/options endpoint for available versions.
    type: str
    required: false
    default: latest
  auto_upgrade:
    description:
      - A boolean value indicating whether the cluster will be automatically upgraded to new patch releases during its maintenance window.
    type: bool
    required: false
    default: false
  surge_upgrade:
    description:
      - A boolean value indicating whether surge upgrade is enabled/disabled for the cluster.
      - Surge upgrade makes cluster upgrades fast and reliable by bringing up new nodes before destroying the outdated nodes.
    type: bool
    required: false
    default: false
  tags:
    description:
      - A flat array of tag names as strings to be applied to the Kubernetes cluster.
      - All clusters will be automatically tagged "k8s" and "k8s:$K8S_CLUSTER_ID" in addition to any tags provided by the user.
    required: false
    type: list
    elements: str
  maintenance_policy:
    description:
      - An object specifying the maintenance window policy for the Kubernetes cluster (see table below).
    type: dict
    required: false
  node_pools:
    description:
      - An object specifying the details of the worker nodes available to the Kubernetes cluster (see table below).
    type: list
    elements: dict
    suboptions:
      name:
        type: str
        description: A human-readable name for the node pool.
      size:
        type: str
        description: The slug identifier for the type of Droplet used as workers in the node pool.
      count:
        type: int
        description: The number of Droplet instances in the node pool.
      tags:
        type: list
        elements: str
        description:
          - An array containing the tags applied to the node pool.
          - All node pools are automatically tagged C("k8s"), C("k8s-worker"), and C("k8s:$K8S_CLUSTER_ID").
      labels:
        type: dict
        description: An object containing a set of Kubernetes labels. The keys are user-defined.
      taints:
        type: list
        elements: dict
        description:
          - An array of taints to apply to all nodes in a pool.
          - Taints will automatically be applied to all existing nodes and any subsequent nodes added to the pool.
          - When a taint is removed, it is removed from all nodes in the pool.
      auto_scale:
        type: bool
        description:
          - A boolean value indicating whether auto-scaling is enabled for this node pool.
      min_nodes:
        type: int
        description:
          - The minimum number of nodes that this node pool can be auto-scaled to.
          - The value will be C(0) if C(auto_scale) is set to C(false).
      max_nodes:
        type: int
        description:
          - The maximum number of nodes that this node pool can be auto-scaled to.
          - The value will be C(0) if C(auto_scale) is set to C(false).
    default:
      - name: worker-pool
        size: s-1vcpu-2gb
        count: 1
        tags: []
        labels: {}
        taints: []
        auto_scale: false
        min_nodes: 0
        max_nodes: 0
  vpc_uuid:
    description:
      - A string specifying the UUID of the VPC to which the Kubernetes cluster will be assigned.
      - If excluded, the cluster will be assigned to your account's default VPC for the region.
    type: str
    required: false
  return_kubeconfig:
    description:
      - Controls whether or not to return the C(kubeconfig).
    type: bool
    required: false
    default: false
  wait:
    description:
      - Wait for the cluster to be running before returning.
    type: bool
    required: false
    default: true
  wait_timeout:
    description:
      - How long before wait gives up, in seconds, when creating a cluster.
    type: int
    default: 600
  ha:
    description:
      - A boolean value indicating whether the control plane is run in a highly available configuration in the cluster.
      - Highly available control planes incur less downtime.
    type: bool
    default: false
"""


EXAMPLES = r"""
- name: Create a new DigitalOcean Kubernetes cluster in New York 1
  community.digitalocean.digital_ocean_kubernetes:
    state: present
    oauth_token: "{{ lookup('env', 'DO_API_TOKEN') }}"
    name: hacktoberfest
    region: nyc1
    node_pools:
      - name: hacktoberfest-workers
        size: s-1vcpu-2gb
        count: 3
    return_kubeconfig: yes
    wait_timeout: 600
    register: my_cluster

- name: Show the kubeconfig for the cluster we just created
  debug:
    msg: "{{ my_cluster.data.kubeconfig }}"

- name: Destroy (delete) an existing DigitalOcean Kubernetes cluster
  community.digitalocean.digital_ocean_kubernetes:
    state: absent
    oauth_token: "{{ lookup('env', 'DO_API_TOKEN') }}"
    name: hacktoberfest
"""


# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/Kubernetes
# The only variance from the documented response is that the kubeconfig is (if return_kubeconfig is True) merged in at data['kubeconfig']
RETURN = r"""
data:
  description: A DigitalOcean Kubernetes cluster (and optional C(kubeconfig))
  returned: changed
  type: dict
  sample:
    kubeconfig: |-
      apiVersion: v1
      clusters:
      - cluster:
          certificate-authority-data: REDACTED
          server: https://REDACTED.k8s.ondigitalocean.com
        name: do-nyc1-hacktoberfest
      contexts:
      - context:
          cluster: do-nyc1-hacktoberfest
          user: do-nyc1-hacktoberfest-admin
        name: do-nyc1-hacktoberfest
      current-context: do-nyc1-hacktoberfest
      kind: Config
      preferences: {}
      users:
      - name: do-nyc1-hacktoberfest-admin
        user:
          token: REDACTED
    kubernetes_cluster:
      auto_upgrade: false
      cluster_subnet: 10.244.0.0/16
      created_at: '2020-09-27T00:55:37Z'
      endpoint: https://REDACTED.k8s.ondigitalocean.com
      id: REDACTED
      ipv4: REDACTED
      maintenance_policy:
        day: any
        duration: 4h0m0s
        start_time: '15:00'
      name: hacktoberfest
      node_pools:
      - auto_scale: false
        count: 1
        id: REDACTED
        labels: null
        max_nodes: 0
        min_nodes: 0
        name: hacktoberfest-workers
        nodes:
        - created_at: '2020-09-27T00:55:37Z'
          droplet_id: '209555245'
          id: REDACTED
          name: hacktoberfest-workers-3tdq1
          status:
            state: running
          updated_at: '2020-09-27T00:58:36Z'
        size: s-1vcpu-2gb
        tags:
        - k8s
        - k8s:REDACTED
        - k8s:worker
        taints: []
      region: nyc1
      service_subnet: 10.245.0.0/16
      status:
        state: running
      surge_upgrade: false
      tags:
      - k8s
      - k8s:REDACTED
      updated_at: '2020-09-27T01:00:37Z'
      version: 1.18.8-do.1
      vpc_uuid: REDACTED
"""


import traceback
import time
import json
from traceback import format_exc
from ansible.module_utils._text import to_native
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
    DigitalOceanHelper,
)


class DOKubernetes(object):
    def __init__(self, module):
        self.rest = DigitalOceanHelper(module)
        self.module = module
        # Pop these values so we don't include them in the POST data
        self.return_kubeconfig = self.module.params.pop("return_kubeconfig", False)
        self.wait = self.module.params.pop("wait", True)
        self.wait_timeout = self.module.params.pop("wait_timeout", 600)
        self.module.params.pop("oauth_token")
        self.cluster_id = None

    def get_by_id(self):
        """Returns an existing DigitalOcean Kubernetes cluster matching on id"""
        response = self.rest.get("kubernetes/clusters/{0}".format(self.cluster_id))
        json_data = response.json
        if response.status_code == 200:
            return json_data
        return None

    def get_all_clusters(self):
        """Returns all DigitalOcean Kubernetes clusters"""
        response = self.rest.get("kubernetes/clusters")
        json_data = response.json
        if response.status_code == 200:
            return json_data
        return None

    def get_by_name(self, cluster_name):
        """Returns an existing DigitalOcean Kubernetes cluster matching on name"""
        if not cluster_name:
            return None
        clusters = self.get_all_clusters()
        for cluster in clusters["kubernetes_clusters"]:
            if cluster["name"] == cluster_name:
                return cluster
        return None

    def get_kubernetes_kubeconfig(self):
        """Returns the kubeconfig for an existing DigitalOcean Kubernetes cluster"""
        response = self.rest.get(
            "kubernetes/clusters/{0}/kubeconfig".format(self.cluster_id)
        )
        if response.status_code == 200:
            return response.body
        else:
            self.module.fail_json(msg="Failed to retrieve kubeconfig")

    def get_kubernetes(self):
        """Returns an existing DigitalOcean Kubernetes cluster by name"""
        json_data = self.get_by_name(self.module.params["name"])
        if json_data:
            self.cluster_id = json_data["id"]
            return json_data
        else:
            return None

    def get_kubernetes_options(self):
        """Fetches DigitalOcean Kubernetes options: regions, sizes, versions.
        API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/list_kubernetes_options
        """
        response = self.rest.get("kubernetes/options")
        json_data = response.json
        if response.status_code == 200:
            return json_data
        return None

    def ensure_running(self):
        """Waits for the newly created DigitalOcean Kubernetes cluster to be running"""
        end_time = time.monotonic() + self.wait_timeout
        while time.monotonic() < end_time:
            cluster = self.get_by_id()
            if cluster["kubernetes_cluster"]["status"]["state"] == "running":
                return cluster
            time.sleep(10)
        self.module.fail_json(msg="Wait for Kubernetes cluster to be running")

    def create(self):
        """Creates a DigitalOcean Kubernetes cluster
        API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/create_kubernetes_cluster
        """
        # Get valid Kubernetes options (regions, sizes, versions)
        kubernetes_options = self.get_kubernetes_options()["options"]
        # Validate region
        valid_regions = [str(x["slug"]) for x in kubernetes_options["regions"]]
        if self.module.params.get("region") not in valid_regions:
            self.module.fail_json(
                msg="Invalid region {0} (valid regions are {1})".format(
                    self.module.params.get("region"), ", ".join(valid_regions)
                )
            )
        # Validate version
        valid_versions = [str(x["slug"]) for x in kubernetes_options["versions"]]
        valid_versions.append("latest")
        if self.module.params.get("version") not in valid_versions:
            self.module.fail_json(
                msg="Invalid version {0} (valid versions are {1})".format(
                    self.module.params.get("version"), ", ".join(valid_versions)
                )
            )
        # Validate size
        valid_sizes = [str(x["slug"]) for x in kubernetes_options["sizes"]]
        for node_pool in self.module.params.get("node_pools"):
            if node_pool["size"] not in valid_sizes:
                self.module.fail_json(
                    msg="Invalid size {0} (valid sizes are {1})".format(
                        node_pool["size"], ", ".join(valid_sizes)
                    )
                )

        # Create the Kubernetes cluster
        json_data = self.get_kubernetes()
        if json_data:
            # Add the kubeconfig to the return
            if self.return_kubeconfig:
                json_data["kubeconfig"] = self.get_kubernetes_kubeconfig()
            self.module.exit_json(changed=False, data=json_data)
        if self.module.check_mode:
            self.module.exit_json(changed=True)
        request_params = dict(self.module.params)
        response = self.rest.post("kubernetes/clusters", data=request_params)
        json_data = response.json
        if response.status_code >= 400:
            self.module.fail_json(changed=False, msg=json_data)
        # Set the cluster_id
        self.cluster_id = json_data["kubernetes_cluster"]["id"]
        if self.wait:
            json_data = self.ensure_running()
        # Add the kubeconfig to the return
        if self.return_kubeconfig:
            json_data["kubeconfig"] = self.get_kubernetes_kubeconfig()
        self.module.exit_json(changed=True, data=json_data["kubernetes_cluster"])

    def delete(self):
        """Deletes a DigitalOcean Kubernetes cluster
        API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/delete_kubernetes_cluster
        """
        json_data = self.get_kubernetes()
        if json_data:
            if self.module.check_mode:
                self.module.exit_json(changed=True)
            response = self.rest.delete(
                "kubernetes/clusters/{0}".format(json_data["id"])
            )
            if response.status_code == 204:
                self.module.exit_json(
                    changed=True, data=json_data, msg="Kubernetes cluster deleted"
                )
            self.module.fail_json(
                changed=False, msg="Failed to delete Kubernetes cluster"
            )
            json_data = response.json
        else:
            self.module.exit_json(changed=False, msg="Kubernetes cluster not found")


def run(module):
    state = module.params.pop("state")
    cluster = DOKubernetes(module)
    if state == "present":
        cluster.create()
    elif state == "absent":
        cluster.delete()


def main():
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(choices=["present", "absent"], default="present"),
            oauth_token=dict(
                aliases=["API_TOKEN"],
                no_log=True,
                fallback=(
                    env_fallback,
                    ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
                ),
                required=True,
            ),
            name=dict(type="str", required=True),
            region=dict(aliases=["region_id"], default="nyc1"),
            version=dict(type="str", default="latest"),
            auto_upgrade=dict(type="bool", default=False),
            surge_upgrade=dict(type="bool", default=False),
            tags=dict(type="list", elements="str"),
            maintenance_policy=dict(type="dict"),
            node_pools=dict(
                type="list",
                elements="dict",
                default=[
                    {
                        "name": "worker-pool",
                        "size": "s-1vcpu-2gb",
                        "count": 1,
                        "tags": [],
                        "labels": {},
                        "taints": [],
                        "auto_scale": False,
                        "min_nodes": 0,
                        "max_nodes": 0,
                    }
                ],
            ),
            vpc_uuid=dict(type="str"),
            return_kubeconfig=dict(type="bool", default=False),
            wait=dict(type="bool", default=True),
            wait_timeout=dict(type="int", default=600),
            ha=dict(type="bool", default=False),
        ),
        required_if=(
            [
                ("state", "present", ["name", "region", "version", "node_pools"]),
            ]
        ),
        supports_check_mode=True,
    )

    run(module)


if __name__ == "__main__":
    main()
