github.com/oam-dev/kubevela@v1.9.11/references/common/application.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 common 18 19 import ( 20 "bytes" 21 "context" 22 j "encoding/json" 23 "fmt" 24 25 "github.com/fatih/color" 26 terraformapi "github.com/oam-dev/terraform-controller/api/v1beta2" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime/serializer/json" 30 apitypes "k8s.io/apimachinery/pkg/types" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/yaml" 33 34 corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 35 "github.com/oam-dev/kubevela/apis/types" 36 "github.com/oam-dev/kubevela/pkg/oam" 37 "github.com/oam-dev/kubevela/pkg/utils" 38 "github.com/oam-dev/kubevela/pkg/utils/apply" 39 "github.com/oam-dev/kubevela/pkg/utils/common" 40 cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" 41 "github.com/oam-dev/kubevela/pkg/velaql/providers/query" 42 querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types" 43 "github.com/oam-dev/kubevela/references/appfile" 44 "github.com/oam-dev/kubevela/references/appfile/api" 45 "github.com/oam-dev/kubevela/references/appfile/template" 46 ) 47 48 // AppfileOptions is some configuration that modify options for an Appfile 49 type AppfileOptions struct { 50 Kubecli client.Client 51 IO cmdutil.IOStreams 52 Namespace string 53 Name string 54 } 55 56 // BuildResult is the export struct from AppFile yaml or AppFile object 57 type BuildResult struct { 58 appFile *api.AppFile 59 application *corev1beta1.Application 60 scopes []oam.Object 61 } 62 63 // PrepareToForceDeleteTerraformComponents sets Terraform typed Component to force-delete mode 64 func PrepareToForceDeleteTerraformComponents(ctx context.Context, k8sClient client.Client, namespace, name string) error { 65 var ( 66 app = new(corev1beta1.Application) 67 forceDelete = true 68 ) 69 err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, app) 70 if err != nil { 71 if apierrors.IsNotFound(err) { 72 return fmt.Errorf("app %s already deleted or not exist", name) 73 } 74 return fmt.Errorf("delete application err: %w", err) 75 } 76 for _, c := range app.Spec.Components { 77 var def corev1beta1.ComponentDefinition 78 if err := k8sClient.Get(ctx, client.ObjectKey{Name: c.Type, Namespace: types.DefaultKubeVelaNS}, &def); err != nil { 79 if !apierrors.IsNotFound(err) { 80 return err 81 } 82 if err := k8sClient.Get(ctx, client.ObjectKey{Name: c.Type, Namespace: namespace}, &def); err != nil { 83 return err 84 } 85 } 86 if def.Spec.Schematic != nil && def.Spec.Schematic.Terraform != nil { 87 var conf terraformapi.Configuration 88 if err := k8sClient.Get(ctx, client.ObjectKey{Name: c.Name, Namespace: namespace}, &conf); err != nil { 89 return err 90 } 91 conf.Spec.ForceDelete = &forceDelete 92 if err := k8sClient.Update(ctx, &conf); err != nil { 93 return err 94 } 95 } 96 } 97 return nil 98 } 99 100 // LoadAppFile will load vela appfile from remote URL or local file system. 101 func LoadAppFile(pathOrURL string) (*api.AppFile, error) { 102 body, err := utils.ReadRemoteOrLocalPath(pathOrURL, false) 103 if err != nil { 104 return nil, err 105 } 106 return api.LoadFromBytes(body) 107 } 108 109 // IsAppfile check if a file is Appfile format or application format, return true if it's appfile, false means application object 110 func IsAppfile(body []byte) bool { 111 if j.Valid(body) { 112 // we only support json format for appfile 113 return true 114 } 115 res := map[string]interface{}{} 116 err := yaml.Unmarshal(body, &res) 117 if err != nil { 118 return false 119 } 120 // appfile didn't have apiVersion 121 if _, ok := res["apiVersion"]; ok { 122 return false 123 } 124 return true 125 } 126 127 // ExportFromAppFile exports Application from appfile object 128 func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, namespace string, quiet bool, c common.Args) (*BuildResult, []byte, error) { 129 tm, err := template.Load(namespace, c) 130 if err != nil { 131 return nil, nil, err 132 } 133 134 appHandler := appfile.NewApplication(app, tm) 135 136 // new 137 retApplication, err := appHandler.ConvertToApplication(o.Namespace, o.IO, appHandler.Tm, quiet) 138 if err != nil { 139 return nil, nil, err 140 } 141 142 var w bytes.Buffer 143 144 options := json.SerializerOptions{Yaml: true, Pretty: false, Strict: false} 145 enc := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, options) 146 err = enc.Encode(retApplication, &w) 147 if err != nil { 148 return nil, nil, fmt.Errorf("yaml encode application failed: %w", err) 149 } 150 w.WriteByte('\n') 151 152 result := &BuildResult{ 153 appFile: app, 154 application: retApplication, 155 } 156 return result, w.Bytes(), nil 157 } 158 159 // Export export Application object from the path of Appfile 160 func (o *AppfileOptions) Export(filePath, namespace string, quiet bool, c common.Args) (*BuildResult, []byte, error) { 161 var app *api.AppFile 162 var err error 163 if !quiet { 164 o.IO.Info("Parsing vela application file ...") 165 } 166 if filePath != "" { 167 app, err = LoadAppFile(filePath) 168 } else { 169 app, err = api.Load() 170 } 171 if err != nil { 172 return nil, nil, err 173 } 174 175 if !quiet { 176 o.IO.Info("Load Template ...") 177 } 178 return o.ExportFromAppFile(app, namespace, quiet, c) 179 } 180 181 // Run starts an application according to Appfile 182 func (o *AppfileOptions) Run(filePath, namespace string, c common.Args) error { 183 result, _, err := o.Export(filePath, namespace, false, c) 184 if err != nil { 185 return err 186 } 187 return o.BaseAppFileRun(result, c) 188 } 189 190 // BaseAppFileRun starts an application according to Appfile 191 func (o *AppfileOptions) BaseAppFileRun(result *BuildResult, args common.Args) error { 192 193 kubernetesComponent, err := appfile.ApplyTerraform(result.application, o.Kubecli, o.IO, o.Namespace, args) 194 if err != nil { 195 return err 196 } 197 result.application.Spec.Components = kubernetesComponent 198 o.Name = result.application.Name 199 o.IO.Infof("\nApplying application ...\n") 200 return o.ApplyApp(result.application, result.scopes) 201 } 202 203 // ApplyApp applys config resources for the app. 204 // It differs by create and update: 205 // - for create, it displays app status along with information of url, metrics, ssh, logging. 206 // - for update, it rolls out a canary deployment and prints its information. User can verify the canary deployment. 207 // This will wait for user approval. If approved, it continues upgrading the whole; otherwise, it would rollback. 208 func (o *AppfileOptions) ApplyApp(app *corev1beta1.Application, scopes []oam.Object) error { 209 key := apitypes.NamespacedName{ 210 Namespace: app.Namespace, 211 Name: app.Name, 212 } 213 o.IO.Infof("Checking if app has been deployed...\n") 214 var tmpApp corev1beta1.Application 215 err := o.Kubecli.Get(context.TODO(), key, &tmpApp) 216 switch { 217 case apierrors.IsNotFound(err): 218 o.IO.Infof("App has not been deployed, creating a new deployment...\n") 219 case err == nil: 220 o.IO.Infof("App exists, updating existing deployment...\n") 221 default: 222 return err 223 } 224 if err := o.apply(app, scopes); err != nil { 225 return err 226 } 227 o.IO.Infof(Info(app)) 228 return nil 229 } 230 231 func (o *AppfileOptions) apply(app *corev1beta1.Application, scopes []oam.Object) error { 232 if err := appfile.Run(context.TODO(), o.Kubecli, app, scopes); err != nil { 233 return err 234 } 235 return nil 236 } 237 238 // Info shows the status of each service in the Appfile 239 func Info(app *corev1beta1.Application) string { 240 yellow := color.New(color.FgYellow) 241 appName := app.Name 242 if app.Namespace != "" && app.Namespace != "default" { 243 appName += " -n " + app.Namespace 244 } 245 var appUpMessage = "✅ App has been deployed 🚀🚀🚀\n" + 246 " Port forward: " + yellow.Sprintf("vela port-forward %s\n", appName) + 247 " SSH: " + yellow.Sprintf("vela exec %s\n", appName) + 248 " Logging: " + yellow.Sprintf("vela logs %s\n", appName) + 249 " App status: " + yellow.Sprintf("vela status %s\n", appName) + 250 " Endpoint: " + yellow.Sprintf("vela status %s --endpoint\n", appName) 251 return appUpMessage 252 } 253 254 // ApplyApplication will apply an application file in K8s GVK format 255 func ApplyApplication(app corev1beta1.Application, ioStream cmdutil.IOStreams, clt client.Client) error { 256 if app.Namespace == "" { 257 app.Namespace = types.DefaultAppNamespace 258 } 259 _, err := ioStream.Out.Write([]byte("Applying an application in vela K8s object format...\n")) 260 if err != nil { 261 return err 262 } 263 applicator := apply.NewAPIApplicator(clt) 264 err = applicator.Apply(context.Background(), &app) 265 if err != nil { 266 return err 267 } 268 ioStream.Infof(Info(&app)) 269 return nil 270 } 271 272 // CollectApplicationResource collects all resources of an application 273 func CollectApplicationResource(ctx context.Context, c client.Client, opt query.Option) ([]unstructured.Unstructured, error) { 274 app := new(corev1beta1.Application) 275 appKey := client.ObjectKey{Name: opt.Name, Namespace: opt.Namespace} 276 if err := c.Get(context.Background(), appKey, app); err != nil { 277 return nil, err 278 } 279 collector := query.NewAppCollector(c, opt) 280 appResList, err := collector.ListApplicationResources(context.Background(), app) 281 if err != nil { 282 return nil, err 283 } 284 var resources = make([]unstructured.Unstructured, 0) 285 for _, res := range appResList { 286 if res.ResourceTree != nil { 287 resources = append(resources, sonLeafResource(res.ResourceTree, opt.Filter.Kind, opt.Filter.APIVersion)...) 288 } 289 if (opt.Filter.Kind == "" && opt.Filter.APIVersion == "") || (res.Kind == opt.Filter.Kind && res.APIVersion == opt.Filter.APIVersion) { 290 var object unstructured.Unstructured 291 object.SetAPIVersion(opt.Filter.APIVersion) 292 object.SetKind(opt.Filter.Kind) 293 if err := c.Get(ctx, apitypes.NamespacedName{Namespace: res.Namespace, Name: res.Name}, &object); err == nil { 294 resources = append(resources, object) 295 } 296 } 297 } 298 return resources, nil 299 } 300 301 func sonLeafResource(node *querytypes.ResourceTreeNode, kind string, apiVersion string) []unstructured.Unstructured { 302 objects := make([]unstructured.Unstructured, 0) 303 if node.LeafNodes != nil { 304 for i := 0; i < len(node.LeafNodes); i++ { 305 objects = append(objects, sonLeafResource(node.LeafNodes[i], kind, apiVersion)...) 306 } 307 } 308 if (kind == "" && apiVersion == "") || (node.Kind == kind && node.APIVersion == apiVersion) { 309 objects = append(objects, node.Object) 310 } 311 return objects 312 }