github.com/oam-dev/kubevela@v1.9.11/references/appfile/addon.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package appfile
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"os/exec"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	v1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    33  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    34  	"github.com/oam-dev/kubevela/apis/types"
    35  	"github.com/oam-dev/kubevela/pkg/appfile"
    36  	"github.com/oam-dev/kubevela/pkg/cue/process"
    37  	util2 "github.com/oam-dev/kubevela/pkg/oam/util"
    38  	"github.com/oam-dev/kubevela/pkg/utils/common"
    39  	"github.com/oam-dev/kubevela/pkg/utils/util"
    40  )
    41  
    42  const (
    43  	// TerraformBaseLocation is the base directory to store all Terraform JSON files
    44  	TerraformBaseLocation = ".vela/terraform/"
    45  	// TerraformLog is the logfile name for terraform
    46  	TerraformLog = "terraform.log"
    47  )
    48  
    49  // ApplyTerraform deploys addon resources
    50  func ApplyTerraform(app *v1beta1.Application, k8sClient client.Client, ioStream util.IOStreams, namespace string, args common.Args) ([]commontypes.ApplicationComponent, error) {
    51  	pd, err := args.GetPackageDiscover()
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	// TODO(zzxwill) Need to check whether authentication credentials of a specific cloud provider are exported as environment variables, like `ALICLOUD_ACCESS_KEY`
    57  	var nativeVelaComponents []commontypes.ApplicationComponent
    58  	// parse template
    59  	appParser := appfile.NewApplicationParser(k8sClient, pd)
    60  
    61  	ctx := util2.SetNamespaceInCtx(context.Background(), namespace)
    62  	appFile, err := appParser.GenerateAppFile(ctx, app)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("failed to parse appfile: %w", err)
    65  	}
    66  	if appFile == nil {
    67  		return nil, fmt.Errorf("failed to parse appfile")
    68  	}
    69  	cwd, err := os.Getwd()
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	for i, wl := range appFile.ParsedComponents {
    75  		switch wl.CapabilityCategory {
    76  		case types.TerraformCategory:
    77  			name := wl.Name
    78  			ioStream.Infof("\nApplying cloud resources %s\n", name)
    79  
    80  			tf, err := getTerraformJSONFiles(wl, appfile.GenerateContextDataFromAppFile(appFile, wl.Name))
    81  			if err != nil {
    82  				return nil, fmt.Errorf("failed to get Terraform JSON files from workload %s: %w", name, err)
    83  			}
    84  
    85  			tfJSONDir := filepath.Join(TerraformBaseLocation, name)
    86  			if _, err = os.Stat(tfJSONDir); err != nil && os.IsNotExist(err) {
    87  				if err = os.MkdirAll(tfJSONDir, 0750); err != nil {
    88  					return nil, fmt.Errorf("failed to create directory for %s: %w", tfJSONDir, err)
    89  				}
    90  			}
    91  			if err := os.WriteFile(filepath.Join(tfJSONDir, "main.tf.json"), tf, 0600); err != nil {
    92  				return nil, fmt.Errorf("failed to convert Terraform template: %w", err)
    93  			}
    94  
    95  			outputs, err := callTerraform(tfJSONDir)
    96  			if err != nil {
    97  				return nil, err
    98  			}
    99  			if err := os.Chdir(cwd); err != nil {
   100  				return nil, err
   101  			}
   102  
   103  			outputList := strings.Split(strings.ReplaceAll(string(outputs), " ", ""), "\n")
   104  			if outputList[len(outputList)-1] == "" {
   105  				outputList = outputList[:len(outputList)-1]
   106  			}
   107  			if err := generateSecretFromTerraformOutput(k8sClient, outputList, name, namespace); err != nil {
   108  				return nil, err
   109  			}
   110  		default:
   111  			nativeVelaComponents = append(nativeVelaComponents, app.Spec.Components[i])
   112  		}
   113  
   114  	}
   115  	return nativeVelaComponents, nil
   116  }
   117  
   118  func callTerraform(tfJSONDir string) ([]byte, error) {
   119  	if err := os.Chdir(tfJSONDir); err != nil {
   120  		return nil, err
   121  	}
   122  	var cmd *exec.Cmd
   123  	cmd = exec.Command("bash", "-c", "terraform init")
   124  	if err := common.RealtimePrintCommandOutput(cmd, TerraformLog); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	cmd = exec.Command("bash", "-c", "terraform apply --auto-approve")
   129  	if err := common.RealtimePrintCommandOutput(cmd, TerraformLog); err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	// Get output from Terraform
   134  	cmd = exec.Command("bash", "-c", "terraform output")
   135  	outputs, err := cmd.Output()
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	return outputs, nil
   140  }
   141  
   142  // generateSecretFromTerraformOutput generates secret from Terraform output
   143  func generateSecretFromTerraformOutput(k8sClient client.Client, outputList []string, name, namespace string) error {
   144  	ctx := context.TODO()
   145  	err := k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})
   146  	if err == nil {
   147  		return fmt.Errorf("namespace %s doesn't exist", namespace)
   148  	}
   149  	var cmData = make(map[string]string, len(outputList))
   150  	for _, i := range outputList {
   151  		line := strings.Split(i, "=")
   152  		if len(line) != 2 {
   153  			return fmt.Errorf("terraform output isn't in the right format")
   154  		}
   155  		k := strings.TrimSpace(line[0])
   156  		v := strings.TrimSpace(line[1])
   157  		if k != "" && v != "" {
   158  			cmData[k] = v
   159  		}
   160  	}
   161  
   162  	objectKey := client.ObjectKey{
   163  		Namespace: namespace,
   164  		Name:      name,
   165  	}
   166  	var secret v1.Secret
   167  	if err := k8sClient.Get(ctx, objectKey, &secret); err != nil && !errors.IsNotFound(err) {
   168  		return fmt.Errorf("retrieving the secret from cloud resource %s hit an issue: %w", name, err)
   169  	} else if err == nil {
   170  		if err := k8sClient.Delete(ctx, &secret); err != nil {
   171  			return fmt.Errorf("failed to store cloud resource %s output to secret: %w", name, err)
   172  		}
   173  	}
   174  
   175  	secret = v1.Secret{
   176  		TypeMeta: metav1.TypeMeta{
   177  			APIVersion: "v1",
   178  			Kind:       "Secret",
   179  		},
   180  		ObjectMeta: metav1.ObjectMeta{
   181  			Namespace: namespace,
   182  			Name:      name,
   183  		},
   184  		StringData: cmData,
   185  	}
   186  
   187  	if err := k8sClient.Create(ctx, &secret); err != nil {
   188  		return fmt.Errorf("failed to store cloud resource %s output to secret: %w", name, err)
   189  	}
   190  	return nil
   191  }
   192  
   193  // getTerraformJSONFiles gets Terraform JSON files or modules from workload
   194  func getTerraformJSONFiles(comp *appfile.Component, ctxData process.ContextData) ([]byte, error) {
   195  	pCtx, err := appfile.PrepareProcessContext(comp, ctxData)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	base, _ := pCtx.Output()
   200  	tf, err := base.Compile()
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	return tf, nil
   205  }