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 +```