mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
Feat: support vela up from remote url file (#3075)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
This commit is contained in:
@@ -24,15 +24,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/builtin"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/appfile/template"
|
||||
)
|
||||
@@ -103,20 +99,17 @@ func LoadFromFile(filename string) (*AppFile, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return LoadFromBytes(b)
|
||||
}
|
||||
|
||||
// LoadFromBytes will load AppFile from bytes
|
||||
func LoadFromBytes(b []byte) (*AppFile, error) {
|
||||
af := NewAppFile()
|
||||
// Add JSON format appfile support
|
||||
ext := filepath.Ext(filename)
|
||||
switch ext {
|
||||
case ".yaml", ".yml":
|
||||
err = yaml.Unmarshal(b, af)
|
||||
case ".json":
|
||||
var err error
|
||||
if json.Valid(b) {
|
||||
af, err = JSONToYaml(b, af)
|
||||
default:
|
||||
if json.Valid(b) {
|
||||
af, err = JSONToYaml(b, af)
|
||||
} else {
|
||||
err = yaml.Unmarshal(b, af)
|
||||
}
|
||||
} else {
|
||||
err = yaml.Unmarshal(b, af)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -140,16 +133,15 @@ func (app *AppFile) ExecuteAppfileTasks(io cmdutil.IOStreams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildOAMApplication renders Appfile into Application, Scopes and other K8s Resources.
|
||||
func (app *AppFile) BuildOAMApplication(namespace string, io cmdutil.IOStreams, tm template.Manager, silence bool) (*v1beta1.Application, []oam.Object, error) {
|
||||
// ConvertToApplication renders Appfile into Application, Scopes and other K8s Resources.
|
||||
func (app *AppFile) ConvertToApplication(namespace string, io cmdutil.IOStreams, tm template.Manager, silence bool) (*v1beta1.Application, error) {
|
||||
if err := app.ExecuteAppfileTasks(io); err != nil {
|
||||
if strings.Contains(err.Error(), "'image' : not found") {
|
||||
return nil, nil, ErrImageNotDefined
|
||||
return nil, ErrImageNotDefined
|
||||
}
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
// auxiliaryObjects currently include OAM Scope Custom Resources and ConfigMaps
|
||||
var auxiliaryObjects []oam.Object
|
||||
servApp := new(v1beta1.Application)
|
||||
servApp.SetNamespace(namespace)
|
||||
servApp.SetName(app.Name)
|
||||
@@ -160,33 +152,10 @@ func (app *AppFile) BuildOAMApplication(namespace string, io cmdutil.IOStreams,
|
||||
for serviceName, svc := range app.GetServices() {
|
||||
comp, err := svc.RenderServiceToApplicationComponent(tm, serviceName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
servApp.Spec.Components = append(servApp.Spec.Components, comp)
|
||||
}
|
||||
servApp.SetGroupVersionKind(v1beta1.SchemeGroupVersion.WithKind("Application"))
|
||||
auxiliaryObjects = append(auxiliaryObjects, addDefaultHealthScopeToApplication(servApp))
|
||||
return servApp, auxiliaryObjects, nil
|
||||
}
|
||||
|
||||
func addDefaultHealthScopeToApplication(app *v1beta1.Application) *v1alpha2.HealthScope {
|
||||
health := &v1alpha2.HealthScope{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1alpha2.HealthScopeGroupVersionKind.GroupVersion().String(),
|
||||
Kind: v1alpha2.HealthScopeKind,
|
||||
},
|
||||
}
|
||||
health.Name = FormatDefaultHealthScopeName(app.Name)
|
||||
health.Namespace = app.Namespace
|
||||
health.Spec.WorkloadReferences = make([]corev1.ObjectReference, 0)
|
||||
for i := range app.Spec.Components {
|
||||
// FIXME(wonderflow): the hardcode health scope should be fixed.
|
||||
app.Spec.Components[i].Scopes = map[string]string{DefaultHealthScopeKey: health.Name}
|
||||
}
|
||||
return health
|
||||
}
|
||||
|
||||
// FormatDefaultHealthScopeName will create a default health scope name.
|
||||
func FormatDefaultHealthScopeName(appName string) string {
|
||||
return appName + "-default-health"
|
||||
return servApp, nil
|
||||
}
|
||||
|
||||
@@ -21,13 +21,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
@@ -80,7 +78,6 @@ func TestBuildOAMApplication2(t *testing.T) {
|
||||
Properties: &runtime.RawExtension{
|
||||
Raw: []byte("{\"image\":\"busybox\"}"),
|
||||
},
|
||||
Scopes: map[string]string{"healthscopes.core.oam.dev": "test-default-health"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -114,7 +111,6 @@ func TestBuildOAMApplication2(t *testing.T) {
|
||||
Properties: &runtime.RawExtension{
|
||||
Raw: []byte("{\"image\":\"busybox\"}"),
|
||||
},
|
||||
Scopes: map[string]string{"healthscopes.core.oam.dev": "test-default-health"},
|
||||
Traits: []common.ApplicationTrait{
|
||||
{
|
||||
Type: "scaler",
|
||||
@@ -132,7 +128,7 @@ func TestBuildOAMApplication2(t *testing.T) {
|
||||
|
||||
for _, tcase := range testCases {
|
||||
tcase.expectApp.Namespace = expectNs
|
||||
o, _, err := tcase.appFile.BuildOAMApplication(expectNs, cmdutil.IOStreams{
|
||||
o, err := tcase.appFile.ConvertToApplication(expectNs, cmdutil.IOStreams{
|
||||
In: os.Stdin,
|
||||
Out: os.Stdout,
|
||||
}, tm, false)
|
||||
@@ -258,9 +254,8 @@ outputs: ingress: {
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{{
|
||||
Type: "webservice",
|
||||
Name: "express-server",
|
||||
Scopes: map[string]string{"healthscopes.core.oam.dev": "myapp-default-health"},
|
||||
Type: "webservice",
|
||||
Name: "express-server",
|
||||
Properties: &runtime.RawExtension{
|
||||
Raw: []byte(`{"image": "oamdev/testapp:v1", "cmd": ["node", "server.js"]}`),
|
||||
},
|
||||
@@ -282,7 +277,6 @@ outputs: ingress: {
|
||||
Raw: []byte(`{"image":"bitnami/mongodb:3.6.20","cmd": ["mongodb"]}`),
|
||||
},
|
||||
Traits: []common.ApplicationTrait{},
|
||||
Scopes: map[string]string{"healthscopes.core.oam.dev": "myapp-default-health"},
|
||||
})
|
||||
|
||||
ac3 := ac1.DeepCopy()
|
||||
@@ -291,15 +285,6 @@ outputs: ingress: {
|
||||
// TODO application 那边补测试:
|
||||
// 2. 1对多的情况,多对1 的情况
|
||||
|
||||
health := &v1alpha2.HealthScope{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1alpha2.HealthScopeGroupVersionKind.GroupVersion().String(),
|
||||
Kind: v1alpha2.HealthScopeKind,
|
||||
},
|
||||
}
|
||||
health.Name = FormatDefaultHealthScopeName("myapp")
|
||||
health.Namespace = "default"
|
||||
health.Spec.WorkloadReferences = make([]corev1.ObjectReference, 0)
|
||||
type args struct {
|
||||
appfileData string
|
||||
workloadTemplates map[string]string
|
||||
@@ -326,7 +311,7 @@ outputs: ingress: {
|
||||
},
|
||||
want: want{
|
||||
app: ac1,
|
||||
objs: []oam.Object{health},
|
||||
objs: []oam.Object{},
|
||||
},
|
||||
},
|
||||
"two services should generate two components and one appconfig": {
|
||||
@@ -342,7 +327,7 @@ outputs: ingress: {
|
||||
},
|
||||
want: want{
|
||||
app: ac2,
|
||||
objs: []oam.Object{health},
|
||||
objs: []oam.Object{},
|
||||
},
|
||||
},
|
||||
"no image should fail": {
|
||||
@@ -378,7 +363,7 @@ outputs: ingress: {
|
||||
}
|
||||
}
|
||||
|
||||
application, objects, err := app.BuildOAMApplication("default", io, tm, false)
|
||||
application, err := app.ConvertToApplication("default", io, tm, false)
|
||||
if c.want.err != nil {
|
||||
assert.Equal(t, c.want.err, err)
|
||||
return
|
||||
@@ -412,9 +397,6 @@ outputs: ingress: {
|
||||
}
|
||||
assert.Equal(t, true, found, "no component found for %s", comp.Name)
|
||||
}
|
||||
for idx, v := range objects {
|
||||
assert.Equal(t, c.want.objs[idx], v)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ limitations under the License.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"sigs.k8s.io/yaml"
|
||||
@@ -38,7 +35,7 @@ func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cob
|
||||
Use: "up",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Apply an appfile or application from file",
|
||||
Long: "Create or update vela application from file, both appfile or application object format are supported.",
|
||||
Long: "Create or update vela application from file or URL, both appfile or application object format are supported.",
|
||||
Annotations: map[string]string{
|
||||
types.TagCommandOrder: order,
|
||||
types.TagCommandType: types.TypeStart,
|
||||
@@ -52,21 +49,13 @@ func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cob
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileContent, err := os.ReadFile(filepath.Clean(*appFilePath))
|
||||
|
||||
body, err := common.ReadRemoteOrLocalPath(*appFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var app corev1beta1.Application
|
||||
err = yaml.Unmarshal(fileContent, &app)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
|
||||
}
|
||||
if app.APIVersion != "" && app.Kind != "" {
|
||||
err = common.ApplyApplication(app, ioStream, kubecli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
if common.IsAppfile(body) {
|
||||
o := &common.AppfileOptions{
|
||||
Kubecli: kubecli,
|
||||
IO: ioStream,
|
||||
@@ -74,11 +63,20 @@ func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cob
|
||||
}
|
||||
return o.Run(*appFilePath, o.Namespace, c)
|
||||
}
|
||||
var app corev1beta1.Application
|
||||
err = yaml.Unmarshal(body, &app)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
|
||||
}
|
||||
err = common.ApplyApplication(app, ioStream, kubecli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.SetOut(ioStream.Out)
|
||||
cmd.Flags().StringVarP(appFilePath, "file", "f", "", "specify file path for appfile or application")
|
||||
cmd.Flags().StringVarP(appFilePath, "file", "f", "", "specify file path for appfile or application, it could be a remote url.")
|
||||
|
||||
addNamespaceAndEnvArg(cmd)
|
||||
return cmd
|
||||
|
||||
@@ -18,26 +18,19 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/common"
|
||||
)
|
||||
|
||||
func TestUp(t *testing.T) {
|
||||
ioStream := util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
|
||||
namespace := "up-ns"
|
||||
o := common.AppfileOptions{
|
||||
IO: ioStream,
|
||||
Namespace: namespace,
|
||||
}
|
||||
|
||||
app := &v1beta1.Application{}
|
||||
app.Name = "app-up"
|
||||
msg := o.Info(app)
|
||||
msg := common.Info(app)
|
||||
assert.Contains(t, msg, "App has been deployed")
|
||||
assert.Contains(t, msg, fmt.Sprintf("App status: vela status %s", app.Name))
|
||||
}
|
||||
|
||||
@@ -26,9 +26,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
||||
"github.com/fatih/color"
|
||||
"github.com/gosuri/uilive"
|
||||
"github.com/pkg/errors"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -240,33 +239,71 @@ func (o *DeleteOptions) DeleteComponent(io cmdutil.IOStreams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveAndLoadRemoteAppfile(url string) (*api.AppFile, error) {
|
||||
body, err := common.HTTPGet(context.Background(), url)
|
||||
// LoadAppFile will load vela appfile from remote URL or local file system.
|
||||
func LoadAppFile(pathOrURL string) (*api.AppFile, error) {
|
||||
body, err := ReadRemoteOrLocalPath(pathOrURL)
|
||||
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)
|
||||
return api.LoadFromBytes(body)
|
||||
}
|
||||
|
||||
// ReadRemoteOrLocalPath will read a path remote or locally
|
||||
func ReadRemoteOrLocalPath(pathOrURL string) ([]byte, error) {
|
||||
var body []byte
|
||||
var err error
|
||||
if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") {
|
||||
body, err = common.HTTPGet(context.Background(), pathOrURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = localSave(pathOrURL, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
body, err = os.ReadFile(filepath.Clean(pathOrURL))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// IsAppfile check if a file is Appfile format or application format, return true if it's appfile, false means application object
|
||||
func IsAppfile(body []byte) bool {
|
||||
if j.Valid(body) {
|
||||
// we only support json format for appfile
|
||||
return true
|
||||
}
|
||||
res := map[string]interface{}{}
|
||||
err := yaml.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return false
|
||||
}
|
||||
// appfile didn't have apiVersion
|
||||
if _, ok := res["apiVersion"]; ok {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func localSave(url string, body []byte) error {
|
||||
var name string
|
||||
ext := filepath.Ext(url)
|
||||
switch ext {
|
||||
case ".json":
|
||||
name = "vela.json"
|
||||
case ".yaml", ".yml":
|
||||
name = "vela.yaml"
|
||||
default:
|
||||
if j.Valid(body) {
|
||||
name = "vela.json"
|
||||
} else {
|
||||
name = "vela.yaml"
|
||||
}
|
||||
}
|
||||
//nolint:gosec
|
||||
return af, os.WriteFile(dest, body, 0644)
|
||||
return os.WriteFile(name, body, 0644)
|
||||
}
|
||||
|
||||
// ExportFromAppFile exports Application from appfile object
|
||||
@@ -279,7 +316,7 @@ func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, namespace string, q
|
||||
appHandler := appfile.NewApplication(app, tm)
|
||||
|
||||
// new
|
||||
retApplication, scopes, err := appHandler.BuildOAMApplication(o.Namespace, o.IO, appHandler.Tm, quiet)
|
||||
retApplication, err := appHandler.ConvertToApplication(o.Namespace, o.IO, appHandler.Tm, quiet)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -294,19 +331,9 @@ func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, namespace string, q
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -316,14 +343,10 @@ func (o *AppfileOptions) Export(filePath, namespace string, quiet bool, c common
|
||||
var app *api.AppFile
|
||||
var err error
|
||||
if !quiet {
|
||||
o.IO.Info("Parsing vela appfile ...")
|
||||
o.IO.Info("Parsing vela application file ...")
|
||||
}
|
||||
if filePath != "" {
|
||||
if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") {
|
||||
app, err = saveAndLoadRemoteAppfile(filePath)
|
||||
} else {
|
||||
app, err = api.LoadFromFile(filePath)
|
||||
}
|
||||
app, err = LoadAppFile(filePath)
|
||||
} else {
|
||||
app, err = api.Load()
|
||||
}
|
||||
@@ -383,7 +406,7 @@ func (o *AppfileOptions) ApplyApp(app *corev1beta1.Application, scopes []oam.Obj
|
||||
if err := o.apply(app, scopes); err != nil {
|
||||
return err
|
||||
}
|
||||
o.IO.Infof(o.Info(app))
|
||||
o.IO.Infof(Info(app))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -395,7 +418,7 @@ func (o *AppfileOptions) apply(app *corev1beta1.Application, scopes []oam.Object
|
||||
}
|
||||
|
||||
// Info shows the status of each service in the Appfile
|
||||
func (o *AppfileOptions) Info(app *corev1beta1.Application) string {
|
||||
func 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) +
|
||||
@@ -413,7 +436,7 @@ func ApplyApplication(app corev1beta1.Application, ioStream cmdutil.IOStreams, c
|
||||
if app.Namespace == "" {
|
||||
app.Namespace = types.DefaultAppNamespace
|
||||
}
|
||||
_, err := ioStream.Out.Write([]byte("Applying an application in K8S format...\n"))
|
||||
_, err := ioStream.Out.Write([]byte("Applying an application in vela K8s object format...\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -422,9 +445,6 @@ func ApplyApplication(app corev1beta1.Application, ioStream cmdutil.IOStreams, c
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = ioStream.Out.Write([]byte("Successfully apply application"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ioStream.Infof(Info(&app))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ import (
|
||||
|
||||
// BuildRun will build application and deploy from Appfile
|
||||
func BuildRun(ctx context.Context, app *api.Application, client client.Client, namespace string, io util.IOStreams) error {
|
||||
o, scopes, err := app.BuildOAMApplication(namespace, io, app.Tm, true)
|
||||
o, err := app.ConvertToApplication(namespace, io, app.Tm, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return appfile.Run(ctx, client, o, scopes)
|
||||
return appfile.Run(ctx, client, o, nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user