github.com/oam-dev/kubevela@v1.9.11/pkg/addon/utils.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 addon
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"reflect"
    25  	"strings"
    26  
    27  	"github.com/pkg/errors"
    28  	"helm.sh/helm/v3/pkg/chart"
    29  	"helm.sh/helm/v3/pkg/chartutil"
    30  	errors2 "k8s.io/apimachinery/pkg/api/errors"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/client-go/rest"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/yaml"
    36  
    37  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    38  	"github.com/oam-dev/kubevela/pkg/definition"
    39  	"github.com/oam-dev/kubevela/pkg/oam"
    40  	"github.com/oam-dev/kubevela/pkg/oam/util"
    41  	"github.com/oam-dev/kubevela/pkg/utils/addon"
    42  	"github.com/oam-dev/kubevela/pkg/utils/common"
    43  )
    44  
    45  const (
    46  	compDefAnnotation         = "addon.oam.dev/componentDefinitions"
    47  	traitDefAnnotation        = "addon.oam.dev/traitDefinitions"
    48  	workflowStepDefAnnotation = "addon.oam.dev/workflowStepDefinitions"
    49  	policyDefAnnotation       = "addon.oam.dev/policyDefinitions"
    50  	defKeytemplate            = "addon-%s-%s"
    51  	compMapKey                = "comp"
    52  	traitMapKey               = "trait"
    53  	wfStepMapKey              = "wfStep"
    54  	policyMapKey              = "policy"
    55  )
    56  
    57  // parse addon's created x-defs in addon-app's annotation, this will be used to check whether app still using it while disabling.
    58  func passDefInAppAnnotation(defs []*unstructured.Unstructured, app *v1beta1.Application) error {
    59  	var comps, traits, workflowSteps, policies []string
    60  	for _, def := range defs {
    61  		if !checkBondComponentExist(*def, *app) {
    62  			// if the definition binding a component, and the component not exist, skip recording.
    63  			continue
    64  		}
    65  		switch def.GetObjectKind().GroupVersionKind().Kind {
    66  		case v1beta1.ComponentDefinitionKind:
    67  			comps = append(comps, def.GetName())
    68  		case v1beta1.TraitDefinitionKind:
    69  			traits = append(traits, def.GetName())
    70  		case v1beta1.WorkflowStepDefinitionKind:
    71  			workflowSteps = append(workflowSteps, def.GetName())
    72  		case v1beta1.PolicyDefinitionKind:
    73  			policies = append(policies, def.GetName())
    74  		default:
    75  			return fmt.Errorf("cannot handle definition types %s, name %s", def.GetObjectKind().GroupVersionKind().Kind, def.GetName())
    76  		}
    77  	}
    78  	if len(comps) != 0 {
    79  		app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{compDefAnnotation: strings.Join(comps, ",")}))
    80  	}
    81  	if len(traits) != 0 {
    82  		app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{traitDefAnnotation: strings.Join(traits, ",")}))
    83  	}
    84  	if len(workflowSteps) != 0 {
    85  		app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{workflowStepDefAnnotation: strings.Join(workflowSteps, ",")}))
    86  	}
    87  	if len(policies) != 0 {
    88  		app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(), map[string]string{policyDefAnnotation: strings.Join(policies, ",")}))
    89  	}
    90  	return nil
    91  }
    92  
    93  // check whether this addon has been used by some applications
    94  func checkAddonHasBeenUsed(ctx context.Context, k8sClient client.Client, name string, addonApp v1beta1.Application, config *rest.Config) ([]v1beta1.Application, error) {
    95  	apps := v1beta1.ApplicationList{}
    96  	if err := k8sClient.List(ctx, &apps, client.InNamespace("")); err != nil {
    97  		return nil, err
    98  	}
    99  
   100  	if len(apps.Items) == 0 {
   101  		return nil, nil
   102  	}
   103  
   104  	createdDefs := make(map[string]bool)
   105  	for key, defNames := range addonApp.GetAnnotations() {
   106  		switch key {
   107  		case compDefAnnotation, traitDefAnnotation, workflowStepDefAnnotation, policyDefAnnotation:
   108  			merge2DefMap(key, defNames, createdDefs)
   109  		}
   110  	}
   111  
   112  	if len(createdDefs) == 0 {
   113  		if err := findLegacyAddonDefs(ctx, k8sClient, name, addonApp.GetLabels()[oam.LabelAddonRegistry], config, createdDefs); err != nil {
   114  			return nil, err
   115  		}
   116  	}
   117  
   118  	var res []v1beta1.Application
   119  CHECKNEXT:
   120  	for _, app := range apps.Items {
   121  		for _, component := range app.Spec.Components {
   122  			if createdDefs[fmt.Sprintf(defKeytemplate, compMapKey, component.Type)] {
   123  				res = append(res, app)
   124  				// this app has used this addon, there is no need check other components
   125  				continue CHECKNEXT
   126  			}
   127  			for _, trait := range component.Traits {
   128  				if createdDefs[fmt.Sprintf(defKeytemplate, traitMapKey, trait.Type)] {
   129  					res = append(res, app)
   130  					continue CHECKNEXT
   131  				}
   132  			}
   133  		}
   134  
   135  		if app.Spec.Workflow != nil && len(app.Spec.Workflow.Steps) != 0 {
   136  			for _, s := range app.Spec.Workflow.Steps {
   137  				if createdDefs[fmt.Sprintf(defKeytemplate, wfStepMapKey, s.Type)] {
   138  					res = append(res, app)
   139  					continue CHECKNEXT
   140  				}
   141  			}
   142  		}
   143  
   144  		if app.Spec.Policies != nil && len(app.Spec.Policies) != 0 {
   145  			for _, p := range app.Spec.Policies {
   146  				if createdDefs[fmt.Sprintf(defKeytemplate, policyMapKey, p.Type)] {
   147  					res = append(res, app)
   148  					continue CHECKNEXT
   149  				}
   150  			}
   151  		}
   152  	}
   153  	return res, nil
   154  }
   155  
   156  // merge2DefMap will parse annotation in addon's app to 'created x-definition'. Then stroe them in defMap
   157  func merge2DefMap(defType string, defNames string, defMap map[string]bool) {
   158  	list := strings.Split(defNames, ",")
   159  	template := "addon-%s-%s"
   160  	for _, defName := range list {
   161  		switch defType {
   162  		case compDefAnnotation:
   163  			defMap[fmt.Sprintf(template, compMapKey, defName)] = true
   164  		case traitDefAnnotation:
   165  			defMap[fmt.Sprintf(template, traitMapKey, defName)] = true
   166  		case workflowStepDefAnnotation:
   167  			defMap[fmt.Sprintf(template, wfStepMapKey, defName)] = true
   168  		case policyDefAnnotation:
   169  			defMap[fmt.Sprintf(template, policyMapKey, defName)] = true
   170  		}
   171  	}
   172  }
   173  
   174  // for old addon's app no 'created x-definitions' annotation, fetch the definitions from alive addon registry. Put them in defMap
   175  func findLegacyAddonDefs(ctx context.Context, k8sClient client.Client, addonName string, registryName string, config *rest.Config, defs map[string]bool) error {
   176  	// if the addon enable by local we cannot fetch the source definitions yet, so skip the check
   177  	if registryName == "local" {
   178  		return nil
   179  	}
   180  
   181  	registryDS := NewRegistryDataStore(k8sClient)
   182  	registries, err := registryDS.ListRegistries(ctx)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	var defObjects []*unstructured.Unstructured
   187  	for i, registry := range registries {
   188  		if registry.Name == registryName {
   189  			var uiData *UIData
   190  			if !IsVersionRegistry(registry) {
   191  				installer := NewAddonInstaller(ctx, k8sClient, nil, nil, config, &registries[i], nil, nil, nil)
   192  				metas, err := installer.getAddonMeta()
   193  				if err != nil {
   194  					return err
   195  				}
   196  				meta := metas[addonName]
   197  				// only fetch definition files from registry.
   198  				uiData, err = registry.GetUIData(&meta, UnInstallOptions)
   199  				if err != nil {
   200  					return errors.Wrapf(err, "cannot fetch addon difinition files from registry")
   201  				}
   202  			} else {
   203  				versionedRegistry := BuildVersionedRegistry(registry.Name, registry.Helm.URL, &common.HTTPOption{
   204  					Username:        registry.Helm.Username,
   205  					Password:        registry.Helm.Password,
   206  					InsecureSkipTLS: registry.Helm.InsecureSkipTLS,
   207  				})
   208  				uiData, err = versionedRegistry.GetAddonUIData(ctx, addonName, "")
   209  				if err != nil {
   210  					return errors.Wrapf(err, "cannot fetch addon difinition files from registry")
   211  				}
   212  			}
   213  
   214  			for _, defYaml := range uiData.Definitions {
   215  				def, err := renderObject(defYaml)
   216  				if err != nil {
   217  					// don't let one error defined definition block whole disable process
   218  					continue
   219  				}
   220  				defObjects = append(defObjects, def)
   221  			}
   222  			for _, cueDef := range uiData.CUEDefinitions {
   223  				def := definition.Definition{Unstructured: unstructured.Unstructured{}}
   224  				err := def.FromCUEString(cueDef.Data, config)
   225  				if err != nil {
   226  					// don't let one error defined cue definition block whole disable process
   227  					continue
   228  				}
   229  				defObjects = append(defObjects, &def.Unstructured)
   230  			}
   231  		}
   232  	}
   233  	for _, defObject := range defObjects {
   234  		switch defObject.GetObjectKind().GroupVersionKind().Kind {
   235  		case v1beta1.ComponentDefinitionKind:
   236  			defs[fmt.Sprintf(defKeytemplate, "comp", defObject.GetName())] = true
   237  		case v1beta1.TraitDefinitionKind:
   238  			defs[fmt.Sprintf(defKeytemplate, "trait", defObject.GetName())] = true
   239  		case v1beta1.WorkflowStepDefinitionKind:
   240  			defs[fmt.Sprintf(defKeytemplate, "wfStep", defObject.GetName())] = true
   241  		case v1beta1.PolicyDefinitionKind:
   242  
   243  		}
   244  	}
   245  	return nil
   246  }
   247  
   248  func appsDependsOnAddonErrInfo(apps []v1beta1.Application) string {
   249  	var appsNamespaceNameList []string
   250  	i := 0
   251  	for _, app := range apps {
   252  		appsNamespaceNameList = append(appsNamespaceNameList, app.Namespace+"/"+app.Name)
   253  		i++
   254  		if i > 2 && len(apps) > i {
   255  			appsNamespaceNameList = append(appsNamespaceNameList, fmt.Sprintf("and other %d more", len(apps)-i))
   256  			break
   257  		}
   258  	}
   259  	return fmt.Sprintf("this addon is being used by: %s applications. Please delete all of them before removing.", strings.Join(appsNamespaceNameList, ", "))
   260  }
   261  
   262  // IsLocalRegistry checks if the registry is local
   263  func IsLocalRegistry(r Registry) bool {
   264  	return r.Name == LocalAddonRegistryName
   265  }
   266  
   267  // IsVersionRegistry  check the repo source if support multi-version addon
   268  func IsVersionRegistry(r Registry) bool {
   269  	return r.Helm != nil
   270  }
   271  
   272  // InstallOption define additional option for installation
   273  type InstallOption func(installer *Installer)
   274  
   275  // SkipValidateVersion means skip validating system version
   276  func SkipValidateVersion(installer *Installer) {
   277  	installer.skipVersionValidate = true
   278  }
   279  
   280  // DryRunAddon means only generate yaml for addon instead of installing it
   281  func DryRunAddon(installer *Installer) {
   282  	installer.dryRun = true
   283  }
   284  
   285  // OverrideDefinitions means override definitions within this addon if some of them already exist
   286  func OverrideDefinitions(installer *Installer) {
   287  	installer.overrideDefs = true
   288  }
   289  
   290  // IsAddonDir validates an addon directory.
   291  // It checks required files like metadata.yaml and template.yaml
   292  func IsAddonDir(dirName string) (bool, error) {
   293  	if fi, err := os.Stat(dirName); err != nil {
   294  		return false, err
   295  	} else if !fi.IsDir() {
   296  		return false, errors.Errorf("%q is not a directory", dirName)
   297  	}
   298  
   299  	// Load metadata.yaml
   300  	metadataYaml := filepath.Join(dirName, MetadataFileName)
   301  	if _, err := os.Stat(metadataYaml); os.IsNotExist(err) {
   302  		return false, errors.Errorf("no %s exists in directory %q", MetadataFileName, dirName)
   303  	}
   304  	metadataYamlContent, err := os.ReadFile(filepath.Clean(metadataYaml))
   305  	if err != nil {
   306  		return false, errors.Errorf("cannot read %s in directory %q", MetadataFileName, dirName)
   307  	}
   308  
   309  	// Check metadata.yaml contents
   310  	metadataContent := new(Meta)
   311  	if err := yaml.Unmarshal(metadataYamlContent, &metadataContent); err != nil {
   312  		return false, err
   313  	}
   314  	if metadataContent == nil {
   315  		return false, errors.Errorf("metadata (%s) missing", MetadataFileName)
   316  	}
   317  	if metadataContent.Name == "" {
   318  		return false, errors.Errorf("addon name is empty")
   319  	}
   320  	if metadataContent.Version == "" {
   321  		return false, errors.Errorf("addon version is empty")
   322  	}
   323  
   324  	// Load template.yaml/cue
   325  	var errYAML error
   326  	var errCUE error
   327  	templateYAML := filepath.Join(dirName, TemplateFileName)
   328  	templateCUE := filepath.Join(dirName, AppTemplateCueFileName)
   329  	_, errYAML = os.Stat(templateYAML)
   330  	_, errCUE = os.Stat(templateCUE)
   331  	if os.IsNotExist(errYAML) && os.IsNotExist(errCUE) {
   332  		return false, fmt.Errorf("no %s or %s exists in directory %q", TemplateFileName, AppTemplateCueFileName, dirName)
   333  	}
   334  	if errYAML != nil && errCUE != nil {
   335  		return false, errors.Errorf("cannot stat %s or %s", TemplateFileName, AppTemplateCueFileName)
   336  	}
   337  
   338  	// template.cue have higher priority
   339  	if errCUE == nil {
   340  		templateContent, err := os.ReadFile(filepath.Clean(templateCUE))
   341  		if err != nil {
   342  			return false, fmt.Errorf("cannot read %s: %w", AppTemplateCueFileName, err)
   343  		}
   344  		// Just look for `output` field is enough.
   345  		// No need to load the whole addon package to render the Application.
   346  		if !strings.Contains(string(templateContent), renderOutputCuePath) {
   347  			return false, fmt.Errorf("no %s field in %s", renderOutputCuePath, AppTemplateCueFileName)
   348  		}
   349  		return true, nil
   350  	}
   351  
   352  	// then check template.yaml
   353  	templateYamlContent, err := os.ReadFile(filepath.Clean(templateYAML))
   354  	if err != nil {
   355  		return false, errors.Errorf("cannot read %s in directory %q", TemplateFileName, dirName)
   356  	}
   357  	// Check template.yaml contents
   358  	template := new(v1beta1.Application)
   359  	if err := yaml.Unmarshal(templateYamlContent, &template); err != nil {
   360  		return false, err
   361  	}
   362  	if template == nil {
   363  		return false, errors.Errorf("template (%s) missing", TemplateFileName)
   364  	}
   365  
   366  	return true, nil
   367  }
   368  
   369  // MakeChartCompatible makes an addon directory compatible with Helm Charts.
   370  // It essentially creates a Chart.yaml file in it (if it doesn't already have one).
   371  // If overwrite is true, a Chart.yaml will always be created.
   372  func MakeChartCompatible(addonDir string, overwrite bool) error {
   373  	// Check if it is an addon dir
   374  	isAddonDir, err := IsAddonDir(addonDir)
   375  	if !isAddonDir {
   376  		return fmt.Errorf("%s is not an addon dir: %w", addonDir, err)
   377  	}
   378  
   379  	// Check if the addon dir has valid Chart.yaml in it.
   380  	// No need to handle error here.
   381  	// If it doesn't contain a valid Chart.yaml (thus errors), we will create it later.
   382  	isChartDir, _ := chartutil.IsChartDir(addonDir)
   383  
   384  	// Only when it is already a Helm Chart, and we don't want to overwrite Chart.yaml,
   385  	// we do nothing.
   386  	if isChartDir && !overwrite {
   387  		return nil
   388  	}
   389  
   390  	// Creating Chart.yaml.
   391  	chartMeta, err := generateChartMetadata(addonDir)
   392  	if err != nil {
   393  		return err
   394  	}
   395  
   396  	err = chartutil.SaveChartfile(filepath.Join(addonDir, chartutil.ChartfileName), chartMeta)
   397  	if err != nil {
   398  		return err
   399  	}
   400  
   401  	return nil
   402  }
   403  
   404  // generateChartMetadata generates a Chart.yaml file (chart.Metadata) from an addon metadata file (metadata.yaml).
   405  // It is mostly used to package an addon into a Helm Chart.
   406  func generateChartMetadata(addonDirPath string) (*chart.Metadata, error) {
   407  	// Load addon metadata.yaml
   408  	meta := &Meta{}
   409  	metaData, err := os.ReadFile(filepath.Clean(filepath.Join(addonDirPath, MetadataFileName)))
   410  	if err != nil {
   411  		return nil, err
   412  	}
   413  
   414  	err = yaml.Unmarshal(metaData, meta)
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	// Generate Chart.yaml from metadata.yaml
   420  	chartMeta := &chart.Metadata{
   421  		Name:        meta.Name,
   422  		Description: meta.Description,
   423  		// Define Vela addon's type to be library in order to prevent installation of a common chart.
   424  		// Please refer to https://helm.sh/docs/topics/library_charts/
   425  		Type:       "library",
   426  		Version:    meta.Version,
   427  		AppVersion: meta.Version,
   428  		APIVersion: chart.APIVersionV2,
   429  		Icon:       meta.Icon,
   430  		Home:       meta.URL,
   431  		Keywords:   meta.Tags,
   432  	}
   433  	annotation := generateAnnotation(meta)
   434  	if len(annotation) != 0 {
   435  		chartMeta.Annotations = annotation
   436  	}
   437  	return chartMeta, nil
   438  }
   439  
   440  // generateAnnotation generate addon annotation info for chart.yaml, will recorded in index.yaml in helm repo
   441  func generateAnnotation(meta *Meta) map[string]string {
   442  	res := map[string]string{}
   443  	if meta.SystemRequirements != nil {
   444  		if len(meta.SystemRequirements.VelaVersion) != 0 {
   445  			res[velaSystemRequirement] = meta.SystemRequirements.VelaVersion
   446  		}
   447  		if len(meta.SystemRequirements.KubernetesVersion) != 0 {
   448  			res[kubernetesSystemRequirement] = meta.SystemRequirements.KubernetesVersion
   449  		}
   450  	}
   451  	res[addonSystemRequirement] = meta.Name
   452  	return res
   453  }
   454  
   455  func checkConflictDefs(ctx context.Context, k8sClient client.Client, defs []*unstructured.Unstructured, appName string) (map[string]string, error) {
   456  	res := map[string]string{}
   457  	for _, def := range defs {
   458  		checkDef := def.DeepCopy()
   459  		err := k8sClient.Get(ctx, client.ObjectKeyFromObject(checkDef), checkDef)
   460  		if err == nil {
   461  			owner := metav1.GetControllerOf(checkDef)
   462  			if owner == nil || owner.Kind != v1beta1.ApplicationKind {
   463  				res[checkDef.GetName()] = fmt.Sprintf("definition: %s already exist and not belong to any addon \n", checkDef.GetName())
   464  				continue
   465  			}
   466  			if owner.Name != appName {
   467  				// if addon not belong to an addon or addon name is another one, we should put them in result
   468  				res[checkDef.GetName()] = fmt.Sprintf("definition: %s in this addon already exist in %s \n", checkDef.GetName(), addon.AppName2Addon(appName))
   469  			}
   470  		}
   471  		if err != nil && !errors2.IsNotFound(err) {
   472  			return nil, errors.Wrapf(err, "check definition %s", checkDef.GetName())
   473  		}
   474  	}
   475  	return res, nil
   476  }
   477  
   478  func produceDefConflictError(conflictDefs map[string]string) error {
   479  	if len(conflictDefs) == 0 {
   480  		return nil
   481  	}
   482  	var errorInfo string
   483  	for _, s := range conflictDefs {
   484  		errorInfo += s
   485  	}
   486  	errorInfo += "if you want override them, please use argument '--override-definitions' to enable \n"
   487  	return errors.New(errorInfo)
   488  }
   489  
   490  // checkBondComponentExist will check the ready-to-apply object(def or auxiliary outputs) whether bind to a component
   491  // if the target component not exist, return false.
   492  func checkBondComponentExist(u unstructured.Unstructured, app v1beta1.Application) bool {
   493  	var comp string
   494  	var existKey bool
   495  	comp, existKey = u.GetAnnotations()[oam.AnnotationAddonDefinitionBondCompKey]
   496  	if !existKey {
   497  		// this is compatibility logic for deprecated annotation
   498  		comp, existKey = u.GetAnnotations()[oam.AnnotationIgnoreWithoutCompKey]
   499  		if !existKey {
   500  			// if an object(def or auxiliary outputs ) binding no components return true
   501  			return true
   502  		}
   503  	}
   504  	for _, component := range app.Spec.Components {
   505  		if component.Name == comp {
   506  			// the bond component exists, return true
   507  			return true
   508  		}
   509  	}
   510  	return false
   511  }
   512  
   513  func validateAddonPackage(addonPkg *InstallPackage) error {
   514  	if reflect.DeepEqual(addonPkg.Meta, Meta{}) {
   515  		return fmt.Errorf("the addon package doesn't have `metadata.yaml`")
   516  	}
   517  	if addonPkg.Name == "" {
   518  		return fmt.Errorf("`matadata.yaml` must define the name of addon")
   519  	}
   520  	if addonPkg.Version == "" {
   521  		return fmt.Errorf("`matadata.yaml` must define the version of addon")
   522  	}
   523  	return nil
   524  }
   525  
   526  // FilterDependencyRegistries will return all registries besides the target registry itself
   527  func FilterDependencyRegistries(i int, rs []Registry) []Registry {
   528  	if i >= len(rs) {
   529  		return rs
   530  	}
   531  	if i < 0 {
   532  		return rs
   533  	}
   534  	ret := make([]Registry, len(rs)-1)
   535  	copy(ret, rs[:i])
   536  	copy(ret[i:], rs[i+1:])
   537  	return ret
   538  }