mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com> Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
575 lines
17 KiB
Go
575 lines
17 KiB
Go
/*
|
|
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 service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
errors3 "github.com/pkg/errors"
|
|
v1 "k8s.io/api/core/v1"
|
|
errors2 "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
|
k8stypes "k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/discovery"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/klog/v2"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
common2 "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"
|
|
pkgaddon "github.com/oam-dev/kubevela/pkg/addon"
|
|
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/clients"
|
|
apis "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
|
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
|
|
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
|
"github.com/oam-dev/kubevela/pkg/definition"
|
|
"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"
|
|
velaerr "github.com/oam-dev/kubevela/pkg/utils/errors"
|
|
)
|
|
|
|
// AddonService handle CRUD and installation of addons
|
|
type AddonService interface {
|
|
GetAddonRegistry(ctx context.Context, name string) (*apis.AddonRegistry, error)
|
|
CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistry, error)
|
|
DeleteAddonRegistry(ctx context.Context, name string) error
|
|
UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistry, error)
|
|
ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistry, error)
|
|
ListAddons(ctx context.Context, registry, query string) ([]*apis.DetailAddonResponse, error)
|
|
StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error)
|
|
GetAddon(ctx context.Context, name string, registry string, version string) (*apis.DetailAddonResponse, error)
|
|
EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error
|
|
DisableAddon(ctx context.Context, name string, force bool) error
|
|
ListEnabledAddon(ctx context.Context) ([]*apis.AddonBaseStatus, error)
|
|
UpdateAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error
|
|
}
|
|
|
|
// AddonImpl2AddonRes convert pkgaddon.UIData to the type apiserver need
|
|
func AddonImpl2AddonRes(impl *pkgaddon.UIData, config *rest.Config) (*apis.DetailAddonResponse, error) {
|
|
var defs []*apis.AddonDefinition
|
|
for _, def := range impl.Definitions {
|
|
obj := &unstructured.Unstructured{}
|
|
dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
|
_, _, err := dec.Decode([]byte(def.Data), nil, obj)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("convert %s file content to definition fail", def.Name)
|
|
}
|
|
defs = append(defs, &apis.AddonDefinition{
|
|
Name: obj.GetName(),
|
|
DefType: obj.GetKind(),
|
|
Description: obj.GetAnnotations()["definition.oam.dev/description"],
|
|
})
|
|
}
|
|
|
|
for _, cueDef := range impl.CUEDefinitions {
|
|
def := definition.Definition{Unstructured: unstructured.Unstructured{}}
|
|
err := def.FromCUEString(cueDef.Data, config)
|
|
if err != nil {
|
|
return nil, errors3.Wrapf(err, "fail to render definition: %s in cue's format", cueDef.Name)
|
|
}
|
|
defs = append(defs, &apis.AddonDefinition{
|
|
Name: def.GetName(),
|
|
DefType: def.GetKind(),
|
|
Description: def.GetAnnotations()["definition.oam.dev/description"],
|
|
})
|
|
}
|
|
|
|
if impl.Meta.DeployTo != nil && impl.Meta.DeployTo.LegacyRuntimeCluster != impl.Meta.DeployTo.RuntimeCluster {
|
|
impl.Meta.DeployTo.LegacyRuntimeCluster = impl.Meta.DeployTo.LegacyRuntimeCluster || impl.Meta.DeployTo.RuntimeCluster
|
|
impl.Meta.DeployTo.RuntimeCluster = impl.Meta.DeployTo.LegacyRuntimeCluster || impl.Meta.DeployTo.RuntimeCluster
|
|
}
|
|
return &apis.DetailAddonResponse{
|
|
Meta: impl.Meta,
|
|
APISchema: impl.APISchema,
|
|
UISchema: impl.UISchema,
|
|
Detail: impl.Detail,
|
|
Definitions: defs,
|
|
RegistryName: impl.RegistryName,
|
|
AvailableVersions: impl.AvailableVersions,
|
|
}, nil
|
|
}
|
|
|
|
// NewAddonService returns an addon service
|
|
func NewAddonService(cacheTime time.Duration) AddonService {
|
|
config, err := clients.GetKubeConfig()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
kubecli, err := clients.GetKubeClient()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
dc, err := clients.GetDiscoveryClient()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ds := pkgaddon.NewRegistryDataStore(kubecli)
|
|
cache := pkgaddon.NewCache(ds)
|
|
|
|
// TODO(@wonderflow): it's better to add a close channel here, but it should be fine as it's only invoke once in APIServer.
|
|
go cache.DiscoverAndRefreshLoop(cacheTime)
|
|
|
|
return &addonServiceImpl{
|
|
addonRegistryCache: cache,
|
|
addonRegistryDS: ds,
|
|
kubeClient: kubecli,
|
|
config: config,
|
|
apply: apply.NewAPIApplicator(kubecli),
|
|
mutex: new(sync.RWMutex),
|
|
discoveryClient: dc,
|
|
}
|
|
}
|
|
|
|
type addonServiceImpl struct {
|
|
addonRegistryCache *pkgaddon.Cache
|
|
addonRegistryDS pkgaddon.RegistryDataStore
|
|
kubeClient client.Client
|
|
config *rest.Config
|
|
apply apply.Applicator
|
|
discoveryClient *discovery.DiscoveryClient
|
|
|
|
mutex *sync.RWMutex
|
|
}
|
|
|
|
// GetAddon will get addon information
|
|
func (u *addonServiceImpl) GetAddon(ctx context.Context, name string, registry string, version string) (*apis.DetailAddonResponse, error) {
|
|
var addon *pkgaddon.UIData
|
|
var err error
|
|
if registry == "" {
|
|
registries, err := u.addonRegistryDS.ListRegistries(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range registries {
|
|
addon, err = u.addonRegistryCache.GetUIData(r, name, version)
|
|
if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
if addon != nil {
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
addonRegistry, err := u.addonRegistryDS.GetRegistry(ctx, registry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addon, err = u.addonRegistryCache.GetUIData(addonRegistry, name, version)
|
|
if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if addon == nil {
|
|
return nil, bcode.ErrAddonNotExist
|
|
}
|
|
|
|
addon.UISchema = renderAddonCustomUISchema(ctx, u.kubeClient, name, renderDefaultUISchema(addon.APISchema))
|
|
|
|
a, err := AddonImpl2AddonRes(addon, u.config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (u *addonServiceImpl) StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) {
|
|
status, err := pkgaddon.GetAddonStatus(ctx, u.kubeClient, name)
|
|
if err != nil {
|
|
return nil, bcode.ErrGetAddonApplication
|
|
}
|
|
var allClusters []apis.NameAlias
|
|
clusters, err := multicluster.ListVirtualClusters(ctx, u.kubeClient)
|
|
if err != nil {
|
|
klog.Errorf("err while list all clusters: %v", err)
|
|
}
|
|
|
|
for _, c := range clusters {
|
|
allClusters = append(allClusters, apis.NameAlias{Name: c.Name, Alias: c.Name})
|
|
}
|
|
if status.AddonPhase == string(apis.AddonPhaseDisabled) {
|
|
return &apis.AddonStatusResponse{
|
|
AddonBaseStatus: apis.AddonBaseStatus{
|
|
Name: name,
|
|
Phase: apis.AddonPhase(status.AddonPhase),
|
|
},
|
|
InstalledVersion: status.InstalledVersion,
|
|
AllClusters: allClusters,
|
|
}, nil
|
|
}
|
|
|
|
res := apis.AddonStatusResponse{
|
|
AddonBaseStatus: apis.AddonBaseStatus{
|
|
Name: name,
|
|
Phase: apis.AddonPhase(status.AddonPhase),
|
|
},
|
|
InstalledVersion: status.InstalledVersion,
|
|
AppStatus: *status.AppStatus,
|
|
Clusters: status.Clusters,
|
|
AllClusters: allClusters,
|
|
}
|
|
|
|
var sec v1.Secret
|
|
err = u.kubeClient.Get(ctx, client.ObjectKey{
|
|
Namespace: types.DefaultKubeVelaNS,
|
|
Name: addonutil.Addon2SecName(name),
|
|
}, &sec)
|
|
if err != nil && !errors2.IsNotFound(err) {
|
|
return nil, bcode.ErrAddonSecretGet
|
|
} else if errors2.IsNotFound(err) {
|
|
return &res, nil
|
|
}
|
|
|
|
res.Args, err = pkgaddon.FetchArgsFromSecret(&sec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
func (u *addonServiceImpl) ListAddons(ctx context.Context, registry, query string) ([]*apis.DetailAddonResponse, error) {
|
|
var addons []*pkgaddon.UIData
|
|
rs, err := u.addonRegistryDS.ListRegistries(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var gatherErr velaerr.ErrorList
|
|
|
|
for _, r := range rs {
|
|
if registry != "" && r.Name != registry {
|
|
continue
|
|
}
|
|
listAddons, err := u.addonRegistryCache.ListUIData(r)
|
|
if err != nil {
|
|
gatherErr = append(gatherErr, err)
|
|
continue
|
|
}
|
|
addons = mergeAddons(addons, listAddons, r.Name)
|
|
}
|
|
|
|
for i, a := range addons {
|
|
if a.Invisible {
|
|
addons = append(addons[:i], addons[i+1:]...)
|
|
}
|
|
}
|
|
|
|
if query != "" {
|
|
var filtered []*pkgaddon.UIData
|
|
for i, addon := range addons {
|
|
if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) {
|
|
filtered = append(filtered, addons[i])
|
|
}
|
|
}
|
|
addons = filtered
|
|
}
|
|
sort.Slice(addons, func(i, j int) bool {
|
|
return addons[i].Name < addons[j].Name
|
|
})
|
|
|
|
for _, addon := range addons {
|
|
// render default ui schema
|
|
addon.UISchema = renderDefaultUISchema(addon.APISchema)
|
|
}
|
|
|
|
var addonResources []*apis.DetailAddonResponse
|
|
for _, a := range addons {
|
|
addonRes, err := AddonImpl2AddonRes(a, u.config)
|
|
if err != nil {
|
|
klog.Errorf("err while converting AddonImpl to DetailAddonResponse: %v", err)
|
|
continue
|
|
}
|
|
addonResources = append(addonResources, addonRes)
|
|
}
|
|
if gatherErr.HasError() {
|
|
return addonResources, gatherErr
|
|
}
|
|
return addonResources, nil
|
|
}
|
|
|
|
func (u *addonServiceImpl) DeleteAddonRegistry(ctx context.Context, name string) error {
|
|
return u.addonRegistryDS.DeleteRegistry(ctx, name)
|
|
}
|
|
|
|
func (u *addonServiceImpl) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistry, error) {
|
|
r := addonRegistryModelFromCreateAddonRegistryRequest(req)
|
|
|
|
err := u.addonRegistryDS.AddRegistry(ctx, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return convertAddonRegistry(r), nil
|
|
}
|
|
|
|
func convertAddonRegistry(r pkgaddon.Registry) *apis.AddonRegistry {
|
|
return &apis.AddonRegistry{
|
|
Name: r.Name,
|
|
Git: r.Git.SafeCopy(),
|
|
Gitee: r.Gitee.SafeCopy(),
|
|
OSS: r.OSS,
|
|
Helm: r.Helm.SafeCopy(),
|
|
Gitlab: r.Gitlab.SafeCopy(),
|
|
}
|
|
}
|
|
|
|
func (u *addonServiceImpl) GetAddonRegistry(ctx context.Context, name string) (*apis.AddonRegistry, error) {
|
|
r, err := u.addonRegistryDS.GetRegistry(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertAddonRegistry(r), nil
|
|
}
|
|
|
|
func (u addonServiceImpl) UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistry, error) {
|
|
r, err := u.addonRegistryDS.GetRegistry(ctx, name)
|
|
if err != nil {
|
|
return nil, bcode.ErrAddonRegistryNotExist
|
|
}
|
|
switch {
|
|
case req.Git != nil:
|
|
r.Git = req.Git
|
|
case req.Gitee != nil:
|
|
r.Gitee = req.Gitee
|
|
case req.Oss != nil:
|
|
r.OSS = req.Oss
|
|
case req.Helm != nil:
|
|
r.Helm = req.Helm
|
|
case req.Gitlab != nil:
|
|
r.Gitlab = req.Gitlab
|
|
}
|
|
|
|
err = u.addonRegistryDS.UpdateRegistry(ctx, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return convertAddonRegistry(r), nil
|
|
}
|
|
|
|
func (u *addonServiceImpl) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistry, error) {
|
|
|
|
var list []*apis.AddonRegistry
|
|
registries, err := u.addonRegistryDS.ListRegistries(ctx)
|
|
if err != nil {
|
|
// the storage configmap still not exist, don't return error add registry will create the configmap
|
|
if errors2.IsNotFound(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
for _, registry := range registries {
|
|
r := convertAddonRegistry(registry)
|
|
list = append(list, r)
|
|
}
|
|
sort.Slice(list, func(i, j int) bool {
|
|
return list[i].Name < list[j].Name
|
|
})
|
|
return list, nil
|
|
}
|
|
|
|
func (u *addonServiceImpl) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error {
|
|
var err error
|
|
registries, err := u.addonRegistryDS.ListRegistries(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(args.RegistryName) != 0 {
|
|
foundRegistry := false
|
|
for _, registry := range registries {
|
|
if registry.Name == args.RegistryName {
|
|
foundRegistry = true
|
|
}
|
|
}
|
|
if !foundRegistry {
|
|
return bcode.ErrAddonRegistryNotExist.SetMessage(fmt.Sprintf("specified registry %s not exist", args.RegistryName))
|
|
}
|
|
}
|
|
for i, r := range registries {
|
|
if len(args.RegistryName) != 0 && args.RegistryName != r.Name {
|
|
continue
|
|
}
|
|
// TODO: response the additional info to velaux users
|
|
_, err = pkgaddon.EnableAddon(ctx, name, args.Version, u.kubeClient, u.discoveryClient, u.apply, u.config, r, args.Args, u.addonRegistryCache, pkgaddon.FilterDependencyRegistries(i, registries))
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// if reach this line error must is not nil
|
|
if errors.Is(err, pkgaddon.ErrNotExist) {
|
|
// one registry return addon not exist error, should not break other registry func
|
|
continue
|
|
}
|
|
if strings.Contains(err.Error(), "specified version") {
|
|
return bcode.ErrAddonInvalidVersion.SetMessage(err.Error())
|
|
}
|
|
|
|
// wrap this error with special bcode
|
|
if errors.As(err, &pkgaddon.VersionUnMatchError{}) {
|
|
return bcode.ErrAddonSystemVersionMismatch.SetMessage(err.Error())
|
|
}
|
|
// except `addon not found`, other errors should return directly
|
|
return err
|
|
}
|
|
return bcode.ErrAddonNotExist
|
|
}
|
|
|
|
func (u *addonServiceImpl) DisableAddon(ctx context.Context, name string, force bool) error {
|
|
err := pkgaddon.DisableAddon(ctx, u.kubeClient, name, u.config, force)
|
|
if err != nil {
|
|
klog.Errorf("delete application fail: %s", err.Error())
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *addonServiceImpl) ListEnabledAddon(ctx context.Context) ([]*apis.AddonBaseStatus, error) {
|
|
apps := &v1beta1.ApplicationList{}
|
|
if err := u.kubeClient.List(ctx, apps, client.InNamespace(types.DefaultKubeVelaNS), client.HasLabels{oam.LabelAddonName}); err != nil {
|
|
return nil, err
|
|
}
|
|
var response []*apis.AddonBaseStatus
|
|
for _, application := range apps.Items {
|
|
if addonName := application.Labels[oam.LabelAddonName]; addonName != "" {
|
|
if application.Status.Phase != common2.ApplicationRunning {
|
|
continue
|
|
}
|
|
response = append(response, &apis.AddonBaseStatus{
|
|
Name: addonName,
|
|
Phase: convertAppStateToAddonPhase(application.Status.Phase),
|
|
})
|
|
}
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (u *addonServiceImpl) UpdateAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error {
|
|
|
|
var app v1beta1.Application
|
|
// check addon application whether exist
|
|
err := u.kubeClient.Get(ctx, client.ObjectKey{
|
|
Namespace: types.DefaultKubeVelaNS,
|
|
Name: addonutil.Addon2AppName(name),
|
|
}, &app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
registries, err := u.addonRegistryDS.ListRegistries(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, r := range registries {
|
|
// TODO: response the additional info to velaux users
|
|
_, err = pkgaddon.EnableAddon(ctx, name, args.Version, u.kubeClient, u.discoveryClient, u.apply, u.config, r, args.Args, u.addonRegistryCache, pkgaddon.FilterDependencyRegistries(i, registries))
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
if errors.Is(err, pkgaddon.ErrNotExist) {
|
|
continue
|
|
}
|
|
|
|
// wrap this error with special bcode
|
|
if errors.As(err, &pkgaddon.VersionUnMatchError{}) {
|
|
return bcode.ErrAddonSystemVersionMismatch
|
|
}
|
|
// except `addon not found`, other errors should return directly
|
|
return err
|
|
}
|
|
return bcode.ErrAddonNotExist
|
|
}
|
|
|
|
func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegistryRequest) pkgaddon.Registry {
|
|
return pkgaddon.Registry{
|
|
Name: req.Name,
|
|
Git: req.Git,
|
|
OSS: req.Oss,
|
|
Gitee: req.Gitee,
|
|
Helm: req.Helm,
|
|
Gitlab: req.Gitlab,
|
|
}
|
|
}
|
|
|
|
func mergeAddons(a1, a2 []*pkgaddon.UIData, registryName string) []*pkgaddon.UIData {
|
|
for i, item := range a2 {
|
|
if hasAddon(a1, item.Name) {
|
|
continue
|
|
}
|
|
a2[i].RegistryName = registryName
|
|
a1 = append(a1, a2[i])
|
|
}
|
|
return a1
|
|
}
|
|
|
|
func hasAddon(addons []*pkgaddon.UIData, name string) bool {
|
|
if name == "" {
|
|
return true
|
|
}
|
|
for _, addon := range addons {
|
|
if addon.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func convertAppStateToAddonPhase(state common2.ApplicationPhase) apis.AddonPhase {
|
|
switch state {
|
|
case common2.ApplicationRunning:
|
|
return apis.AddonPhaseEnabled
|
|
default:
|
|
return apis.AddonPhaseEnabling
|
|
}
|
|
}
|
|
|
|
func renderAddonCustomUISchema(ctx context.Context, cli client.Client, addonName string, defaultSchema []*utils.UIParameter) []*utils.UIParameter {
|
|
var cm v1.ConfigMap
|
|
if err := cli.Get(ctx, k8stypes.NamespacedName{
|
|
Namespace: types.DefaultKubeVelaNS,
|
|
Name: fmt.Sprintf("addon-uischema-%s", addonName),
|
|
}, &cm); err != nil {
|
|
if !errors2.IsNotFound(err) {
|
|
klog.Errorf("find uischema configmap from cluster failure %s", err.Error())
|
|
}
|
|
return defaultSchema
|
|
}
|
|
data, ok := cm.Data[types.UISchema]
|
|
if !ok {
|
|
return defaultSchema
|
|
}
|
|
schema := []*utils.UIParameter{}
|
|
if err := json.Unmarshal([]byte(data), &schema); err != nil {
|
|
klog.Errorf("unmarshal ui schema failure %s", err.Error())
|
|
return defaultSchema
|
|
}
|
|
return patchSchema(defaultSchema, schema)
|
|
}
|