From c309e5838f3902b016d1666469c662870a3c1d17 Mon Sep 17 00:00:00 2001 From: Nikita Kunets Date: Thu, 30 Dec 2021 02:09:43 +0300 Subject: [PATCH] Add MKS kubeconfig data source (#178) * Add MKS kubeconfig data source For #145 * Fix due to recommendations * Use sensitive fields in mks kubeconfig data source --- go.mod | 2 +- go.sum | 6 +- .../data_source_selectel_mks_kubeconfig_v1.go | 107 ++++++++++++++++++ ..._source_selectel_mks_kubeconfig_v1_test.go | 72 ++++++++++++ selectel/provider.go | 2 + .../selectel/mks-go/pkg/v1/client.go | 9 +- .../selectel/mks-go/pkg/v1/cluster/doc.go | 12 ++ .../mks-go/pkg/v1/cluster/requests.go | 55 +++++++++ .../selectel/mks-go/pkg/v1/cluster/schemas.go | 79 +++++++------ vendor/modules.txt | 2 +- .../docs/d/mks_kubeconfig_v1.html.markdown | 68 +++++++++++ website/selectel.erb | 3 + 12 files changed, 375 insertions(+), 42 deletions(-) create mode 100644 selectel/data_source_selectel_mks_kubeconfig_v1.go create mode 100644 selectel/data_source_selectel_mks_kubeconfig_v1_test.go create mode 100644 website/docs/d/mks_kubeconfig_v1.html.markdown diff --git a/go.mod b/go.mod index 456e0052..90713549 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,6 @@ require ( github.com/selectel/dbaas-go v0.4.0 github.com/selectel/domains-go v0.3.0 github.com/selectel/go-selvpcclient v1.12.0 - github.com/selectel/mks-go v0.8.0 + github.com/selectel/mks-go v0.9.0 github.com/stretchr/testify v1.7.0 ) diff --git a/go.sum b/go.sum index 1524e373..318106c1 100644 --- a/go.sum +++ b/go.sum @@ -289,8 +289,10 @@ github.com/selectel/domains-go v0.3.0 h1:0shjqQmpkWc6eM1SwgKqbTTNiT5G2BOEnvS7Jvu github.com/selectel/domains-go v0.3.0/go.mod h1:AhXhwyMSTkpEWFiBLUvzFP76W+WN+ZblwmjLJLt7y58= github.com/selectel/go-selvpcclient v1.12.0 h1:LsT074HOVF1dWYapsAWjaaJDQhmDPpcsVjSwQ1r1fj0= github.com/selectel/go-selvpcclient v1.12.0/go.mod h1:HNteVXevZMjUCRR6lImTsGZZSTeKu89S/qbEDWDqmgc= -github.com/selectel/mks-go v0.8.0 h1:tZJ248zZmJEGYZaygqLzboTFml0qxKkRjEAPzVAN0Ro= -github.com/selectel/mks-go v0.8.0/go.mod h1:OrlLnGes+HK7HNxUab/8Rll5iT0XQQnx4+dW1Ysph2o= +github.com/selectel/mks-go v0.6.0 h1:5ZlqvwlVrjrrbNygsiFfUGZTyTPzPGGo8aYJkbtrYV0= +github.com/selectel/mks-go v0.6.0/go.mod h1:OrlLnGes+HK7HNxUab/8Rll5iT0XQQnx4+dW1Ysph2o= +github.com/selectel/mks-go v0.9.0 h1:j0uvdUkbKtQmNWU5cjadsplPbCggWvTETAUHJQMWawU= +github.com/selectel/mks-go v0.9.0/go.mod h1:FcFqF3WvZIhztyAt1+ZySKf0zWmCEvg9e2gRwxVyQOw= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= diff --git a/selectel/data_source_selectel_mks_kubeconfig_v1.go b/selectel/data_source_selectel_mks_kubeconfig_v1.go new file mode 100644 index 00000000..7cf9af37 --- /dev/null +++ b/selectel/data_source_selectel_mks_kubeconfig_v1.go @@ -0,0 +1,107 @@ +package selectel + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/selectel/go-selvpcclient/selvpcclient/resell/v2/tokens" + v1 "github.com/selectel/mks-go/pkg/v1" + "github.com/selectel/mks-go/pkg/v1/cluster" +) + +func dataSourceMKSKubeconfigV1() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceMKSKubeconfigV1Read, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cluster_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "region": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + ru1Region, + ru2Region, + ru3Region, + ru7Region, + ru8Region, + ru9Region, + }, false), + }, + "raw_config": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "server": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "cluster_ca_cert": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "client_cert": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "client_key": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +func dataSourceMKSKubeconfigV1Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + resellV2Client := config.resellV2Client() + tokenOpts := tokens.TokenOpts{ + ProjectID: d.Get("project_id").(string), + } + + log.Print(msgCreate(objectToken, tokenOpts)) + token, _, err := tokens.Create(ctx, resellV2Client, tokenOpts) + if err != nil { + return diag.FromErr(errCreatingObject(objectToken, err)) + } + + region := d.Get("region").(string) + endpoint := getMKSClusterV1Endpoint(region) + mksClient := v1.NewMKSClientV1(token.ID, endpoint) + + clusterID := d.Get("cluster_id").(string) + + mksCluster, _, err := cluster.Get(ctx, mksClient, clusterID) + if err != nil { + return diag.FromErr(errGettingObject(objectCluster, clusterID, err)) + } + + parsedKubeconfig, _, err := cluster.GetParsedKubeconfig(ctx, mksClient, mksCluster.ID) + if err != nil { + return diag.FromErr(errGettingObject(objectKubeConfig, clusterID, err)) + } + + d.SetId(clusterID) + d.Set("raw_config", parsedKubeconfig.KubeconfigRaw) + d.Set("server", parsedKubeconfig.Server) + d.Set("cluster_ca_cert", parsedKubeconfig.ClusterCA) + d.Set("client_cert", parsedKubeconfig.ClientCert) + d.Set("client_key", parsedKubeconfig.ClientKey) + + return nil +} diff --git a/selectel/data_source_selectel_mks_kubeconfig_v1_test.go b/selectel/data_source_selectel_mks_kubeconfig_v1_test.go new file mode 100644 index 00000000..be209aa6 --- /dev/null +++ b/selectel/data_source_selectel_mks_kubeconfig_v1_test.go @@ -0,0 +1,72 @@ +package selectel + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccMKSKubeconfigV1DataSourceBasic(t *testing.T) { + projectName := acctest.RandomWithPrefix("tf-acc") + clusterName := acctest.RandomWithPrefix("tf-acc-cl") + kubeVersion := testAccMKSClusterV1GetDefaultKubeVersion(t) + maintenanceWindowStart := testAccMKSClusterV1GetMaintenanceWindowStart(12 * time.Hour) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccSelectelPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckVPCV2ProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMKSKubeconfigV1Basic(projectName, clusterName, kubeVersion, maintenanceWindowStart), + Check: resource.ComposeTestCheckFunc( + testAccCheckMKSKubeconfigV1("data.selectel_mks_kubeconfig_v1.kubeconfig_tf_acc_test_1"), + ), + }, + }, + }) +} + +func testAccCheckMKSKubeconfigV1(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("can't find kubeconfig data source: %s", name) + } + + if _, ok = rs.Primary.Attributes["raw_config"]; !ok { + return errors.New("empty 'raw_config' field in kubeconfigs data source") + } + if _, ok = rs.Primary.Attributes["server"]; !ok { + return errors.New("empty 'server' field in kubeconfigs data source") + } + if _, ok = rs.Primary.Attributes["cluster_ca_cert"]; !ok { + return errors.New("empty 'cluster_ca_cert' field in kubeconfigs data source") + } + if _, ok = rs.Primary.Attributes["client_cert"]; !ok { + return errors.New("empty 'client_cert' field in kubeconfigs data source") + } + if _, ok = rs.Primary.Attributes["client_key"]; !ok { + return errors.New("empty 'client_key' field in kubeconfigs data source") + } + + return nil + } +} + +func testAccMKSKubeconfigV1Basic(projectName, clusterName, kubeVersion, maintenanceWindowStart string) string { + return fmt.Sprintf(` +%s + +data "selectel_mks_kubeconfig_v1" "kubeconfig_tf_acc_test_1" { + cluster_id = "${selectel_mks_cluster_v1.cluster_tf_acc_test_1.id}" + project_id = "${selectel_vpc_project_v2.project_tf_acc_test_1.id}" + region = "ru-3" +} +`, testAccMKSClusterV1Basic(projectName, clusterName, kubeVersion, maintenanceWindowStart)) +} diff --git a/selectel/provider.go b/selectel/provider.go index d2c1f8de..f7700aeb 100644 --- a/selectel/provider.go +++ b/selectel/provider.go @@ -20,6 +20,7 @@ const ( objectUser = "user" objectVRRPSubnet = "VRRP subnet" objectCluster = "cluster" + objectKubeConfig = "kubeconfig" objectNodegroup = "nodegroup" objectDomain = "domain" objectRecord = "record" @@ -75,6 +76,7 @@ func Provider() *schema.Provider { "selectel_dbaas_flavor_v1": dataSourceDBaaSFlavorV1(), "selectel_dbaas_configuration_parameter_v1": dataSourceDBaaSConfigurationParameterV1(), "selectel_dbaas_prometheus_metric_token_v1": dataSourceDBaaSPrometheusMetricTokenV1(), + "selectel_mks_kubeconfig_v1": dataSourceMKSKubeconfigV1(), "selectel_mks_feature_gates_v1": dataSourceMKSFeatureGatesV1(), "selectel_mks_admission_controllers_v1": dataSourceMKSAdmissionControllersV1(), }, diff --git a/vendor/github.com/selectel/mks-go/pkg/v1/client.go b/vendor/github.com/selectel/mks-go/pkg/v1/client.go index 3ff2f590..7b1e102c 100644 --- a/vendor/github.com/selectel/mks-go/pkg/v1/client.go +++ b/vendor/github.com/selectel/mks-go/pkg/v1/client.go @@ -93,6 +93,7 @@ func NewMKSClientV1WithCustomHTTP(customHTTPClient *http.Client, tokenID, endpoi if customHTTPClient == nil { customHTTPClient = newHTTPClient() } + return &ServiceClient{ HTTPClient: customHTTPClient, TokenID: tokenID, @@ -228,15 +229,17 @@ func (result *ResponseResult) extractErr() error { if len(body) == 0 { result.Err = fmt.Errorf(errGotHTTPStatusCodeFmt, result.StatusCode) + return nil } if result.StatusCode == http.StatusNotFound { - err = json.Unmarshal(body, &result.ErrNotFound) + _ = json.Unmarshal(body, &result.ErrNotFound) } else { - err = json.Unmarshal(body, &result.ErrGeneric) + _ = json.Unmarshal(body, &result.ErrGeneric) } - if err != nil { + if result.ErrNotFound == nil && result.ErrGeneric == nil { result.Err = fmt.Errorf(errGotHTTPStatusCodeFmt, result.StatusCode) + return nil } diff --git a/vendor/github.com/selectel/mks-go/pkg/v1/cluster/doc.go b/vendor/github.com/selectel/mks-go/pkg/v1/cluster/doc.go index 3466537e..973edd16 100644 --- a/vendor/github.com/selectel/mks-go/pkg/v1/cluster/doc.go +++ b/vendor/github.com/selectel/mks-go/pkg/v1/cluster/doc.go @@ -91,6 +91,18 @@ Example of getting a kubeconfig referenced by cluster id } fmt.Print(string(kubeconfig)) +Example of getting fields from Kubeconfig referenced by cluster id + + parsedKubeconfig, _, err := cluster.GetParsedKubeconfig(ctx, mksClient, clusterID) + if err != nil { + log.Fatal(err) + } + fmt.Println("Server IP:", string(parsedKubeconfig.Server)) + fmt.Println("Cluster CA:", string(parsedKubeconfig.ClusterCA)) + fmt.Println("Client cert:", string(parsedKubeconfig.ClientCert)) + fmt.Println("Client key:", string(parsedKubeconfig.ClientKey)) + fmt.Println("Raw kubeconfig:", string(parsedKubeconfig.KubeconfigRaw)) + Example of rotating certificates by cluster id _, err := cluster.RotateCerts(ctx, mksClient, clusterID) diff --git a/vendor/github.com/selectel/mks-go/pkg/v1/cluster/requests.go b/vendor/github.com/selectel/mks-go/pkg/v1/cluster/requests.go index 4c80d99b..10e7a110 100644 --- a/vendor/github.com/selectel/mks-go/pkg/v1/cluster/requests.go +++ b/vendor/github.com/selectel/mks-go/pkg/v1/cluster/requests.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" + "regexp" "strings" v1 "github.com/selectel/mks-go/pkg/v1" @@ -156,6 +158,59 @@ func GetKubeconfig(ctx context.Context, client *v1.ServiceClient, clusterID stri return kubeconfig, responseResult, nil } +func getFieldFromKubeconfig(kubeconfig []byte, fieldName string) (string, error) { + r := regexp.MustCompile(fmt.Sprintf("%s.*", fieldName)) + if s := r.FindString(string(kubeconfig)); len(s) != 0 { + if subS := strings.Split(s, " "); len(subS) > 1 { + return subS[1], nil + } + + return "", fmt.Errorf("invalid %s field in the kubeconfig", fieldName) + } + + return "", fmt.Errorf("unable to find %s field in kubeconfig", fieldName) +} + +// GetParsedKubeconfig is a small helper function to get KubeconfigFields struct. +func GetParsedKubeconfig(ctx context.Context, client *v1.ServiceClient, clusterID string) (*KubeconfigFields, *v1.ResponseResult, error) { + kubeconfig, responseResult, err := GetKubeconfig(ctx, client, clusterID) + if err != nil { + return nil, nil, err + } + if responseResult.Err != nil { + return nil, responseResult, responseResult.Err + } + + fieldsToParse := []string{ + "certificate-authority-data", + "server", + "client-certificate-data", + "client-key-data", + } + parsedKubeconfig := KubeconfigFields{} + + for _, rawName := range fieldsToParse { + if s, err := getFieldFromKubeconfig(kubeconfig, rawName); err == nil { + switch rawName { + case "certificate-authority-data": + parsedKubeconfig.ClusterCA = s + case "server": + parsedKubeconfig.Server = s + case "client-certificate-data": + parsedKubeconfig.ClientCert = s + case "client-key-data": + parsedKubeconfig.ClientKey = s + } + } else { + return nil, responseResult, err + } + } + + parsedKubeconfig.KubeconfigRaw = string(kubeconfig) + + return &parsedKubeconfig, responseResult, nil +} + // RotateCerts requests a rotation of cluster certificates by cluster id. func RotateCerts(ctx context.Context, client *v1.ServiceClient, clusterID string) (*v1.ResponseResult, error) { url := strings.Join([]string{client.Endpoint, v1.ResourceURLCluster, clusterID, v1.ResourceURLRotateCerts}, "/") diff --git a/vendor/github.com/selectel/mks-go/pkg/v1/cluster/schemas.go b/vendor/github.com/selectel/mks-go/pkg/v1/cluster/schemas.go index 38e8c4df..e4e6451b 100644 --- a/vendor/github.com/selectel/mks-go/pkg/v1/cluster/schemas.go +++ b/vendor/github.com/selectel/mks-go/pkg/v1/cluster/schemas.go @@ -27,6 +27,36 @@ const ( StatusUnknown Status = "UNKNOWN" ) +func getSupportedStatuses() []Status { + return []Status{ + StatusActive, + StatusPendingCreate, + StatusPendingUpdate, + StatusPendingUpgrade, + StatusPendingRotateCerts, + StatusPendingDelete, + StatusPendingResize, + StatusPendingNodeReinstall, + StatusPendingUpgradePatchVersion, + StatusPendingUpgradeMinorVersion, + StatusPendingUpdateNodegroup, + StatusPendingUpgradeMastersConfiguration, + StatusPendingUpgradeClusterConfiguration, + StatusMaintenance, + StatusError, + } +} + +func isStatusSupported(s Status) bool { + for _, v := range getSupportedStatuses() { + if s == v { + return true + } + } + + return false +} + // View represents an unmarshalled cluster body from an API response. type View struct { // ID is the identifier of the cluster. @@ -103,50 +133,20 @@ func (result *View) UnmarshalJSON(b []byte) error { tmp Status Status `json:"status"` } - err := json.Unmarshal(b, &s) - if err != nil { + if err := json.Unmarshal(b, &s); err != nil { return err } *result = View(s.tmp) // Check cluster status. - switch s.Status { - case StatusActive: - result.Status = StatusActive - case StatusPendingCreate: - result.Status = StatusPendingCreate - case StatusPendingUpdate: - result.Status = StatusPendingUpdate - case StatusPendingUpgrade: - result.Status = StatusPendingUpgrade - case StatusPendingRotateCerts: - result.Status = StatusPendingRotateCerts - case StatusPendingDelete: - result.Status = StatusPendingDelete - case StatusPendingResize: - result.Status = StatusPendingResize - case StatusPendingNodeReinstall: - result.Status = StatusPendingNodeReinstall - case StatusPendingUpgradePatchVersion: - result.Status = StatusPendingUpgradePatchVersion - case StatusPendingUpgradeMinorVersion: - result.Status = StatusPendingUpgradeMinorVersion - case StatusPendingUpdateNodegroup: - result.Status = StatusPendingUpdateNodegroup - case StatusPendingUpgradeMastersConfiguration: - result.Status = StatusPendingUpgradeMastersConfiguration - case StatusPendingUpgradeClusterConfiguration: - result.Status = StatusPendingUpgradeClusterConfiguration - case StatusMaintenance: - result.Status = StatusMaintenance - case StatusError: - result.Status = StatusError - default: + if isStatusSupported(s.Status) { + result.Status = s.Status + } else { result.Status = StatusUnknown } - return err + return nil } // KubernetesOptions represents additional k8s options such as pod security policy, @@ -162,3 +162,12 @@ type KubernetesOptions struct { // AdmissionControllers represents admission controllers that should be enabled. AdmissionControllers []string `json:"admission_controllers"` } + +// KubeconfigFields is a struct that contains Kubeconfigs parsed fields and raw kubeconfig. +type KubeconfigFields struct { + ClusterCA string + Server string + ClientCert string + ClientKey string + KubeconfigRaw string +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 36b70518..653cc469 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -215,7 +215,7 @@ github.com/selectel/go-selvpcclient/selvpcclient/resell/v2/subnets github.com/selectel/go-selvpcclient/selvpcclient/resell/v2/tokens github.com/selectel/go-selvpcclient/selvpcclient/resell/v2/users github.com/selectel/go-selvpcclient/selvpcclient/resell/v2/vrrpsubnets -# github.com/selectel/mks-go v0.8.0 +# github.com/selectel/mks-go v0.9.0 ## explicit github.com/selectel/mks-go/pkg/v1 github.com/selectel/mks-go/pkg/v1/cluster diff --git a/website/docs/d/mks_kubeconfig_v1.html.markdown b/website/docs/d/mks_kubeconfig_v1.html.markdown new file mode 100644 index 00000000..8e3f12b3 --- /dev/null +++ b/website/docs/d/mks_kubeconfig_v1.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "selectel" +page_title: "Selectel: selectel_mks_kubeconfig_v1" +sidebar_current: "docs-selectel-datasource-mks-kubeconfig-v1" +description: |- + Get kubeconfig for a Selectel Managed Kubernetes cluster. +--- + +# selectel\_mks\_kubeconfig_v1 + +Use this data source to get kubeconfig and its fields for a Managed Kubernetes cluster. + +## Example Usage + +```hcl +resource "selectel_mks_cluster_v1" "cluster_1" { + name = var.cluster_name + project_id = var.project_id + region = var.region + kube_version = var.kube_version + enable_autorepair = var.enable_autorepair + enable_patch_version_auto_upgrade = var.enable_patch_version_auto_upgrade + network_id = var.network_id + subnet_id = var.subnet_id + maintenance_window_start = var.maintenance_window_start +} + +data "selectel_mks_kubeconfig_v1" "kubeconfig" { + cluster_id = selectel_mks_cluster_v1.cluster_1.id + project_id = var.project_id + region = var.region +} + +provider "kubernetes" { + host = data.selectel_mks_kubeconfig_v1.kubeconfig.server + client_certificate = data.selectel_mks_kubeconfig_v1.kubeconfig.cluster_ca_cert + client_key = data.selectel_mks_kubeconfig_v1.kubeconfig.client_key + cluster_ca_certificate = data.selectel_mks_kubeconfig_v1.kubeconfig.client_cert +} + +output "kubeconfig" { + value = data.selectel_mks_kubeconfig_v1.kubeconfig.raw_config +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cluster_id` - (Required) ID of the Managed Kubernetes cluster. + +* `project_id` - (Required) Project ID where the cluster is placed. + +* `region` - (Required) Region where the cluster is placed. + +## Attributes Reference + +The following attributes are exported: + +* `raw_config` - Raw content of a kubeconfig file. + +* `server` - IP address and port for a kube-API server. + +* `cluster_ca_cert` - K8s cluster CA certificate. + +* `client_key` - Client key for authorization. + +* `client_cert` - Client cert for authorization. diff --git a/website/selectel.erb b/website/selectel.erb index 36f5cd48..0a22aedd 100644 --- a/website/selectel.erb +++ b/website/selectel.erb @@ -34,6 +34,9 @@ > selectel_mks_admission_controllers_v1 + > + selectel_mks_kubeconfig_v1 +