From 5491d0842cc874733509e51af3c582f3d89dc991 Mon Sep 17 00:00:00 2001 From: Eric Hayes Date: Wed, 6 Oct 2021 17:54:12 -0700 Subject: [PATCH] Add support for outputName, outputNamespace overrides. --- api/v1alpha1/workspace_types.go | 14 +++++ .../bases/app.terraform.io_workspaces.yaml | 6 +++ workspacehelper/k8s_configmap.go | 53 ++++++++++++++++--- workspacehelper/workspace_helper.go | 25 +++++++-- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/api/v1alpha1/workspace_types.go b/api/v1alpha1/workspace_types.go index 201b12eb..df8456d2 100644 --- a/api/v1alpha1/workspace_types.go +++ b/api/v1alpha1/workspace_types.go @@ -126,6 +126,12 @@ type WorkspaceSpec struct { // 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"` + // Overrides default output name. Default output name is `-outputs`. + // +optional + OutputName string `json:"outputName,omitempty"` + // Overrides default output namespace. Default namespace is the same namespace as the workspace. + // +optional + OutputNamespace string `json:"outputNamespace,omitempty"` // Outputs denote outputs wanted // +optional Outputs []*OutputSpec `json:"outputs,omitempty"` @@ -157,6 +163,14 @@ type WorkspaceStatus struct { RunID string `json:"runID"` // Configuration Version ID ConfigVersionID string `json:"configVersionID"` + // Current output name + // +optional + // +nullable + OutputName string `json:"outputName"` + // Current output namespace + // +optional + // +nullable + OutputNamespace string `json:"outputNamespace"` // Outputs from state file // +optional Outputs []*OutputStatus `json:"outputs,omitempty"` diff --git a/config/crd/bases/app.terraform.io_workspaces.yaml b/config/crd/bases/app.terraform.io_workspaces.yaml index 770ec825..583932fe 100644 --- a/config/crd/bases/app.terraform.io_workspaces.yaml +++ b/config/crd/bases/app.terraform.io_workspaces.yaml @@ -104,6 +104,12 @@ spec: organization: description: Terraform Cloud organization type: string + outputName: + description: Overrides default output name. Default output name is -outputs. + type: string + outputNamespace: + description: Overrides default output namespace. Default namespace is the same namespace as the workspace. + type: string outputs: description: Outputs denote outputs wanted items: diff --git a/workspacehelper/k8s_configmap.go b/workspacehelper/k8s_configmap.go index 049faccc..dbac9318 100644 --- a/workspacehelper/k8s_configmap.go +++ b/workspacehelper/k8s_configmap.go @@ -3,7 +3,6 @@ package workspacehelper import ( "context" "errors" - "fmt" "reflect" "github.com/hashicorp/terraform-k8s/api/v1alpha1" @@ -116,13 +115,53 @@ func outputsToMap(outputs []*v1alpha1.OutputStatus) map[string][]byte { return data } +// CleanPreviousSecretOutputsIfChanged cleans previous secret if output has changed +func (r *WorkspaceHelper) CleanPreviousSecretOutputsIfChanged(outputName string, outputNamespace string, status *v1alpha1.WorkspaceStatus) error { + currentName := status.OutputName + currentNamespace := status.OutputNamespace + + // no previous name was set, do nothing + if currentName == "" || currentNamespace == "" { + return nil + } + + // no changes to name/namespace + if currentName == outputName && currentNamespace == outputNamespace { + return nil + } + + // attempt to cleanup whatever is configured, if it exists + r.reqLogger.Info("Terraform Output Secret name has changed. Removing previous version.") + found := &corev1.Secret{} + + err := r.client.Get(context.TODO(), types.NamespacedName{Name: currentName, Namespace: currentNamespace}, found) + if err != nil && k8serrors.IsNotFound(err) { + // resource already removed, do nothing + return nil + } else if err != nil { + return err + } + + if err := r.client.Delete(context.TODO(), found); err != nil { + // unable to remove object + return err + } + + return nil +} + // UpsertSecretOutputs creates a Secret for the outputs -func (r *WorkspaceHelper) UpsertSecretOutputs(w *v1alpha1.Workspace, outputs []*v1alpha1.OutputStatus) error { +func (r *WorkspaceHelper) UpsertSecretOutputs(outputName string, outputNamespace string, w *v1alpha1.Workspace) error { found := &corev1.Secret{} - outputName := fmt.Sprintf("%s-outputs", w.Name) - err := r.client.Get(context.TODO(), types.NamespacedName{Name: outputName, Namespace: w.Namespace}, found) + + if err := r.CleanPreviousSecretOutputsIfChanged(outputName, outputNamespace, &w.Status); err != nil { + r.reqLogger.Error(err, "Failed to clean previous output Secret") + return err + } + + err := r.client.Get(context.TODO(), types.NamespacedName{Name: outputName, Namespace: outputNamespace}, found) if err != nil && k8serrors.IsNotFound(err) { - secret := secretForOutputs(outputName, w.Namespace, outputs) + secret := secretForOutputs(outputName, outputNamespace, w.Status.Outputs) err = controllerutil.SetControllerReference(w, secret, r.scheme) if err != nil { return err @@ -138,12 +177,12 @@ func (r *WorkspaceHelper) UpsertSecretOutputs(w *v1alpha1.Workspace, outputs []* return err } - currentOutputs := outputsToMap(outputs) + currentOutputs := outputsToMap(w.Status.Outputs) if !reflect.DeepEqual(found.Data, currentOutputs) { r.reqLogger.Info("Updating secrets", "name", outputName) found.Data = currentOutputs if err := r.client.Update(context.TODO(), found); err != nil { - r.reqLogger.Error(err, "Failed to update output secrets", "Namespace", w.Namespace, "Name", outputName) + r.reqLogger.Error(err, "Failed to update output secrets", "Namespace", outputNamespace, "Name", outputName) return err } return nil diff --git a/workspacehelper/workspace_helper.go b/workspacehelper/workspace_helper.go index 710328df..9a2e6539 100644 --- a/workspacehelper/workspace_helper.go +++ b/workspacehelper/workspace_helper.go @@ -244,8 +244,26 @@ func (r *WorkspaceHelper) processFinishedRun(instance *appv1alpha1.Workspace) er return err } - if !reflect.DeepEqual(outputs, instance.Status.Outputs) { + outputName := fmt.Sprintf("%s-outputs", instance.Name) + if instance.Spec.OutputName != "" { + outputName = instance.Spec.OutputName + } + + outputNamespace := instance.Namespace + if instance.Spec.OutputNamespace != "" { + outputNamespace = instance.Spec.OutputNamespace + } + + if err = r.UpsertSecretOutputs(outputName, outputNamespace, instance); err != nil { + r.reqLogger.Error(err, "Error with creating Secret for Terraform Outputs") + return err + } + + if !reflect.DeepEqual(outputs, instance.Status.Outputs) || instance.Status.OutputName != outputName || instance.Status.OutputNamespace != outputNamespace { instance.Status.Outputs = outputs + instance.Status.OutputName = outputName + instance.Status.OutputNamespace = outputNamespace + err := r.client.Status().Update(context.TODO(), instance) if err != nil { r.reqLogger.Error(err, "Failed to update output status") @@ -256,10 +274,7 @@ func (r *WorkspaceHelper) processFinishedRun(instance *appv1alpha1.Workspace) er r.recorder.Event(instance, corev1.EventTypeNormal, "WorkspaceEvent", fmt.Sprintf("Updated outputs for run %s", instance.Status.RunID)) } - if err = r.UpsertSecretOutputs(instance, instance.Status.Outputs); err != nil { - r.reqLogger.Error(err, "Error with creating ConfigMap for Terraform Outputs") - return err - } + return nil }