diff --git a/api/v1alpha1/workspace_types.go b/api/v1alpha1/workspace_types.go index 6fbc6aad..51c63542 100644 --- a/api/v1alpha1/workspace_types.go +++ b/api/v1alpha1/workspace_types.go @@ -123,6 +123,9 @@ type WorkspaceSpec struct { RunTriggers []*RunTrigger `json:"runTriggers,omitempty"` // File path within operator pod to load workspace secrets SecretsMountPath string `json:"secretsMountPath"` + // Name of the secret in the same namespace as the Workspace CR that contains workspace secrets. Those secrets are merged with secrets in the secretsMountPath. + // +optional + SecretName string `json:"secretName"` // SSH Key ID. This key must already exist in the TF Cloud organization. This can either be the user assigned name of the SSH Key, or the system assigned ID. // +optional SSHKeyID string `json:"sshKeyID,omitempty"` diff --git a/config/crd/bases/app.terraform.io_workspaces.yaml b/config/crd/bases/app.terraform.io_workspaces.yaml index 67e2c741..19217339 100644 --- a/config/crd/bases/app.terraform.io_workspaces.yaml +++ b/config/crd/bases/app.terraform.io_workspaces.yaml @@ -140,6 +140,11 @@ spec: - sourceableName type: object type: array + secretName: + description: Name of the secret in the same namespace as the Workspace + CR that contains workspace secrets. Those secrets are merged with + secrets in the secretsMountPath. + type: string secretsMountPath: description: File path within operator pod to load workspace secrets type: string diff --git a/workspacehelper/k8s_secret.go b/workspacehelper/k8s_secret.go index 5ef0ecfd..2a2e8045 100644 --- a/workspacehelper/k8s_secret.go +++ b/workspacehelper/k8s_secret.go @@ -11,14 +11,22 @@ import ( // GetSecretData retrieves the data from a secret in a given namespace func (r *WorkspaceHelper) GetSecretData(namespace string, name string) (map[string][]byte, error) { + // If no secretName defined, return empty map + if name == "" { + return make(map[string][]byte), nil + } + r.reqLogger.Info("Getting Secret", "Namespace", namespace, "Name", name) secret := &corev1.Secret{} err := r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, secret) + if err != nil { r.reqLogger.Error(err, "Failed to get Secret", "Namespace", namespace, "Name", name) + return nil, err } + return secret.Data, nil } diff --git a/workspacehelper/tfc_variable.go b/workspacehelper/tfc_variable.go index db012e2f..adf735fc 100644 --- a/workspacehelper/tfc_variable.go +++ b/workspacehelper/tfc_variable.go @@ -59,12 +59,12 @@ func (t *TerraformCloudClient) deleteVariablesFromTFC(specTFCVariables []*tfc.Va return nil } -func (t *TerraformCloudClient) createVariablesOnTFC(workspace *tfc.Workspace, specTFCVariables []*tfc.Variable, workspaceVariables []*tfc.Variable) (bool, error) { +func (t *TerraformCloudClient) createVariablesOnTFC(workspace *tfc.Workspace, specTFCVariables []*tfc.Variable, workspaceVariables []*tfc.Variable, secretData map[string][]byte) (bool, error) { updated := false for _, v := range specTFCVariables { index := find(workspaceVariables, v.Key) if index < 0 { - err := t.CreateTerraformVariable(workspace, v) + err := t.CreateTerraformVariable(workspace, v, secretData) if err != nil { return false, err } @@ -104,7 +104,7 @@ func getNonSensitiveVariablesToUpdate(specTFCVariables []*tfc.Variable, workspac return variablesToUpdate } -func getSensitiveVariablesToUpdate(specTFCVariables []*tfc.Variable, workspaceVariables []*tfc.Variable, secretsMountPath string) ([]*tfc.Variable, error) { +func getSensitiveVariablesToUpdate(specTFCVariables []*tfc.Variable, workspaceVariables []*tfc.Variable, secretsMountPath string, secretData map[string][]byte) ([]*tfc.Variable, error) { variablesToUpdate := []*tfc.Variable{} for _, v := range specTFCVariables { index := find(workspaceVariables, v.Key) @@ -112,7 +112,7 @@ func getSensitiveVariablesToUpdate(specTFCVariables []*tfc.Variable, workspaceVa continue } if workspaceVariables[index].Sensitive { - if err := checkAndRetrieveIfSensitive(v, secretsMountPath); err != nil { + if err := checkAndRetrieveIfSensitive(v, secretsMountPath, secretData); err != nil { return nil, err } v.ID = workspaceVariables[index].ID @@ -124,7 +124,7 @@ func getSensitiveVariablesToUpdate(specTFCVariables []*tfc.Variable, workspaceVa return variablesToUpdate, nil } -func generateUpdateVariableList(specTFCVariables []*tfc.Variable, workspaceVariables []*tfc.Variable, secretsMountPath string) ([]*tfc.Variable, error) { +func generateUpdateVariableList(specTFCVariables []*tfc.Variable, workspaceVariables []*tfc.Variable, secretsMountPath string, secretData map[string][]byte) ([]*tfc.Variable, error) { updateList := []*tfc.Variable{} nonSensitiveVariablesToUpdate := getNonSensitiveVariablesToUpdate(specTFCVariables, workspaceVariables) @@ -132,7 +132,7 @@ func generateUpdateVariableList(specTFCVariables []*tfc.Variable, workspaceVaria return updateList, nil } - sensitiveVariablesToUpdate, err := getSensitiveVariablesToUpdate(specTFCVariables, workspaceVariables, secretsMountPath) + sensitiveVariablesToUpdate, err := getSensitiveVariablesToUpdate(specTFCVariables, workspaceVariables, secretsMountPath, secretData) if err != nil { return nonSensitiveVariablesToUpdate, err } @@ -143,7 +143,7 @@ func generateUpdateVariableList(specTFCVariables []*tfc.Variable, workspaceVaria } // CheckVariables creates, updates, or deletes variables as needed -func (t *TerraformCloudClient) CheckVariables(workspace string, specTFCVariables []*tfc.Variable) (bool, error) { +func (t *TerraformCloudClient) CheckVariables(workspace string, specTFCVariables []*tfc.Variable, secretData map[string][]byte) (bool, error) { tfcWorkspace, err := t.Client.Workspaces.Read(context.TODO(), t.Organization, workspace) if err != nil { return false, err @@ -156,12 +156,12 @@ func (t *TerraformCloudClient) CheckVariables(workspace string, specTFCVariables return false, err } - createdVariables, err := t.createVariablesOnTFC(tfcWorkspace, specTFCVariables, workspaceVariables) + createdVariables, err := t.createVariablesOnTFC(tfcWorkspace, specTFCVariables, workspaceVariables, secretData) if err != nil { return false, err } - variablesToUpdate, err := generateUpdateVariableList(specTFCVariables, workspaceVariables, t.SecretsMountPath) + variablesToUpdate, err := generateUpdateVariableList(specTFCVariables, workspaceVariables, t.SecretsMountPath, secretData) if err != nil || len(variablesToUpdate) == 0 { return false, err } @@ -222,25 +222,28 @@ func (t *TerraformCloudClient) UpdateTerraformVariables(variables []*tfc.Variabl return nil } -func checkAndRetrieveIfSensitive(variable *tfc.Variable, secretsMountPath string) error { - // Try to read variables with empty value from file. If the value isn't empty, - // it was already read fromValue.SecretKeyRef. - if variable.Sensitive && variable.Value == "" { - filePath := fmt.Sprintf("%s/%s", secretsMountPath, variable.Key) - - data, err := ioutil.ReadFile(filePath) - if err != nil { - return fmt.Errorf("could not get secret, %s", err) +func checkAndRetrieveIfSensitive(variable *tfc.Variable, secretsMountPath string, secretData map[string][]byte) error { + if variable.Sensitive { + // First check if the key is in the namespaced Secret + if val, ok := secretData[variable.Key]; ok { + variable.Value = string(val) + } else { + // Try to find key in the mounted Secret + filePath := fmt.Sprintf("%s/%s", secretsMountPath, variable.Key) + data, err := ioutil.ReadFile(filePath) + if err != nil { + return fmt.Errorf("could not get secret, %s", err) + } + secret := string(data) + variable.Value = secret } - secret := string(data) - variable.Value = secret } return nil } // CreateTerraformVariable creates a Terraform variable based on key and value -func (t *TerraformCloudClient) CreateTerraformVariable(workspace *tfc.Workspace, variable *tfc.Variable) error { - if err := checkAndRetrieveIfSensitive(variable, t.SecretsMountPath); err != nil { +func (t *TerraformCloudClient) CreateTerraformVariable(workspace *tfc.Workspace, variable *tfc.Variable, secretData map[string][]byte) error { + if err := checkAndRetrieveIfSensitive(variable, t.SecretsMountPath, secretData); err != nil { return err } options := tfc.VariableCreateOptions{ diff --git a/workspacehelper/tfc_variable_test.go b/workspacehelper/tfc_variable_test.go index 81386780..28373f5a 100644 --- a/workspacehelper/tfc_variable_test.go +++ b/workspacehelper/tfc_variable_test.go @@ -99,7 +99,8 @@ func TestShouldGetSensitiveVariablesForUpdate(t *testing.T) { Sensitive: true, }, } - update, err := getSensitiveVariablesToUpdate(specVariables, workspaceVariables, secretsMount) + secretData := make(map[string][]byte) + update, err := getSensitiveVariablesToUpdate(specVariables, workspaceVariables, secretsMount, secretData) assert.NoError(t, err) assert.Len(t, update, 1) assert.Equal(t, update[0].Key, specVariables[0].Key) @@ -132,7 +133,8 @@ func TestShouldUpdateVariables(t *testing.T) { HCL: false, }, } - update, err := generateUpdateVariableList(specVariables, workspaceVariables, secretsMount) + secretData := make(map[string][]byte) + update, err := generateUpdateVariableList(specVariables, workspaceVariables, secretsMount, secretData) assert.NoError(t, err) assert.Len(t, update, 2) assert.False(t, update[0].Sensitive) @@ -171,7 +173,8 @@ func TestShouldNotUpdateVariables(t *testing.T) { HCL: true, }, } - update, err := generateUpdateVariableList(specVariables, workspaceVariables, secretsMount) + secretData := make(map[string][]byte) + update, err := generateUpdateVariableList(specVariables, workspaceVariables, secretsMount, secretData) assert.NoError(t, err) assert.Len(t, update, 0) } diff --git a/workspacehelper/workspace_helper.go b/workspacehelper/workspace_helper.go index 8e0ad998..8988470a 100644 --- a/workspacehelper/workspace_helper.go +++ b/workspacehelper/workspace_helper.go @@ -299,8 +299,14 @@ func (r *WorkspaceHelper) updateVariables(instance *appv1alpha1.Workspace) (bool } } + secretData, err := r.GetSecretData(instance.Namespace, instance.Spec.SecretName) + if err != nil { + r.reqLogger.Error(err, "Could not get namespaced Secret data") + return false, err + } + specTFCVariables := MapToTFCVariable(instance.Spec.Variables) - updatedVariables, err := r.tfclient.CheckVariables(workspace, specTFCVariables) + updatedVariables, err := r.tfclient.CheckVariables(workspace, specTFCVariables, secretData) if err != nil { r.reqLogger.Error(err, "Could not update variables") return false, err