github.com/oam-dev/kubevela@v1.9.11/pkg/addon/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 addon 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "net/url" 27 "os" 28 "path" 29 "path/filepath" 30 "sort" 31 "strings" 32 "sync" 33 "time" 34 35 "github.com/Masterminds/semver/v3" 36 "github.com/google/go-github/v32/github" 37 "github.com/imdario/mergo" 38 "github.com/kubevela/workflow/pkg/cue/model/value" 39 "github.com/pkg/errors" 40 "github.com/xanzy/go-gitlab" 41 "go.uber.org/multierr" 42 "golang.org/x/oauth2" 43 "helm.sh/helm/v3/pkg/chart/loader" 44 "helm.sh/helm/v3/pkg/chartutil" 45 appsv1 "k8s.io/api/apps/v1" 46 v1 "k8s.io/api/core/v1" 47 apierrors "k8s.io/apimachinery/pkg/api/errors" 48 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 49 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 50 "k8s.io/apimachinery/pkg/runtime" 51 k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 52 types2 "k8s.io/apimachinery/pkg/types" 53 "k8s.io/apimachinery/pkg/util/sets" 54 "k8s.io/client-go/discovery" 55 "k8s.io/client-go/rest" 56 "k8s.io/client-go/util/retry" 57 "k8s.io/klog/v2" 58 stringslices "k8s.io/utils/strings/slices" 59 "sigs.k8s.io/controller-runtime/pkg/client" 60 "sigs.k8s.io/yaml" 61 62 common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 63 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 64 "github.com/oam-dev/kubevela/apis/types" 65 "github.com/oam-dev/kubevela/pkg/config" 66 "github.com/oam-dev/kubevela/pkg/cue/script" 67 "github.com/oam-dev/kubevela/pkg/definition" 68 "github.com/oam-dev/kubevela/pkg/multicluster" 69 "github.com/oam-dev/kubevela/pkg/oam" 70 "github.com/oam-dev/kubevela/pkg/oam/util" 71 "github.com/oam-dev/kubevela/pkg/utils" 72 addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" 73 "github.com/oam-dev/kubevela/pkg/utils/apply" 74 "github.com/oam-dev/kubevela/pkg/utils/common" 75 "github.com/oam-dev/kubevela/pkg/velaql" 76 version2 "github.com/oam-dev/kubevela/version" 77 ) 78 79 const ( 80 // ReadmeFileName is the addon readme file name 81 ReadmeFileName string = "README.md" 82 83 // LegacyReadmeFileName is the addon readme lower case file name 84 LegacyReadmeFileName string = "readme.md" 85 86 // MetadataFileName is the addon meatadata.yaml file name 87 MetadataFileName string = "metadata.yaml" 88 89 // TemplateFileName is the addon template.yaml file name 90 TemplateFileName string = "template.yaml" 91 92 // AppTemplateCueFileName is the addon application template.cue file name 93 AppTemplateCueFileName string = "template.cue" 94 95 // NotesCUEFileName is the addon notes print to end users when installed 96 NotesCUEFileName string = "NOTES.cue" 97 98 // KeyWordNotes is the keyword in NOTES.cue which will render the notes message out. 99 KeyWordNotes string = "notes" 100 101 // GlobalParameterFileName is the addon global parameter.cue file name 102 GlobalParameterFileName string = "parameter.cue" 103 104 // ResourcesDirName is the addon resources/ dir name 105 ResourcesDirName string = "resources" 106 107 // DefinitionsDirName is the addon definitions/ dir name 108 DefinitionsDirName string = "definitions" 109 110 // ConfigTemplateDirName is the addon config-templates/ dir name 111 ConfigTemplateDirName string = "config-templates" 112 113 // DefSchemaName is the addon definition schemas dir name 114 DefSchemaName string = "schemas" 115 116 // ViewDirName is the addon views dir name 117 ViewDirName string = "views" 118 119 // AddonParameterDataKey is the key of parameter in addon args secrets 120 AddonParameterDataKey string = "addonParameterDataKey" 121 122 // DefaultGiteeURL is the addon repository of gitee api 123 DefaultGiteeURL string = "https://gitee.com/api/v5/" 124 125 // InstallerRuntimeOption inject install runtime info into addon options 126 InstallerRuntimeOption string = "installerRuntimeOption" 127 128 // CUEExtension with the expected extension for CUE files 129 CUEExtension = ".cue" 130 ) 131 132 // ParameterFileName is the addon resources/parameter.cue file name 133 var ParameterFileName = strings.Join([]string{"resources", "parameter.cue"}, "/") 134 135 // ListOptions contains flags mark what files should be read in an addon directory 136 type ListOptions struct { 137 GetDetail bool 138 GetDefinition bool 139 GetConfigTemplate bool 140 GetResource bool 141 GetParameter bool 142 GetTemplate bool 143 GetDefSchema bool 144 } 145 146 var ( 147 // UIMetaOptions get Addon metadata for UI display 148 UIMetaOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true, GetConfigTemplate: true} 149 150 // CLIMetaOptions get Addon metadata for CLI display 151 CLIMetaOptions = ListOptions{} 152 153 // UnInstallOptions used for addon uninstalling 154 UnInstallOptions = ListOptions{GetDefinition: true} 155 ) 156 157 const ( 158 // LocalAddonRegistryName is the addon-registry name for those installed by local dir 159 LocalAddonRegistryName = "local" 160 // ClusterLabelSelector define the key of topology cluster label selector 161 ClusterLabelSelector = "clusterLabelSelector" 162 ) 163 164 // Pattern indicates the addon framework file pattern, all files should match at least one of the pattern. 165 type Pattern struct { 166 IsDir bool 167 Value string 168 } 169 170 // Patterns is the file pattern that the addon should be in 171 var Patterns = []Pattern{ 172 // config-templates pattern 173 {IsDir: true, Value: ConfigTemplateDirName}, 174 // single file reader pattern 175 {Value: ReadmeFileName}, {Value: MetadataFileName}, {Value: TemplateFileName}, 176 // parameter in resource directory 177 {Value: ParameterFileName}, 178 // directory files 179 {IsDir: true, Value: ResourcesDirName}, {IsDir: true, Value: DefinitionsDirName}, {IsDir: true, Value: DefSchemaName}, {IsDir: true, Value: ViewDirName}, 180 // CUE app template, parameter and notes 181 {Value: AppTemplateCueFileName}, {Value: GlobalParameterFileName}, {Value: NotesCUEFileName}, 182 {Value: LegacyReadmeFileName}} 183 184 // GetPatternFromItem will check if the file path has a valid pattern, return empty string if it's invalid. 185 // AsyncReader is needed to calculate relative path 186 func GetPatternFromItem(it Item, r AsyncReader, rootPath string) string { 187 relativePath := r.RelativePath(it) 188 for _, p := range Patterns { 189 if strings.HasPrefix(relativePath, strings.Join([]string{rootPath, p.Value}, "/")) { 190 return p.Value 191 } 192 if strings.HasPrefix(relativePath, filepath.Join(rootPath, p.Value)) { 193 // for enable addon by load dir, compatible with linux or windows os 194 return p.Value 195 } 196 } 197 return "" 198 } 199 200 // ListAddonUIDataFromReader list addons from AsyncReader 201 func ListAddonUIDataFromReader(r AsyncReader, registryMeta map[string]SourceMeta, registryName string, opt ListOptions) ([]*UIData, error) { 202 var addons []*UIData 203 var err error 204 var wg sync.WaitGroup 205 var errs []error 206 errCh := make(chan error) 207 waitCh := make(chan struct{}) 208 209 var l sync.Mutex 210 for _, subItem := range registryMeta { 211 wg.Add(1) 212 go func(addonMeta SourceMeta) { 213 defer wg.Done() 214 addonRes, err := GetUIDataFromReader(r, &addonMeta, opt) 215 if err != nil { 216 errCh <- err 217 return 218 } 219 addonRes.RegistryName = registryName 220 l.Lock() 221 addons = append(addons, addonRes) 222 l.Unlock() 223 }(subItem) 224 } 225 // in another goroutine for wait group to finish 226 go func() { 227 wg.Wait() 228 close(waitCh) 229 }() 230 forLoop: 231 for { 232 select { 233 case <-waitCh: 234 break forLoop 235 case err = <-errCh: 236 errs = append(errs, err) 237 } 238 } 239 if len(errs) != 0 { 240 return addons, compactErrors("error(s) happen when reading from registry: ", errs) 241 } 242 return addons, nil 243 } 244 245 func compactErrors(message string, errs []error) error { 246 errForPrint := make([]string, 0) 247 for _, e := range errs { 248 errForPrint = append(errForPrint, e.Error()) 249 } 250 251 return errors.New(message + strings.Join(errForPrint, ",")) 252 253 } 254 255 // GetUIDataFromReader read ui metadata of addon from Reader, used to be displayed in UI 256 func GetUIDataFromReader(r AsyncReader, meta *SourceMeta, opt ListOptions) (*UIData, error) { 257 addonContentsReader := map[string]struct { 258 skip bool 259 read func(a *UIData, reader AsyncReader, readPath string) error 260 }{ 261 ReadmeFileName: {!opt.GetDetail, readReadme}, 262 LegacyReadmeFileName: {!opt.GetDetail, readReadme}, 263 MetadataFileName: {false, readMetadata}, 264 DefinitionsDirName: {!opt.GetDefinition, readDefFile}, 265 ConfigTemplateDirName: {!opt.GetConfigTemplate, readConfigTemplateFile}, 266 ParameterFileName: {!opt.GetParameter, readParamFile}, 267 GlobalParameterFileName: {!opt.GetParameter, readGlobalParamFile}, 268 } 269 ptItems := ClassifyItemByPattern(meta, r) 270 var addon = &UIData{} 271 for contentType, method := range addonContentsReader { 272 if method.skip { 273 continue 274 } 275 items := ptItems[contentType] 276 for _, it := range items { 277 err := method.read(addon, r, r.RelativePath(it)) 278 if err != nil { 279 return nil, fmt.Errorf("fail to read addon %s file %s: %w", meta.Name, r.RelativePath(it), err) 280 } 281 } 282 } 283 284 if opt.GetParameter && (len(addon.Parameters) != 0 || len(addon.GlobalParameters) != 0) { 285 if addon.GlobalParameters != "" { 286 if addon.Parameters != "" { 287 klog.Warning("both legacy parameter and global parameter are provided, but only global parameter will be used. Consider removing the legacy parameters.") 288 } 289 addon.Parameters = addon.GlobalParameters 290 } 291 err := genAddonAPISchema(addon) 292 if err != nil { 293 return nil, fmt.Errorf("fail to generate openAPIschema for addon %s : %w (parameter: %s)", meta.Name, err, addon.Parameters) 294 } 295 } 296 addon.AvailableVersions = []string{addon.Version} 297 return addon, nil 298 } 299 300 // GetInstallPackageFromReader get install package of addon from Reader, this is used to enable an addon 301 func GetInstallPackageFromReader(r AsyncReader, meta *SourceMeta, uiData *UIData) (*InstallPackage, error) { 302 addonContentsReader := map[string]func(a *InstallPackage, reader AsyncReader, readPath string) error{ 303 TemplateFileName: readTemplate, 304 ResourcesDirName: readResFile, 305 DefSchemaName: readDefSchemaFile, 306 ViewDirName: readViewFile, 307 AppTemplateCueFileName: readAppCueTemplate, 308 NotesCUEFileName: readNotesFile, 309 } 310 ptItems := ClassifyItemByPattern(meta, r) 311 312 // Read the installed data from UI metadata object to reduce network payload 313 var addon = &InstallPackage{ 314 Meta: uiData.Meta, 315 Definitions: uiData.Definitions, 316 CUEDefinitions: uiData.CUEDefinitions, 317 Parameters: uiData.Parameters, 318 ConfigTemplates: uiData.ConfigTemplates, 319 } 320 321 for contentType, method := range addonContentsReader { 322 items := ptItems[contentType] 323 for _, it := range items { 324 err := method(addon, r, r.RelativePath(it)) 325 if err != nil { 326 return nil, fmt.Errorf("fail to read addon %s file %s: %w", meta.Name, r.RelativePath(it), err) 327 } 328 } 329 } 330 331 return addon, nil 332 } 333 334 func readTemplate(a *InstallPackage, reader AsyncReader, readPath string) error { 335 data, err := reader.ReadFile(readPath) 336 if err != nil { 337 return err 338 } 339 dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) 340 a.AppTemplate = &v1beta1.Application{} 341 342 // try to check it's a valid app template 343 _, _, err = dec.Decode([]byte(data), nil, a.AppTemplate) 344 if err != nil { 345 return err 346 } 347 return nil 348 } 349 350 func readAppCueTemplate(a *InstallPackage, reader AsyncReader, readPath string) error { 351 data, err := reader.ReadFile(readPath) 352 if err != nil { 353 return err 354 } 355 a.AppCueTemplate = ElementFile{Data: data, Name: filepath.Base(readPath)} 356 return nil 357 } 358 359 // readParamFile read single resource/parameter.cue file 360 func readParamFile(a *UIData, reader AsyncReader, readPath string) error { 361 b, err := reader.ReadFile(readPath) 362 if err != nil { 363 return err 364 } 365 a.Parameters = b 366 return nil 367 } 368 369 // readNotesFile read single NOTES.cue file 370 func readNotesFile(a *InstallPackage, reader AsyncReader, readPath string) error { 371 data, err := reader.ReadFile(readPath) 372 if err != nil { 373 return err 374 } 375 a.Notes = ElementFile{Data: data, Name: filepath.Base(readPath)} 376 return nil 377 } 378 379 // readGlobalParamFile read global parameter file. 380 func readGlobalParamFile(a *UIData, reader AsyncReader, readPath string) error { 381 b, err := reader.ReadFile(readPath) 382 if err != nil { 383 return err 384 } 385 a.GlobalParameters = b 386 return nil 387 } 388 389 // readResFile read single resource file 390 func readResFile(a *InstallPackage, reader AsyncReader, readPath string) error { 391 filename := path.Base(readPath) 392 b, err := reader.ReadFile(readPath) 393 if err != nil { 394 return err 395 } 396 397 if filename == "parameter.cue" { 398 return nil 399 } 400 file := ElementFile{Data: b, Name: filepath.Base(readPath)} 401 switch filepath.Ext(filename) { 402 case CUEExtension: 403 a.CUETemplates = append(a.CUETemplates, file) 404 case ".yaml", ".yml": 405 a.YAMLTemplates = append(a.YAMLTemplates, file) 406 default: 407 // skip other file formats 408 } 409 return nil 410 } 411 412 // readDefSchemaFile read single file of definition schema 413 func readDefSchemaFile(a *InstallPackage, reader AsyncReader, readPath string) error { 414 b, err := reader.ReadFile(readPath) 415 if err != nil { 416 return err 417 } 418 a.DefSchemas = append(a.DefSchemas, ElementFile{Data: b, Name: filepath.Base(readPath)}) 419 return nil 420 } 421 422 // readDefFile read single definition file 423 func readDefFile(a *UIData, reader AsyncReader, readPath string) error { 424 b, err := reader.ReadFile(readPath) 425 if err != nil { 426 return err 427 } 428 filename := path.Base(readPath) 429 file := ElementFile{Data: b, Name: filepath.Base(readPath)} 430 switch filepath.Ext(filename) { 431 case CUEExtension: 432 a.CUEDefinitions = append(a.CUEDefinitions, file) 433 case ".yaml", ".yml": 434 a.Definitions = append(a.Definitions, file) 435 default: 436 // skip other file formats 437 } 438 return nil 439 } 440 441 // readConfigTemplateFile read single template file of the config 442 func readConfigTemplateFile(a *UIData, reader AsyncReader, readPath string) error { 443 b, err := reader.ReadFile(readPath) 444 if err != nil { 445 return err 446 } 447 filename := path.Base(readPath) 448 if filepath.Ext(filename) != CUEExtension { 449 return nil 450 } 451 file := ElementFile{Data: b, Name: filepath.Base(readPath)} 452 a.ConfigTemplates = append(a.ConfigTemplates, file) 453 return nil 454 } 455 456 // readViewFile read single view file 457 func readViewFile(a *InstallPackage, reader AsyncReader, readPath string) error { 458 b, err := reader.ReadFile(readPath) 459 if err != nil { 460 return err 461 } 462 filename := path.Base(readPath) 463 switch filepath.Ext(filename) { 464 case CUEExtension: 465 a.CUEViews = append(a.CUEViews, ElementFile{Data: b, Name: filepath.Base(readPath)}) 466 case ".yaml", ".yml": 467 a.YAMLViews = append(a.YAMLViews, ElementFile{Data: b, Name: filepath.Base(readPath)}) 468 default: 469 // skip other file formats 470 } 471 return nil 472 } 473 474 func readMetadata(a *UIData, reader AsyncReader, readPath string) error { 475 b, err := reader.ReadFile(readPath) 476 if err != nil { 477 return err 478 } 479 err = yaml.Unmarshal([]byte(b), &a.Meta) 480 if err != nil { 481 return err 482 } 483 return nil 484 } 485 486 func readReadme(a *UIData, reader AsyncReader, readPath string) error { 487 // the detail will contain readme.md or README.md, if the content already is filled, don't read another. 488 if len(a.Detail) != 0 { 489 return nil 490 } 491 content, err := reader.ReadFile(readPath) 492 if err != nil { 493 return err 494 } 495 a.Detail = content 496 return nil 497 } 498 499 func createGitHelper(content *utils.Content, token string) *gitHelper { 500 var ts oauth2.TokenSource 501 if token != "" { 502 ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 503 } 504 tc := oauth2.NewClient(context.Background(), ts) 505 tc.Timeout = time.Second * 20 506 cli := github.NewClient(tc) 507 return &gitHelper{ 508 Client: cli, 509 Meta: content, 510 } 511 } 512 513 func createGiteeHelper(content *utils.Content, token string) *giteeHelper { 514 var ts oauth2.TokenSource 515 if token != "" { 516 ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 517 } 518 tc := oauth2.NewClient(context.Background(), ts) 519 tc.Timeout = time.Second * 20 520 cli := NewGiteeClient(tc, nil) 521 return &giteeHelper{ 522 Client: cli, 523 Meta: content, 524 } 525 } 526 527 func createGitlabHelper(content *utils.Content, token string) (*gitlabHelper, error) { 528 newClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(content.GitlabContent.Host)) 529 530 return &gitlabHelper{ 531 Client: newClient, 532 Meta: content, 533 }, err 534 } 535 536 // readRepo will read relative path (relative to Meta.Path) 537 func (h *gitHelper) readRepo(relativePath string) (*github.RepositoryContent, []*github.RepositoryContent, error) { 538 file, items, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.GithubContent.Owner, h.Meta.GithubContent.Repo, path.Join(h.Meta.GithubContent.Path, relativePath), nil) 539 if err != nil { 540 return nil, nil, WrapErrRateLimit(err) 541 } 542 return file, items, nil 543 } 544 545 // readRepo will read relative path (relative to Meta.Path) 546 func (h *giteeHelper) readRepo(relativePath string) (*github.RepositoryContent, []*github.RepositoryContent, error) { 547 file, items, err := h.Client.GetGiteeContents(context.Background(), h.Meta.GiteeContent.Owner, h.Meta.GiteeContent.Repo, path.Join(h.Meta.GiteeContent.Path, relativePath), h.Meta.GiteeContent.Ref) 548 if err != nil { 549 return nil, nil, WrapErrRateLimit(err) 550 } 551 return file, items, nil 552 } 553 554 // GetGiteeContents can return either the metadata and content of a single file 555 func (c *Client) GetGiteeContents(ctx context.Context, owner, repo, path, ref string) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, err error) { 556 escapedPath := (&url.URL{Path: path}).String() 557 u := fmt.Sprintf(c.BaseURL.String()+"repos/%s/%s/contents/%s", owner, repo, escapedPath) 558 if ref != "" { 559 u = fmt.Sprintf(u+"?ref=%s", ref) 560 } 561 562 req, err := http.NewRequest("GET", u, nil) 563 if err != nil { 564 return nil, nil, err 565 } 566 response, err := c.Client.Do(req.WithContext(ctx)) 567 if err != nil { 568 return nil, nil, err 569 } 570 //nolint:errcheck 571 defer response.Body.Close() 572 body, err := io.ReadAll(response.Body) 573 if err != nil { 574 return nil, nil, err 575 } 576 return unmarshalToContent(body) 577 } 578 579 func unmarshalToContent(content []byte) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, err error) { 580 fileUnmarshalError := json.Unmarshal(content, &fileContent) 581 if fileUnmarshalError == nil { 582 return fileContent, nil, nil 583 } 584 directoryUnmarshalError := json.Unmarshal(content, &directoryContent) 585 if directoryUnmarshalError == nil { 586 return nil, directoryContent, nil 587 } 588 return nil, nil, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %w", fileUnmarshalError.Error(), directoryUnmarshalError) 589 } 590 591 func genAddonAPISchema(addonRes *UIData) error { 592 cueScript := script.CUE(addonRes.Parameters) 593 schema, err := cueScript.ParsePropertiesToSchema() 594 if err != nil { 595 return err 596 } 597 addonRes.APISchema = schema 598 return nil 599 } 600 601 func getClusters(args map[string]interface{}) []string { 602 ccr, ok := args[types.ClustersArg] 603 if !ok { 604 return nil 605 } 606 cc, ok := ccr.([]string) 607 if ok { 608 return cc 609 } 610 ccrslice, ok := ccr.([]interface{}) 611 if !ok { 612 return nil 613 } 614 var ccstring []string 615 for _, c := range ccrslice { 616 if cstring, ok := c.(string); ok { 617 ccstring = append(ccstring, cstring) 618 } 619 } 620 return ccstring 621 } 622 623 // renderNeededNamespaceAsComps will convert namespace as app components to create namespace for managed clusters 624 func renderNeededNamespaceAsComps(addon *InstallPackage) []common2.ApplicationComponent { 625 var nscomps []common2.ApplicationComponent 626 // create namespace for managed clusters 627 for _, namespace := range addon.NeedNamespace { 628 // vela-system must exist before rendering vela addon 629 if namespace == types.DefaultKubeVelaNS { 630 continue 631 } 632 comp := common2.ApplicationComponent{ 633 Type: "raw", 634 Name: fmt.Sprintf("%s-namespace", namespace), 635 Properties: util.Object2RawExtension(renderNamespace(namespace)), 636 } 637 nscomps = append(nscomps, comp) 638 } 639 return nscomps 640 } 641 642 func checkDeployClusters(ctx context.Context, k8sClient client.Client, args map[string]interface{}) ([]string, error) { 643 deployClusters := getClusters(args) 644 if len(deployClusters) == 0 || k8sClient == nil { 645 return nil, nil 646 } 647 648 clusters, err := multicluster.NewClusterClient(k8sClient).List(ctx) 649 if err != nil { 650 return nil, errors.Wrap(err, "fail to get registered cluster") 651 } 652 653 clusterNames := sets.Set[string]{} 654 if len(clusters.Items) != 0 { 655 for _, cluster := range clusters.Items { 656 clusterNames.Insert(cluster.Name) 657 } 658 } 659 660 var res []string 661 for _, c := range deployClusters { 662 c = strings.TrimSpace(c) 663 if c == "" { 664 continue 665 } 666 if !clusterNames.Has(c) { 667 return nil, errors.Errorf("cluster %s not exist", c) 668 } 669 res = append(res, c) 670 } 671 return res, nil 672 } 673 674 // RenderDefinitions render definition objects if needed 675 func RenderDefinitions(addon *InstallPackage, config *rest.Config) ([]*unstructured.Unstructured, error) { 676 defObjs := make([]*unstructured.Unstructured, 0) 677 678 // No matter runtime mode or control mode, definition only needs to control plane k8s. 679 for _, def := range addon.Definitions { 680 obj, err := renderObject(def) 681 if err != nil { 682 return nil, errors.Wrapf(err, "render definition file %s", def.Name) 683 } 684 // we should ignore the namespace defined in definition yaml, override the filed by DefaultKubeVelaNS 685 obj.SetNamespace(types.DefaultKubeVelaNS) 686 defObjs = append(defObjs, obj) 687 } 688 for _, cueDef := range addon.CUEDefinitions { 689 def := definition.Definition{Unstructured: unstructured.Unstructured{}} 690 err := def.FromCUEString(cueDef.Data, config) 691 if err != nil { 692 return nil, errors.Wrapf(err, "fail to render definition: %s in cue's format", cueDef.Name) 693 } 694 // we should ignore the namespace defined in definition yaml, override the filed by DefaultKubeVelaNS 695 def.SetNamespace(types.DefaultKubeVelaNS) 696 defObjs = append(defObjs, &def.Unstructured) 697 } 698 699 return defObjs, nil 700 } 701 702 // RenderConfigTemplates render the config template 703 func RenderConfigTemplates(addon *InstallPackage, cli client.Client) ([]*unstructured.Unstructured, error) { 704 templates := make([]*unstructured.Unstructured, 0) 705 706 factory := config.NewConfigFactory(cli) 707 for _, templateFile := range addon.ConfigTemplates { 708 t, err := factory.ParseTemplate("", []byte(templateFile.Data)) 709 if err != nil { 710 return nil, err 711 } 712 t.ConfigMap.Namespace = types.DefaultKubeVelaNS 713 obj, err := util.Object2Unstructured(t.ConfigMap) 714 if err != nil { 715 return nil, err 716 } 717 obj.SetKind("ConfigMap") 718 obj.SetAPIVersion("v1") 719 templates = append(templates, obj) 720 } 721 722 return templates, nil 723 } 724 725 // RenderDefinitionSchema will render definitions' schema in addons. 726 func RenderDefinitionSchema(addon *InstallPackage) ([]*unstructured.Unstructured, error) { 727 schemaConfigmaps := make([]*unstructured.Unstructured, 0) 728 729 // No matter runtime mode or control mode , definition schemas only needs to control plane k8s. 730 for _, teml := range addon.DefSchemas { 731 u, err := renderSchemaConfigmap(teml) 732 if err != nil { 733 return nil, errors.Wrapf(err, "render uiSchema file %s", teml.Name) 734 } 735 schemaConfigmaps = append(schemaConfigmaps, u) 736 } 737 return schemaConfigmaps, nil 738 } 739 740 // RenderViews will render views in addons. 741 func RenderViews(addon *InstallPackage) ([]*unstructured.Unstructured, error) { 742 views := make([]*unstructured.Unstructured, 0) 743 for _, view := range addon.YAMLViews { 744 obj, err := renderObject(view) 745 if err != nil { 746 return nil, errors.Wrapf(err, "render velaQL view file %s", view.Name) 747 } 748 views = append(views, obj) 749 } 750 for _, view := range addon.CUEViews { 751 obj, err := renderCUEView(view) 752 if err != nil { 753 return nil, errors.Wrapf(err, "render velaQL view file %s", view.Name) 754 } 755 views = append(views, obj) 756 } 757 return views, nil 758 } 759 760 func renderObject(elem ElementFile) (*unstructured.Unstructured, error) { 761 obj := &unstructured.Unstructured{} 762 dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) 763 _, _, err := dec.Decode([]byte(elem.Data), nil, obj) 764 if err != nil { 765 return nil, err 766 } 767 return obj, nil 768 } 769 770 func renderNamespace(namespace string) *unstructured.Unstructured { 771 u := &unstructured.Unstructured{} 772 u.SetAPIVersion("v1") 773 u.SetKind("Namespace") 774 u.SetName(namespace) 775 return u 776 } 777 778 func renderK8sObjectsComponent(elems []ElementFile, addonName string) (*common2.ApplicationComponent, error) { 779 var objects []*unstructured.Unstructured 780 for _, elem := range elems { 781 obj, err := renderObject(elem) 782 if err != nil { 783 return nil, errors.Wrapf(err, "render resource file %s", elem.Name) 784 } 785 objects = append(objects, obj) 786 } 787 properties := map[string]interface{}{"objects": objects} 788 propJSON, err := json.Marshal(properties) 789 if err != nil { 790 return nil, err 791 } 792 baseRawComponent := common2.ApplicationComponent{ 793 Type: "k8s-objects", 794 Name: addonName + "-resources", 795 Properties: &runtime.RawExtension{Raw: propJSON}, 796 } 797 return &baseRawComponent, nil 798 } 799 800 func renderSchemaConfigmap(elem ElementFile) (*unstructured.Unstructured, error) { 801 jsonData, err := yaml.YAMLToJSON([]byte(elem.Data)) 802 if err != nil { 803 return nil, err 804 } 805 cm := v1.ConfigMap{ 806 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}, 807 ObjectMeta: metav1.ObjectMeta{Namespace: types.DefaultKubeVelaNS, Name: strings.Split(elem.Name, ".")[0]}, 808 Data: map[string]string{ 809 types.UISchema: string(jsonData), 810 }} 811 return util.Object2Unstructured(cm) 812 } 813 814 func renderCUEView(elem ElementFile) (*unstructured.Unstructured, error) { 815 name, err := utils.GetFilenameFromLocalOrRemote(elem.Name) 816 if err != nil { 817 return nil, err 818 } 819 820 cm, err := velaql.ParseViewIntoConfigMap(elem.Data, name) 821 if err != nil { 822 return nil, err 823 } 824 825 return util.Object2Unstructured(*cm) 826 } 827 828 // RenderArgsSecret render addon enable argument to secret to remember when restart or upgrade 829 func RenderArgsSecret(addon *InstallPackage, args map[string]interface{}) *unstructured.Unstructured { 830 argsByte, err := json.Marshal(args) 831 if err != nil { 832 return nil 833 } 834 sec := v1.Secret{ 835 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, 836 ObjectMeta: metav1.ObjectMeta{ 837 Name: addonutil.Addon2SecName(addon.Name), 838 Namespace: types.DefaultKubeVelaNS, 839 }, 840 Data: map[string][]byte{ 841 AddonParameterDataKey: argsByte, 842 }, 843 Type: v1.SecretTypeOpaque, 844 } 845 u, err := util.Object2Unstructured(sec) 846 if err != nil { 847 return nil 848 } 849 return u 850 } 851 852 // deleteArgsSecret delete the addon's args secret file 853 func deleteArgsSecret(ctx context.Context, k8sClient client.Client, addonName string) error { 854 var sec v1.Secret 855 if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2SecName(addonName)}, &sec); err == nil { 856 // Handle successful get operation 857 if deleteErr := k8sClient.Delete(ctx, &sec); deleteErr != nil { 858 return deleteErr 859 } 860 return nil 861 } else if !apierrors.IsNotFound(err) { 862 return err 863 } 864 return nil 865 } 866 867 // FetchArgsFromSecret fetch addon args from secrets 868 func FetchArgsFromSecret(sec *v1.Secret) (map[string]interface{}, error) { 869 res := map[string]interface{}{} 870 if args, ok := sec.Data[AddonParameterDataKey]; ok { 871 err := json.Unmarshal(args, &res) 872 if err != nil { 873 return nil, err 874 } 875 return res, nil 876 } 877 878 // this is backward compatibility code for old way to storage parameter 879 res = make(map[string]interface{}, len(sec.Data)) 880 for k, v := range sec.Data { 881 res[k] = string(v) 882 } 883 return res, nil 884 } 885 886 // Installer helps addon enable, dependency-check, dispatch resources 887 type Installer struct { 888 ctx context.Context 889 config *rest.Config 890 addon *InstallPackage 891 cli client.Client 892 apply apply.Applicator 893 r *Registry 894 registryMeta map[string]SourceMeta 895 args map[string]interface{} 896 cache *Cache 897 dc *discovery.DiscoveryClient 898 skipVersionValidate bool 899 overrideDefs bool 900 901 dryRun bool 902 dryRunBuff *bytes.Buffer 903 904 installerRuntime map[string]interface{} 905 906 registries []Registry 907 } 908 909 // NewAddonInstaller will create an installer for addon 910 func NewAddonInstaller(ctx context.Context, cli client.Client, discoveryClient *discovery.DiscoveryClient, apply apply.Applicator, config *rest.Config, r *Registry, args map[string]interface{}, cache *Cache, registries []Registry, opts ...InstallOption) Installer { 911 if args == nil { 912 args = map[string]interface{}{} 913 } 914 i := Installer{ 915 ctx: ctx, 916 config: config, 917 cli: cli, 918 apply: apply, 919 r: r, 920 args: args, 921 cache: cache, 922 dc: discoveryClient, 923 dryRunBuff: &bytes.Buffer{}, 924 registries: registries, 925 } 926 ir := args[InstallerRuntimeOption] 927 if irr, ok := ir.(map[string]interface{}); ok { 928 i.installerRuntime = irr 929 } else { 930 i.installerRuntime = map[string]interface{}{} 931 } 932 // clean injected data from runtime option 933 delete(args, InstallerRuntimeOption) 934 935 for _, opt := range opts { 936 opt(&i) 937 } 938 return i 939 } 940 941 func (h *Installer) enableAddon(addon *InstallPackage) (string, error) { 942 var err error 943 h.addon = addon 944 if !h.skipVersionValidate { 945 err = checkAddonVersionMeetRequired(h.ctx, addon.SystemRequirements, h.cli, h.dc) 946 if err != nil { 947 version := h.getAddonVersionMeetSystemRequirement(addon.Name) 948 return "", VersionUnMatchError{addonName: addon.Name, err: err, userSelectedAddonVersion: addon.Version, availableVersion: version} 949 } 950 } 951 952 if err = h.installDependency(addon); err != nil { 953 return "", err 954 } 955 if err = h.dispatchAddonResource(addon); err != nil { 956 return "", err 957 } 958 // we shouldn't put continue func into dispatchAddonResource, because the re-apply app maybe already update app and 959 // the suspend will set with false automatically 960 if err := h.continueOrRestartWorkflow(); err != nil { 961 return "", err 962 } 963 additionalInfo, err := h.renderNotes(addon) 964 if err != nil { 965 klog.Warningf("fail to render notes for addon %s: %v\n", addon.Name, err) 966 // notes don't affect the installation, so just print warn logs instead of abort with errors 967 return "", nil 968 } 969 return additionalInfo, nil 970 } 971 972 func (h *Installer) loadInstallPackage(name, version string) (*InstallPackage, error) { 973 var installPackage *InstallPackage 974 var err error 975 if !IsVersionRegistry(*h.r) { 976 metas, err := h.getAddonMeta() 977 if err != nil { 978 return nil, errors.Wrap(err, "fail to get addon meta") 979 } 980 981 meta, ok := metas[name] 982 if !ok { 983 return nil, ErrNotExist 984 } 985 var uiData *UIData 986 uiData, err = h.cache.GetUIData(*h.r, name, version) 987 if err != nil { 988 return nil, err 989 } 990 // enable this addon if it's invisible 991 installPackage, err = h.r.GetInstallPackage(&meta, uiData) 992 if err != nil { 993 return nil, errors.Wrap(err, "fail to find dependent addon in source repository") 994 } 995 } else { 996 versionedRegistry := BuildVersionedRegistry(h.r.Name, h.r.Helm.URL, &common.HTTPOption{ 997 Username: h.r.Helm.Username, 998 Password: h.r.Helm.Password, 999 InsecureSkipTLS: h.r.Helm.InsecureSkipTLS, 1000 }) 1001 installPackage, err = versionedRegistry.GetAddonInstallPackage(context.Background(), name, version) 1002 if err != nil { 1003 return nil, err 1004 } 1005 } 1006 1007 return installPackage, nil 1008 } 1009 1010 func (h *Installer) getAddonMeta() (map[string]SourceMeta, error) { 1011 var err error 1012 if h.registryMeta == nil { 1013 if h.registryMeta, err = h.cache.ListAddonMeta(*h.r); err != nil { 1014 return nil, err 1015 } 1016 } 1017 return h.registryMeta, nil 1018 } 1019 1020 // installDependency checks if addon's dependency and install it 1021 func (h *Installer) installDependency(addon *InstallPackage) error { 1022 installedAddons, err := listInstalledAddons(h.ctx, h.cli) 1023 if err != nil { 1024 return err 1025 } 1026 1027 var registries []ItemInfoLister 1028 registries = append(registries, h.r) 1029 for _, registry := range h.registries { 1030 r := registry 1031 registries = append(registries, &r) 1032 } 1033 availableAddons, err := listAvailableAddons(registries) 1034 if err != nil { 1035 return err 1036 } 1037 1038 err = validateAddonDependencies(addon, installedAddons, availableAddons) 1039 if err != nil { 1040 return err 1041 } 1042 1043 var dependencies []string 1044 var addonClusters = getClusters(h.args) 1045 for _, dep := range addon.Dependencies { 1046 needInstallAddonDep, err := checkDependencyNeedInstall(h.ctx, h.cli, dep.Name, addonClusters) 1047 if err != nil { 1048 return err 1049 } 1050 if !needInstallAddonDep { 1051 continue 1052 } 1053 1054 dependencies = append(dependencies, dep.Name) 1055 if h.dryRun { 1056 continue 1057 } 1058 depHandler := *h 1059 // reset dependency addon clusters parameter 1060 depArgs, depArgsErr := getDependencyArgs(h.ctx, h.cli, dep.Name, addonClusters) 1061 if depArgsErr != nil { 1062 return depArgsErr 1063 } 1064 1065 depHandler.args = depArgs 1066 1067 var depAddon *InstallPackage 1068 depVersion, err := calculateDependencyVersionToInstall(*dep, installedAddons, availableAddons) 1069 if err != nil { 1070 return err 1071 } 1072 // try to install the dependent addon from the same registry with the current addon 1073 depAddon, err = h.loadInstallPackage(dep.Name, depVersion) 1074 if err == nil { 1075 additionalInfo, err := depHandler.enableAddon(depAddon) 1076 if err != nil { 1077 return errors.Wrap(err, "fail to dispatch dependent addon resource") 1078 } 1079 if len(additionalInfo) > 0 { 1080 klog.Infof("addon %s installed with additional info: %s\n", addon.Name, additionalInfo) 1081 } 1082 return nil 1083 } 1084 if !errors.Is(err, ErrNotExist) { 1085 return err 1086 } 1087 for _, registry := range h.registries { 1088 // try to install dependent addon from other registries 1089 depHandler.r = &Registry{ 1090 Name: registry.Name, Helm: registry.Helm, OSS: registry.OSS, Git: registry.Git, Gitee: registry.Gitee, Gitlab: registry.Gitlab, 1091 } 1092 depAddon, err = depHandler.loadInstallPackage(dep.Name, depVersion) 1093 if err == nil { 1094 break 1095 } 1096 if errors.Is(err, ErrNotExist) { 1097 continue 1098 } 1099 return err 1100 } 1101 if err == nil { 1102 additionalInfo, err := depHandler.enableAddon(depAddon) 1103 if err != nil { 1104 return errors.Wrap(err, "fail to dispatch dependent addon resource") 1105 } 1106 if len(additionalInfo) > 0 { 1107 klog.Infof("addon %s installed with additional info: %s\n", addon.Name, additionalInfo) 1108 } 1109 return nil 1110 } 1111 return fmt.Errorf("dependency addon: %s with version: %s cannot be found from all registries", dep.Name, depVersion) 1112 } 1113 if h.dryRun && len(dependencies) > 0 { 1114 klog.Warningf("dry run addon won't install dependencies, please make sure your system has already installed these addons: %v", strings.Join(dependencies, ", ")) 1115 return nil 1116 } 1117 return nil 1118 } 1119 1120 // validateAddonDependencies checks if addon's dependencies can be satisfied. 1121 // If dependency is installed, check if the version matches required version. 1122 // If dependency is not installed, check available addons for dependency 1123 // matching the required version. 1124 // Return error if any dependency cannot be satisfied. 1125 func validateAddonDependencies(addon *InstallPackage, installedAddons itemInfoMap, availableAddons itemInfoMap) error { 1126 var merr error 1127 for _, dep := range addon.Dependencies { 1128 _, err := calculateDependencyVersionToInstall(*dep, installedAddons, availableAddons) 1129 if err != nil { 1130 merr = multierr.Append(merr, fmt.Errorf("addon %s has unresolvable dependency %s: %w", addon.Name, dep.Name, err)) 1131 } 1132 } 1133 return merr 1134 } 1135 1136 // calculateDependencyVersionToInstall compares an addon's dependency to a list 1137 // of installed and available addons and returns a version to install. 1138 // If dependency is installed, return the installed version if it matches the 1139 // required version. 1140 // If dependency is not installed, return the latest available version that 1141 // satisfies the dependency version. 1142 // Return error if dependency version cannot be satisfied. 1143 func calculateDependencyVersionToInstall(dependency Dependency, installedAddons itemInfoMap, availableAddons itemInfoMap) (string, error) { 1144 if dependency.Name == "" { 1145 return "", fmt.Errorf("dependency name cannot be empty") 1146 } 1147 1148 // if dependency is installed, return the installed version if it matches 1149 // the required version 1150 if installedAddons != nil { 1151 installedAddon, ok := installedAddons[dependency.Name] 1152 if ok { 1153 // versions length must be 1 1154 if len(installedAddon.AvailableVersions) != 1 { 1155 return "", errors.New("installedAddon.Versions length must be 1") 1156 } 1157 installedVersion := installedAddon.AvailableVersions[0] 1158 1159 if dependency.Version == "" { 1160 return installedVersion, nil 1161 } 1162 1163 match, _ := checkSemVer(installedVersion, dependency.Version) 1164 if match { 1165 return installedVersion, nil 1166 } 1167 1168 return "", fmt.Errorf("addon %s version '%s' does not match installed version '%s'", 1169 dependency.Name, dependency.Version, installedVersion) 1170 } 1171 } 1172 1173 availableAddon, ok := availableAddons[dependency.Name] 1174 if !ok { 1175 return "", fmt.Errorf("no available addon with name %s", dependency.Name) 1176 } 1177 1178 sortedVersions := sortVersionsDescending(availableAddon.AvailableVersions) 1179 1180 // if no version is specified, return the latest version 1181 if dependency.Version == "" { 1182 return sortedVersions[0], nil 1183 } 1184 1185 // check if the dependency version is satisfied 1186 var match bool 1187 for _, version := range sortedVersions { 1188 match, _ = checkSemVer(version, dependency.Version) 1189 if match { 1190 return version, nil 1191 } 1192 } 1193 1194 // no available version satisfies the dependency version 1195 return "", fmt.Errorf("no available addon with name %s and version '%s', available versions %s", 1196 dependency.Name, dependency.Version, availableAddon.AvailableVersions) 1197 } 1198 1199 func sortVersionsDescending(versions []string) []string { 1200 var sortedVersions []*semver.Version 1201 var sortedVersionStrings []string 1202 for _, v := range versions { 1203 var err error 1204 // Note: NewVersion attempts to convert SemVer-ish formats into SemVer 1205 parsedVersion, err := semver.NewVersion(v) 1206 if err == nil { 1207 sortedVersions = append(sortedVersions, parsedVersion) 1208 } 1209 } 1210 // sort versions in descending order 1211 sort.Sort(sort.Reverse(semver.Collection(sortedVersions))) 1212 for _, v := range sortedVersions { 1213 sortedVersionStrings = append(sortedVersionStrings, v.String()) 1214 } 1215 return sortedVersionStrings 1216 } 1217 1218 // ItemInfoLister is an interface for Registry.ListAddonInfo() to enable easier 1219 // testing with mocks. 1220 type ItemInfoLister interface { 1221 ListAddonInfo() (map[string]ItemInfo, error) 1222 } 1223 1224 // listAvailableAddons fetches a collection of addons available in a list of 1225 // registries. Returns a map of ItemInfo grouped by addon name. 1226 func listAvailableAddons(registries []ItemInfoLister) (itemInfoMap, error) { 1227 availableAddons := make(itemInfoMap) 1228 1229 for _, registry := range registries { 1230 addons, err := registry.ListAddonInfo() 1231 if err != nil { 1232 return nil, err 1233 } 1234 availableAddons = mergeAddonInfoMaps(availableAddons, addons) 1235 } 1236 return availableAddons, nil 1237 } 1238 1239 func mergeAddonInfoMaps(existingAddons itemInfoMap, newAddons itemInfoMap) itemInfoMap { 1240 mergedAddons := existingAddons 1241 for _, newAddon := range newAddons { 1242 if existingAddon, ok := existingAddons[newAddon.Name]; ok { 1243 // merge addon versions 1244 existingVersions := existingAddon.AvailableVersions 1245 newVersions := newAddon.AvailableVersions 1246 1247 mergedVersionsSet := make(map[string]bool) 1248 1249 for _, item := range existingVersions { 1250 mergedVersionsSet[item] = true 1251 } 1252 for _, item := range newVersions { 1253 mergedVersionsSet[item] = true 1254 } 1255 1256 mergedVersions := make([]string, 0, len(mergedVersionsSet)) 1257 for item := range mergedVersionsSet { 1258 mergedVersions = append(mergedVersions, item) 1259 } 1260 1261 mergedVersions = sortVersionsDescending(mergedVersions) 1262 1263 existingAddon.AvailableVersions = mergedVersions 1264 mergedAddons[existingAddon.Name] = existingAddon 1265 } else { 1266 mergedAddons[newAddon.Name] = newAddon 1267 } 1268 } 1269 return mergedAddons 1270 } 1271 1272 // listInstalledAddons fetches a collection of addons installed in the cluster. 1273 // Returns a map of ItemInfo grouped by addon name. 1274 func listInstalledAddons(ctx context.Context, k8sClient client.Client) (itemInfoMap, error) { 1275 installedAddons := make(itemInfoMap) 1276 // get all addons from cluster 1277 // for each addon, get the version and add it to addonVersions 1278 appList := &v1beta1.ApplicationList{} 1279 err := k8sClient.List(ctx, appList, client.InNamespace(types.DefaultKubeVelaNS), client.HasLabels{oam.LabelAddonName, oam.LabelAddonVersion}) 1280 if err != nil { 1281 return nil, err 1282 } 1283 for _, app := range appList.Items { 1284 addonName := app.Labels[oam.LabelAddonName] 1285 addonVersion := app.Labels[oam.LabelAddonVersion] 1286 if addonName == "" || addonVersion == "" { 1287 continue 1288 } 1289 installedAddons[addonName] = ItemInfo{ 1290 Name: addonName, 1291 AvailableVersions: []string{addonVersion}, 1292 } 1293 } 1294 return installedAddons, nil 1295 } 1296 1297 // checkDependencyNeedInstall checks whether dependency addon needs to be reinstalled 1298 // If the dep addon is not installed, need to install 1299 // If the dep addon is installed locally, don't need to install 1300 // If the dep addon is installed from registry and not defined clusters, don't need to install 1301 // If the dep addon is installed from registry and is defined clusters, and clusters value is nil, don't need to install 1302 // If the dep addon is installed from registry and is defined clusters, and clusters value is not nil, 1303 // and the upstream addon's clusters is nil, need to install 1304 // If the dep addon is installed from registry and is defined clusters, and clusters value is not nil, 1305 // and the upstream addon's clusters is not nil, the re-installation is based on whether the dep clusters value can contain the upstream clusters value 1306 func checkDependencyNeedInstall(ctx context.Context, k8sClient client.Client, depName string, addonClusters []string) (bool, error) { 1307 depApp, err := FetchAddonRelatedApp(ctx, k8sClient, depName) 1308 if err != nil { 1309 if !apierrors.IsNotFound(err) { 1310 return false, err 1311 } 1312 // dependent addon is not exist, need to install it 1313 return true, nil 1314 } 1315 1316 // We should not automatically override addons installed locally by the user, so skip to reinstall it 1317 labels := depApp.GetLabels() 1318 installedRegistry := labels[oam.LabelAddonRegistry] 1319 isLocalRegistry := installedRegistry == LocalAddonRegistryName 1320 if isLocalRegistry { 1321 klog.Warningf("%v is installed locally. Please ensure that it has been installed to the required clusters, if not, please manually install it.", depName) 1322 return false, nil 1323 } 1324 1325 // Addons without the clusters parameter can only be installed on the local cluster. So we don't need to reinstall it 1326 hasClustersArg, hasClustersArgsErr := hasClustersParameters(ctx, k8sClient, depName) 1327 if hasClustersArgsErr != nil { 1328 return false, hasClustersArgsErr 1329 } 1330 if !hasClustersArg { 1331 return false, nil 1332 } 1333 1334 // get addon current parameter value 1335 depArgs, depArgsErr := GetAddonLegacyParameters(ctx, k8sClient, depName) 1336 if depArgsErr != nil && !apierrors.IsNotFound(depArgsErr) { 1337 return false, depArgsErr 1338 } 1339 clusterArgValue := depArgs[types.ClustersArg] 1340 1341 // nil clusters indicates that the dependent addon is installed on all clusters 1342 if clusterArgValue == nil { 1343 return false, nil 1344 } 1345 1346 // nil addonClusters indicates the addon will be installed all clusters, thus the dependent addon should also to be installed on all clusters. 1347 if addonClusters == nil { 1348 return true, nil 1349 } 1350 1351 // Determine whether the dependent addon's existing clusters can cover the new addon's clusters 1352 needInstallAddonDep, _ := hasNotCoveredClusters(clusterArgValue, addonClusters) 1353 return needInstallAddonDep, nil 1354 } 1355 1356 // getDependencyArgs get the dependent addon's install args according to the upstream addon's clusters parameter's value 1357 // If dep addon has not defined clusters parameter, don't need to set clusters parameter value, 1358 // If dep addon has not installed, set the clusters value same to addon's clusters 1359 // If dep addon clusters parameter's value is nil, the dependent addon is installed on all clusters, 1360 // don't need to reset clusters parameter value 1361 // If dep addon has defined clusters parameter, and clusters is not nil, and addon clusters is nil, 1362 // set clusters value as nil 1363 // If dep addon has defined clusters parameter, and clusters is not nil, and addon clusters is not nil, 1364 // set clusters value as the union of the dependent addon's and the upstream addon's clusters 1365 func getDependencyArgs(ctx context.Context, k8sClient client.Client, depName string, addonClusters []string) (map[string]interface{}, error) { 1366 hasClustersArg, err := hasClustersParameters(ctx, k8sClient, depName) 1367 if err != nil { 1368 return nil, err 1369 } 1370 // dep addon is not install, installed it by assigning clusters value 1371 _, depErr := FetchAddonRelatedApp(ctx, k8sClient, depName) 1372 if depErr != nil { 1373 if !apierrors.IsNotFound(depErr) { 1374 return nil, err 1375 } 1376 depArgs := map[string]interface{}{} 1377 if addonClusters != nil { 1378 depArgs = map[string]interface{}{ 1379 types.ClustersArg: addonClusters, 1380 } 1381 } 1382 return depArgs, nil 1383 } 1384 1385 // dep addon is installed 1386 depArgs, depArgsErr := GetAddonLegacyParameters(ctx, k8sClient, depName) 1387 if depArgsErr != nil && !apierrors.IsNotFound(depArgsErr) { 1388 return nil, depArgsErr 1389 } 1390 if !hasClustersArg || depArgs[types.ClustersArg] == nil { 1391 return depArgs, nil 1392 } 1393 if addonClusters == nil { 1394 delete(depArgs, types.ClustersArg) 1395 } else { 1396 clusterArgValue := depArgs[types.ClustersArg] 1397 notCovered, depClusters := hasNotCoveredClusters(clusterArgValue, addonClusters) 1398 if notCovered { 1399 depArgs[types.ClustersArg] = depClusters 1400 } 1401 } 1402 return depArgs, nil 1403 } 1404 1405 // hasClustersParameters checks whether the addon defines the clusters parameter. 1406 // If the addon has been installed, get addon package from the installed registry, 1407 // Otherwise, get addon package from one of registries where this addon exists. 1408 func hasClustersParameters(ctx context.Context, k8sClient client.Client, addonName string) (bool, error) { 1409 var installedRegistry []string 1410 depApp, err := FetchAddonRelatedApp(ctx, k8sClient, addonName) 1411 if err == nil { 1412 labels := depApp.GetLabels() 1413 registryName, ok := labels[oam.LabelAddonRegistry] 1414 if ok { 1415 installedRegistry = []string{registryName} 1416 } 1417 } 1418 addonPackages, err := FindAddonPackagesDetailFromRegistry(context.Background(), k8sClient, []string{addonName}, installedRegistry) 1419 // If the state of addon is not disabled, we don't check the error, because it could be installed from local. 1420 if err != nil { 1421 return false, err 1422 } 1423 var addonPackage *WholeAddonPackage 1424 if len(addonPackages) != 0 { 1425 addonPackage = addonPackages[0] 1426 } 1427 if addonPackage.APISchema == nil { 1428 return false, nil 1429 } 1430 schemas := addonPackage.APISchema.Properties 1431 _, hasClusters := schemas[types.ClustersArg] 1432 return hasClusters, nil 1433 } 1434 1435 // hasNotCoveredClusters check if the clusterArgsValue can cover the values of addonClusters, 1436 // and if not covered, also return the merged clusters array 1437 func hasNotCoveredClusters(clusterArgValue interface{}, addonClusters []string) (bool, []string) { 1438 var needInstallAddonDep = false 1439 var depClusters []string 1440 originClusters := clusterArgValue.([]interface{}) 1441 for _, r := range originClusters { 1442 depClusters = append(depClusters, r.(string)) 1443 } 1444 for _, addonCluster := range addonClusters { 1445 if !stringslices.Contains(depClusters, addonCluster) { 1446 depClusters = append(depClusters, addonCluster) 1447 needInstallAddonDep = true 1448 } 1449 } 1450 return needInstallAddonDep, depClusters 1451 } 1452 1453 // checkDependency checks if addon's dependency 1454 func (h *Installer) checkDependency(addon *InstallPackage) ([]string, error) { 1455 var app v1beta1.Application 1456 var needEnable []string 1457 for _, dep := range addon.Dependencies { 1458 err := h.cli.Get(h.ctx, client.ObjectKey{ 1459 Namespace: types.DefaultKubeVelaNS, 1460 Name: addonutil.Addon2AppName(dep.Name), 1461 }, &app) 1462 if err == nil { 1463 continue 1464 } 1465 if !apierrors.IsNotFound(err) { 1466 return nil, err 1467 } 1468 needEnable = append(needEnable, dep.Name) 1469 } 1470 return needEnable, nil 1471 } 1472 1473 // createOrUpdate will return true if updated 1474 func (h *Installer) createOrUpdate(app *v1beta1.Application) (bool, error) { 1475 // Set the publish version for the addon application 1476 oam.SetPublishVersion(app, util.GenerateVersion("addon")) 1477 var existApp v1beta1.Application 1478 err := h.cli.Get(h.ctx, client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, &existApp) 1479 if apierrors.IsNotFound(err) { 1480 return false, h.cli.Create(h.ctx, app) 1481 } 1482 if err != nil { 1483 return false, err 1484 } 1485 existApp.Spec = app.Spec 1486 existApp.Labels = app.Labels 1487 existApp.Annotations = app.Annotations 1488 err = h.cli.Update(h.ctx, &existApp) 1489 if err != nil { 1490 klog.Errorf("fail to create application: %v", err) 1491 return false, errors.Wrap(err, "fail to create application") 1492 } 1493 existApp.DeepCopyInto(app) 1494 return true, nil 1495 } 1496 1497 func (h *Installer) dispatchAddonResource(addon *InstallPackage) error { 1498 app, auxiliaryOutputs, err := RenderApp(h.ctx, addon, h.cli, h.args) 1499 if err != nil { 1500 return errors.Wrap(err, "render addon application fail") 1501 } 1502 appName, err := determineAddonAppName(h.ctx, h.cli, h.addon.Name) 1503 if err != nil { 1504 return err 1505 } 1506 app.Name = appName 1507 1508 app.SetLabels(util.MergeMapOverrideWithDst(app.GetLabels(), map[string]string{oam.LabelAddonRegistry: h.r.Name})) 1509 1510 // Step1: Render the definitions 1511 defs, err := RenderDefinitions(addon, h.config) 1512 if err != nil { 1513 return errors.Wrap(err, "render addon definitions fail") 1514 } 1515 1516 if !h.overrideDefs { 1517 existDefs, err := checkConflictDefs(h.ctx, h.cli, defs, app.Name) 1518 if err != nil { 1519 return err 1520 } 1521 if len(existDefs) != 0 { 1522 return produceDefConflictError(existDefs) 1523 } 1524 } 1525 1526 // Step2: Render the config templates 1527 templates, err := RenderConfigTemplates(addon, h.cli) 1528 if err != nil { 1529 return errors.Wrap(err, "render the config template fail") 1530 } 1531 1532 // Step3: Render the definition schemas 1533 schemas, err := RenderDefinitionSchema(addon) 1534 if err != nil { 1535 return errors.Wrap(err, "render addon definitions' schema fail") 1536 } 1537 1538 // Step4: Render the velaQL views 1539 views, err := RenderViews(addon) 1540 if err != nil { 1541 return errors.Wrap(err, "render addon views fail") 1542 } 1543 1544 if err := passDefInAppAnnotation(defs, app); err != nil { 1545 return errors.Wrapf(err, "cannot pass definition to addon app's annotation") 1546 } 1547 1548 if h.dryRun { 1549 result, err := yaml.Marshal(app) 1550 if err != nil { 1551 return errors.Wrapf(err, "dry-run marshal app into yaml %s", app.Name) 1552 } 1553 h.dryRunBuff.Write(result) 1554 h.dryRunBuff.WriteString("\n") 1555 } else { 1556 updated, err := h.createOrUpdate(app) 1557 if err != nil { 1558 return err 1559 } 1560 if updated { 1561 h.installerRuntime["upgrade"] = true 1562 } 1563 } 1564 1565 auxiliaryOutputs = append(auxiliaryOutputs, defs...) 1566 auxiliaryOutputs = append(auxiliaryOutputs, templates...) 1567 auxiliaryOutputs = append(auxiliaryOutputs, schemas...) 1568 auxiliaryOutputs = append(auxiliaryOutputs, views...) 1569 1570 for _, o := range auxiliaryOutputs { 1571 // bind-component means the content is related with the component 1572 // if component not exists, the resources shouldn't be applied 1573 if !checkBondComponentExist(*o, *app) { 1574 continue 1575 } 1576 if h.dryRun { 1577 result, err := yaml.Marshal(o) 1578 if err != nil { 1579 return errors.Wrapf(err, "dry-run marshal auxiliary object into yaml %s", o.GetName()) 1580 } 1581 h.dryRunBuff.WriteString("---\n") 1582 h.dryRunBuff.Write(result) 1583 h.dryRunBuff.WriteString("\n") 1584 continue 1585 } 1586 addOwner(o, app) 1587 err = h.apply.Apply(h.ctx, o, apply.DisableUpdateAnnotation()) 1588 if err != nil { 1589 return err 1590 } 1591 } 1592 1593 if h.dryRun { 1594 fmt.Print(h.dryRunBuff.String()) 1595 return nil 1596 } 1597 1598 if h.args != nil && len(h.args) > 0 { 1599 sec := RenderArgsSecret(addon, h.args) 1600 addOwner(sec, app) 1601 err = h.apply.Apply(h.ctx, sec, apply.DisableUpdateAnnotation()) 1602 if err != nil { 1603 return err 1604 } 1605 } else { 1606 // delete addon args secret file 1607 deleteErr := deleteArgsSecret(h.ctx, h.cli, addon.Name) 1608 if deleteErr != nil { 1609 return deleteErr 1610 } 1611 } 1612 return nil 1613 } 1614 1615 func (h *Installer) renderNotes(addon *InstallPackage) (string, error) { 1616 if len(addon.Notes.Data) == 0 { 1617 return "", nil 1618 } 1619 r := addonCueTemplateRender{ 1620 addon: addon, 1621 inputArgs: h.args, 1622 contextInfo: map[string]interface{}{ 1623 "installer": h.installerRuntime, 1624 }, 1625 } 1626 contextFile, err := r.formatContext() 1627 if err != nil { 1628 return "", err 1629 } 1630 notesFile := contextFile + "\n" + addon.Notes.Data 1631 val, err := value.NewValue(notesFile, nil, "") 1632 if err != nil { 1633 return "", errors.Wrap(err, "build values for NOTES.cue") 1634 } 1635 notes, err := val.LookupValue(KeyWordNotes) 1636 if err != nil { 1637 return "", errors.Wrap(err, "look up notes in NOTES.cue") 1638 } 1639 notesStr, err := notes.CueValue().String() 1640 if err != nil { 1641 return "", errors.Wrap(err, "convert notes to string") 1642 } 1643 return notesStr, nil 1644 } 1645 1646 // this func will handle such two case 1647 // 1. if last apply failed an workflow have suspend, this func will continue the workflow 1648 // 2. restart the workflow, if the new cluster have been added in KubeVela 1649 func (h *Installer) continueOrRestartWorkflow() error { 1650 if h.dryRun { 1651 return nil 1652 } 1653 app, err := FetchAddonRelatedApp(h.ctx, h.cli, h.addon.Name) 1654 if err != nil { 1655 return err 1656 } 1657 1658 switch { 1659 // this case means user add a new cluster and user want to restart workflow to dispatch addon resources to new cluster 1660 // re-apply app won't help app restart workflow 1661 case app.Status.Phase == common2.ApplicationRunning: 1662 // we can use retry on conflict here in CLI, because we want to update the status in this CLI operation. 1663 return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { 1664 if err = h.cli.Get(h.ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, app); err != nil { 1665 return 1666 } 1667 app.Status.Workflow = nil 1668 return h.cli.Status().Update(h.ctx, app) 1669 }) 1670 // this case means addon last installation meet some error and workflow has been suspended by app controller 1671 // re-apply app won't help app workflow continue 1672 case app.Status.Workflow != nil && app.Status.Workflow.Suspend: 1673 // we can use retry on conflict here in CLI, because we want to update the status in this CLI operation. 1674 return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { 1675 if err = h.cli.Get(h.ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, app); err != nil { 1676 return 1677 } 1678 mergePatch := client.MergeFrom(app.DeepCopy()) 1679 app.Status.Workflow.Suspend = false 1680 return h.cli.Status().Patch(h.ctx, app, mergePatch) 1681 }) 1682 } 1683 return nil 1684 } 1685 1686 // getAddonVersionMeetSystemRequirement return the addon's latest version which meet the system requirements 1687 func (h *Installer) getAddonVersionMeetSystemRequirement(addonName string) string { 1688 if h.r != nil && IsVersionRegistry(*h.r) { 1689 versionedRegistry := BuildVersionedRegistry(h.r.Name, h.r.Helm.URL, &common.HTTPOption{ 1690 Username: h.r.Helm.Username, 1691 Password: h.r.Helm.Password, 1692 }) 1693 versions, err := versionedRegistry.GetAddonAvailableVersion(addonName) 1694 if err != nil { 1695 return "" 1696 } 1697 for _, version := range versions { 1698 req := LoadSystemRequirements(version.Annotations) 1699 if checkAddonVersionMeetRequired(h.ctx, req, h.cli, h.dc) == nil { 1700 return version.Version 1701 } 1702 } 1703 } 1704 return "" 1705 } 1706 1707 func addOwner(child *unstructured.Unstructured, app *v1beta1.Application) { 1708 child.SetOwnerReferences(append(child.GetOwnerReferences(), 1709 *metav1.NewControllerRef(app, v1beta1.ApplicationKindVersionKind))) 1710 } 1711 1712 // determine app name, if app is already exist, use the application name 1713 func determineAddonAppName(ctx context.Context, cli client.Client, addonName string) (string, error) { 1714 app, err := FetchAddonRelatedApp(ctx, cli, addonName) 1715 if err != nil { 1716 if !apierrors.IsNotFound(err) { 1717 return "", err 1718 } 1719 // if the app still not exist, use addon-{addonName} 1720 return addonutil.Addon2AppName(addonName), nil 1721 } 1722 return app.Name, nil 1723 } 1724 1725 // FetchAddonRelatedApp will fetch the addon related app, this func will use NamespacedName(vela-system, addon-addonName) to get app 1726 // if not find will try to get 1.1 legacy addon related app by using NamespacedName(vela-system, `addonName`) 1727 func FetchAddonRelatedApp(ctx context.Context, cli client.Client, addonName string) (*v1beta1.Application, error) { 1728 app := &v1beta1.Application{} 1729 if err := cli.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2AppName(addonName)}, app); err != nil { 1730 if !apierrors.IsNotFound(err) { 1731 return nil, err 1732 } 1733 // for 1.1 addon app compatibility code 1734 if err := cli.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: addonName}, app); err != nil { 1735 return nil, err 1736 } 1737 } 1738 return app, nil 1739 } 1740 1741 // checkAddonVersionMeetRequired will check the version of cli/ux and kubevela-core-controller whether meet the addon requirement, if not will return an error 1742 // please notice that this func is for check production environment which vela cli/ux or vela core is officalVersion 1743 // if version is for test or debug eg: latest/commit-id/branch-name this func will return nil error 1744 func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequirements, k8sClient client.Client, dc *discovery.DiscoveryClient) error { 1745 if require == nil { 1746 return nil 1747 } 1748 1749 // if not semver version, bypass check cli/ux. eg: {branch name/git commit id/UNKNOWN} 1750 if version2.IsOfficialKubeVelaVersion(version2.VelaVersion) { 1751 res, err := checkSemVer(version2.VelaVersion, require.VelaVersion) 1752 if err != nil { 1753 return err 1754 } 1755 if !res { 1756 return fmt.Errorf("vela cli/ux version: %s require: %s", version2.VelaVersion, require.VelaVersion) 1757 } 1758 } 1759 1760 // check vela core controller version 1761 imageVersion, err := fetchVelaCoreImageTag(ctx, k8sClient) 1762 if err != nil { 1763 return err 1764 } 1765 1766 // if not semver version, bypass check vela-core. 1767 if version2.IsOfficialKubeVelaVersion(imageVersion) { 1768 res, err := checkSemVer(imageVersion, require.VelaVersion) 1769 if err != nil { 1770 return err 1771 } 1772 if !res { 1773 return fmt.Errorf("the vela core controller: %s require: %s", imageVersion, require.VelaVersion) 1774 } 1775 } 1776 1777 // discovery client is nil so bypass check kubernetes version 1778 if dc == nil { 1779 return nil 1780 } 1781 1782 k8sVersion, err := dc.ServerVersion() 1783 if err != nil { 1784 return err 1785 } 1786 // if not semver version, bypass check kubernetes version. 1787 if version2.IsOfficialKubeVelaVersion(k8sVersion.GitVersion) { 1788 res, err := checkSemVer(k8sVersion.GitVersion, require.KubernetesVersion) 1789 if err != nil { 1790 return err 1791 } 1792 1793 if !res { 1794 return fmt.Errorf("the kubernetes version %s require: %s", k8sVersion.GitVersion, require.KubernetesVersion) 1795 } 1796 } 1797 1798 return nil 1799 } 1800 1801 func checkSemVer(actual string, require string) (bool, error) { 1802 if len(require) == 0 { 1803 return true, nil 1804 } 1805 semVer := strings.TrimPrefix(actual, "v") 1806 l := strings.ReplaceAll(require, "v", " ") 1807 constraint, err := semver.NewConstraint(l) 1808 if err != nil { 1809 klog.Errorf("fail to new constraint: %s", err.Error()) 1810 return false, err 1811 } 1812 v, err := semver.NewVersion(semVer) 1813 if err != nil { 1814 klog.Errorf("fail to new version %s: %s", semVer, err.Error()) 1815 return false, err 1816 } 1817 if constraint.Check(v) { 1818 return true, nil 1819 } 1820 if strings.Contains(actual, "-") && !strings.Contains(require, "-") { 1821 semVer := strings.TrimPrefix(actual[:strings.Index(actual, "-")], "v") // nolint 1822 if strings.Contains(require, ">=") && require[strings.Index(require, "=")+1:] == semVer { 1823 // for case: `actual` is 1.5.0-beta.1 require is >=`1.5.0` 1824 return false, nil 1825 } 1826 v, err := semver.NewVersion(semVer) 1827 if err != nil { 1828 klog.Errorf("fail to new version %s: %s", semVer, err.Error()) 1829 return false, err 1830 } 1831 if constraint.Check(v) { 1832 return true, nil 1833 } 1834 } 1835 return false, nil 1836 } 1837 1838 func fetchVelaCoreImageTag(ctx context.Context, k8sClient client.Client) (string, error) { 1839 deployList := &appsv1.DeploymentList{} 1840 if err := k8sClient.List(ctx, deployList, client.MatchingLabels{oam.LabelControllerName: oam.ApplicationControllerName}); err != nil { 1841 return "", err 1842 } 1843 deploy := appsv1.Deployment{} 1844 if len(deployList.Items) == 0 { 1845 // backward compatible logic old version which vela-core controller has no this label 1846 if err := k8sClient.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: types.KubeVelaControllerDeployment}, &deploy); err != nil { 1847 if apierrors.IsNotFound(err) { 1848 return "", errors.New("can't find a running KubeVela instance, please install it first") 1849 } 1850 return "", err 1851 } 1852 } else { 1853 deploy = deployList.Items[0] 1854 } 1855 1856 var tag string 1857 for _, c := range deploy.Spec.Template.Spec.Containers { 1858 if c.Name == types.DefaultKubeVelaReleaseName { 1859 l := strings.Split(c.Image, ":") 1860 if len(l) == 1 { 1861 // if tag is empty mean use latest image 1862 return "latest", nil 1863 } 1864 tag = l[1] 1865 } 1866 } 1867 return tag, nil 1868 } 1869 1870 // PackageAddon package vela addon directory into a helm chart compatible archive and return its absolute path 1871 func PackageAddon(addonDictPath string) (string, error) { 1872 // save the Chart.yaml file in order to be compatible with helm chart 1873 err := MakeChartCompatible(addonDictPath, true) 1874 if err != nil { 1875 return "", err 1876 } 1877 1878 ch, err := loader.LoadDir(addonDictPath) 1879 if err != nil { 1880 return "", err 1881 } 1882 1883 dest, err := os.Getwd() 1884 if err != nil { 1885 return "", err 1886 } 1887 archive, err := chartutil.Save(ch, dest) 1888 if err != nil { 1889 return "", err 1890 } 1891 return archive, nil 1892 } 1893 1894 // GetAddonLegacyParameters get addon's legacy parameters, that is stored in Secret 1895 func GetAddonLegacyParameters(ctx context.Context, k8sClient client.Client, addonName string) (map[string]interface{}, error) { 1896 var sec v1.Secret 1897 err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2SecName(addonName)}, &sec) 1898 if err != nil { 1899 return nil, err 1900 } 1901 args, err := FetchArgsFromSecret(&sec) 1902 if err != nil { 1903 return nil, err 1904 } 1905 return args, nil 1906 } 1907 1908 // MergeAddonInstallArgs merge addon's legacy parameter and new input args 1909 func MergeAddonInstallArgs(ctx context.Context, k8sClient client.Client, addonName string, args map[string]interface{}) (map[string]interface{}, error) { 1910 legacyParams, err := GetAddonLegacyParameters(ctx, k8sClient, addonName) 1911 if err != nil { 1912 if !apierrors.IsNotFound(err) { 1913 return nil, err 1914 } 1915 return args, nil 1916 } 1917 1918 if args == nil && legacyParams == nil { 1919 return args, nil 1920 } 1921 1922 r := make(map[string]interface{}) 1923 if err := mergo.Merge(&r, legacyParams, mergo.WithOverride); err != nil { 1924 return nil, err 1925 } 1926 1927 if err := mergo.Merge(&r, args, mergo.WithOverride); err != nil { 1928 return nil, err 1929 } 1930 return r, nil 1931 }