diff --git a/api/types/state.go b/api/types/state.go index 2aa86291..342c6e10 100644 --- a/api/types/state.go +++ b/api/types/state.go @@ -29,6 +29,30 @@ const ( ConfigurationApplyFailed ConfigurationState = "ApplyFailed" ConfigurationDestroyFailed ConfigurationState = "DestroyFailed" ConfigurationReloading ConfigurationState = "ConfigurationReloading" + GeneratingOutputs ConfigurationState = "GeneratingTerraformOutputs" +) + +const ( + // MessageDestroyJobNotCompleted is the message when Configuration deletion isn't completed + MessageDestroyJobNotCompleted = "Configuration deletion isn't completed" + // MessageApplyJobNotCompleted is the message when cloud resources are not created completed + MessageApplyJobNotCompleted = "cloud resources are not created completed" + // MessageCloudResourceProvisioningAndChecking is the message when cloud resource is being provisioned + MessageCloudResourceProvisioningAndChecking = "Cloud resources are being provisioned and provisioning status is checking..." + // ErrUpdateTerraformApplyJob means hitting an issue to update Terraform apply job + ErrUpdateTerraformApplyJob = "Hit an issue to update Terraform apply job" + // MessageCloudResourceDeployed means Cloud resources are deployed and ready to use + MessageCloudResourceDeployed = "Cloud resources are deployed and ready to use" + // MessageCloudResourceDestroying is the message when cloud resource is being destroyed + MessageCloudResourceDestroying = "Cloud resources is being destroyed..." + // ErrProviderNotReady means provider object is not ready + ErrProviderNotReady = "Provider is not ready" + // ConfigurationReloadingAsHCLChanged means Configuration changed and needs reloading + ConfigurationReloadingAsHCLChanged = "Configuration's HCL has changed, and starts reloading" + // ConfigurationReloadingAsVariableChanged means Configuration changed and needs reloading + ConfigurationReloadingAsVariableChanged = "Configuration's variable has changed, and starts reloading" + // ErrGenerateOutputs means error to generate outputs + ErrGenerateOutputs = "Hit an issue to generate outputs" ) // ProviderState is the type for Provider state diff --git a/chart/templates/terraform_controller.yaml b/chart/templates/terraform_controller.yaml index 9e224fca..dc4d486b 100644 --- a/chart/templates/terraform_controller.yaml +++ b/chart/templates/terraform_controller.yaml @@ -27,4 +27,6 @@ spec: fieldPath: metadata.namespace - name: TERRAFORM_IMAGE value: {{ .Values.terraformImage}} + - name: TERRAFORM_BACKEND_NAMESPACE + value: {{ .Values.backend.namespace }} serviceAccountName: tf-controller-service-account diff --git a/chart/values.yaml b/chart/values.yaml index 346aabe5..4f754733 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -6,3 +6,6 @@ image: pullPolicy: Always terraformImage: oamdev/docker-terraform:1.0.9 + +backend: + namespace: vela-system diff --git a/controllers/configuration/configuration.go b/controllers/configuration/configuration.go index 14612eef..ad9e7dab 100644 --- a/controllers/configuration/configuration.go +++ b/controllers/configuration/configuration.go @@ -30,7 +30,7 @@ func ValidConfigurationObject(configuration *v1beta1.Configuration) (types.Confi } // RenderConfiguration will compose the Terraform configuration with hcl/json and backend -func RenderConfiguration(configuration *v1beta1.Configuration, controllerNamespace string, configurationType types.ConfigurationType) (string, error) { +func RenderConfiguration(configuration *v1beta1.Configuration, terraformBackendNamespace string, configurationType types.ConfigurationType) (string, error) { if configuration.Spec.Backend != nil { if configuration.Spec.Backend.SecretSuffix == "" { configuration.Spec.Backend.SecretSuffix = configuration.Name @@ -42,7 +42,7 @@ func RenderConfiguration(configuration *v1beta1.Configuration, controllerNamespa InClusterConfig: true, } } - backendTF, err := RenderTemplate(configuration.Spec.Backend, controllerNamespace) + backendTF, err := RenderTemplate(configuration.Spec.Backend, terraformBackendNamespace) if err != nil { return "", errors.Wrap(err, "failed to prepare Terraform backend configuration") } diff --git a/controllers/configuration_controller.go b/controllers/configuration_controller.go index 804b56e0..b8d8fb28 100644 --- a/controllers/configuration_controller.go +++ b/controllers/configuration_controller.go @@ -90,27 +90,6 @@ const ( ServiceAccountName = "tf-executor-service-account" ) -const ( - // MessageDestroyJobNotCompleted is the message when Configuration deletion isn't completed - MessageDestroyJobNotCompleted = "Configuration deletion isn't completed" - // MessageApplyJobNotCompleted is the message when cloud resources are not created completed - MessageApplyJobNotCompleted = "cloud resources are not created completed" - // MessageCloudResourceProvisioningAndChecking is the message when cloud resource is being provisioned - MessageCloudResourceProvisioningAndChecking = "Cloud resources are being provisioned and provisioning status is checking..." - // ErrUpdateTerraformApplyJob means hitting an issue to update Terraform apply job - ErrUpdateTerraformApplyJob = "Hit an issue to update Terraform apply job" - // MessageCloudResourceDeployed means Cloud resources are deployed and ready to use - MessageCloudResourceDeployed = "Cloud resources are deployed and ready to use" - // MessageCloudResourceDestroying is the message when cloud resource is being destroyed - MessageCloudResourceDestroying = "Cloud resources is being destroyed..." - // ErrProviderNotReady means provider object is not ready - ErrProviderNotReady = "Provider is not ready" - // ConfigurationReloadingAsHCLChanged means Configuration changed and needs reloading - ConfigurationReloadingAsHCLChanged = "Configuration's HCL has changed, and starts reloading" - // ConfigurationReloadingAsVariableChanged means Configuration changed and needs reloading - ConfigurationReloadingAsVariableChanged = "Configuration's variable has changed, and starts reloading" -) - // ConfigurationReconciler reconciles a Configuration object. type ConfigurationReconciler struct { client.Client @@ -121,7 +100,8 @@ type ConfigurationReconciler struct { var ( // terraformImage is the Terraform image which can run `terraform init/plan/apply` - terraformImage = os.Getenv("TERRAFORM_IMAGE") + terraformImage = os.Getenv("TERRAFORM_IMAGE") + terraformBackendNamespace = os.Getenv("TERRAFORM_BACKEND_NAMESPACE") ) // TFConfigurationMeta is all the metadata of a Configuration @@ -153,14 +133,6 @@ func (r *ConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro var ( configuration v1beta1.Configuration ctx = context.Background() - meta = &TFConfigurationMeta{ - Namespace: req.Namespace, - Name: req.Name, - ConfigurationCMName: fmt.Sprintf(TFInputConfigMapName, req.Name), - VariableSecretName: fmt.Sprintf(TFVariableSecret, req.Name), - ApplyJobName: req.Name + "-" + string(TerraformApply), - DestroyJobName: req.Name + "-" + string(TerraformDestroy), - } ) klog.InfoS("reconciling Terraform Configuration...", "NamespacedName", req.NamespacedName) @@ -172,33 +144,7 @@ func (r *ConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro return ctrl.Result{}, err } - meta.RemoteGit = configuration.Spec.Remote - meta.DeleteResource = configuration.Spec.DeleteResource - if configuration.Spec.Path == "" { - meta.RemoteGitPath = "." - } else { - meta.RemoteGitPath = configuration.Spec.Path - } - - if configuration.Spec.ProviderReference != nil { - meta.ProviderReference = configuration.Spec.ProviderReference - } else { - meta.ProviderReference = &crossplane.Reference{ - Name: util.ProviderDefaultName, - Namespace: util.ProviderDefaultNamespace, - } - } - - // Check the existence of Terraform state secret which is used to store TF state file. For detailed information, - // please refer to https://www.terraform.io/docs/language/settings/backends/kubernetes.html#configuration-variables - var backendSecretSuffix string - if configuration.Spec.Backend != nil && configuration.Spec.Backend.SecretSuffix != "" { - backendSecretSuffix = configuration.Spec.Backend.SecretSuffix - } else { - backendSecretSuffix = configuration.Name - } - // Secrets will be named in the format: tfstate-{workspace}-{secret_suffix} - meta.BackendSecretName = fmt.Sprintf(TFBackendSecret, terraformWorkspace, backendSecretSuffix) + meta := initTFConfigurationMeta(req, configuration) // add finalizer if configuration.ObjectMeta.DeletionTimestamp.IsZero() { @@ -218,7 +164,7 @@ func (r *ConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro var tfExecutionJob = &batchv1.Job{} //nolint:gofmt if err := r.Client.Get(ctx, client.ObjectKey{Name: meta.ApplyJobName, Namespace: meta.Namespace}, tfExecutionJob); err == nil { if tfExecutionJob.Status.Succeeded == int32(1) { - if err := meta.updateApplyStatus(ctx, r.Client, types.Available, MessageCloudResourceDeployed); err != nil { + if err := meta.updateApplyStatus(ctx, r.Client, types.Available, types.MessageCloudResourceDeployed); err != nil { return ctrl.Result{}, err } } @@ -236,7 +182,7 @@ func (r *ConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro } if err := r.terraformDestroy(ctx, req.Namespace, configuration, meta); err != nil { - if err.Error() == MessageDestroyJobNotCompleted { + if err.Error() == types.MessageDestroyJobNotCompleted { return ctrl.Result{RequeueAfter: 3 * time.Second}, nil } return ctrl.Result{RequeueAfter: 3 * time.Second}, errors.Wrap(err, "continue reconciling to destroy cloud resource") @@ -253,7 +199,7 @@ func (r *ConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro // Terraform apply (create or update) klog.InfoS("performing Terraform Apply (cloud resource create/update)", "Namespace", req.Namespace, "Name", req.Name) if err := r.terraformApply(ctx, req.Namespace, configuration, meta); err != nil { - if err.Error() == MessageApplyJobNotCompleted { + if err.Error() == types.MessageApplyJobNotCompleted { return ctrl.Result{RequeueAfter: 3 * time.Second}, nil } return ctrl.Result{RequeueAfter: 3 * time.Second}, errors.Wrap(err, "failed to create/update cloud resource") @@ -268,6 +214,47 @@ func (r *ConfigurationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, erro return ctrl.Result{}, nil } +func initTFConfigurationMeta(req ctrl.Request, configuration v1beta1.Configuration) *TFConfigurationMeta { + var meta = &TFConfigurationMeta{ + Namespace: req.Namespace, + Name: req.Name, + ConfigurationCMName: fmt.Sprintf(TFInputConfigMapName, req.Name), + VariableSecretName: fmt.Sprintf(TFVariableSecret, req.Name), + ApplyJobName: req.Name + "-" + string(TerraformApply), + DestroyJobName: req.Name + "-" + string(TerraformDestroy), + } + + meta.RemoteGit = configuration.Spec.Remote + meta.DeleteResource = configuration.Spec.DeleteResource + if configuration.Spec.Path == "" { + meta.RemoteGitPath = "." + } else { + meta.RemoteGitPath = configuration.Spec.Path + } + + if configuration.Spec.ProviderReference != nil { + meta.ProviderReference = configuration.Spec.ProviderReference + } else { + meta.ProviderReference = &crossplane.Reference{ + Name: util.ProviderDefaultName, + Namespace: util.ProviderDefaultNamespace, + } + } + + // Check the existence of Terraform state secret which is used to store TF state file. For detailed information, + // please refer to https://www.terraform.io/docs/language/settings/backends/kubernetes.html#configuration-variables + var backendSecretSuffix string + if configuration.Spec.Backend != nil && configuration.Spec.Backend.SecretSuffix != "" { + backendSecretSuffix = configuration.Spec.Backend.SecretSuffix + } else { + backendSecretSuffix = configuration.Name + } + // Secrets will be named in the format: tfstate-{workspace}-{secret_suffix} + meta.BackendSecretName = fmt.Sprintf(TFBackendSecret, terraformWorkspace, backendSecretSuffix) + + return meta +} + func (r *ConfigurationReconciler) terraformApply(ctx context.Context, namespace string, configuration v1beta1.Configuration, meta *TFConfigurationMeta) error { klog.InfoS("terraform apply job", "Namespace", namespace, "Name", meta.ApplyJobName) @@ -283,18 +270,18 @@ func (r *ConfigurationReconciler) terraformApply(ctx context.Context, namespace } if err := meta.updateTerraformJobIfNeeded(ctx, k8sClient, configuration, tfExecutionJob); err != nil { - klog.ErrorS(err, ErrUpdateTerraformApplyJob, "Name", meta.ApplyJobName) - return errors.Wrap(err, ErrUpdateTerraformApplyJob) + klog.ErrorS(err, types.ErrUpdateTerraformApplyJob, "Name", meta.ApplyJobName) + return errors.Wrap(err, types.ErrUpdateTerraformApplyJob) } if tfExecutionJob.Status.Succeeded == int32(1) { - if err := meta.updateApplyStatus(ctx, k8sClient, types.Available, MessageCloudResourceDeployed); err != nil { + if err := meta.updateApplyStatus(ctx, k8sClient, types.Available, types.MessageCloudResourceDeployed); err != nil { return err } } else { // start provisioning and check the status of the provision if configuration.Status.Apply.State != types.ConfigurationProvisioningAndChecking { - if err := meta.updateApplyStatus(ctx, r.Client, types.ConfigurationProvisioningAndChecking, MessageCloudResourceProvisioningAndChecking); err != nil { + if err := meta.updateApplyStatus(ctx, r.Client, types.ConfigurationProvisioningAndChecking, types.MessageCloudResourceProvisioningAndChecking); err != nil { return err } } @@ -308,7 +295,7 @@ func (r *ConfigurationReconciler) terraformDestroy(ctx context.Context, namespac k8sClient = r.Client ) if configuration.Status.Apply.State == types.ConfigurationProvisioningAndChecking { - warning := fmt.Sprintf("Destroy could not complete and needs to wait for Provision to complet first: %s", MessageCloudResourceProvisioningAndChecking) + warning := fmt.Sprintf("Destroy could not complete and needs to wait for Provision to complet first: %s", types.MessageCloudResourceProvisioningAndChecking) klog.Warning(warning) return errors.New(warning) } @@ -339,13 +326,13 @@ func (r *ConfigurationReconciler) terraformDestroy(ctx context.Context, namespac } // destroying - if err := meta.updateDestroyStatus(ctx, k8sClient, types.ConfigurationDestroying, MessageCloudResourceDestroying); err != nil { + if err := meta.updateDestroyStatus(ctx, k8sClient, types.ConfigurationDestroying, types.MessageCloudResourceDestroying); err != nil { return err } if err := meta.updateTerraformJobIfNeeded(ctx, k8sClient, configuration, destroyJob); err != nil { - klog.ErrorS(err, ErrUpdateTerraformApplyJob, "Name", meta.ApplyJobName) - return errors.Wrap(err, ErrUpdateTerraformApplyJob) + klog.ErrorS(err, types.ErrUpdateTerraformApplyJob, "Name", meta.ApplyJobName) + return errors.Wrap(err, types.ErrUpdateTerraformApplyJob) } // When the deletion Job process succeeded, clean up work is starting. @@ -392,14 +379,14 @@ func (r *ConfigurationReconciler) terraformDestroy(ctx context.Context, namespac // 6. delete Kubernetes backend secret klog.InfoS("Deleting the secret which stores Kubernetes backend", "Name", meta.BackendSecretName) var kubernetesBackendSecret v1.Secret - if err := r.Client.Get(ctx, client.ObjectKey{Name: meta.BackendSecretName, Namespace: meta.Namespace}, &kubernetesBackendSecret); err == nil { + if err := r.Client.Get(ctx, client.ObjectKey{Name: meta.BackendSecretName, Namespace: terraformBackendNamespace}, &kubernetesBackendSecret); err == nil { if err := r.Client.Delete(ctx, &kubernetesBackendSecret); err != nil { return err } } return nil } - return errors.New(MessageDestroyJobNotCompleted) + return errors.New(types.MessageDestroyJobNotCompleted) } func (r *ConfigurationReconciler) preCheck(ctx context.Context, configuration *v1beta1.Configuration, meta *TFConfigurationMeta) error { @@ -418,7 +405,7 @@ func (r *ConfigurationReconciler) preCheck(ctx context.Context, configuration *v // TODO(zzxwill) Need to find an alternative to check whether there is an state backend in the Configuration // Render configuration with backend - completeConfiguration, err := tfcfg.RenderConfiguration(configuration, meta.Namespace, configurationType) + completeConfiguration, err := tfcfg.RenderConfiguration(configuration, terraformBackendNamespace, configurationType) if err != nil { return err } @@ -435,7 +422,7 @@ func (r *ConfigurationReconciler) preCheck(ctx context.Context, configuration *v if meta.ConfigurationChanged { klog.InfoS("Configuration hanged, reloading...") - if err := meta.updateApplyStatus(ctx, k8sClient, types.ConfigurationReloading, ConfigurationReloadingAsHCLChanged); err != nil { + if err := meta.updateApplyStatus(ctx, k8sClient, types.ConfigurationReloading, types.ConfigurationReloadingAsHCLChanged); err != nil { return err } // store configuration to ConfigMap @@ -445,11 +432,11 @@ func (r *ConfigurationReconciler) preCheck(ctx context.Context, configuration *v // Check provider if err := meta.checkProvider(ctx, k8sClient); err != nil { if configuration.Status.Apply.State != types.ProviderNotReady { - if updateStatusErr := meta.updateApplyStatus(ctx, k8sClient, types.ProviderNotReady, ErrProviderNotReady); updateStatusErr != nil { + if updateStatusErr := meta.updateApplyStatus(ctx, k8sClient, types.ProviderNotReady, types.ErrProviderNotReady); updateStatusErr != nil { return errors.Wrap(updateStatusErr, errSettingStatus) } } - return errors.Wrap(err, ErrProviderNotReady) + return errors.Wrap(err, types.ErrProviderNotReady) } // Apply ClusterRole @@ -466,9 +453,13 @@ func (meta *TFConfigurationMeta) updateApplyStatus(ctx context.Context, k8sClien if state == types.Available { outputs, err := meta.getTFOutputs(ctx, k8sClient, configuration) if err != nil { - return err + configuration.Status.Apply = v1beta1.ConfigurationApplyStatus{ + State: types.GeneratingOutputs, + Message: types.ErrGenerateOutputs + ": " + err.Error(), + } + } else { + configuration.Status.Apply.Outputs = outputs } - configuration.Status.Apply.Outputs = outputs } return k8sClient.Status().Update(ctx, &configuration) @@ -543,7 +534,7 @@ func (meta *TFConfigurationMeta) updateTerraformJobIfNeeded(ctx context.Context, if val, ok := meta.VariableSecretData[k]; !ok || !bytes.Equal(v, val) { envChanged = true klog.Info("Job's env changed") - if err := meta.updateApplyStatus(ctx, k8sClient, types.ConfigurationReloading, ConfigurationReloadingAsVariableChanged); err != nil { + if err := meta.updateApplyStatus(ctx, k8sClient, types.ConfigurationReloading, types.ConfigurationReloadingAsVariableChanged); err != nil { return err } } @@ -706,7 +697,7 @@ type TFState struct { //nolint:funlen func (meta *TFConfigurationMeta) getTFOutputs(ctx context.Context, k8sClient client.Client, configuration v1beta1.Configuration) (map[string]v1beta1.Property, error) { var s = v1.Secret{} - if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.BackendSecretName, Namespace: configuration.Namespace}, &s); err != nil { + if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.BackendSecretName, Namespace: terraformBackendNamespace}, &s); err != nil { return nil, errors.Wrap(err, "terraform state file backend secret is not generated") } tfStateData, ok := s.Data[TerraformStateNameInSecret]