From 3743fb9d61fa8a1a277fc7a937bb59536ee8c717 Mon Sep 17 00:00:00 2001 From: Will May Date: Fri, 29 Mar 2019 12:56:14 +0000 Subject: [PATCH] Add support for importing service accounts (#377) Add support for importing service accounts Implements importing of service accounts using the behaviour implemented in source code of the Kubernetes service account controller to discover the default service account token. Also adds support for updating the `automount_service_account_token` attribute when it changes. --- .../resource_kubernetes_service_account.go | 97 ++++++++++++++++++- ...esource_kubernetes_service_account_test.go | 26 ++++- website/docs/r/service_account.html.markdown | 8 ++ 3 files changed, 125 insertions(+), 6 deletions(-) diff --git a/kubernetes/resource_kubernetes_service_account.go b/kubernetes/resource_kubernetes_service_account.go index 6738a5ffef..b63f7e416f 100644 --- a/kubernetes/resource_kubernetes_service_account.go +++ b/kubernetes/resource_kubernetes_service_account.go @@ -3,6 +3,7 @@ package kubernetes import ( "fmt" "log" + "strings" "time" "github.com/hashicorp/terraform/helper/resource" @@ -21,10 +22,9 @@ func resourceKubernetesServiceAccount() *schema.Resource { Exists: resourceKubernetesServiceAccountExists, Update: resourceKubernetesServiceAccountUpdate, Delete: resourceKubernetesServiceAccountDelete, - - // This resource is not importable because the API doesn't offer - // any way to differentiate between default & user-defined secret - // after the account was created. + Importer: &schema.ResourceImporter{ + State: resourceKubernetesServiceAccountImportState, + }, Schema: map[string]*schema.Schema{ "metadata": namespacedMetadataSchema("service account", true), @@ -60,7 +60,6 @@ func resourceKubernetesServiceAccount() *schema.Resource { Type: schema.TypeBool, Description: "True to enable automatic mounting of the service account token", Optional: true, - Default: false, }, "default_secret_name": { Type: schema.TypeString, @@ -169,6 +168,18 @@ func resourceKubernetesServiceAccountRead(d *schema.ResourceData, meta interface if err != nil { return err } + + if svcAcc.AutomountServiceAccountToken == nil { + err = d.Set("automount_service_account_token", false) + if err != nil { + return err + } + } else { + err = d.Set("automount_service_account_token", *svcAcc.AutomountServiceAccountToken) + if err != nil { + return err + } + } d.Set("image_pull_secret", flattenLocalObjectReferenceArray(svcAcc.ImagePullSecrets)) defaultSecretName := d.Get("default_secret_name").(string) @@ -205,6 +216,13 @@ func resourceKubernetesServiceAccountUpdate(d *schema.ResourceData, meta interfa Value: expandServiceAccountSecrets(v, defaultSecretName), }) } + if d.HasChange("automount_service_account_token") { + v := d.Get("automount_service_account_token").(bool) + ops = append(ops, &ReplaceOperation{ + Path: "/automountServiceAccountToken", + Value: v, + }) + } data, err := ops.MarshalJSON() if err != nil { return fmt.Errorf("Failed to marshal update operations: %s", err) @@ -258,3 +276,72 @@ func resourceKubernetesServiceAccountExists(d *schema.ResourceData, meta interfa } return true, err } + +func resourceKubernetesServiceAccountImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + conn := meta.(*kubernetes.Clientset) + + namespace, name, err := idParts(d.Id()) + if err != nil { + return nil, fmt.Errorf("Unable to parse identifier %s: %s", d.Id(), err) + } + + sa, err := conn.CoreV1().ServiceAccounts(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("Unable to fetch service account from Kubernetes: %s", err) + } + defaultSecret, err := findDefaultServiceAccount(sa, conn) + if err != nil { + return nil, fmt.Errorf("Failed to discover the default service account token: %s", err) + } + + err = d.Set("default_secret_name", defaultSecret) + if err != nil { + return nil, fmt.Errorf("Unable to set default_secret_name: %s", err) + } + d.SetId(buildId(sa.ObjectMeta)) + + return []*schema.ResourceData{d}, nil +} + +func findDefaultServiceAccount(sa *api.ServiceAccount, conn *kubernetes.Clientset) (string, error) { + /* + The default service account token secret would have: + - been created either at the same moment as the service account or _just_ after (Kubernetes controllers appears to work off a queue) + - have a name starting with "[service account name]-token-" + + See this for where the default token is created in Kubernetes + https://github.com/kubernetes/kubernetes/blob/release-1.13/pkg/controller/serviceaccount/tokens_controller.go#L384 + */ + for _, saSecret := range sa.Secrets { + if !strings.HasPrefix(saSecret.Name, fmt.Sprintf("%s-token-", sa.Name)) { + log.Printf("[DEBUG] Skipping %s as it doesn't have the right name", saSecret.Name) + continue + } + + secret, err := conn.CoreV1().Secrets(sa.Namespace).Get(saSecret.Name, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("Unable to fetch secret %s/%s from Kubernetes: %s", sa.Namespace, saSecret.Name, err) + } + + if secret.Type != api.SecretTypeServiceAccountToken { + log.Printf("[DEBUG] Skipping %s as it is of the wrong type", saSecret.Name) + continue + } + + if secret.CreationTimestamp.Before(&sa.CreationTimestamp) { + log.Printf("[DEBUG] Skipping %s as it existed before the service account", saSecret.Name) + continue + } + + if secret.CreationTimestamp.Sub(sa.CreationTimestamp.Time) > (1 * time.Second) { + log.Printf("[DEBUG] Skipping %s as it wasn't created at the same time as the service account", saSecret.Name) + continue + } + + log.Printf("[DEBUG] Found %s as a candidate for the default service account token", saSecret.Name) + + return saSecret.Name, nil + } + + return "", fmt.Errorf("Unable to find any service accounts tokens which could have been the default one") +} diff --git a/kubernetes/resource_kubernetes_service_account_test.go b/kubernetes/resource_kubernetes_service_account_test.go index 84287b6c3c..48ba2ef48f 100644 --- a/kubernetes/resource_kubernetes_service_account_test.go +++ b/kubernetes/resource_kubernetes_service_account_test.go @@ -166,7 +166,7 @@ func TestAccKubernetesServiceAccount_update(t *testing.T) { resource.TestCheckResourceAttrSet("kubernetes_service_account.test", "metadata.0.uid"), resource.TestCheckResourceAttr("kubernetes_service_account.test", "secret.#", "1"), resource.TestCheckResourceAttr("kubernetes_service_account.test", "image_pull_secret.#", "3"), - resource.TestCheckResourceAttr("kubernetes_service_account.test", "automount_service_account_token", "false"), + resource.TestCheckResourceAttr("kubernetes_service_account.test", "automount_service_account_token", "true"), testAccCheckServiceAccountImagePullSecrets(&conf, []*regexp.Regexp{ regexp.MustCompile("^" + name + "-three$"), regexp.MustCompile("^" + name + "-four$"), @@ -238,6 +238,28 @@ func TestAccKubernetesServiceAccount_generatedName(t *testing.T) { }) } +func TestAccKubernetesServiceAccount_importBasic(t *testing.T) { + resourceName := "kubernetes_service_account.test" + name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckKubernetesServiceAccountDestroy, + Steps: []resource.TestStep{ + { + Config: testAccKubernetesServiceAccountConfig_basic(name), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"metadata.0.resource_version", "automount_service_account_token"}, + }, + }, + }) +} + func testAccCheckServiceAccountImagePullSecrets(m *api.ServiceAccount, expected []*regexp.Regexp) resource.TestCheckFunc { return func(s *terraform.State) error { if len(expected) == 0 && len(m.ImagePullSecrets) == 0 { @@ -431,6 +453,8 @@ resource "kubernetes_service_account" "test" { image_pull_secret { name = "${kubernetes_secret.four.metadata.0.name}" } + + automount_service_account_token = "true" } resource "kubernetes_secret" "one" { diff --git a/website/docs/r/service_account.html.markdown b/website/docs/r/service_account.html.markdown index a88bedbb66..ac7185ae5e 100644 --- a/website/docs/r/service_account.html.markdown +++ b/website/docs/r/service_account.html.markdown @@ -77,3 +77,11 @@ In addition to the arguments listed above, the following computed attributes are exported: * `default_secret_name` - Name of the default secret, containing service account token, created & managed by the service. + +## Import + +Service account can be imported using the namespace and name, e.g. + +``` +$ terraform import kubernetes_service_account.example default/terraform-example +```