mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-27 16:17:34 +00:00
Feat: support vela kube apply for CUE and JSON files (#4420)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
This commit is contained in:
+4
-3
@@ -86,11 +86,12 @@ func LoadDataFromPath(ctx context.Context, path string, pathFilter func(string)
|
||||
return []FileData{{Path: path, Data: bs}}, nil
|
||||
}
|
||||
|
||||
// IsJSONOrYAMLFile check if the path is a json or yaml file
|
||||
func IsJSONOrYAMLFile(path string) bool {
|
||||
// IsJSONYAMLorCUEFile check if the path is a json or yaml file
|
||||
func IsJSONYAMLorCUEFile(path string) bool {
|
||||
return strings.HasSuffix(path, ".json") ||
|
||||
strings.HasSuffix(path, ".yaml") ||
|
||||
strings.HasSuffix(path, ".yml")
|
||||
strings.HasSuffix(path, ".yml") ||
|
||||
strings.HasSuffix(path, ".cue")
|
||||
}
|
||||
|
||||
// IsEmptyDir checks if a given path is an empty directory
|
||||
|
||||
@@ -22,16 +22,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
pkgaddon "github.com/oam-dev/kubevela/pkg/addon"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"gotest.tools/assert"
|
||||
|
||||
pkgaddon "github.com/oam-dev/kubevela/pkg/addon"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
)
|
||||
|
||||
+85
-29
@@ -18,6 +18,8 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -40,6 +42,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/cmd/util"
|
||||
"github.com/oam-dev/kubevela/pkg/cue/model/value"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
@@ -76,10 +79,7 @@ type KubeApplyOptions struct {
|
||||
}
|
||||
|
||||
// Complete .
|
||||
func (opt *KubeApplyOptions) Complete(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
opt.namespace = velacmd.GetNamespace(f, cmd)
|
||||
opt.clusters = velacmd.GetClusters(cmd)
|
||||
|
||||
func (opt *KubeApplyOptions) Complete(ctx context.Context) error {
|
||||
var paths []string
|
||||
for _, file := range opt.files {
|
||||
path := strings.TrimSpace(file)
|
||||
@@ -88,7 +88,7 @@ func (opt *KubeApplyOptions) Complete(f velacmd.Factory, cmd *cobra.Command) err
|
||||
}
|
||||
}
|
||||
for _, path := range paths {
|
||||
data, err := utils.LoadDataFromPath(cmd.Context(), path, utils.IsJSONOrYAMLFile)
|
||||
data, err := utils.LoadDataFromPath(ctx, path, utils.IsJSONYAMLorCUEFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func (opt *KubeApplyOptions) Complete(f velacmd.Factory, cmd *cobra.Command) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate .
|
||||
// Validate will not only validate the args but also read from files and generate the objects
|
||||
func (opt *KubeApplyOptions) Validate() error {
|
||||
if len(opt.files) == 0 {
|
||||
return fmt.Errorf("at least one file should be specified with the --file flag")
|
||||
@@ -105,21 +105,61 @@ func (opt *KubeApplyOptions) Validate() error {
|
||||
if len(opt.filesData) == 0 {
|
||||
return fmt.Errorf("not file found")
|
||||
}
|
||||
if len(opt.clusters) == 0 {
|
||||
opt.clusters = []string{"local"}
|
||||
}
|
||||
jsonObj := func(data []byte, path string) (*unstructured.Unstructured, error) {
|
||||
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
err := json.Unmarshal(data, &obj.Object)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode object in %s: %w", path, err)
|
||||
}
|
||||
if opt.namespace != "" {
|
||||
obj.SetNamespace(opt.namespace)
|
||||
} else if obj.GetNamespace() == "" {
|
||||
obj.SetNamespace(metav1.NamespaceDefault)
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
for _, fileData := range opt.filesData {
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(fileData.Data))
|
||||
for {
|
||||
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
err := decoder.Decode(obj.Object)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
switch {
|
||||
case strings.HasSuffix(fileData.Path, ".yaml"), strings.HasSuffix(fileData.Path, ".yml"):
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(fileData.Data))
|
||||
for {
|
||||
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
err := decoder.Decode(obj.Object)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("failed to decode object in %s: %w", fileData.Path, err)
|
||||
}
|
||||
if opt.namespace != "" {
|
||||
obj.SetNamespace(opt.namespace)
|
||||
} else if obj.GetNamespace() == "" {
|
||||
obj.SetNamespace(metav1.NamespaceDefault)
|
||||
}
|
||||
opt.objects = append(opt.objects, obj)
|
||||
}
|
||||
case strings.HasSuffix(fileData.Path, ".json"):
|
||||
obj, err := jsonObj(fileData.Data, fileData.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.objects = append(opt.objects, obj)
|
||||
case strings.HasSuffix(fileData.Path, ".cue"):
|
||||
val, err := value.NewValue(string(fileData.Data), nil, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode object in %s: %w", fileData.Path, err)
|
||||
}
|
||||
if opt.namespace != "" {
|
||||
obj.SetNamespace(opt.namespace)
|
||||
} else if obj.GetNamespace() == "" {
|
||||
obj.SetNamespace(metav1.NamespaceDefault)
|
||||
data, err := val.CueValue().MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marhsal to json for CUE object in %s: %w", fileData.Path, err)
|
||||
}
|
||||
obj, err := jsonObj(data, fileData.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.objects = append(opt.objects, obj)
|
||||
}
|
||||
@@ -127,8 +167,8 @@ func (opt *KubeApplyOptions) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run .
|
||||
func (opt *KubeApplyOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
// Run will apply objects to clusters
|
||||
func (opt *KubeApplyOptions) Run(ctx context.Context, cli client.Client) error {
|
||||
if opt.dryRun {
|
||||
for i, obj := range opt.objects {
|
||||
if i > 0 {
|
||||
@@ -147,8 +187,9 @@ func (opt *KubeApplyOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
_, _ = fmt.Fprintf(opt.Out, "\n")
|
||||
}
|
||||
_, _ = fmt.Fprintf(opt.Out, "Apply objects in cluster %s.\n", cluster)
|
||||
ctx := multicluster.ContextWithClusterName(cmd.Context(), cluster)
|
||||
ctx := multicluster.ContextWithClusterName(ctx, cluster)
|
||||
for _, obj := range opt.objects {
|
||||
fmt.Println("XXXXXX creating", obj.GetName())
|
||||
copiedObj := &unstructured.Unstructured{}
|
||||
bs, err := obj.MarshalJSON()
|
||||
if err != nil {
|
||||
@@ -157,7 +198,7 @@ func (opt *KubeApplyOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
if err = copiedObj.UnmarshalJSON(bs); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := utils.CreateOrUpdate(ctx, f.Client(), copiedObj)
|
||||
res, err := utils.CreateOrUpdate(ctx, cli, copiedObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -175,21 +216,33 @@ var (
|
||||
Apply Kubernetes objects in multiple clusters. Use --clusters to specify which clusters to
|
||||
apply. If -n/--namespace is used, the original object namespace will be overrode.
|
||||
|
||||
You can use -f/--file to specify the object file to apply. Multiple file inputs are allowed.
|
||||
Directory input and web url input is supported as well.`))
|
||||
You can use -f/--file to specify the object file/folder to apply. Multiple file inputs are allowed.
|
||||
Directory input and web url input is supported as well.
|
||||
File format can be in YAML, JSON or CUE.
|
||||
`))
|
||||
|
||||
kubeApplyExample = templates.Examples(i18n.T(`
|
||||
# Apply single object file in managed cluster
|
||||
vela kube apply -f my.yaml --cluster cluster-1
|
||||
|
||||
|
||||
# Apply object in CUE, the whole CUE file MUST follow the kubernetes API and contain only one object.
|
||||
vela kube apply -f my.cue --cluster cluster-1
|
||||
|
||||
# Apply object in JSON, the whole JSON file MUST follow the kubernetes API and contain only one object.
|
||||
vela kube apply -f my.json --cluster cluster-1
|
||||
|
||||
# Apply multiple object files in multiple managed clusters
|
||||
vela kube apply -f my-1.yaml -f my-2.yaml --cluster cluster-1 --cluster cluster-2
|
||||
vela kube apply -f my-1.yaml -f my-2.cue --cluster cluster-1 --cluster cluster-2
|
||||
|
||||
# Apply object file with web url in control plane
|
||||
vela kube apply -f https://raw.githubusercontent.com/kubevela/kubevela/master/docs/examples/app-with-probe/app-with-probe.yaml
|
||||
|
||||
# Apply object files in directory to specified namespace in managed clusters
|
||||
vela kube apply -f ./resources -n demo --cluster cluster-1 --cluster cluster-2`))
|
||||
vela kube apply -f ./resources -n demo --cluster cluster-1 --cluster cluster-2
|
||||
|
||||
# Use dry-run to see what will be rendered out in YAML
|
||||
vela kube apply -f my.cue --cluster cluster-1 --dry-run
|
||||
`))
|
||||
)
|
||||
|
||||
// NewKubeApplyCommand kube apply command
|
||||
@@ -205,13 +258,16 @@ func NewKubeApplyCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Comma
|
||||
},
|
||||
Args: cobra.ExactValidArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(o.Complete(f, cmd))
|
||||
o.namespace = velacmd.GetNamespace(f, cmd)
|
||||
o.clusters = velacmd.GetClusters(cmd)
|
||||
|
||||
cmdutil.CheckErr(o.Complete(cmd.Context()))
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run(f, cmd))
|
||||
cmdutil.CheckErr(o.Run(cmd.Context(), f.Client()))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringSliceVarP(&o.files, "file", "f", o.files, "Files that include native Kubernetes objects to apply.")
|
||||
cmd.Flags().BoolVarP(&o.dryRun, "dryrun", "", o.dryRun, "Setting this flag will not apply resources in clusters. It will print out the resource to be applied.")
|
||||
cmd.Flags().BoolVarP(&o.dryRun, FlagDryRun, "", o.dryRun, "Setting this flag will not apply resources in clusters. It will print out the resource to be applied.")
|
||||
return velacmd.NewCommandBuilder(f, cmd).
|
||||
WithNamespaceFlag(
|
||||
velacmd.NamespaceFlagDisableEnvOption{},
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2022 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 cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
)
|
||||
|
||||
var _ = Describe("Test kube apply cli", func() {
|
||||
|
||||
When("test vela kube apply", func() {
|
||||
|
||||
It("should not have err and applied all objects", func() {
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
ioStreams := util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer}
|
||||
|
||||
o := &KubeApplyOptions{}
|
||||
o.IOStreams = ioStreams
|
||||
o.files = []string{"./test-data/kubeapply"}
|
||||
ctx := context.Background()
|
||||
Expect(o.Complete(ctx)).Should(BeNil())
|
||||
Expect(o.Validate()).Should(BeNil())
|
||||
Expect(o.Run(ctx, k8sClient)).Should(BeNil())
|
||||
|
||||
By("test kube apply in dry-run mod")
|
||||
o.dryRun = true
|
||||
Expect(o.Run(ctx, k8sClient)).Should(BeNil())
|
||||
buf, ok := ioStreams.Out.(*bytes.Buffer)
|
||||
Expect(ok).Should(BeTrue())
|
||||
Expect(strings.Contains(buf.String(), "error")).Should(BeFalse())
|
||||
|
||||
By("test kube apply in different namespace, new namespace")
|
||||
var newns = "test-kube-apply"
|
||||
err := k8sClient.Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: v1.ObjectMeta{Name: newns},
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("apply objects")
|
||||
o = &KubeApplyOptions{}
|
||||
o.IOStreams = ioStreams
|
||||
o.files = []string{"./test-data/kubeapply"}
|
||||
o.namespace = newns
|
||||
Expect(o.Complete(ctx)).Should(BeNil())
|
||||
Expect(o.Validate()).Should(BeNil())
|
||||
Expect(o.Run(ctx, k8sClient)).Should(BeNil())
|
||||
|
||||
By("check objects configmap created")
|
||||
cml := corev1.ConfigMapList{}
|
||||
Expect(k8sClient.List(ctx, &cml, client.InNamespace(newns))).Should(BeNil())
|
||||
Expect(len(cml.Items)).Should(BeEquivalentTo(3))
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import "encoding/json"
|
||||
|
||||
_clickhouse: {
|
||||
group: "clickhouse.altinity.com"
|
||||
kind: "ClickHouseInstallation"
|
||||
}
|
||||
_statefulset: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "StatefulSet"
|
||||
}
|
||||
_service: {
|
||||
apiVersion: "v1"
|
||||
kind: "Service"
|
||||
}
|
||||
|
||||
_seldon: {
|
||||
group: "machinelearning.seldon.io"
|
||||
kind: "SeldonDeployment"
|
||||
}
|
||||
|
||||
_rule1: {
|
||||
parentResourceType: _clickhouse
|
||||
childrenResourceType: [_statefulset, _service]
|
||||
}
|
||||
|
||||
_rule2: {
|
||||
parentResourceType: _seldon
|
||||
childrenResourceType: [_service]
|
||||
}
|
||||
|
||||
apiVersion: "v1"
|
||||
kind: "ConfigMap"
|
||||
metadata: {
|
||||
name: "toplogy-cue"
|
||||
namespace: "vela-system"
|
||||
labels: {
|
||||
"rules.oam.dev/resources": "true"
|
||||
"rules.oam.dev/resource-format": "json"
|
||||
}
|
||||
}
|
||||
data: rules: json.Marshal([_rule1, _rule2])
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "toplogy-json",
|
||||
"namespace": "vela-system",
|
||||
"labels": {
|
||||
"rules.oam.dev/resources": "true",
|
||||
"rules.oam.dev/resource-format": "json"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"rules": "[{\"parentResourceType\":{\"group\":\"clickhouse.altinity.com\",\"kind\":\"ClickHouseInstallation\"},\"childrenResourceType\":[{\"apiVersion\":\"apps/v1\",\"kind\":\"StatefulSet\"},{\"apiVersion\":\"v1\",\"kind\":\"Service\"}]},{\"parentResourceType\":{\"group\":\"machinelearning.seldon.io\",\"kind\":\"SeldonDeployment\"},\"childrenResourceType\":[{\"apiVersion\":\"v1\",\"kind\":\"Service\"}]}]"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: kubevela-io
|
||||
namespace: vela-system
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 10
|
||||
selector:
|
||||
matchLabels:
|
||||
app.oam.dev/component: kubevela-io
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app.oam.dev/component: kubevela-io
|
||||
spec:
|
||||
containers:
|
||||
- image: oamdev/kubevela-io:latest
|
||||
imagePullPolicy: Always
|
||||
name: kubevela-io
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: port-80
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 100Mi
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 100Mi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: kruise-rollout-relation
|
||||
namespace: vela-system
|
||||
labels:
|
||||
"rules.oam.dev/resources": "true"
|
||||
data:
|
||||
rules: |-
|
||||
- parentResourceType:
|
||||
group: rollouts.kruise.io
|
||||
kind: Rollout
|
||||
childrenResourceType:
|
||||
- apiVersion: rollouts.kruise.io/v1alpha1
|
||||
kind: BatchRelease
|
||||
- parentResourceType:
|
||||
group: rollouts.kruise.io
|
||||
kind: BatchRelease
|
||||
childrenResourceType:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
Reference in New Issue
Block a user