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 }