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  }