Skip to content

Commit

Permalink
fix: allow env.valueFrom on Terraform (GalleyBytes#11)
Browse files Browse the repository at this point in the history
* fix: allow env.valueFrom on Terraform

so that we can populate terraform values from secrets without putting secrets into the Terraform resource directly

fixes GalleyBytes#8

* replace custom EnvVar with v1 implementation

Co-authored-by: Isa Aguilar <[email protected]>
  • Loading branch information
jstrachan and isaaguilar authored Feb 12, 2021
1 parent 00c378e commit 71bf3ed
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 54 deletions.
101 changes: 98 additions & 3 deletions deploy/crds/tf.isaaguilar.com_terraforms_crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ spec:
required:
- name
type: object
serviceAccountAnnotations:
additionalProperties:
type: string
description: ServiceAccountAnnotations allows the service account
to be annotated with cloud IAM roles such as Workload Identity
on GCP
type: object
type: object
type: array
customBackend:
Expand All @@ -116,16 +123,99 @@ spec:
type: string
env:
items:
description: EnvVar defines key/value pairs of env vars that get picked
up by terraform
description: EnvVar represents an environment variable present in
a Container.
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
type: string
value:
description: 'Variable references $(VAR_NAME) are expanded using
the previous defined environment variables in the container
and any service environment variables. If a variable cannot
be resolved, the reference in the input string will be unchanged.
The $(VAR_NAME) syntax can be escaped with a double $$, ie:
$$(VAR_NAME). Escaped references will never be expanded, regardless
of whether the variable exists or not. Defaults to "".'
type: string
valueFrom:
description: Source for the environment variable's value. Cannot
be used if value is not empty.
properties:
configMapKeyRef:
description: Selects a key of a ConfigMap.
properties:
key:
description: The key to select.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the ConfigMap or its key
must be defined
type: boolean
required:
- key
type: object
fieldRef:
description: 'Selects a field of the pod: supports metadata.name,
metadata.namespace, metadata.labels, metadata.annotations,
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP.'
properties:
apiVersion:
description: Version of the schema the FieldPath is written
in terms of, defaults to "v1".
type: string
fieldPath:
description: Path of the field to select in the specified
API version.
type: string
required:
- fieldPath
type: object
resourceFieldRef:
description: 'Selects a resource of the container: only resources
limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage,
requests.cpu, requests.memory and requests.ephemeral-storage)
are currently supported.'
properties:
containerName:
description: 'Container name: required for volumes, optional
for env vars'
type: string
divisor:
description: Specifies the output format of the exposed
resources, defaults to "1"
type: string
resource:
description: 'Required: resource to select'
type: string
required:
- resource
type: object
secretKeyRef:
description: Selects a key of a secret in the pod's namespace
properties:
key:
description: The key of the secret to select from. Must
be a valid secret key.
type: string
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
optional:
description: Specify whether the Secret or its key must
be defined
type: boolean
required:
- key
type: object
type: object
required:
- name
- value
type: object
type: array
exportRepo:
Expand Down Expand Up @@ -253,6 +343,11 @@ spec:
- host
type: object
type: array
serviceAccount:
description: ServiceAccount use a specific kubernetes ServiceAccount
for running the create + destroy pods. If not specified we create
a new ServiceAccount per Terraform
type: string
sources:
items:
description: SrcOpts defines a terraform source location (eg git::SSH
Expand Down
11 changes: 3 additions & 8 deletions pkg/apis/tf/v1alpha1/terraform_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package v1alpha1

import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -42,8 +43,8 @@ type TerraformSpec struct {
// git protocol over SSH or HTTPS
TerraformModule *SrcOpts `json:"terraformModule"`

Sources []*SrcOpts `json:"sources,omitempty"`
Env []EnvVar `json:"env,omitempty"`
Sources []*SrcOpts `json:"sources,omitempty"`
Env []v1.EnvVar `json:"env,omitempty"`

// ServiceAccount use a specific kubernetes ServiceAccount for running the create + destroy pods.
// If not specified we create a new ServiceAccount per Terraform
Expand Down Expand Up @@ -173,12 +174,6 @@ type ReconcileTerraformDeployment struct {
SyncPeriod int64 `json:"syncPeriod,omitempty"`
}

// EnvVar defines key/value pairs of env vars that get picked up by terraform
type EnvVar struct {
Name string `json:"name"`
Value string `json:"value"`
}

// Source is used to describe details of where to find configs
type Source struct {
Source *SrcOpts `json:"source,omitempty"`
Expand Down
34 changes: 15 additions & 19 deletions pkg/apis/tf/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 33 additions & 24 deletions pkg/controller/terraform/terraform_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ type RunOptions struct {
namespace string
name string
jobNameLabel string
envVars map[string]string
envVars []corev1.EnvVar
credentials []tfv1alpha1.Credentials
stack ParsedAddress
token string
Expand Down Expand Up @@ -133,7 +133,7 @@ func newRunOptions(instance *tfv1alpha1.Terraform, isDestroy bool) RunOptions {
namespace: instance.Namespace,
name: name,
jobNameLabel: jobNameLabel,
envVars: make(map[string]string),
envVars: instance.Spec.Env,
isNewResource: isNewResource,
applyAction: applyAction,
terraformVersion: terraformVersion,
Expand All @@ -148,8 +148,8 @@ func (r *RunOptions) updateDownloadedModules(module string) {
r.moduleConfigMaps = append(r.moduleConfigMaps, module)
}

func (r *RunOptions) updateEnvVars(k, v string) {
r.envVars[k] = v
func (r *RunOptions) updateEnvVars(v corev1.EnvVar) {
r.envVars = append(r.envVars, v)
}

const terraformFinalizer = "finalizer.tf.isaaguilar.com"
Expand Down Expand Up @@ -550,7 +550,10 @@ func (r *ReconcileTerraform) setupAndRun(reqLogger logr.Logger, instance *tfv1al
// resources that are not owned by this CR, like a PVC.

runOpts := newRunOptions(instance, isFinalize)
runOpts.updateEnvVars("DEPLOYMENT", instance.Name)
runOpts.updateEnvVars(corev1.EnvVar{
Name: "DEPLOYMENT",
Value: instance.Name,
})
// runOpts.namespace = instance.Namespace

// Stack Download
Expand Down Expand Up @@ -682,11 +685,6 @@ func (r *ReconcileTerraform) setupAndRun(reqLogger logr.Logger, instance *tfv1al
runOpts.configMapData["postrun.sh"] = instance.Spec.PostrunScript
}

// TODO Validate spec.env
for _, env := range instance.Spec.Env {
runOpts.updateEnvVars(env.Name, env.Value)
}

// Flatten all the .tfvars and TF_VAR envs into a single file and push
if instance.Spec.ExportRepo != nil && !isFinalize {
e := instance.Spec.ExportRepo
Expand All @@ -708,7 +706,7 @@ func (r *ReconcileTerraform) setupAndRun(reqLogger logr.Logger, instance *tfv1al
}

if isFinalize {
runOpts.envVars["DESTROY"] = "true"
runOpts.getOrCreateEnv("DESTROY", "true")
}

// RUN
Expand Down Expand Up @@ -878,10 +876,10 @@ func (r RunOptions) generateJob(tfvarsConfigMap *corev1.ConfigMap) *batchv1.Job
// reqLogger.Info(fmt.Sprintf("Running job with this setup: %+v", r))

// TF Module
envs := []corev1.EnvVar{}
if r.mainModule == "" {
r.mainModule = "main_module"
}
envs := r.envVars
envs = append(envs, []corev1.EnvVar{
{
Name: "TFOPS_MAIN_MODULE",
Expand Down Expand Up @@ -969,16 +967,6 @@ func (r RunOptions) generateJob(tfvarsConfigMap *corev1.ConfigMap) *batchv1.Job
}...)
}

// TF Vars
for k, v := range r.envVars {
envs = append(envs, []corev1.EnvVar{
{
Name: k,
Value: v,
},
}...)
}

// This resource is used to create a volumeMount which have 63 char limits.
// Truncate the instance.Name enough to fit "-tfvars" wich will be the
// configmapName and volumeMount name.
Expand Down Expand Up @@ -1120,6 +1108,20 @@ func (r RunOptions) generateJob(tfvarsConfigMap *corev1.ConfigMap) *batchv1.Job
return job
}

// getOrCreateEnv will check if an env exists. If it does not, it will be
// created with the value
func (r *RunOptions) getOrCreateEnv(name, value string) {
for _, i := range r.envVars {
if i.Name == name {
return
}
}
r.envVars = append(r.envVars, corev1.EnvVar{
Name: name,
Value: value,
})
}

func (r ReconcileTerraform) run(reqLogger logr.Logger, instance *tfv1alpha1.Terraform, runOpts RunOptions) (jobName string, err error) {
tfvarsConfigMap := runOpts.generateConfigMap()
secret := generateSecretObject(runOpts.sshConfig, instance.Namespace, runOpts.sshConfigData)
Expand Down Expand Up @@ -2047,11 +2049,18 @@ func (d GitRepoAccessOptions) commitTfvars(k8sclient client.Client, tfvars, tfva
}

// Format TFVars File
// Fist read in the tfvar file that gets created earlier. This tfvar
// First read in the tfvar file that gets created earlier. This tfvar
// file should have already concatenated all the tfvars found
// from the git repos
tfvarsFileContent := tfvars
for k, v := range runOpts.envVars {
for _, i := range runOpts.envVars {
k := i.Name
v := i.Value
// TODO Attempt to resolve other kinds of TF_VAR_ values via
// an EnvVarSource other than `Value`
if v == "" {
continue
}
if !strings.Contains(k, "TF_VAR") {
continue
}
Expand Down

0 comments on commit 71bf3ed

Please sign in to comment.