kubernetes/kops · error

cannot apply changes to ServiceAccount: %v

Error message

cannot apply changes to ServiceAccount: %v

What it means

After handling Description/DisplayName changes, RenderGCE nils them out and checks that no other fields remain in the changes struct (via reflect.DeepEqual against an empty ServiceAccount). If leftover changes remain, kOps has no API to apply them to an existing service account (GCP service accounts are nearly immutable), so it fails rather than silently ignoring drift.

Source

Thrown at upup/pkg/fi/cloudup/gcetasks/serviceaccount.go:160

		if changes.Description != nil || changes.DisplayName != nil {
			sa := &iam.ServiceAccount{
				Email:       email,
				Description: fi.ValueOf(e.Description),
				DisplayName: fi.ValueOf(e.DisplayName),
			}

			_, err := cloud.IAM().ServiceAccounts().Update(ctx, fqn, sa)
			if err != nil {
				return fmt.Errorf("error creating ServiceAccount %q: %w", fqn, err)
			}

			changes.Description = nil
			changes.DisplayName = nil
		}

		empty := &ServiceAccount{}
		if !reflect.DeepEqual(empty, changes) {
			return fmt.Errorf("cannot apply changes to ServiceAccount: %v", changes)
		}
	}

	return nil
}

type terraformServiceAccount struct {
	AccountID   *string `cty:"account_id"`
	ProjectID   *string `cty:"project"`
	Description *string `cty:"description"`
	DisplayName *string `cty:"display_name"`
}

func (_ *ServiceAccount) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *ServiceAccount) error {
	shared := fi.ValueOf(e.Shared)
	if shared {
		// Not terraform owned / managed
		return nil

View on GitHub (pinned to 4c8573c808)

Solutions

  1. Identify the leftover changed field in the printed `changes` value; if it is Email, treat it as create-new/delete-old rather than an in-place rename.
  2. Create a new task (or new cluster add-on name) with the new email and remove the old service account explicitly.
  3. If Shared changed, revert the spec change or delete and recreate the task so Find/apply agree on lifecycle ownership.
  4. If the diff is spurious (fields kOps should have ignored), report/fix the Find method to normalize those fields (compare with serviceaccount.go:80-88).

Example fix

// before: trying to rename in place
Email: fi.String("new-robot@example-project.iam.gserviceaccount.com") // same task name
// after: new task / new email identity, delete old one separately
Name: fi.String("newrobot-sa"), Email: fi.String("new-robot@example-project.iam.gserviceaccount.com")
Defensive patterns

Strategy: validation

Validate before calling

// Reject in-place email "rename" before applying
if existing != nil && existing.Email != nil && e.Email != nil && *existing.Email != *e.Email {
    return fmt.Errorf("service account email is immutable; create a new task for %q and delete %q", *e.Email, *existing.Email)
}

Type guard

func onlyMutableFieldsChanged(changes *ServiceAccount) bool {
    return changes.Email == nil && changes.Shared == nil && changes.Name == nil
}

Try / catch

if err := kopsUpdate(); err != nil {
    if strings.Contains(err.Error(), "cannot apply changes to ServiceAccount") {
        log.Print("unsupported field change; print changes and split into create-new/delete-old")
    }
    return err
}

Prevention

When it happens

Trigger: Applying a non-shared ServiceAccount task where `a != nil` and the computed `changes` still contains non-zero fields after Description/DisplayName are cleared — i.e. changes to immutable fields like Email, Name, Shared, or Lifecycle.

Common situations: Renaming the service account email in the cluster spec while reusing the same task name (email is the identity, it cannot be updated in place); changing Shared from false to true (or vice versa) without recreating; a Find-phase bug failing to copy back a field causing spurious diffs.

Related errors


AI-assisted analysis of kubernetes/kops@4c8573c808 (2026-09-05). Data as JSON: /api/errors/814974ad073eb84d. Report an issue: GitHub.