mirror of
https://github.com/kubevela/kubevela.git
synced 2026-05-06 01:17:09 +00:00
Fix: add client validation and severside dry run for vela dry-run (#3485)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
This commit is contained in:
103
docs/examples/custom-trait/README.md
Normal file
103
docs/examples/custom-trait/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# How to use
|
||||
|
||||
1. define a stateful component with StatefulSet as output
|
||||
|
||||
```shell
|
||||
$ vela def apply stateful.cue
|
||||
ComponentDefinition test-stateful created in namespace vela-system.
|
||||
```
|
||||
|
||||
2. define a custom trait with patch volume
|
||||
|
||||
```shell
|
||||
$ vela def apply volume-trait.cue
|
||||
TraitDefinition storageclass created in namespace vela-system.
|
||||
```
|
||||
|
||||
3. You can validate it by:
|
||||
```
|
||||
$ vela def vet volume-trait.cue
|
||||
Validation succeed.
|
||||
```
|
||||
|
||||
|
||||
|
||||
4. try dry run your app:
|
||||
```
|
||||
vela dry-run -f app.yaml
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Application(website) -- Component(custom-component)
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
annotations: {}
|
||||
labels:
|
||||
app.oam.dev/appRevision: ""
|
||||
app.oam.dev/component: custom-component
|
||||
app.oam.dev/name: website
|
||||
app.oam.dev/namespace: default
|
||||
app.oam.dev/resourceType: WORKLOAD
|
||||
workload.oam.dev/type: test-stateful
|
||||
name: custom-component
|
||||
namespace: default
|
||||
spec:
|
||||
minReadySeconds: 10
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: custom-component
|
||||
serviceName: custom-component
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: custom-component
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx:latest
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: web
|
||||
volumeMounts:
|
||||
- mountPath: /usr/share/nginx/html
|
||||
name: test
|
||||
terminationGracePeriodSeconds: 10
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: test
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
storageClassName: cbs
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations: {}
|
||||
labels:
|
||||
app: custom-component
|
||||
app.oam.dev/appRevision: ""
|
||||
app.oam.dev/component: custom-component
|
||||
app.oam.dev/name: website
|
||||
app.oam.dev/namespace: default
|
||||
app.oam.dev/resourceType: TRAIT
|
||||
trait.oam.dev/resource: web
|
||||
trait.oam.dev/type: AuxiliaryWorkload
|
||||
name: custom-component
|
||||
namespace: default
|
||||
spec:
|
||||
clusterIP: None
|
||||
ports:
|
||||
- name: web
|
||||
port: 80
|
||||
selector:
|
||||
app: custom-component
|
||||
```
|
||||
20
docs/examples/custom-trait/app.yaml
Normal file
20
docs/examples/custom-trait/app.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: website
|
||||
namespace: default
|
||||
spec:
|
||||
components:
|
||||
- name: custom-component
|
||||
type: test-stateful
|
||||
properties:
|
||||
image: nginx:latest
|
||||
replicas: 1
|
||||
traits:
|
||||
- type: storageclass
|
||||
properties:
|
||||
volumeClaimTemplates:
|
||||
- name: test
|
||||
requests: 10Gi
|
||||
storageClassName: cbs
|
||||
mountPath: /usr/share/nginx/html
|
||||
58
docs/examples/custom-trait/stateful.cue
Normal file
58
docs/examples/custom-trait/stateful.cue
Normal file
@@ -0,0 +1,58 @@
|
||||
"test-stateful": {
|
||||
annotations: {}
|
||||
attributes: workload: definition: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "StatefulSet"
|
||||
}
|
||||
description: "StatefulSet component."
|
||||
labels: {}
|
||||
type: "component"
|
||||
}
|
||||
|
||||
template: {
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "StatefulSet"
|
||||
metadata: name: context.name
|
||||
spec: {
|
||||
selector: matchLabels: app: context.name
|
||||
minReadySeconds: 10
|
||||
replicas: parameter.replicas
|
||||
serviceName: context.name
|
||||
template: {
|
||||
metadata: labels: app: context.name
|
||||
spec: {
|
||||
containers: [{
|
||||
name: "nginx"
|
||||
ports: [{
|
||||
name: "web"
|
||||
containerPort: 80
|
||||
}]
|
||||
image: parameter.image
|
||||
}]
|
||||
terminationGracePeriodSeconds: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outputs: web: {
|
||||
apiVersion: "v1"
|
||||
kind: "Service"
|
||||
metadata: {
|
||||
name: context.name
|
||||
labels: app: context.name
|
||||
}
|
||||
spec: {
|
||||
clusterIP: "None"
|
||||
ports: [{
|
||||
name: "web"
|
||||
port: 80
|
||||
}]
|
||||
selector: app: context.name
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
image: string
|
||||
replicas: int
|
||||
}
|
||||
}
|
||||
56
docs/examples/custom-trait/volume-trait.cue
Normal file
56
docs/examples/custom-trait/volume-trait.cue
Normal file
@@ -0,0 +1,56 @@
|
||||
storageclass: {
|
||||
type: "trait"
|
||||
annotations: {}
|
||||
labels: {}
|
||||
description: "Add storageclass on K8s pod for your workload which follows the pod spec in path 'spec.template'."
|
||||
attributes: {
|
||||
appliesToWorkloads: ["*"]
|
||||
}
|
||||
}
|
||||
template: {
|
||||
|
||||
volumeClaimTemplatesList: *[
|
||||
for v in parameter.volumeClaimTemplates {
|
||||
{
|
||||
metadata: name: v.name
|
||||
spec: {
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: requests: storage: v.requests
|
||||
storageClassName: v.storageClassName
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
volumeClaimTemplateVolumeMountsList: *[
|
||||
for v in parameter.volumeClaimTemplates {
|
||||
{
|
||||
name: v.name
|
||||
mountPath: v.mountPath
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
patch: {
|
||||
// +patchKey=name
|
||||
spec: {
|
||||
template: spec: {
|
||||
containers: [...{
|
||||
// +patchKey=name
|
||||
volumeMounts: volumeClaimTemplateVolumeMountsList
|
||||
}]
|
||||
}
|
||||
// +patchKey=name
|
||||
volumeClaimTemplates: volumeClaimTemplatesList
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
volumeClaimTemplates?: [...{
|
||||
name: string
|
||||
requests: string
|
||||
storageClassName: string
|
||||
mountPath: string
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -1657,7 +1657,7 @@ func dryRunApplication(ctx context.Context, app *v1beta1.Application) (bytes.Buf
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
dryRunOpt := dryrun.NewDryRunOption(newClient, dm, pd, objs)
|
||||
dryRunOpt := dryrun.NewDryRunOption(newClient, config, dm, pd, objs)
|
||||
comps, err := dryRunOpt.ExecuteDryRun(ctx, app)
|
||||
if err != nil {
|
||||
return buff, errors.New("generate OAM objects")
|
||||
@@ -1735,7 +1735,7 @@ func compare(ctx context.Context, newApp *v1beta1.Application, oldApp *v1beta1.A
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
}
|
||||
liveDiffOption := dryrun.NewLiveDiffOption(client, dm, pd, objs)
|
||||
liveDiffOption := dryrun.NewLiveDiffOption(client, config, dm, pd, objs)
|
||||
diffResult, err := liveDiffOption.DiffApps(ctx, newApp, oldApp)
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
|
||||
@@ -353,7 +353,6 @@ func (td *traitDef) Complete(ctx process.Context, abstractTemplate string, param
|
||||
if err := base.Unify(p, sets.CreateUnifyOptionsForPatcher(patcher)...); err != nil {
|
||||
return errors.WithMessagef(err, "invalid patch trait %s into workload", td.name)
|
||||
}
|
||||
|
||||
for _, auxiliary := range auxiliaries {
|
||||
target := patcher.Lookup("context", model.OutputsFieldName, auxiliary.Name)
|
||||
if target.Exists() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/aryann/difflib"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
@@ -35,9 +36,9 @@ import (
|
||||
)
|
||||
|
||||
// NewLiveDiffOption creates a live-diff option
|
||||
func NewLiveDiffOption(c client.Client, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover, as []oam.Object) *LiveDiffOption {
|
||||
func NewLiveDiffOption(c client.Client, cfg *rest.Config, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover, as []oam.Object) *LiveDiffOption {
|
||||
parser := appfile.NewApplicationParser(c, dm, pd)
|
||||
return &LiveDiffOption{DryRun: NewDryRunOption(c, dm, pd, as), Parser: parser}
|
||||
return &LiveDiffOption{DryRun: NewDryRunOption(c, cfg, dm, pd, as), Parser: parser}
|
||||
}
|
||||
|
||||
// ManifestKind enums the kind of OAM objects
|
||||
|
||||
@@ -18,10 +18,20 @@ package dryrun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
"k8s.io/kubectl/pkg/util/openapi/validation"
|
||||
kval "k8s.io/kubectl/pkg/validation"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
@@ -38,8 +48,8 @@ type DryRun interface {
|
||||
}
|
||||
|
||||
// NewDryRunOption creates a dry-run option
|
||||
func NewDryRunOption(c client.Client, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover, as []oam.Object) *Option {
|
||||
return &Option{c, dm, pd, as}
|
||||
func NewDryRunOption(c client.Client, cfg *rest.Config, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover, as []oam.Object) *Option {
|
||||
return &Option{c, dm, pd, cfg, as}
|
||||
}
|
||||
|
||||
// Option contains options to execute dry-run
|
||||
@@ -47,12 +57,67 @@ type Option struct {
|
||||
Client client.Client
|
||||
DiscoveryMapper discoverymapper.DiscoveryMapper
|
||||
PackageDiscover *packages.PackageDiscover
|
||||
|
||||
cfg *rest.Config
|
||||
// Auxiliaries are capability definitions used to parse application.
|
||||
// DryRun will use capabilities in Auxiliaries as higher priority than
|
||||
// getting one from cluster.
|
||||
Auxiliaries []oam.Object
|
||||
}
|
||||
|
||||
// validateObjectFromFile will read file into Unstructured object
|
||||
func (d *Option) validateObjectFromFile(filename string) (*unstructured.Unstructured, error) {
|
||||
fileContent, err := os.ReadFile(filepath.Clean(filename))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileType := filepath.Ext(filename)
|
||||
switch fileType {
|
||||
case ".yaml", ".yml":
|
||||
fileContent, err = yaml.YAMLToJSON(fileContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dc, err := discovery.NewDiscoveryClientForConfig(d.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openAPIGetter := openapi.NewOpenAPIGetter(dc)
|
||||
resources, err := openapi.NewOpenAPIParser(openAPIGetter).Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valids := kval.ConjunctiveSchema{validation.NewSchemaValidation(resources), kval.NoDoubleKeySchema{}}
|
||||
if err = valids.ValidateBytes(fileContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
app := new(unstructured.Unstructured)
|
||||
err = json.Unmarshal(fileContent, app)
|
||||
return app, err
|
||||
}
|
||||
|
||||
// ValidateApp will validate app with client schema check and server side dry-run
|
||||
func (d *Option) ValidateApp(ctx context.Context, filename string) error {
|
||||
app, err := d.validateObjectFromFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app2 := app.DeepCopy()
|
||||
|
||||
err = d.Client.Get(ctx, client.ObjectKey{Namespace: app.GetNamespace(), Name: app.GetName()}, app2)
|
||||
if err == nil {
|
||||
app.SetResourceVersion(app2.GetResourceVersion())
|
||||
return d.Client.Update(ctx, app, client.DryRunAll)
|
||||
}
|
||||
return d.Client.Create(ctx, app, client.DryRunAll)
|
||||
}
|
||||
|
||||
// ExecuteDryRun simulates applying an application into cluster and returns rendered
|
||||
// resources but not persist them into cluster.
|
||||
func (d *Option) ExecuteDryRun(ctx context.Context, app *v1beta1.Application) ([]*types.ComponentManifest, error) {
|
||||
|
||||
@@ -107,7 +107,7 @@ var _ = BeforeSuite(func(done Done) {
|
||||
tdMyScaler, err := oamutil.Object2Unstructured(myscalerDef)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
dryrunOpt = NewDryRunOption(k8sClient, dm, pd, []oam.Object{cdMyWorker, tdMyIngress, tdMyScaler})
|
||||
dryrunOpt = NewDryRunOption(k8sClient, cfg, dm, pd, []oam.Object{cdMyWorker, tdMyIngress, tdMyScaler})
|
||||
diffOpt = &LiveDiffOption{DryRun: dryrunOpt, Parser: appfile.NewApplicationParser(k8sClient, dm, pd)}
|
||||
|
||||
close(done)
|
||||
|
||||
20
references/appfile/dryrun/testdata/invalid-app-format.yaml
vendored
Normal file
20
references/appfile/dryrun/testdata/invalid-app-format.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: website
|
||||
namespace: default
|
||||
spec:
|
||||
components:
|
||||
- name: custom-component
|
||||
type: test-stateful
|
||||
properties:
|
||||
image: nginx:latest
|
||||
replicas: 1
|
||||
traits:
|
||||
- type: storageclass
|
||||
properties:
|
||||
volumeClaimTemplates:
|
||||
- name: test
|
||||
requests: 10Gi
|
||||
storageClassName: cbs
|
||||
mountPath: /usr/share/nginx/html
|
||||
@@ -107,13 +107,19 @@ func DryRunApplication(cmdOption *DryRunCmdOptions, c common.Args, namespace str
|
||||
return buff, err
|
||||
}
|
||||
|
||||
dryRunOpt := dryrun.NewDryRunOption(newClient, config, dm, pd, objs)
|
||||
ctx := oamutil.SetNamespaceInCtx(context.Background(), namespace)
|
||||
|
||||
err = dryRunOpt.ValidateApp(ctx, cmdOption.ApplicationFile)
|
||||
if err != nil {
|
||||
return buff, errors.WithMessagef(err, "validate application: %s by dry-run", cmdOption.ApplicationFile)
|
||||
}
|
||||
|
||||
app, err := readApplicationFromFile(cmdOption.ApplicationFile)
|
||||
if err != nil {
|
||||
return buff, errors.WithMessagef(err, "read application file: %s", cmdOption.ApplicationFile)
|
||||
}
|
||||
|
||||
dryRunOpt := dryrun.NewDryRunOption(newClient, dm, pd, objs)
|
||||
ctx := oamutil.SetNamespaceInCtx(context.Background(), namespace)
|
||||
comps, err := dryRunOpt.ExecuteDryRun(ctx, app)
|
||||
if err != nil {
|
||||
return buff, errors.WithMessage(err, "generate OAM objects")
|
||||
|
||||
@@ -143,7 +143,7 @@ func LiveDiffApplication(cmdOption *LiveDiffCmdOptions, c common.Args, namespace
|
||||
}
|
||||
}
|
||||
|
||||
liveDiffOption := dryrun.NewLiveDiffOption(newClient, dm, pd, objs)
|
||||
liveDiffOption := dryrun.NewLiveDiffOption(newClient, config, dm, pd, objs)
|
||||
diffResult, err := liveDiffOption.Diff(context.Background(), app, appRevision)
|
||||
if err != nil {
|
||||
return buff, errors.WithMessage(err, "cannot calculate diff")
|
||||
|
||||
Reference in New Issue
Block a user