Skip to content

Commit

Permalink
Add support for importing service accounts (#377)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wjam authored and alexsomesan committed Mar 29, 2019
1 parent 2c87cda commit 3743fb9
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 6 deletions.
97 changes: 92 additions & 5 deletions kubernetes/resource_kubernetes_service_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kubernetes
import (
"fmt"
"log"
"strings"
"time"

"github.com/hashicorp/terraform/helper/resource"
Expand All @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
26 changes: 25 additions & 1 deletion kubernetes/resource_kubernetes_service_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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$"),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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" {
Expand Down
8 changes: 8 additions & 0 deletions website/docs/r/service_account.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

0 comments on commit 3743fb9

Please sign in to comment.