From e0dcd053c510d1df0b93d2ceb805381a3a2855c3 Mon Sep 17 00:00:00 2001 From: kjubybot Date: Wed, 2 Feb 2022 15:58:20 +0300 Subject: [PATCH] kubernetes resource (currently broken) --- decort/controller.go | 13 +- decort/models_api.go | 48 +++++++ decort/node_subresource.go | 95 +++++++++++++ decort/provider.go | 1 + decort/resource_k8s.go | 277 +++++++++++++++++++++++++++++++++++++ decort/utility_k8s.go | 30 ++++ docs/resources/k8s.md | 51 +++++++ docs/resources/pfw.md | 12 ++ 8 files changed, 520 insertions(+), 7 deletions(-) create mode 100644 decort/node_subresource.go create mode 100644 decort/resource_k8s.go create mode 100644 decort/utility_k8s.go create mode 100644 docs/resources/k8s.md diff --git a/decort/controller.go b/decort/controller.go index 31e7af0..6950c51 100644 --- a/decort/controller.go +++ b/decort/controller.go @@ -384,12 +384,13 @@ func (config *ControllerCfg) decortAPICall(method string, api_name string, url_v } defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + log.Debugf("decortAPICall: %s %s\n %s", method, api_name, body) + if resp.StatusCode == http.StatusOK { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - log.Debugf("decortAPICall: %s %s\n %s", method, api_name, body) return string(body), nil } else { return "", fmt.Errorf("decortAPICall: unexpected status code %d when calling API %q with request Body %q", @@ -401,6 +402,4 @@ func (config *ControllerCfg) decortAPICall(method string, api_name string, url_v return nil, fmt.Errorf("decortAPICall method called for incompatible authorization mode %q.", config.auth_mode_txt) } */ - - return "", err } diff --git a/decort/models_api.go b/decort/models_api.go index 633f2f4..f61f68e 100644 --- a/decort/models_api.go +++ b/decort/models_api.go @@ -572,6 +572,54 @@ const VinsExtNetDisconnectAPI = "/restmachine/cloudapi/vins/extNetDisconnect" const VinsDeleteAPI = "/restmachine/cloudapi/vins/delete" +// +// K8s structures +// + +//K8sNodeRecord represents a worker/master group +type K8sNodeRecord struct { + ID int `json:"id"` + Disk int `json:"disk"` + Cpu int `json:"cpu"` + Num int `json:"num"` + Ram int `json:"ram"` +} + +//K8sRecord represents k8s instance +type K8sRecord struct { + AccountID int `json:"accountId"` + AccountName string `json:"accountName"` + CI int `json:"ciId"` + ID int `json:"id"` + Groups struct { + Masters K8sNodeRecord `json:"masters"` + Workers []K8sNodeRecord `json:"workers"` + } `json:"k8sGroups"` + Name string `json:"name"` + RgID int `json:"rgId"` + RgName string `json:"rgName"` +} + +const K8sCreateAPI = "/restmachine/cloudapi/k8s/create" +const K8sGetAPI = "/restmachine/cloudapi/k8s/get" +const K8sUpdateAPI = "/restmachine/cloudapi/k8s/update" +const K8sDeleteAPI = "/restmachine/cloudapi/k8s/delete" + +//AsyncTask represents a long task completion status +type AsyncTask struct { + AuditID string `json:"auditId"` + Completed bool `json:"completed"` + Error string `json:"error"` + Log []string `json:"log"` + Result string `json:"result"` + Stage string `json:"stage"` + Status string `json:"status"` + UpdateTime uint64 `json:"updateTime"` + UpdatedTime uint64 `json:"updatedTime"` +} + +const AsyncTaskGetAPI = "/restmachine/cloudapi/tasks/get" + // // Grid ID structures // diff --git a/decort/node_subresource.go b/decort/node_subresource.go new file mode 100644 index 0000000..dcb0b71 --- /dev/null +++ b/decort/node_subresource.go @@ -0,0 +1,95 @@ +/* +Copyright (c) 2019-2022 Digital Energy Cloud Solutions LLC. All Rights Reserved. +Author: Petr Krutov, + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +This file is part of Terraform (by Hashicorp) provider for Digital Energy Cloud Orchestration +Technology platfom. + +Visit https://github.com/rudecs/terraform-provider-decort for full source code package and updates. +*/ + +package decort + +import "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + +func nodeMasterDefault() K8sNodeRecord { + return K8sNodeRecord{ + Num: 1, + Cpu: 2, + Ram: 2048, + Disk: 0, + } +} + +func nodeWorkerDefault() K8sNodeRecord { + return K8sNodeRecord{ + Num: 1, + Cpu: 1, + Ram: 1024, + Disk: 0, + } +} + +func parseNode(nodeList []interface{}) K8sNodeRecord { + node := nodeList[0].(map[string]interface{}) + + return K8sNodeRecord{ + Num: node["num"].(int), + Cpu: node["cpu"].(int), + Ram: node["ram"].(int), + Disk: node["disk"].(int), + } +} + +func nodeToResource(node K8sNodeRecord) []interface{} { + mp := make(map[string]interface{}) + + mp["num"] = node.Num + mp["cpu"] = node.Cpu + mp["ram"] = node.Ram + mp["disk"] = node.Disk + + return []interface{}{mp} +} + +func nodeK8sSubresourceSchemaMake() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "num": { + Type: schema.TypeInt, + Required: true, + Description: "Number of nodes to create.", + }, + + "cpu": { + Type: schema.TypeInt, + Required: true, + Description: "Node CPU count.", + }, + + "ram": { + Type: schema.TypeInt, + Required: true, + Description: "Node RAM in MB.", + }, + + "disk": { + Type: schema.TypeInt, + Required: true, + Description: "Node boot disk size in GB.", + }, + } +} diff --git a/decort/provider.go b/decort/provider.go index 2a12f08..62620bd 100644 --- a/decort/provider.go +++ b/decort/provider.go @@ -104,6 +104,7 @@ func Provider() *schema.Provider { "decort_disk": resourceDisk(), "decort_vins": resourceVins(), "decort_pfw": resourcePfw(), + "decort_k8s": resourceK8s(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/decort/resource_k8s.go b/decort/resource_k8s.go new file mode 100644 index 0000000..ccdd625 --- /dev/null +++ b/decort/resource_k8s.go @@ -0,0 +1,277 @@ +/* +Copyright (c) 2019-2022 Digital Energy Cloud Solutions LLC. All Rights Reserved. +Author: Petr Krutov, + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +This file is part of Terraform (by Hashicorp) provider for Digital Energy Cloud Orchestration +Technology platfom. + +Visit https://github.com/rudecs/terraform-provider-decort for full source code package and updates. +*/ + +package decort + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + log "github.com/sirupsen/logrus" +) + +func resourceK8sCreate(d *schema.ResourceData, m interface{}) error { + log.Debugf("resourceK8sCreate: called with name %s, rg %d", d.Get("name").(string), d.Get("rg_id").(int)) + + controller := m.(*ControllerCfg) + urlValues := &url.Values{} + urlValues.Add("name", d.Get("name").(string)) + urlValues.Add("rgId", strconv.Itoa(d.Get("rg_id").(int))) + urlValues.Add("k8ciId", strconv.Itoa(d.Get("k8sci_id").(int))) + + var masterNode K8sNodeRecord + if masters, ok := d.GetOk("masters"); ok { + masterNode = parseNode(masters.([]interface{})) + } else { + masterNode = nodeMasterDefault() + } + urlValues.Add("masterNum", strconv.Itoa(masterNode.Num)) + urlValues.Add("masterCpu", strconv.Itoa(masterNode.Cpu)) + urlValues.Add("masterRam", strconv.Itoa(masterNode.Ram)) + urlValues.Add("masterDisk", strconv.Itoa(masterNode.Disk)) + + var workerNode K8sNodeRecord + if workers, ok := d.GetOk("workers"); ok { + workerNode = parseNode(workers.([]interface{})) + } else { + workerNode = nodeWorkerDefault() + } + urlValues.Add("workerNum", strconv.Itoa(workerNode.Num)) + urlValues.Add("workerCpu", strconv.Itoa(workerNode.Cpu)) + urlValues.Add("workerRam", strconv.Itoa(workerNode.Ram)) + urlValues.Add("workerDisk", strconv.Itoa(workerNode.Disk)) + + //TODO find a way to avoid hardcoding these values + //if withLB, ok := d.GetOk("with_lb"); ok { + //urlValues.Add("withLB", strconv.FormatBool(withLB.(bool))) + //} + urlValues.Add("withLB", strconv.FormatBool(true)) + + //if extNet, ok := d.GetOk("extnet_id"); ok { + //urlValues.Add("extnetId", strconv.Itoa(extNet.(int))) + //} + urlValues.Add("extnetId", strconv.Itoa(0)) + + //if desc, ok := d.GetOk("desc"); ok { + //urlValues.Add("desc", desc.(string)) + //} + + resp, err := controller.decortAPICall("POST", K8sCreateAPI, urlValues) + if err != nil { + return err + } + + urlValues = &url.Values{} + urlValues.Add("auditId", strings.Trim(resp, `"`)) + + for { + resp, err := controller.decortAPICall("POST", AsyncTaskGetAPI, urlValues) + if err != nil { + return err + } + + task := AsyncTask{} + if err := json.Unmarshal([]byte(resp), &task); err != nil { + return err + } + log.Debugf("resourceK8sCreate: instance creating - %s", task.Stage) + + if task.Completed { + if task.Error != "" { + return fmt.Errorf("cannot create k8s instance: %v", task.Error) + } + + d.SetId(task.Result) + break + } + + time.Sleep(time.Second * 10) + } + + return nil +} + +func resourceK8sRead(d *schema.ResourceData, m interface{}) error { + log.Debugf("resourceK8sRead: called with id %s, rg %d", d.Id(), d.Get("rg_id").(int)) + + k8s, err := utilityK8sCheckPresence(d, m) + if k8s == nil { + if err != nil { + return err + } + return nil + } + + d.Set("name", k8s.Name) + d.Set("rg_id", k8s.RgID) + d.Set("k8sci_id", k8s.CI) + d.Set("masters", nodeToResource(k8s.Groups.Masters)) + d.Set("workers", nodeToResource(k8s.Groups.Workers[0])) + + return nil +} + +func resourceK8sUpdate(d *schema.ResourceData, m interface{}) error { + log.Debugf("resourceK8sUpdate: called with id %s, rg %d", d.Id(), d.Get("rg_id").(int)) + + controller := m.(*ControllerCfg) + urlValues := &url.Values{} + urlValues.Add("k8sId", d.Id()) + urlValues.Add("name", d.Get("name").(string)) + + _, err := controller.decortAPICall("POST", K8sUpdateAPI, urlValues) + if err != nil { + return err + } + + return nil +} + +func resourceK8sDelete(d *schema.ResourceData, m interface{}) error { + log.Debugf("resourceK8sDelete: called with id %s, rg %d", d.Id(), d.Get("rg_id").(int)) + + k8s, err := utilityK8sCheckPresence(d, m) + if k8s == nil { + if err != nil { + return err + } + return nil + } + + controller := m.(*ControllerCfg) + urlValues := &url.Values{} + urlValues.Add("k8sId", d.Id()) + urlValues.Add("permanently", "true") + + _, err = controller.decortAPICall("POST", K8sDeleteAPI, urlValues) + if err != nil { + return err + } + + return nil +} + +func resourceK8sExists(d *schema.ResourceData, m interface{}) (bool, error) { + log.Debugf("resourceK8sExists: called with id %s, rg %d", d.Id(), d.Get("rg_id").(int)) + + k8s, err := utilityK8sCheckPresence(d, m) + if k8s == nil { + return false, err + } + + return true, nil +} + +func resourceK8sSchemaMake() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the cluster.", + }, + + "rg_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "Resource group ID that this instance belongs to.", + }, + + "k8sci_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "ID of the k8s catalog item to base this instance on.", + }, + + "masters": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: nodeK8sSubresourceSchemaMake(), + }, + Description: "Master node(s) configuration.", + }, + + "workers": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: nodeK8sSubresourceSchemaMake(), + }, + Description: "Worker node(s) configuration.", + }, + + //"with_lb": { + //Type: schema.TypeBool, + //Optional: true, + //ForceNew: true, + //Default: true, + //Description: "Create k8s with load balancer if true.", + //}, + + //"extnet_id": { + //Type: schema.TypeInt, + //Optional: true, + //ForceNew: true, + //Default: 0, + //Description: "ID of the external network to connect workers to.", + //}, + + //"desc": { + //Type: schema.TypeString, + //Optional: true, + //Description: "Text description of this instance.", + //}, + } +} + +func resourceK8s() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + Create: resourceK8sCreate, + Read: resourceK8sRead, + Update: resourceK8sUpdate, + Delete: resourceK8sDelete, + Exists: resourceK8sExists, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + //TODO timeouts + + Schema: resourceK8sSchemaMake(), + } +} diff --git a/decort/utility_k8s.go b/decort/utility_k8s.go new file mode 100644 index 0000000..58200ac --- /dev/null +++ b/decort/utility_k8s.go @@ -0,0 +1,30 @@ +package decort + +import ( + "encoding/json" + "net/url" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func utilityK8sCheckPresence(d *schema.ResourceData, m interface{}) (*K8sRecord, error) { + controller := m.(*ControllerCfg) + urlValues := &url.Values{} + urlValues.Add("k8sId", d.Id()) + + resp, err := controller.decortAPICall("POST", K8sGetAPI, urlValues) + if err != nil { + return nil, err + } + + if resp == "" { + return nil, nil + } + + var k8s K8sRecord + if err := json.Unmarshal([]byte(resp), &k8s); err != nil { + return nil, err + } + + return &k8s, nil +} diff --git a/docs/resources/k8s.md b/docs/resources/k8s.md new file mode 100644 index 0000000..5f97751 --- /dev/null +++ b/docs/resources/k8s.md @@ -0,0 +1,51 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "decort_k8s Resource - terraform-provider-decort" +subcategory: "" +description: |- + +--- + +# decort_k8s (Resource) + + + + + + +## Schema + +### Required + +- **k8sci_id** (Number) ID of the k8s catalog item to base this instance on. +- **name** (String) Name of the cluster. +- **rg_id** (Number) Resource group ID that this instance belongs to. + +### Optional + +- **id** (String) The ID of this resource. +- **masters** (Block List, Max: 1) Master node(s) configuration. (see [below for nested schema](#nestedblock--masters)) +- **workers** (Block List, Max: 1) Worker node(s) configuration. (see [below for nested schema](#nestedblock--workers)) + + +### Nested Schema for `masters` + +Required: + +- **cpu** (Number) Node CPU count. +- **disk** (Number) Node boot disk size in GB. +- **num** (Number) Number of nodes to create. +- **ram** (Number) Node RAM in MB. + + + +### Nested Schema for `workers` + +Required: + +- **cpu** (Number) Node CPU count. +- **disk** (Number) Node boot disk size in GB. +- **num** (Number) Number of nodes to create. +- **ram** (Number) Node RAM in MB. + + diff --git a/docs/resources/pfw.md b/docs/resources/pfw.md index b12806d..c09c5c5 100644 --- a/docs/resources/pfw.md +++ b/docs/resources/pfw.md @@ -26,9 +26,21 @@ description: |- - **id** (String) The ID of this resource. - **public_port_end** (Number) End port number (inclusive) for the ranged rule. +- **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) ### Read-Only - **local_ip** (String) IP address of compute instance. + +### Nested Schema for `timeouts` + +Optional: + +- **create** (String) +- **default** (String) +- **delete** (String) +- **read** (String) +- **update** (String) +