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, ®istries[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 }