Compare commits

..

3 Commits

Author SHA1 Message Date
github-actions[bot]
8c9d0ae314 Chore: refactor addon enable with package (#4468)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
(cherry picked from commit 702fa36621)

Co-authored-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-07-26 15:23:33 +08:00
Somefive
b6c024c205 Feat: add featuregates to disallow url in ref-objects (#4466)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
2022-07-26 13:37:11 +08:00
github-actions[bot]
3ab0b503c5 Fix: docker file fail to build for vela cli (#4465)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
(cherry picked from commit 62fcb152e2)

Co-authored-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-07-25 22:45:13 +08:00
13 changed files with 138 additions and 97 deletions

View File

@@ -39,6 +39,5 @@ RUN apk add --no-cache ca-certificates bash expat
WORKDIR /
ARG TARGETARCH
COPY --from=builder /workspace/vela-${TARGETARCH} /vela
COPY /vela /bin/
ENTRYPOINT ["/vela"]
COPY --from=builder /workspace/vela-${TARGETARCH} /bin/vela
ENTRYPOINT ["/bin/vela"]

View File

@@ -1052,7 +1052,7 @@ func (h *Installer) createOrUpdate(app *v1beta1.Application) error {
}
func (h *Installer) dispatchAddonResource(addon *InstallPackage) error {
app, err := RenderApp(h.ctx, addon, h.cli, h.args)
app, auxiliaryOutputs, err := RenderApp(h.ctx, addon, h.cli, h.args)
if err != nil {
return errors.Wrap(err, "render addon application fail")
}
@@ -1083,12 +1083,6 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error {
return errors.Wrapf(err, "cannot pass definition to addon app's annotation")
}
var auxiliaryOutputs []*unstructured.Unstructured
auxiliaryOutputs, err = renderOutputs(addon, h.args)
if err != nil {
return err
}
if err = h.createOrUpdate(app); err != nil {
return err
}

View File

@@ -307,7 +307,7 @@ var _ = Describe("Test render addon with specified clusters", func() {
args := map[string]interface{}{
"clusters": []string{"add-c1", "ne"},
}
_, err := RenderApp(ctx, i, k8sClient, args)
_, _, err := RenderApp(ctx, i, k8sClient, args)
Expect(err.Error()).Should(BeEquivalentTo("cluster ne not exist"))
})
It("test render normal addon with specified clusters", func() {
@@ -317,7 +317,7 @@ var _ = Describe("Test render addon with specified clusters", func() {
args := map[string]interface{}{
"clusters": []string{"add-c1", "add-c2"},
}
ap, err := RenderApp(ctx, i, k8sClient, args)
ap, _, err := RenderApp(ctx, i, k8sClient, args)
Expect(err).Should(BeNil())
Expect(ap.Spec.Policies).Should(BeEquivalentTo([]v1beta1.AppPolicy{{Name: specifyAddonClustersTopologyPolicy,
Type: v1alpha12.TopologyPolicyType,

View File

@@ -281,7 +281,7 @@ func TestRender(t *testing.T) {
func TestRenderApp(t *testing.T) {
addon := baseAddon
app, err := RenderApp(ctx, &addon, nil, map[string]interface{}{})
app, _, err := RenderApp(ctx, &addon, nil, map[string]interface{}{})
assert.NoError(t, err, "render app fail")
// definition should not be rendered
assert.Equal(t, len(app.Spec.Components), 1)
@@ -290,7 +290,7 @@ func TestRenderApp(t *testing.T) {
func TestRenderAppWithNeedNamespace(t *testing.T) {
addon := baseAddon
addon.NeedNamespace = append(addon.NeedNamespace, types.DefaultKubeVelaNS, "test-ns2")
app, err := RenderApp(ctx, &addon, nil, map[string]interface{}{})
app, _, err := RenderApp(ctx, &addon, nil, map[string]interface{}{})
assert.NoError(t, err, "render app fail")
assert.Equal(t, len(app.Spec.Components), 2)
for _, c := range app.Spec.Components {
@@ -311,7 +311,7 @@ func TestRenderDeploy2RuntimeAddon(t *testing.T) {
assert.Equal(t, def.GetAPIVersion(), "core.oam.dev/v1beta1")
assert.Equal(t, def.GetKind(), "TraitDefinition")
app, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{})
app, _, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{})
assert.NoError(t, err)
policies := app.Spec.Policies
assert.True(t, len(policies) == 1)
@@ -331,7 +331,7 @@ func TestRenderDefinitions(t *testing.T) {
assert.Equal(t, def.GetAPIVersion(), "core.oam.dev/v1beta1")
assert.Equal(t, def.GetKind(), "TraitDefinition")
app, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{})
app, _, err := RenderApp(ctx, &addonDeployToRuntime, nil, map[string]interface{}{})
assert.NoError(t, err)
// addon which app work on no-runtime-cluster mode workflow is nil
assert.Nil(t, app.Spec.Workflow)
@@ -368,7 +368,7 @@ func TestRenderK8sObjects(t *testing.T) {
RuntimeCluster: false,
}
app, err := RenderApp(ctx, &addonMultiYaml, nil, map[string]interface{}{})
app, _, err := RenderApp(ctx, &addonMultiYaml, nil, map[string]interface{}{})
assert.NoError(t, err)
assert.Equal(t, len(app.Spec.Components), 1)
comp := app.Spec.Components[0]

View File

@@ -50,7 +50,7 @@ const (
enabling = "enabling"
// disabling indicates the addon related app is deleting
disabling = "disabling"
// suspend indicates the addon related app is suspend
// suspend indicates the addon related app is suspended
suspend = "suspend"
)

View File

@@ -503,9 +503,14 @@ parameter: {
validate:
required: true
`
appTemplate = `output: {
appTemplate = `package main
output: {
apiVersion: "core.oam.dev/v1beta1"
kind: "Application"
metadata: {
name: "my-addon-name"
namespace: "vela-system"
}
spec: {
components: []
policies: []

View File

@@ -23,6 +23,7 @@ import (
"path"
"strings"
"cuelang.org/go/cue/parser"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -37,6 +38,7 @@ import (
"github.com/oam-dev/kubevela/pkg/cue/model/value"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
)
@@ -103,46 +105,77 @@ func (a addonCueTemplateRender) toObject(cueTemplate string, path string, object
}
// renderApp will render Application from CUE files
func (a addonCueTemplateRender) renderApp() (*v1beta1.Application, error) {
func (a addonCueTemplateRender) renderApp() (*v1beta1.Application, []*unstructured.Unstructured, error) {
var app v1beta1.Application
var outputs = map[string]interface{}{}
var res []*unstructured.Unstructured
contextFile, err := a.formatContext()
if err != nil {
return nil, errors.Wrap(err, "format context for app render")
return nil, nil, errors.Wrap(err, "format context for app render")
}
contextCue, err := parser.ParseFile("parameter.cue", contextFile, parser.ParseComments)
if err != nil {
return nil, nil, errors.Wrap(err, "parse parameter context")
}
if contextCue.PackageName() == "" {
contextFile = value.DefaultPackageHeader + contextFile
}
var files = []string{contextFile}
for _, cuef := range a.addon.CUETemplates {
files = append(files, cuef.Data)
}
// TODO(wonderflow): add package discover to support vela own packages if needed
v, err := value.NewValueWithFiles(a.addon.AppCueTemplate.Data, files, nil, "")
v, err := value.NewValueWithMainAndFiles(a.addon.AppCueTemplate.Data, files, nil, "")
if err != nil {
return nil, errors.Wrap(err, "load app template with CUE files")
return nil, nil, errors.Wrap(err, "load app template with CUE files")
}
outputContent, err := v.LookupValue(renderOutputCuePath)
if err != nil {
return nil, errors.Wrap(err, "render app from output field from CUE")
return nil, nil, errors.Wrap(err, "render app from output field from CUE")
}
err = outputContent.UnmarshalTo(&app)
if err != nil {
return nil, errors.Wrap(err, "decode app from CUE")
return nil, nil, errors.Wrap(err, "decode app from CUE")
}
return &app, nil
auxiliaryContent, err := v.LookupValue(renderAuxiliaryOutputsPath)
if err != nil {
// no outputs defined in app template, return normal data
if isErrorCueRenderPathNotFound(err, renderAuxiliaryOutputsPath) {
return &app, res, nil
}
return nil, nil, errors.Wrap(err, "render app from output field from CUE")
}
err = auxiliaryContent.UnmarshalTo(&outputs)
if err != nil {
return nil, nil, errors.Wrap(err, "decode app from CUE")
}
for k, o := range outputs {
if ao, ok := o.(map[string]interface{}); ok {
auxO := &unstructured.Unstructured{Object: ao}
auxO.SetLabels(util.MergeMapOverrideWithDst(auxO.GetLabels(), map[string]string{oam.LabelAddonAuxiliaryName: k}))
res = append(res, auxO)
}
}
return &app, res, nil
}
// generateAppFramework generate application from yaml defined by template.yaml or cue file from template.cue
func generateAppFramework(addon *InstallPackage, parameters map[string]interface{}) (*v1beta1.Application, error) {
func generateAppFramework(addon *InstallPackage, parameters map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) {
if len(addon.AppCueTemplate.Data) != 0 && addon.AppTemplate != nil {
return nil, ErrBothCueAndYamlTmpl
return nil, nil, ErrBothCueAndYamlTmpl
}
var app *v1beta1.Application
var auxiliaryObjects []*unstructured.Unstructured
var err error
if len(addon.AppCueTemplate.Data) != 0 {
app, err = renderAppAccordingToCueTemplate(addon, parameters)
app, auxiliaryObjects, err = renderAppAccordingToCueTemplate(addon, parameters)
if err != nil {
return nil, err
return nil, nil, err
}
} else {
app = addon.AppTemplate
@@ -164,19 +197,20 @@ func generateAppFramework(addon *InstallPackage, parameters map[string]interface
}
app.Labels[oam.LabelAddonName] = addon.Name
app.Labels[oam.LabelAddonVersion] = addon.Version
return app, nil
for _, aux := range auxiliaryObjects {
aux.SetLabels(util.MergeMapOverrideWithDst(aux.GetLabels(), map[string]string{oam.LabelAddonName: addon.Name, oam.LabelAddonVersion: addon.Version}))
}
return app, auxiliaryObjects, nil
}
func renderAppAccordingToCueTemplate(addon *InstallPackage, args map[string]interface{}) (*v1beta1.Application, error) {
func renderAppAccordingToCueTemplate(addon *InstallPackage, args map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) {
r := addonCueTemplateRender{
addon: addon,
inputArgs: args,
}
app, err := r.renderApp()
if err != nil {
return nil, err
}
return app, nil
return r.renderApp()
}
// renderCompAccordingCUETemplate will return a component from cue template
@@ -199,19 +233,19 @@ func renderCompAccordingCUETemplate(cueTemplate ElementFile, addon *InstallPacka
}
// RenderApp render a K8s application
func RenderApp(ctx context.Context, addon *InstallPackage, k8sClient client.Client, args map[string]interface{}) (*v1beta1.Application, error) {
func RenderApp(ctx context.Context, addon *InstallPackage, k8sClient client.Client, args map[string]interface{}) (*v1beta1.Application, []*unstructured.Unstructured, error) {
if args == nil {
args = map[string]interface{}{}
}
app, err := generateAppFramework(addon, args)
app, auxiliaryObjects, err := generateAppFramework(addon, args)
if err != nil {
return nil, err
return nil, nil, err
}
app.Spec.Components = append(app.Spec.Components, renderNeededNamespaceAsComps(addon)...)
resources, err := renderResources(addon, args)
if err != nil {
return nil, err
return nil, nil, err
}
app.Spec.Components = append(app.Spec.Components, resources...)
@@ -219,10 +253,10 @@ func RenderApp(ctx context.Context, addon *InstallPackage, k8sClient client.Clie
// attach topology policy to application.
if checkNeedAttachTopologyPolicy(app, addon) {
if err := attachPolicyForLegacyAddon(ctx, app, addon, args, k8sClient); err != nil {
return nil, err
return nil, nil, err
}
}
return app, nil
return app, auxiliaryObjects, nil
}
func attachPolicyForLegacyAddon(ctx context.Context, app *v1beta1.Application, addon *InstallPackage, args map[string]interface{}, k8sClient client.Client) error {
@@ -317,27 +351,6 @@ func checkNeedAttachTopologyPolicy(app *v1beta1.Application, addon *InstallPacka
return true
}
func renderOutputs(addon *InstallPackage, args map[string]interface{}) ([]*unstructured.Unstructured, error) {
outputs := map[string]interface{}{}
r := addonCueTemplateRender{
addon: addon,
inputArgs: args,
}
if err := r.toObject(addon.AppCueTemplate.Data, renderAuxiliaryOutputsPath, &outputs); err != nil {
if isErrorCueRenderPathNotFound(err, renderAuxiliaryOutputsPath) {
return nil, nil
}
return nil, err
}
var res []*unstructured.Unstructured
for _, o := range outputs {
if ao, ok := o.(map[string]interface{}); ok {
res = append(res, &unstructured.Unstructured{Object: ao})
}
}
return res, nil
}
func isDeployToRuntime(addon *InstallPackage) bool {
if addon.DeployTo == nil {
return false

View File

@@ -96,7 +96,12 @@ myref: {
"namespace": "vela-system",
},
}
app, err := render.renderApp()
app, _, err := render.renderApp()
assert.Equal(t, err.Error(), `load app template with CUE files: reference "myref" not found`)
assert.Nil(t, app)
addon.CUETemplates = []ElementFile{{Data: "package main\n" + resourceComponent1}}
app, _, err = render.renderApp()
assert.NoError(t, err)
assert.Equal(t, len(app.Spec.Components), 2)
str, err := json.Marshal(app.Spec.Components[0].Properties)
@@ -111,26 +116,29 @@ myref: {
assert.NoError(t, err)
assert.True(t, strings.Contains(string(str), `"clusterLabelSelector":{}`))
addon.CUETemplates = []ElementFile{{Data: resourceComponent1}}
addon.AppCueTemplate = ElementFile{Data: "package main\n" + appTemplate}
app, err = render.renderApp()
addon.Parameters = "package newp\n" + paraDefined
addon.CUETemplates = []ElementFile{{Data: "package newp\n" + resourceComponent1}}
addon.AppCueTemplate = ElementFile{Data: "package newp\n" + appTemplate}
app, _, err = render.renderApp()
assert.NoError(t, err)
assert.Equal(t, len(app.Spec.Components), 2)
addon.Parameters = "package main\n" + paraDefined
app, err = render.renderApp()
addon.CUETemplates = []ElementFile{{Data: "package main\n" + resourceComponent1}}
addon.Parameters = paraDefined
addon.AppCueTemplate = ElementFile{Data: appTemplate}
app, _, err = render.renderApp()
assert.NoError(t, err)
assert.Equal(t, len(app.Spec.Components), 2)
addon.CUETemplates = []ElementFile{{Data: "package hello\n" + resourceComponent1}}
addon.AppCueTemplate = ElementFile{Data: "package main\n" + appTemplate}
_, err = render.renderApp()
_, _, err = render.renderApp()
assert.Equal(t, err.Error(), `load app template with CUE files: reference "myref" not found`)
addon.CUETemplates = []ElementFile{{Data: "package hello\n" + resourceComponent1}}
addon.Parameters = paraDefined
addon.AppCueTemplate = ElementFile{Data: appTemplate}
_, err = render.renderApp()
_, _, err = render.renderApp()
assert.Equal(t, err.Error(), `load app template with CUE files: reference "myref" not found`)
}
@@ -234,21 +242,28 @@ func TestOutputsRender(t *testing.T) {
Parameters: paraDefined,
AppCueTemplate: ElementFile{Data: appTemplate},
}
list, err := renderOutputs(addon, nil)
assert.NoError(t, err)
assert.Equal(t, true, len(list) == 1)
addon = &InstallPackage{
Meta: Meta{
Name: "velaux",
DeployTo: &DeployTo{
RuntimeCluster: true,
},
render := addonCueTemplateRender{
addon: addon,
inputArgs: map[string]interface{}{
"namespace": "vela-system",
},
Parameters: paraDefined,
AppCueTemplate: ElementFile{Data: appTemplateNoOutputs},
}
_, err = renderOutputs(addon, nil)
app, auxdata, err := render.renderApp()
assert.NoError(t, err)
assert.Equal(t, len(app.Spec.Components), 1)
str, err := json.Marshal(app.Spec.Components[0].Properties)
assert.NoError(t, err)
assert.True(t, strings.Contains(string(str), `{"name":"vela-system"}`))
assert.Equal(t, len(auxdata), 1)
auxStr, err := json.Marshal(auxdata[0])
assert.NoError(t, err)
assert.True(t, strings.Contains(string(auxStr), "myData"))
assert.True(t, strings.Contains(string(auxStr), "addons.oam.dev/auxiliary-name"))
assert.True(t, strings.Contains(string(auxStr), "configmap"))
// test no error when no outputs
addon.AppCueTemplate = ElementFile{Data: appTemplateNoOutputs}
_, _, err = render.renderApp()
assert.NoError(t, err)
}
@@ -369,7 +384,7 @@ func TestGenerateAppFrameworkWithCue(t *testing.T) {
AppCueTemplate: ElementFile{Data: cueTemplate},
Parameters: paraDefined,
}
app, err := generateAppFramework(cueAddon, map[string]interface{}{
app, _, err := generateAppFramework(cueAddon, map[string]interface{}{
"namespace": "vela-system",
})
assert.NoError(t, err)
@@ -389,7 +404,7 @@ func TestGenerateAppFrameworkWithYamlTemplate(t *testing.T) {
Meta: Meta{Name: "velaux"},
AppTemplate: nil,
}
app, err := generateAppFramework(yamlAddon, nil)
app, _, err := generateAppFramework(yamlAddon, nil)
assert.NoError(t, err)
assert.Equal(t, app.Spec.Components != nil, true)
assert.Equal(t, len(app.Labels), 2)
@@ -398,7 +413,7 @@ func TestGenerateAppFrameworkWithYamlTemplate(t *testing.T) {
Meta: Meta{Name: "velaux"},
AppTemplate: &v1beta1.Application{},
}
app, err = generateAppFramework(noCompAddon, nil)
app, _, err = generateAppFramework(noCompAddon, nil)
assert.NoError(t, err)
assert.Equal(t, app.Spec.Components != nil, true)
assert.Equal(t, len(app.Labels), 2)

View File

@@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
ktypes "k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
@@ -38,6 +39,7 @@ import (
"github.com/oam-dev/kubevela/pkg/component"
"github.com/oam-dev/kubevela/pkg/cue/definition"
"github.com/oam-dev/kubevela/pkg/cue/packages"
"github.com/oam-dev/kubevela/pkg/features"
monitorContext "github.com/oam-dev/kubevela/pkg/monitor/context"
"github.com/oam-dev/kubevela/pkg/monitor/metrics"
"github.com/oam-dev/kubevela/pkg/oam"
@@ -350,6 +352,9 @@ func (p *Parser) parseReferredObjects(ctx context.Context, af *Appfile) error {
}
af.ReferredObjects = component.AppendUnstructuredObjects(af.ReferredObjects, objs...)
}
if utilfeature.DefaultMutableFeatureGate.Enabled(features.DisableReferObjectsFromURL) && len(spec.URLs) > 0 {
return fmt.Errorf("referring objects from url is disabled")
}
for _, url := range spec.URLs {
objs, err := utilscommon.HTTPGetKubernetesObjects(ctx, url)
if err != nil {

View File

@@ -36,6 +36,9 @@ import (
"github.com/oam-dev/kubevela/pkg/stdlib"
)
// DefaultPackageHeader describes the default package header for CUE files.
const DefaultPackageHeader = "package main\n"
// Value is an object with cue.runtime and vendors
type Value struct {
v cue.Value
@@ -75,14 +78,21 @@ func (val *Value) UnmarshalTo(x interface{}) error {
return json.Unmarshal(data, x)
}
// NewValueWithFiles new a value from main and appendix files
func NewValueWithFiles(main string, slaveFiles []string, pd *packages.PackageDiscover, tagTempl string, opts ...func(*ast.File) error) (*Value, error) {
// NewValueWithMainAndFiles new a value from main and appendix files
func NewValueWithMainAndFiles(main string, slaveFiles []string, pd *packages.PackageDiscover, tagTempl string, opts ...func(*ast.File) error) (*Value, error) {
builder := &build.Instance{}
mainFile, err := parser.ParseFile("main.cue", main, parser.ParseComments)
if err != nil {
return nil, errors.Wrap(err, "parse main file")
}
if mainFile.PackageName() == "" {
// add a default package main if not exist
mainFile, err = parser.ParseFile("main.cue", DefaultPackageHeader+main, parser.ParseComments)
if err != nil {
return nil, errors.Wrap(err, "parse main file with added package main header")
}
}
for _, opt := range opts {
if err := opt(mainFile); err != nil {
return nil, errors.Wrap(err, "run option func for main file")
@@ -97,12 +107,6 @@ func NewValueWithFiles(main string, slaveFiles []string, pd *packages.PackageDis
if err != nil {
return nil, errors.Wrap(err, "parse added file "+strconv.Itoa(idx)+" \n"+sf)
}
if mainFile.PackageName() != "" && cueSF.PackageName() == "" {
cueSF, err = parser.ParseFile("sf-"+strconv.Itoa(idx)+".cue", "package "+mainFile.PackageName()+"\n"+sf, parser.ParseComments)
if err != nil {
return nil, errors.Wrap(err, "add package for added file")
}
}
if cueSF.PackageName() != mainFile.PackageName() {
continue
}

View File

@@ -40,6 +40,8 @@ const (
// LegacyResourceOwnerValidation if enabled, the resource dispatch will allow existing resource not to have owner
// application and the current application will take over it
LegacyResourceOwnerValidation featuregate.Feature = "LegacyResourceOwnerValidation"
// DisableReferObjectsFromURL if set, the url ref objects will be disallowed
DisableReferObjectsFromURL featuregate.Feature = "DisableReferObjectsFromURL"
// Edge Features
@@ -55,6 +57,7 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
EnableSuspendOnFailure: {Default: false, PreRelease: featuregate.Alpha},
LegacyComponentRevision: {Default: false, PreRelease: featuregate.Alpha},
LegacyResourceOwnerValidation: {Default: false, PreRelease: featuregate.Alpha},
DisableReferObjectsFromURL: {Default: false, PreRelease: featuregate.Alpha},
AuthenticateApplication: {Default: false, PreRelease: featuregate.Alpha},
}

View File

@@ -69,6 +69,9 @@ const (
// LabelAddonName indicates the name of the corresponding Addon
LabelAddonName = "addons.oam.dev/name"
// LabelAddonAuxiliaryName indicates the name of the auxiliary resource in addon app template
LabelAddonAuxiliaryName = "addons.oam.dev/auxiliary-name"
// LabelAddonVersion indicates the version of the corresponding installed Addon
LabelAddonVersion = "addons.oam.dev/version"

View File

@@ -10,7 +10,7 @@ spec:
type: webservice
properties:
image: oamdev/vela-cli:v1.5.0-beta.1
cmd: ["/vela","show"]
cmd: ["/bin/vela","show"]
ports:
- port: 18081
expose: true