mirror of https://github.com/kubevela/kubevela.git
458 lines
16 KiB
Go
458 lines
16 KiB
Go
/*
|
|
Copyright 2022 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 cli
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"cuelang.org/go/cue"
|
|
"cuelang.org/go/cue/cuecontext"
|
|
"github.com/spf13/cobra"
|
|
"helm.sh/helm/v3/pkg/action"
|
|
"helm.sh/helm/v3/pkg/release"
|
|
"helm.sh/helm/v3/pkg/releaseutil"
|
|
"helm.sh/helm/v3/pkg/storage"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
apitypes "k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/kubectl/pkg/util/i18n"
|
|
"k8s.io/kubectl/pkg/util/templates"
|
|
"k8s.io/utils/strings/slices"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
velaslices "github.com/kubevela/pkg/util/slices"
|
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/apis/types"
|
|
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
|
|
cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util"
|
|
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
|
"github.com/oam-dev/kubevela/pkg/utils/util"
|
|
)
|
|
|
|
const (
|
|
adoptTypeNative = "native"
|
|
adoptTypeHelm = "helm"
|
|
adoptModeReadOnly = v1alpha1.ReadOnlyPolicyType
|
|
adoptModeTakeOver = v1alpha1.TakeOverPolicyType
|
|
helmDriverEnvKey = "HELM_DRIVER"
|
|
defaultHelmDriver = "secret"
|
|
adoptCUETempVal = "adopt"
|
|
adoptCUETempFunc = "#Adopt"
|
|
)
|
|
|
|
//go:embed adopt-templates/default.cue
|
|
var defaultAdoptTemplate string
|
|
|
|
var (
|
|
adoptTypes = []string{adoptTypeNative, adoptTypeHelm}
|
|
adoptModes = []string{adoptModeReadOnly, adoptModeTakeOver}
|
|
)
|
|
|
|
type resourceRef struct {
|
|
schema.GroupVersionKind
|
|
apitypes.NamespacedName
|
|
Arg string
|
|
}
|
|
|
|
// AdoptOptions options for vela adopt command
|
|
type AdoptOptions struct {
|
|
Type string `json:"type"`
|
|
Mode string `json:"mode"`
|
|
AppName string `json:"appName"`
|
|
AppNamespace string `json:"appNamespace"`
|
|
|
|
HelmReleaseName string
|
|
HelmReleaseNamespace string
|
|
HelmDriver string
|
|
HelmStore *storage.Storage
|
|
HelmRelease *release.Release
|
|
HelmReleases []*release.Release
|
|
|
|
NativeResourceRefs []*resourceRef
|
|
|
|
Apply bool
|
|
Recycle bool
|
|
|
|
AdoptTemplateFile string
|
|
AdoptTemplate string
|
|
AdoptTemplateCUEValue cue.Value
|
|
|
|
Resources []*unstructured.Unstructured `json:"resources"`
|
|
|
|
util.IOStreams
|
|
}
|
|
|
|
func (opt *AdoptOptions) parseResourceRef(f velacmd.Factory, cmd *cobra.Command, arg string) (*resourceRef, error) {
|
|
parts := strings.Split(arg, "/")
|
|
_, gr := schema.ParseResourceArg(parts[0])
|
|
gvks, err := f.Client().RESTMapper().KindsFor(gr.WithVersion(""))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find types for resource %s: %w", arg, err)
|
|
}
|
|
if len(gvks) == 0 {
|
|
return nil, fmt.Errorf("no schema found for resource %s: %w", arg, err)
|
|
}
|
|
gvk := gvks[0]
|
|
mappings, err := f.Client().RESTMapper().RESTMappings(gvk.GroupKind(), gvk.Version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find mappings for resource %s: %w", arg, err)
|
|
}
|
|
if len(mappings) == 0 {
|
|
return nil, fmt.Errorf("no mappings found for resource %s: %w", arg, err)
|
|
}
|
|
mapping := mappings[0]
|
|
or := &resourceRef{GroupVersionKind: gvk, Arg: arg}
|
|
switch len(parts) {
|
|
case 2:
|
|
or.Name = parts[1]
|
|
if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
|
|
or.Namespace = velacmd.GetNamespace(f, cmd)
|
|
}
|
|
case 3:
|
|
or.Namespace = parts[1]
|
|
or.Name = parts[2]
|
|
default:
|
|
return nil, fmt.Errorf("resource should be like <type>/<name> or <type>/<namespace>/<name>")
|
|
}
|
|
return or, nil
|
|
}
|
|
|
|
// Complete autofill fields in opts
|
|
func (opt *AdoptOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) error {
|
|
opt.AppNamespace = velacmd.GetNamespace(f, cmd)
|
|
switch opt.Type {
|
|
case adoptTypeNative:
|
|
for _, arg := range args {
|
|
or, err := opt.parseResourceRef(f, cmd, arg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opt.NativeResourceRefs = append(opt.NativeResourceRefs, or)
|
|
}
|
|
if opt.AppName == "" && velaslices.All(opt.NativeResourceRefs, func(ref *resourceRef) bool {
|
|
return ref.Name == opt.NativeResourceRefs[0].Name
|
|
}) {
|
|
opt.AppName = opt.NativeResourceRefs[0].Name
|
|
}
|
|
case adoptTypeHelm:
|
|
if len(args) > 0 {
|
|
opt.HelmReleaseName = args[0]
|
|
}
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("helm type adoption only support one helm release by far")
|
|
}
|
|
if len(opt.HelmDriver) == 0 {
|
|
opt.HelmDriver = os.Getenv(helmDriverEnvKey)
|
|
}
|
|
if len(opt.HelmDriver) == 0 {
|
|
opt.HelmDriver = defaultHelmDriver
|
|
}
|
|
if opt.AppName == "" {
|
|
opt.AppName = opt.HelmReleaseName
|
|
}
|
|
opt.HelmReleaseNamespace = opt.AppNamespace
|
|
default:
|
|
}
|
|
if opt.AdoptTemplateFile != "" {
|
|
bs, err := os.ReadFile(opt.AdoptTemplateFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load file %s", opt.AdoptTemplateFile)
|
|
}
|
|
opt.AdoptTemplate = string(bs)
|
|
} else {
|
|
opt.AdoptTemplate = defaultAdoptTemplate
|
|
}
|
|
opt.AdoptTemplateCUEValue = cuecontext.New().CompileString(fmt.Sprintf("%s\n\n%s: %s", opt.AdoptTemplate, adoptCUETempVal, adoptCUETempFunc))
|
|
return nil
|
|
}
|
|
|
|
// Validate if opts is valid
|
|
func (opt *AdoptOptions) Validate() error {
|
|
switch opt.Type {
|
|
case adoptTypeNative:
|
|
if len(opt.NativeResourceRefs) == 0 {
|
|
return fmt.Errorf("at least one resource should be specified")
|
|
}
|
|
if opt.AppName == "" {
|
|
return fmt.Errorf("app-name flag must be set for native resource adoption when multiple resources have different names")
|
|
}
|
|
if opt.Recycle {
|
|
return fmt.Errorf("native resource adoption does not support --recycle flag")
|
|
}
|
|
case adoptTypeHelm:
|
|
if len(opt.HelmReleaseName) == 0 {
|
|
return fmt.Errorf("helm release name must not be empty")
|
|
}
|
|
default:
|
|
return fmt.Errorf("invalid adopt type: %s, available types: [%s]", opt.Type, strings.Join(adoptTypes, ", "))
|
|
}
|
|
if slices.Index(adoptModes, opt.Mode) < 0 {
|
|
return fmt.Errorf("invalid adopt mode: %s, available modes: [%s]", opt.Mode, strings.Join(adoptModes, ", "))
|
|
}
|
|
if opt.Recycle && !opt.Apply {
|
|
return fmt.Errorf("old data can only be recycled when the adoption application is applied")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (opt *AdoptOptions) loadNative(f velacmd.Factory, cmd *cobra.Command) error {
|
|
for _, ref := range opt.NativeResourceRefs {
|
|
obj := &unstructured.Unstructured{}
|
|
obj.SetGroupVersionKind(ref.GroupVersionKind)
|
|
if err := f.Client().Get(cmd.Context(), apitypes.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, obj); err != nil {
|
|
return fmt.Errorf("failed to get resource for %s: %w", ref.Arg, err)
|
|
}
|
|
opt.Resources = append(opt.Resources, obj)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (opt *AdoptOptions) loadHelm(f velacmd.Factory) error {
|
|
actionConfig := new(action.Configuration)
|
|
err := actionConfig.Init(
|
|
util.NewRestConfigGetterByConfig(f.Config(), opt.HelmReleaseNamespace),
|
|
opt.HelmReleaseNamespace,
|
|
opt.HelmDriver,
|
|
klog.Infof)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
opt.HelmStore = actionConfig.Releases
|
|
releases, err := opt.HelmStore.History(opt.HelmReleaseName)
|
|
if err != nil {
|
|
return fmt.Errorf("helm release %s/%s not loaded: %w", opt.HelmReleaseNamespace, opt.HelmReleaseName, err)
|
|
}
|
|
if len(releases) == 0 {
|
|
return fmt.Errorf("helm release %s/%s not found", opt.HelmReleaseNamespace, opt.HelmReleaseName)
|
|
}
|
|
releaseutil.SortByRevision(releases)
|
|
opt.HelmRelease = releases[len(releases)-1]
|
|
opt.HelmReleases = releases
|
|
manifests := releaseutil.SplitManifests(opt.HelmRelease.Manifest)
|
|
var objs []*unstructured.Unstructured
|
|
for _, val := range manifests {
|
|
obj := &unstructured.Unstructured{}
|
|
if err = yaml.Unmarshal([]byte(val), obj); err != nil {
|
|
klog.Warningf("unable to decode object %s: %s", val, err)
|
|
continue
|
|
}
|
|
objs = append(objs, obj)
|
|
}
|
|
opt.Resources = objs
|
|
return nil
|
|
}
|
|
|
|
func (opt *AdoptOptions) render() (*v1beta1.Application, error) {
|
|
app := &v1beta1.Application{}
|
|
val := opt.AdoptTemplateCUEValue.FillPath(cue.ParsePath(adoptCUETempVal+".$args"), opt)
|
|
bs, err := val.LookupPath(cue.ParsePath(adoptCUETempVal + ".$returns")).MarshalJSON()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse adoption template: %w", err)
|
|
}
|
|
if err = json.Unmarshal(bs, app); err != nil {
|
|
return nil, fmt.Errorf("failed to parse template $returns into application: %w", err)
|
|
}
|
|
return app, nil
|
|
}
|
|
|
|
// Run collect resources, assemble into application and print/apply
|
|
func (opt *AdoptOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
|
switch opt.Type {
|
|
case adoptTypeNative:
|
|
if err := opt.loadNative(f, cmd); err != nil {
|
|
return fmt.Errorf("failed to load native resources: %w", err)
|
|
}
|
|
case adoptTypeHelm:
|
|
if err := opt.loadHelm(f); err != nil {
|
|
return fmt.Errorf("failed to load resources from helm release %s/%s: %w", opt.HelmReleaseNamespace, opt.HelmReleaseName, err)
|
|
}
|
|
default:
|
|
}
|
|
app, err := opt.render()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to make adoption application for resources: %w", err)
|
|
}
|
|
if opt.Apply {
|
|
if err = apply.NewAPIApplicator(f.Client()).Apply(cmd.Context(), app); err != nil {
|
|
return fmt.Errorf("failed to apply application %s/%s: %w", app.Namespace, app.Name, err)
|
|
}
|
|
_, _ = fmt.Fprintf(opt.Out, "resources adopted in app %s/%s\n", app.Namespace, app.Name)
|
|
} else {
|
|
var bs []byte
|
|
if bs, err = yaml.Marshal(app); err != nil {
|
|
return fmt.Errorf("failed to encode application into YAML format: %w", err)
|
|
}
|
|
_, _ = opt.Out.Write(bs)
|
|
}
|
|
if opt.Recycle && opt.Apply {
|
|
spinner := newTrackingSpinner("")
|
|
spinner.Writer = opt.Out
|
|
spinner.Start()
|
|
err = wait.PollImmediate(time.Second, time.Minute, func() (done bool, err error) {
|
|
_app := &v1beta1.Application{}
|
|
if err = f.Client().Get(cmd.Context(), client.ObjectKeyFromObject(app), _app); err != nil {
|
|
return false, err
|
|
}
|
|
spinner.UpdateCharSet([]string{fmt.Sprintf("waiting application %s/%s to be running, current status: %s", app.Namespace, app.Name, _app.Status.Phase)})
|
|
return _app.Status.Phase == common.ApplicationRunning, nil
|
|
})
|
|
spinner.Stop()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to wait application %s/%s to be running: %w", app.Namespace, app.Name, err)
|
|
}
|
|
switch opt.Type {
|
|
case adoptTypeHelm:
|
|
for _, r := range opt.HelmReleases {
|
|
if _, err = opt.HelmStore.Delete(r.Name, r.Version); err != nil {
|
|
return fmt.Errorf("failed to clean up helm release: %w", err)
|
|
}
|
|
}
|
|
_, _ = fmt.Fprintf(opt.Out, "successfully clean up old helm release\n")
|
|
default:
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
adoptLong = templates.LongDesc(i18n.T(`
|
|
Adopt resources into applications
|
|
|
|
Adopt resources into a KubeVela application. This command is useful when you already
|
|
have resources applied in your Kubernetes cluster. These resources could be applied
|
|
natively or with other tools, such as Helm. This command will automatically find out
|
|
the resources to be adopted and assemble them into a new application which won't
|
|
trigger any damage such as restart on the adoption.
|
|
|
|
There are two types of adoption supported by far, 'native' Kubernetes resources (by
|
|
default) and 'helm' releases.
|
|
1. For 'native' type, you can specify a list of resources you want to adopt in the
|
|
application. Only resources in local cluster are supported for now.
|
|
2. For 'helm' type, you can specify a helm release name. This helm release should
|
|
be already published in the local cluster. The command will find the resources
|
|
managed by the helm release and convert them into an adoption application.
|
|
|
|
There are two working mechanism (called 'modes' here) for the adoption by far,
|
|
'read-only' mode (by default) and 'take-over' mode.
|
|
1. In 'read-only' mode, adopted resources will not be touched. You can leverage vela
|
|
tools (like Vela CLI or VelaUX) to observe those resources and attach traits to add
|
|
new capabilities. The adopted resources will not be recycled or updated. This mode
|
|
is recommended if you still want to keep using other tools to manage resources updates
|
|
or deletion, like Helm.
|
|
2. In 'take-over' mode, adopted resources are completely managed by application which
|
|
means they can be modified. You can use traits or directly modify the component to make
|
|
edits to those resources. This mode can be helpful if you want to migrate existing
|
|
resources into KubeVela system and let KubeVela to handle the life-cycle of target
|
|
resources.
|
|
|
|
The adopted application can be customized. You can provide a CUE template file to
|
|
the command and make your own assemble rules for the adoption application. You can
|
|
refer to https://github.com/kubevela/kubevela/blob/master/references/cli/adopt-templates/default.cue
|
|
to see the default implementation of adoption rules.
|
|
`))
|
|
adoptExample = templates.Examples(i18n.T(`
|
|
# Native Resources Adoption
|
|
|
|
## Adopt resources into new application
|
|
## Use: vela adopt <resources-type>[/<resource-namespace>]/<resource-name> <resources-type>[/<resource-namespace>]/<resource-name> ...
|
|
vela adopt deployment/my-app configmap/my-app
|
|
|
|
## Adopt resources into new application with specified app name
|
|
vela adopt deployment/my-deploy configmap/my-config --app-name my-app
|
|
|
|
## Adopt resources into new application in specified namespace
|
|
vela adopt deployment/my-app configmap/my-app -n demo
|
|
|
|
## Adopt resources into new application across multiple namespace
|
|
vela adopt deployment/ns-1/my-app configmap/ns-2/my-app
|
|
|
|
## Adopt resources into new application with take-over mode
|
|
vela adopt deployment/my-app configmap/my-app --mode take-over
|
|
|
|
## Adopt resources into new application and apply it into cluster
|
|
vela adopt deployment/my-app configmap/my-app --apply
|
|
|
|
-----------------------------------------------------------
|
|
|
|
# Helm Chart Adoption
|
|
|
|
## Adopt resources in a deployed helm chart
|
|
vela adopt my-chart -n my-namespace --type helm
|
|
|
|
## Adopt resources in a deployed helm chart with take-over mode
|
|
vela adopt my-chart --type helm --mode take-over
|
|
|
|
## Adopt resources in a deployed helm chart in an application and apply it into cluster
|
|
vela adopt my-chart --type helm --apply
|
|
|
|
## Adopt resources in a deployed helm chart in an application, apply it into cluster, and recycle the old helm release after the adoption application successfully runs
|
|
vela adopt my-chart --type helm --apply --recycle
|
|
|
|
-----------------------------------------------------------
|
|
|
|
## Customize your adoption rules
|
|
vela adopt my-chart -n my-namespace --type helm --adopt-template my-rules.cue
|
|
`))
|
|
)
|
|
|
|
// NewAdoptCommand command for adopt resources into KubeVela Application
|
|
func NewAdoptCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
|
|
o := &AdoptOptions{
|
|
Type: adoptTypeNative,
|
|
Mode: adoptModeReadOnly,
|
|
IOStreams: streams,
|
|
}
|
|
cmd := &cobra.Command{
|
|
Use: "adopt",
|
|
Short: i18n.T("Adopt resources into new application"),
|
|
Long: adoptLong,
|
|
Example: adoptExample,
|
|
Annotations: map[string]string{
|
|
types.TagCommandType: types.TypeCD,
|
|
},
|
|
Args: cobra.MinimumNArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
|
cmdutil.CheckErr(o.Validate())
|
|
cmdutil.CheckErr(o.Run(f, cmd))
|
|
},
|
|
}
|
|
cmd.Flags().StringVarP(&o.Type, "type", "t", o.Type, fmt.Sprintf("The type of adoption. Available values: [%s]", strings.Join(adoptTypes, ", ")))
|
|
cmd.Flags().StringVarP(&o.Mode, "mode", "m", o.Mode, fmt.Sprintf("The mode of adoption. Available values: [%s]", strings.Join(adoptModes, ", ")))
|
|
cmd.Flags().StringVarP(&o.AppName, "app-name", "", o.AppName, "The name of application for adoption. If empty for helm type adoption, it will inherit the helm chart's name.")
|
|
cmd.Flags().StringVarP(&o.AdoptTemplateFile, "adopt-template", "", o.AdoptTemplate, "The CUE template for adoption. If not provided, the default template will be used when --auto is switched on.")
|
|
cmd.Flags().StringVarP(&o.HelmDriver, "driver", "d", o.HelmDriver, "The storage backend of helm adoption. Only take effect when --type=helm.")
|
|
cmd.Flags().BoolVarP(&o.Apply, "apply", "", o.Apply, "If true, the application for adoption will be applied. Otherwise, it will only be printed.")
|
|
cmd.Flags().BoolVarP(&o.Recycle, "recycle", "", o.Recycle, "If true, when the adoption application is successfully applied, the old storage (like Helm secret) will be recycled.")
|
|
return velacmd.NewCommandBuilder(f, cmd).
|
|
WithNamespaceFlag().
|
|
WithResponsiveWriter().
|
|
Build()
|
|
}
|