How to create a Kubernetes secret with multiple KVPs from a variable collection

I’ve been trying to figure out how to create a Kubernetes secret that will have an id, description and collection of key value pares, all from an input variable.
I’m having two issues.

  1. I can’t figure out how to create a TF .12 variable that has those three properties, one of which is a list object
  2. I can’t figure out how to spit out a list in the data field.
    Here’s some sample code

This is the kind of thing I’m trying to do in creating the variable. This does not work

# Optional additional secrets to be created in Rancher / Kubernetes for injection into a build pod
variable "additional_text_secrets" {
  type = object ({
    id = string
    description = string
    list(object({
      key = string
      value = string
    }))
  })
  default = {[]}
  description = "An additional list of Rancher secrets to create for the user to inject into build pods"
}

This produces the error:
On .terraform\modules\jenkins\variables.tf line 189: Expected an attribute value, introduced by an equals sign ("=").

To create the secret I’m trying something like this.

# Additional Rancher / Kubernetes secrets that the user might need to inject into their build pods
# Text based secrets, we will base64 encode these for the user
resource "rancher2_secret" "additional-text-secrets" {
  name = var.additional_text_secrets.id
  description = var.additional_text_secrets.description
  project_id = var.rancher_project
  namespace_id = rancher2_namespace.jenkins-namespace.id
  labels = {
    "cattle.io/creator" = "norman"
  }
  data = {
    %{ for secret in var.additional_text_secrets.secret }
    secret.key = base64encode("${secret.value}")
    %{ endfor }
  }
}

This produces the error
On .terraform\modules\jenkins\secrets.tf line 11: Expected the start of an expression, but found an invalid expression token.
Note: The above is a Rancher2 secret, i.e. not a straight up k8s secret but I don’t think that matters??
I’m trying the above because I found out that this works

# Create a list of container repository auths used to store Docker images, usually in Artifactory
resource "rancher2_secret" "container-repositories" {
  name = "container-repositories"
  description = "List of container repository auths used to store Docker images, usually in Artifactory"
  project_id = var.rancher_project
  namespace_id = rancher2_namespace.jenkins-namespace.id
  # Using a heredoc becuase you can't pass a complex object to a template
  data = {
    "config.json" = base64encode(<<EOT
{
	"auths": {
		%{ for auth in var.container_repositories }
		"${auth.url}": {
			"username": "${auth.username}",
			"password": "${auth.password}"
		},
		%{ endfor }
	}
}
EOT
      )
  }
}

Which is basically the same thing, except I’m creating a list of auths in JSON format as the value inside of base64encode (so that’s probably why it works, it is inside the function . . . )

Also note that I know I could just create a list of secrets and do this

# A file that has already been base64 encoded
resource "rancher2_secret" "additional-file-secrets" {
  count = length(var.additional_file_secrets)
  name = var.additional_file_secrets[count.index].id
  description = var.additional_file_secrets[count.index].description
  project_id = var.rancher_project
  namespace_id = rancher2_namespace.jenkins-namespace.id
  labels = {
    "cattle.io/creator" = "norman"
  }
  data = {
    var.additional_file_secrets[count.index].file_name = var.additional_file_secrets[count.index].encoded_bytes
  }
}

However that is not as useful as it creates many secrets, one per item in the list and NOT one secret with many key value pares.
This is what we are doing now and found the one major issue is that you cannot mount more than secret to a volume location at a time.

The expected input and output should look like the below
Variable assignment

 additional_text_secrets = {
    id = "sarge-test-secret-1"
    description = "This is a test of a multi value secret"
    secret = [
      {
        key = "secret1"
        value = "This is the value"
      },
      {
        key = "secret2"
        value = "This is the second value."
      }
    ]
  }

Output

resource "rancher2_secret" "additional-text-secrets" {
  name = "sarge-test-secret-1"
  description = "This is a test of a multi value secret"
  project_id = var.rancher_project
  namespace_id = rancher2_namespace.jenkins-namespace.id
  labels = {
    "cattle.io/creator" = "norman"
  }
  data = {
    "secret1" = base64encode("This is the value")
    "secret2" = base64encode("This is the second value")
  }
}

