Files
kubevela/references/common/application.go
wyike 74a82e0397 add header-check.sh (#1310)
add ci workflow

update scripts

add missed licence  header for all files
2021-03-26 15:24:19 +08:00

523 lines
16 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 common
import (
"bytes"
"context"
j "encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
apitypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
corev1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/apiserver/apis"
"github.com/oam-dev/kubevela/references/appfile"
"github.com/oam-dev/kubevela/references/appfile/api"
"github.com/oam-dev/kubevela/references/appfile/template"
)
// nolint:golint
const (
DefaultChosenAllSvc = "ALL SERVICES"
FlagNotSet = "FlagNotSet"
FlagIsInvalid = "FlagIsInvalid"
FlagIsValid = "FlagIsValid"
)
type componentMetaList []apis.ComponentMeta
type applicationMetaList []apis.ApplicationMeta
// AppfileOptions is some configuration that modify options for an Appfile
type AppfileOptions struct {
Kubecli client.Client
IO cmdutil.IOStreams
Env *types.EnvMeta
}
// BuildResult is the export struct from AppFile yaml or AppFile object
type BuildResult struct {
appFile *api.AppFile
application *corev1beta1.Application
scopes []oam.Object
}
func (comps componentMetaList) Len() int {
return len(comps)
}
func (comps componentMetaList) Swap(i, j int) {
comps[i], comps[j] = comps[j], comps[i]
}
func (comps componentMetaList) Less(i, j int) bool {
return comps[i].CreatedTime > comps[j].CreatedTime
}
func (a applicationMetaList) Len() int {
return len(a)
}
func (a applicationMetaList) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a applicationMetaList) Less(i, j int) bool {
return a[i].CreatedTime > a[j].CreatedTime
}
// Option is option work with dashboard api server
type Option struct {
// Optional filter, if specified, only components in such app will be listed
AppName string
Namespace string
}
// DeleteOptions is options for delete
type DeleteOptions struct {
AppName string
CompName string
Client client.Client
Env *types.EnvMeta
C common.Args
}
// ListApplications lists all applications
func ListApplications(ctx context.Context, c client.Reader, opt Option) ([]apis.ApplicationMeta, error) {
var applicationMetaList applicationMetaList
var appList corev1beta1.ApplicationList
if opt.AppName != "" {
var app corev1beta1.Application
if err := c.Get(ctx, client.ObjectKey{Name: opt.AppName, Namespace: opt.Namespace}, &app); err != nil {
return applicationMetaList, err
}
appList.Items = append(appList.Items, app)
} else {
err := c.List(ctx, &appList, &client.ListOptions{Namespace: opt.Namespace})
if err != nil {
return applicationMetaList, err
}
}
for _, a := range appList.Items {
// ignore the deleted resource
if a.GetDeletionGracePeriodSeconds() != nil {
continue
}
applicationMeta, err := RetrieveApplicationStatusByName(ctx, c, a.Name, a.Namespace)
if err != nil {
return applicationMetaList, err
}
applicationMeta.Components = nil
applicationMetaList = append(applicationMetaList, applicationMeta)
}
sort.Stable(applicationMetaList)
return applicationMetaList, nil
}
// ListApplicationConfigurations lists all OAM ApplicationConfiguration
func ListApplicationConfigurations(ctx context.Context, c client.Reader, opt Option) (corev1alpha2.ApplicationConfigurationList, error) {
var appConfigList corev1alpha2.ApplicationConfigurationList
if opt.AppName != "" {
var appConfig corev1alpha2.ApplicationConfiguration
if err := c.Get(ctx, client.ObjectKey{Name: opt.AppName, Namespace: opt.Namespace}, &appConfig); err != nil {
return appConfigList, err
}
appConfigList.Items = append(appConfigList.Items, appConfig)
} else {
err := c.List(ctx, &appConfigList, &client.ListOptions{Namespace: opt.Namespace})
if err != nil {
return appConfigList, err
}
}
return appConfigList, nil
}
// ListComponents will list all components for dashboard
func ListComponents(ctx context.Context, c client.Reader, opt Option) ([]apis.ComponentMeta, error) {
var componentMetaList componentMetaList
var appConfigList corev1alpha2.ApplicationConfigurationList
var err error
if appConfigList, err = ListApplicationConfigurations(ctx, c, opt); err != nil {
return nil, err
}
for _, a := range appConfigList.Items {
for _, com := range a.Spec.Components {
component, _, err := oamutil.GetComponent(ctx, c, com, opt.Namespace)
if err != nil {
return componentMetaList, err
}
componentMetaList = append(componentMetaList, apis.ComponentMeta{
Name: com.ComponentName,
Status: types.StatusDeployed,
CreatedTime: a.ObjectMeta.CreationTimestamp.String(),
Component: *component,
AppConfig: a,
App: a.Name,
})
}
}
sort.Stable(componentMetaList)
return componentMetaList, nil
}
// RetrieveApplicationStatusByName will get app status
func RetrieveApplicationStatusByName(ctx context.Context, c client.Reader, applicationName string,
namespace string) (apis.ApplicationMeta, error) {
var applicationMeta apis.ApplicationMeta
var app corev1beta1.Application
if err := c.Get(ctx, client.ObjectKey{Name: applicationName, Namespace: namespace}, &app); err != nil {
return applicationMeta, err
}
applicationMeta.Name = app.Name
applicationMeta.Status = string(app.Status.Phase)
applicationMeta.CreatedTime = app.CreationTimestamp.Format(time.RFC3339)
return applicationMeta, nil
}
// DeleteApp will delete app including server side
func (o *DeleteOptions) DeleteApp() (string, error) {
if err := appfile.Delete(o.Env.Name, o.AppName); err != nil && !os.IsNotExist(err) {
return "", err
}
ctx := context.Background()
var app = new(corev1beta1.Application)
err := o.Client.Get(ctx, client.ObjectKey{Name: o.AppName, Namespace: o.Env.Namespace}, app)
if err != nil {
if apierrors.IsNotFound(err) {
return fmt.Sprintf("app \"%s\" already deleted", o.AppName), nil
}
return "", fmt.Errorf("delete appconfig err: %w", err)
}
err = o.Client.Delete(ctx, app)
if err != nil && !apierrors.IsNotFound(err) {
return "", fmt.Errorf("delete application err: %w", err)
}
for _, cmp := range app.Spec.Components {
healthScopeName, ok := cmp.Scopes[api.DefaultHealthScopeKey]
if ok {
var healthScope corev1alpha2.HealthScope
if err := o.Client.Get(ctx, client.ObjectKey{Namespace: o.Env.Namespace, Name: healthScopeName}, &healthScope); err != nil {
if apierrors.IsNotFound(err) {
continue
}
return "", fmt.Errorf("delete health scope %s err: %w", healthScopeName, err)
}
if err = o.Client.Delete(ctx, &healthScope); err != nil {
return "", fmt.Errorf("delete health scope %s err: %w", healthScopeName, err)
}
}
}
return fmt.Sprintf("app \"%s\" deleted from env \"%s\"", o.AppName, o.Env.Name), nil
}
// DeleteComponent will delete one component including server side.
func (o *DeleteOptions) DeleteComponent(io cmdutil.IOStreams) (string, error) {
var err error
if o.AppName == "" {
return "", errors.New("app name is required")
}
app, err := appfile.LoadApplication(o.Env.Namespace, o.AppName, o.C)
if err != nil {
return "", err
}
if len(appfile.GetComponents(app)) <= 1 {
return o.DeleteApp()
}
// Remove component from local appfile
if err := appfile.RemoveComponent(app, o.CompName); err != nil {
return "", err
}
// Remove component from appConfig in k8s cluster
ctx := context.Background()
if err := o.Client.Update(ctx, app); err != nil {
return "", err
}
// It's the server responsibility to GC component
return fmt.Sprintf("component \"%s\" deleted from \"%s\"", o.CompName, o.AppName), nil
}
func chooseSvc(services []string) (string, error) {
var svcName string
services = append(services, DefaultChosenAllSvc)
prompt := &survey.Select{
Message: "Please choose one service: ",
Options: services,
Default: DefaultChosenAllSvc,
}
err := survey.AskOne(prompt, &svcName)
if err != nil {
return "", fmt.Errorf("failed to retrieve services of the application, err %w", err)
}
return svcName, nil
}
// GetServicesWhenDescribingApplication gets the target services list either from cli `--svc` flag or from survey
func GetServicesWhenDescribingApplication(cmd *cobra.Command, app *api.Application) ([]string, error) {
var svcFlag string
var svcFlagStatus string
// to store the value of flag `--svc` set in Cli, or selected value in survey
var targetServices []string
if svcFlag = cmd.Flag("svc").Value.String(); svcFlag == "" {
svcFlagStatus = FlagNotSet
} else {
svcFlagStatus = FlagIsInvalid
}
// all services name of the application `appName`
var services []string
for svcName := range app.Services {
services = append(services, svcName)
if svcFlag == svcName {
svcFlagStatus = FlagIsValid
targetServices = append(targetServices, svcName)
}
}
totalServices := len(services)
if svcFlagStatus == FlagNotSet && totalServices == 1 {
targetServices = services
}
if svcFlagStatus == FlagIsInvalid || (svcFlagStatus == FlagNotSet && totalServices > 1) {
if svcFlagStatus == FlagIsInvalid {
cmd.Printf("The service name '%s' is not valid\n", svcFlag)
}
chosenSvc, err := chooseSvc(services)
if err != nil {
return []string{}, err
}
if chosenSvc == DefaultChosenAllSvc {
targetServices = services
} else {
targetServices = targetServices[:0]
targetServices = append(targetServices, chosenSvc)
}
}
return targetServices, nil
}
func saveAndLoadRemoteAppfile(url string) (*api.AppFile, error) {
body, err := common.HTTPGet(context.Background(), url)
if err != nil {
return nil, err
}
af := api.NewAppFile()
ext := filepath.Ext(url)
dest := "Appfile"
switch ext {
case ".json":
dest = "vela.json"
af, err = api.JSONToYaml(body, af)
case ".yaml", ".yml":
dest = "vela.yaml"
err = yaml.Unmarshal(body, af)
default:
if j.Valid(body) {
af, err = api.JSONToYaml(body, af)
} else {
err = yaml.Unmarshal(body, af)
}
}
if err != nil {
return nil, err
}
//nolint:gosec
return af, ioutil.WriteFile(dest, body, 0644)
}
// ExportFromAppFile exports Application from appfile object
func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, namespace string, quiet bool, c common.Args) (*BuildResult, []byte, error) {
tm, err := template.Load(namespace, c)
if err != nil {
return nil, nil, err
}
appHandler := appfile.NewApplication(app, tm)
// new
retApplication, scopes, err := appHandler.BuildOAMApplication(o.Env, o.IO, appHandler.Tm, quiet)
if err != nil {
return nil, nil, err
}
var w bytes.Buffer
options := json.SerializerOptions{Yaml: true, Pretty: false, Strict: false}
enc := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, options)
err = enc.Encode(retApplication, &w)
if err != nil {
return nil, nil, fmt.Errorf("yaml encode application failed: %w", err)
}
w.WriteByte('\n')
for _, scope := range scopes {
w.WriteString("---\n")
err = enc.Encode(scope, &w)
if err != nil {
return nil, nil, fmt.Errorf("yaml encode scope (%s) failed: %w", scope.GetName(), err)
}
w.WriteByte('\n')
}
result := &BuildResult{
appFile: app,
application: retApplication,
scopes: scopes,
}
return result, w.Bytes(), nil
}
// Export export Application object from the path of Appfile
func (o *AppfileOptions) Export(filePath, namespace string, quiet bool, c common.Args) (*BuildResult, []byte, error) {
var app *api.AppFile
var err error
if !quiet {
o.IO.Info("Parsing vela appfile ...")
}
if filePath != "" {
if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") {
app, err = saveAndLoadRemoteAppfile(filePath)
} else {
app, err = api.LoadFromFile(filePath)
}
} else {
app, err = api.Load()
}
if err != nil {
return nil, nil, err
}
if !quiet {
o.IO.Info("Load Template ...")
}
return o.ExportFromAppFile(app, namespace, quiet, c)
}
// Run starts an application according to Appfile
func (o *AppfileOptions) Run(filePath, namespace string, c common.Args) error {
result, data, err := o.Export(filePath, namespace, false, c)
if err != nil {
return err
}
return o.BaseAppFileRun(result, data, c)
}
// BaseAppFileRun starts an application according to Appfile
func (o *AppfileOptions) BaseAppFileRun(result *BuildResult, data []byte, args common.Args) error {
deployFilePath := ".vela/deploy.yaml"
o.IO.Infof("Writing deploy config to (%s)\n", deployFilePath)
if err := os.MkdirAll(filepath.Dir(deployFilePath), 0700); err != nil {
return err
}
if err := ioutil.WriteFile(deployFilePath, data, 0600); err != nil {
return errors.Wrap(err, "write deploy config manifests failed")
}
if err := o.saveToAppDir(result.appFile); err != nil {
return errors.Wrap(err, "save to app dir failed")
}
kubernetesComponent, err := appfile.ApplyTerraform(result.application, o.Kubecli, o.IO, o.Env.Namespace, args)
if err != nil {
return err
}
result.application.Spec.Components = kubernetesComponent
o.IO.Infof("\nApplying application ...\n")
return o.ApplyApp(result.application, result.scopes)
}
func (o *AppfileOptions) saveToAppDir(f *api.AppFile) error {
app := &api.Application{AppFile: f}
return appfile.Save(app, o.Env.Name)
}
// ApplyApp applys config resources for the app.
// It differs by create and update:
// - for create, it displays app status along with information of url, metrics, ssh, logging.
// - for update, it rolls out a canary deployment and prints its information. User can verify the canary deployment.
// This will wait for user approval. If approved, it continues upgrading the whole; otherwise, it would rollback.
func (o *AppfileOptions) ApplyApp(app *corev1beta1.Application, scopes []oam.Object) error {
key := apitypes.NamespacedName{
Namespace: app.Namespace,
Name: app.Name,
}
o.IO.Infof("Checking if app has been deployed...\n")
var tmpApp corev1beta1.Application
err := o.Kubecli.Get(context.TODO(), key, &tmpApp)
switch {
case apierrors.IsNotFound(err):
o.IO.Infof("App has not been deployed, creating a new deployment...\n")
case err == nil:
o.IO.Infof("App exists, updating existing deployment...\n")
default:
return err
}
if err := o.apply(app, scopes); err != nil {
return err
}
o.IO.Infof(o.Info(app))
return nil
}
func (o *AppfileOptions) apply(app *corev1beta1.Application, scopes []oam.Object) error {
if err := appfile.Run(context.TODO(), o.Kubecli, app, scopes); err != nil {
return err
}
return nil
}
// Info shows the status of each service in the Appfile
func (o *AppfileOptions) Info(app *corev1beta1.Application) string {
appName := app.Name
var appUpMessage = "✅ App has been deployed 🚀🚀🚀\n" +
fmt.Sprintf(" Port forward: vela port-forward %s\n", appName) +
fmt.Sprintf(" SSH: vela exec %s\n", appName) +
fmt.Sprintf(" Logging: vela logs %s\n", appName) +
fmt.Sprintf(" App status: vela status %s\n", appName)
for _, comp := range app.Spec.Components {
appUpMessage += fmt.Sprintf(" Service status: vela status %s --svc %s\n", appName, comp.Name)
}
return appUpMessage
}