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:
Jianbo Sun
2022-07-21 16:02:57 +08:00
committed by GitHub
parent 285a5cce18
commit 957302cb9d
7 changed files with 299 additions and 38 deletions
+4 -3
View File
@@ -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
+4 -6
View File
@@ -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
View File
@@ -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{},
+81
View File
@@ -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