I’m starting to think there isn’t a way to do this, would love it if I’m proved wrong.

Hi @dsargent3220,

There’s a lot going on here so I’m going to focus on the first case for the moment, and then we can see where that takes us.

Your variable "additional_text_secrets" blocks includes two syntax errors, both of the same kind: an object attribute declaration with only one expression, rather than a key/value pair as required.

For example, your type constraint declares id and description attributes but then gives no name for the third field that’s a list. You’ll need to also give that one a name:

  type = object({
    id          = string
    description = string
    items = list(object({
      key   = string
      value = string
    }))
  })

(Incidentally: if you expect to only have key and value arguments for items, and the keys will all be unique, then map(string) would likely be a more natural type to use for that attribute, with the map keys being the keys and the map values being the values.)

Your default has a similar problem: it’s an object { ... } but inside the braces is only a single expression, rather than a name = value sequence. I’m not really sure what would be a real-world suitable default for this variable, but here at least is an example one that is syntactically valid and matches the type constraint:

  default = {
    id          = "example"
    description = "example"
    items       = []
  }

Perhaps we can start by seeing if the above fixes your first set of errors, and then see where to go from here based on whether you see some new errors or whether the result is what you were intending.

Yes, that fixed it.
I can’t believe I didn’t see the missing key!
Thank you!
Any idea how to accomplish multi-value secret?

Hi @apparentlymart
I think I’m a little closer but still getting an error. Here’s what I have now

dynamic "data" {
    for_each = var.additional_text_secrets.items
    iterator = item
   
    content {
      item.value["key"] = base64encode(item.value["value"])
    }
  }

This almost works but it still doesn’t like me setting the key dynamically. This is generating the error
On .terraform\modules\jenkins\secrets.tf line 17: An argument or block definition is required here. To set an argument, use the equals sign "=" to introduce the argument value.
If I change item.value["key"] to simple word like key then it passes, but that doesn’t solve my problem. I need to get the key from the collection.
The above code comes from this page


in the “dynamic blocks” section.

I’m not really familiar with this provider in particular but from your earlier examples it seemed like data is an argument rather than a nested block type, in which case a dynamic block is not the right way to set it.

Instead, you can use a for expression to construct a suitable map value, like this:

  data = {
    for item in var.additional_text_secrets.items : item.key => base64encode(item.value)
  }

The above is essentially equivalent to writing out the map manually using the syntax you illustrated earlier:

  data = {
    secret1 = base64encode("This is the value")
    secret2 = base64encode("This is the second value")
  }

The dynamic block example you tried, on the other hand, is essentially equivalent to the following set of nested blocks, which doesn’t seem to be what this provider is expecting:

data {
  secret1 = base64encode("This is the value")
}
data {
  secret2 = base64encode("This is the second value")
}

(What you were trying to do with the dynamic block is also not possible because the content block inside a dynamic block is required to statically match the schema of the target block type, and so dynamic argument names are not valid. That is different than a dynamically-constructed map where the keys are chosen by you, rather than by the schema.)


With that said, this furthers my sense that map(string) would be a more natural type for items than your list of object types, because that seems to be what this data argument is expecting anyway.

If you’d like to do that, you could redefine your variable type like this:

  type = object({
    id          = string
    description = string
    items       = map(string)
  })

That would then change your resource block only a little, to apply the for expression over this map instead of over the list of objects (because you still need to apply the base64encode to the values):

  data = {
    for k, v in var.additional_text_secrets.items : k => base64encode(v)
  }

From the perspective of your module’s caller, this would then allow setting the variable using the map syntax, rather than the list-of-objects syntax:

  additional_text_secrets = {
    id          = "example"
    description = "example"
    items = {
      secret1 = "This is the value"
      secret2 = "This is the second value"
    }
  }

@apparentlymart , Genius!
That fixed it!
And you are quite correct, using a Map is much nicer when using the var.
Thank you sir!