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:
Jianbo Sun
2022-03-22 16:39:36 +08:00
committed by GitHub
parent d041d8c35d
commit e5fd150cd5
12 changed files with 339 additions and 11 deletions

View 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
```

View 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

View 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
}
}

View 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
}]
}
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)

View 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

View File

@@ -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")

View File

@@ -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")