/* Copyright 2021 The KubeVela Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package addon import ( "context" "fmt" "path/filepath" "github.com/pkg/errors" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/multicluster" "github.com/oam-dev/kubevela/pkg/oam" addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" "github.com/oam-dev/kubevela/pkg/utils/apply" "github.com/oam-dev/kubevela/pkg/utils/common" ) const ( // disabled indicates the addon is disabled disabled = "disabled" // enabled indicates the addon is enabled enabled = "enabled" // enabling indicates the addon is enabling enabling = "enabling" // disabling indicates the addon related app is deleting disabling = "disabling" // suspend indicates the addon related app is suspended suspend = "suspend" ) // EnableAddon will enable addon with dependency check, source is where addon from. func EnableAddon(ctx context.Context, name string, version string, cli client.Client, discoveryClient *discovery.DiscoveryClient, apply apply.Applicator, config *rest.Config, r Registry, args map[string]interface{}, cache *Cache, registries []Registry, opts ...InstallOption) (string, error) { h := NewAddonInstaller(ctx, cli, discoveryClient, apply, config, &r, args, cache, registries, opts...) pkg, err := h.loadInstallPackage(name, version) if err != nil { return "", err } if err := validateAddonPackage(pkg); err != nil { return "", errors.Wrap(err, fmt.Sprintf("failed to enable addon: %s", name)) } return h.enableAddon(ctx, pkg) } // DisableAddon will disable addon from cluster. func DisableAddon(ctx context.Context, cli client.Client, name string, config *rest.Config, force bool) error { app, err := FetchAddonRelatedApp(ctx, cli, name) // if app not exist, report error if err != nil { return err } if !force { var usingAddonApp []v1beta1.Application usingAddonApp, err = checkAddonHasBeenUsed(ctx, cli, name, *app, config) if err != nil { return err } if len(usingAddonApp) != 0 { return errors.New(appsDependsOnAddonErrInfo(usingAddonApp)) } } return cli.Delete(ctx, app) } // EnableAddonByLocalDir enable an addon from local dir // The allowGoDefOverride parameter allows Go definitions to override CUE definitions when conflicts are detected func EnableAddonByLocalDir(ctx context.Context, name string, dir string, cli client.Client, dc *discovery.DiscoveryClient, applicator apply.Applicator, config *rest.Config, args map[string]interface{}, allowGoDefOverride bool, opts ...InstallOption) (string, error) { absDir, err := filepath.Abs(dir) if err != nil { return "", err } r := localReader{dir: absDir, name: name} metas, err := r.ListAddonMeta() if err != nil { return "", err } meta := metas[r.name] UIData, err := GetUIDataFromReader(r, &meta, UIMetaOptions) if err != nil { return "", err } pkg, err := GetInstallPackageFromReader(r, &meta, UIData) if err != nil { return "", err } // Compile Go definitions if godef/ folder exists if HasGoDefFolder(absDir) { compiledDefs, err := CompileGoDefinitionsFromAddon(ctx, absDir) if err != nil { return "", errors.Wrap(err, "failed to compile Go definitions") } // Check for conflicts between CUE and Go definitions conflicts := DetectDefinitionConflicts(pkg.CUEDefinitions, compiledDefs) if len(conflicts) > 0 { if !allowGoDefOverride { return "", fmt.Errorf("definition name conflicts detected between definitions/ and godef/: %v. "+ "Use --override-definitions flag to allow Go definitions to override CUE definitions", conflicts) } // Remove conflicting CUE definitions when override is allowed pkg.CUEDefinitions = removeConflictingDefinitions(pkg.CUEDefinitions, conflicts) } // Merge compiled Go definitions with existing CUE definitions pkg.CUEDefinitions = append(pkg.CUEDefinitions, compiledDefs...) } if err := validateAddonPackage(pkg); err != nil { return "", errors.Wrap(err, fmt.Sprintf("failed to enable addon by local dir: %s", dir)) } h := NewAddonInstaller(ctx, cli, dc, applicator, config, &Registry{Name: LocalAddonRegistryName}, args, nil, nil, opts...) needEnableAddonNames, err := h.checkDependency(pkg) if err != nil { return "", err } if len(needEnableAddonNames) > 0 { return "", fmt.Errorf("you must first enable dependencies: %v", needEnableAddonNames) } return h.enableAddon(ctx, pkg) } // removeConflictingDefinitions removes definitions from the list that match the conflicting names func removeConflictingDefinitions(definitions []ElementFile, conflicts []string) []ElementFile { conflictMap := make(map[string]bool) for _, name := range conflicts { conflictMap[name] = true } var result []ElementFile for _, def := range definitions { name := extractDefinitionNameFromFile(def) if !conflictMap[name] { result = append(result, def) } } return result } // extractDefinitionNameFromFile extracts the definition name from an ElementFile (same as extractDefinitionName in godef.go) func extractDefinitionNameFromFile(def ElementFile) string { name := filepath.Base(def.Name) name = name[:len(name)-len(filepath.Ext(name))] // Remove extension return name } // GetAddonStatus is general func for cli and apiServer get addon status func GetAddonStatus(ctx context.Context, cli client.Client, name string) (Status, error) { var addonStatus Status joinedClusters, err := multicluster.NewClusterClient(cli).List(ctx) if err != nil { return addonStatus, errors.Wrap(err, "failed to list registered clusters") } var joinedClusterMap = make(map[string]bool) for _, joinedCluster := range joinedClusters.Items { joinedClusterMap[joinedCluster.Name] = true } app, err := FetchAddonRelatedApp(ctx, cli, name) if err != nil { if apierrors.IsNotFound(err) { addonStatus.AddonPhase = disabled return addonStatus, nil } return addonStatus, err } labels := app.GetLabels() addonStatus.AppStatus = &app.Status addonStatus.InstalledVersion = labels[oam.LabelAddonVersion] addonStatus.InstalledRegistry = labels[oam.LabelAddonRegistry] var clusters = make(map[string]map[string]interface{}) for _, r := range app.Status.AppliedResources { if r.Cluster == "" { r.Cluster = multicluster.ClusterLocalName } // TODO(wonderflow): we should collect all the necessary information as observability, currently we only collect cluster name // If cluster is not registered in KubeVela then skip it. if joinedClusterMap[r.Cluster] { clusters[r.Cluster] = make(map[string]interface{}) } } addonStatus.Clusters = clusters if app.Status.Workflow != nil && app.Status.Workflow.Suspend { addonStatus.AddonPhase = suspend return addonStatus, nil } // Get addon parameters var sec v1.Secret err = cli.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2SecName(name)}, &sec) if err != nil { // Not found error can be ignored. Others can't. if !apierrors.IsNotFound(err) { return addonStatus, err } } else { // Although normally `else` is not preferred, we must use `else` here. args, err := FetchArgsFromSecret(&sec) if err != nil { return addonStatus, err } addonStatus.Parameters = args } switch app.Status.Phase { case commontypes.ApplicationRunning: addonStatus.AddonPhase = enabled return addonStatus, nil case commontypes.ApplicationDeleting: addonStatus.AddonPhase = disabling return addonStatus, nil default: addonStatus.AddonPhase = enabling return addonStatus, nil } } // FindAddonPackagesDetailFromRegistry find addons' WholeInstallPackage from registries, empty registryName indicates matching all func FindAddonPackagesDetailFromRegistry(ctx context.Context, k8sClient client.Client, addonNames []string, registryNames []string) ([]*WholeAddonPackage, error) { var addons []*WholeAddonPackage var registries []Registry if len(addonNames) == 0 { return nil, fmt.Errorf("no addon name specified") } registryDataStore := NewRegistryDataStore(k8sClient) // Find matched registries if len(registryNames) == 0 { // Empty registryNames will match all registries regs, err := registryDataStore.ListRegistries(ctx) if err != nil { return nil, err } registries = regs } else { // Only match specified registries for _, registryName := range registryNames { r, err := registryDataStore.GetRegistry(ctx, registryName) if err != nil { continue } registries = append(registries, r) } } if len(registries) == 0 { return nil, ErrRegistryNotExist } // Found addons, for deduplication purposes foundAddons := make(map[string]bool) merge := func(addon *WholeAddonPackage) { if _, ok := foundAddons[addon.Name]; !ok { foundAddons[addon.Name] = true } addons = append(addons, addon) } // Find matched addons in registries for _, r := range registries { if IsVersionRegistry(r) { vr := BuildVersionedRegistry(r.Name, r.Helm.URL, &common.HTTPOption{ Username: r.Helm.Username, Password: r.Helm.Password, InsecureSkipTLS: r.Helm.InsecureSkipTLS, }) for _, addonName := range addonNames { wholePackage, err := vr.GetDetailedAddon(ctx, addonName, "") if err != nil { continue } merge(wholePackage) } } else { meta, err := r.ListAddonMeta() if err != nil { continue } for _, addonName := range addonNames { sourceMeta, ok := meta[addonName] if !ok { continue } uiData, err := r.GetUIData(&sourceMeta, UIMetaOptions) if err != nil { continue } installPackage, err := r.GetInstallPackage(&sourceMeta, uiData) if err != nil { continue } // Combine UIData and InstallPackage into WholeAddonPackage wholePackage := &WholeAddonPackage{ InstallPackage: *installPackage, APISchema: uiData.APISchema, Detail: uiData.Detail, AvailableVersions: uiData.AvailableVersions, RegistryName: uiData.RegistryName, } merge(wholePackage) } } } if len(addons) == 0 { return nil, ErrNotExist } return addons, nil } // Status contain addon phase and related app status type Status struct { AddonPhase string AppStatus *commontypes.AppStatus // the status of multiple clusters Clusters map[string]map[string]interface{} `json:"clusters,omitempty"` InstalledVersion string Parameters map[string]interface{} // Where the addon is from. Can be empty if not installed. InstalledRegistry string }