mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 20:17:04 +00:00
Feat: vela status --tree (#3609)
* Feat: vela status --tree Signed-off-by: Somefive <yd219913@alibaba-inc.com> * Feat: support show not-deployed clusters Signed-off-by: Somefive <yd219913@alibaba-inc.com> * Fix: add tests Signed-off-by: Somefive <yd219913@alibaba-inc.com> * Fix: add multicluster e2e coverage Signed-off-by: Somefive <yd219913@alibaba-inc.com> * Chore: minor fix Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
@@ -96,7 +96,7 @@ jobs:
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: /tmp/e2e-profile.out
|
||||
files: /tmp/e2e-profile.out,/tmp/e2e_multicluster_test.out
|
||||
flags: e2e-multicluster-test
|
||||
name: codecov-umbrella
|
||||
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ coverage:
|
||||
default:
|
||||
target: 70%
|
||||
ignore:
|
||||
- "**/zz_generated.deepcopy.go"
|
||||
- "**/zz_generated.deepcopy.go"
|
||||
- "references/"
|
||||
|
||||
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
package oam
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
@@ -49,3 +51,14 @@ func GetPublishVersion(o client.Object) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetLastAppliedTime .
|
||||
func GetLastAppliedTime(o client.Object) time.Time {
|
||||
if annotations := o.GetAnnotations(); annotations != nil {
|
||||
s := annotations[AnnotationLastAppliedTime]
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return o.GetCreationTimestamp().Time
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ const (
|
||||
// resource for use in a three way diff during a patching apply
|
||||
AnnotationLastAppliedConfig = "app.oam.dev/last-applied-configuration"
|
||||
|
||||
// AnnotationLastAppliedTime indicates the last applied time
|
||||
AnnotationLastAppliedTime = "app.oam.dev/last-applied-time"
|
||||
|
||||
// AnnotationAppRollout indicates that the application is still rolling out
|
||||
// the application controller should treat it differently
|
||||
AnnotationAppRollout = "app.oam.dev/rollout-template"
|
||||
|
||||
@@ -17,10 +17,17 @@ limitations under the License.
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
// GetClusterLabelSelectorInTopology get cluster label selector in topology policy spec
|
||||
@@ -33,3 +40,57 @@ func GetClusterLabelSelectorInTopology(topology *v1alpha1.TopologyPolicySpec) ma
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlacementsFromTopologyPolicies get placements from topology policies with provided client
|
||||
func GetPlacementsFromTopologyPolicies(ctx context.Context, cli client.Client, app *v1beta1.Application, policies []v1beta1.AppPolicy, allowCrossNamespace bool) ([]v1alpha1.PlacementDecision, error) {
|
||||
var placements []v1alpha1.PlacementDecision
|
||||
placementMap := map[string]struct{}{}
|
||||
addCluster := func(cluster string, ns string, validateCluster bool) error {
|
||||
if validateCluster {
|
||||
if _, e := multicluster.GetVirtualCluster(ctx, cli, cluster); e != nil {
|
||||
return errors.Wrapf(e, "failed to get cluster %s", cluster)
|
||||
}
|
||||
}
|
||||
if !allowCrossNamespace && (ns != app.GetNamespace() && ns != "") {
|
||||
return errors.Errorf("cannot cross namespace")
|
||||
}
|
||||
placement := v1alpha1.PlacementDecision{Cluster: cluster, Namespace: ns}
|
||||
name := placement.String()
|
||||
if _, found := placementMap[name]; !found {
|
||||
placementMap[name] = struct{}{}
|
||||
placements = append(placements, placement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, policy := range policies {
|
||||
if policy.Type == v1alpha1.TopologyPolicyType {
|
||||
topologySpec := &v1alpha1.TopologyPolicySpec{}
|
||||
if err := utils.StrictUnmarshal(policy.Properties.Raw, topologySpec); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse topology policy %s", policy.Name)
|
||||
}
|
||||
clusterLabelSelector := GetClusterLabelSelectorInTopology(topologySpec)
|
||||
switch {
|
||||
case topologySpec.Clusters != nil:
|
||||
for _, cluster := range topologySpec.Clusters {
|
||||
if err := addCluster(cluster, topologySpec.Namespace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case clusterLabelSelector != nil:
|
||||
clusters, err := multicluster.FindVirtualClustersByLabels(context.Background(), cli, clusterLabelSelector)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find clusters in topology %s", policy.Name)
|
||||
}
|
||||
if len(clusters) == 0 {
|
||||
return nil, errors.New("failed to find any cluster matches given labels")
|
||||
}
|
||||
for _, cluster := range clusters {
|
||||
if err = addCluster(cluster.Name, topologySpec.Namespace, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return placements, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
Copyright 2021 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 resourcetracker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/gosuri/uitable"
|
||||
"github.com/gosuri/uitable/util/strutil"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
)
|
||||
|
||||
// ResourceDetailRetriever retriever to get details for resource
|
||||
type ResourceDetailRetriever func(*resourceRow) error
|
||||
|
||||
// ResourceTreePrintOptions print options for resource tree
|
||||
type ResourceTreePrintOptions struct {
|
||||
DetailRetriever ResourceDetailRetriever
|
||||
}
|
||||
|
||||
const (
|
||||
resourceRowStatusUpdated = "updated"
|
||||
resourceRowStatusNotDeployed = "not-deployed"
|
||||
resourceRowStatusOutdated = "outdated"
|
||||
)
|
||||
|
||||
type resourceRow struct {
|
||||
mr *v1beta1.ManagedResource
|
||||
status string
|
||||
cluster string
|
||||
namespace string
|
||||
resourceName string
|
||||
connectClusterUp bool
|
||||
connectClusterDown bool
|
||||
connectNamespaceUp bool
|
||||
connectNamespaceDown bool
|
||||
applyTime string
|
||||
details string
|
||||
}
|
||||
|
||||
func (options *ResourceTreePrintOptions) loadResourceRows(currentRT *v1beta1.ResourceTracker, historyRT []*v1beta1.ResourceTracker) []*resourceRow {
|
||||
var rows []*resourceRow
|
||||
if currentRT != nil {
|
||||
for _, mr := range currentRT.Spec.ManagedResources {
|
||||
if mr.Deleted {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, &resourceRow{
|
||||
mr: mr.DeepCopy(),
|
||||
status: resourceRowStatusUpdated,
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, rt := range historyRT {
|
||||
for _, mr := range rt.Spec.ManagedResources {
|
||||
var matchedRow *resourceRow
|
||||
for _, row := range rows {
|
||||
if row.mr.ResourceKey() == mr.ResourceKey() {
|
||||
matchedRow = row
|
||||
}
|
||||
}
|
||||
if matchedRow == nil {
|
||||
rows = append(rows, &resourceRow{
|
||||
mr: mr.DeepCopy(),
|
||||
status: resourceRowStatusOutdated,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (options *ResourceTreePrintOptions) sortRows(rows []*resourceRow) {
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].mr.Cluster != rows[j].mr.Cluster {
|
||||
return rows[i].mr.Cluster < rows[j].mr.Cluster
|
||||
}
|
||||
if rows[i].mr.Namespace != rows[j].mr.Namespace {
|
||||
return rows[i].mr.Namespace < rows[j].mr.Namespace
|
||||
}
|
||||
return rows[i].mr.ResourceKey() < rows[j].mr.ResourceKey()
|
||||
})
|
||||
}
|
||||
|
||||
func (options *ResourceTreePrintOptions) fillResourceRows(rows []*resourceRow, colsWidth []int) {
|
||||
for i := 0; i < 4; i++ {
|
||||
colsWidth[i] = 10
|
||||
}
|
||||
connectLastRow := func(rowIdx int, cluster bool, namespace bool) {
|
||||
rows[rowIdx].connectClusterUp = cluster
|
||||
rows[rowIdx-1].connectClusterDown = cluster
|
||||
rows[rowIdx].connectNamespaceUp = namespace
|
||||
rows[rowIdx-1].connectNamespaceDown = namespace
|
||||
}
|
||||
for rowIdx, row := range rows {
|
||||
if row.mr.Cluster == "" {
|
||||
row.mr.Cluster = multicluster.ClusterLocalName
|
||||
}
|
||||
if row.mr.Namespace == "" {
|
||||
row.mr.Namespace = "-"
|
||||
}
|
||||
row.cluster, row.namespace, row.resourceName = row.mr.Cluster, row.mr.Namespace, fmt.Sprintf("%s/%s", row.mr.Kind, row.mr.Name)
|
||||
if row.status == resourceRowStatusNotDeployed {
|
||||
row.resourceName = "-"
|
||||
}
|
||||
if rowIdx > 0 && row.mr.Cluster == rows[rowIdx-1].mr.Cluster {
|
||||
connectLastRow(rowIdx, true, false)
|
||||
row.cluster = ""
|
||||
if row.mr.Namespace == rows[rowIdx-1].mr.Namespace {
|
||||
connectLastRow(rowIdx, true, true)
|
||||
row.namespace = ""
|
||||
}
|
||||
}
|
||||
for i, val := range []string{row.cluster, row.namespace, row.resourceName, row.status} {
|
||||
if size := len(val) + 1; size > colsWidth[i] {
|
||||
colsWidth[i] = size
|
||||
}
|
||||
}
|
||||
}
|
||||
for rowIdx := len(rows); rowIdx >= 1; rowIdx-- {
|
||||
if rowIdx == len(rows) || rows[rowIdx].cluster != "" {
|
||||
for j := rowIdx - 1; j >= 1; j-- {
|
||||
if rows[j].cluster == "" && rows[j].namespace == "" {
|
||||
connectLastRow(j, false, rows[j].connectNamespaceUp)
|
||||
if j+1 < len(rows) {
|
||||
connectLastRow(j+1, false, rows[j+1].connectNamespaceUp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add extra spaces for tree connectors
|
||||
colsWidth[0] += 4
|
||||
colsWidth[1] += 4
|
||||
}
|
||||
|
||||
func (options *ResourceTreePrintOptions) writeResourceTree(writer io.Writer, rows []*resourceRow, colsWidth []int) {
|
||||
applyTimeWidth := 20
|
||||
|
||||
writePaddedString := func(sb *strings.Builder, head string, tail string, width int) {
|
||||
sb.WriteString(head)
|
||||
for c := strutil.StringWidth(head) + strutil.StringWidth(tail); c < width; c++ {
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
sb.WriteString(tail)
|
||||
}
|
||||
|
||||
var headerWriter strings.Builder
|
||||
for colIdx, colName := range []string{"CLUSTER", "NAMESPACE", "RESOURCE", "STATUS"} {
|
||||
writePaddedString(&headerWriter, colName, "", colsWidth[colIdx])
|
||||
}
|
||||
if options.DetailRetriever != nil {
|
||||
writePaddedString(&headerWriter, "APPLY_TIME", "", applyTimeWidth)
|
||||
_, _ = writer.Write([]byte(headerWriter.String() + "DETAIL" + "\n"))
|
||||
} else {
|
||||
_, _ = writer.Write([]byte(headerWriter.String() + "\n"))
|
||||
}
|
||||
|
||||
connectorColorizer := color.WhiteString
|
||||
outdatedColorizer := color.WhiteString
|
||||
|
||||
for _, row := range rows {
|
||||
if options.DetailRetriever != nil && row.status != resourceRowStatusNotDeployed {
|
||||
if err := options.DetailRetriever(row); err != nil {
|
||||
row.details = "Error: " + err.Error()
|
||||
}
|
||||
}
|
||||
for lineIdx, line := range strings.Split(row.details, "\n") {
|
||||
var sb strings.Builder
|
||||
rscName, rscStatus, applyTime := row.resourceName, row.status, row.applyTime
|
||||
if row.status != resourceRowStatusUpdated {
|
||||
rscName, rscStatus, applyTime, line = outdatedColorizer(row.resourceName), outdatedColorizer(row.status), outdatedColorizer(applyTime), outdatedColorizer(line)
|
||||
}
|
||||
if lineIdx == 0 {
|
||||
writePaddedString(&sb, row.cluster, connectorColorizer(utils.GetBoxDrawingString(row.connectClusterUp, row.connectClusterDown, row.cluster != "", row.namespace != "", 1, 1))+" ", colsWidth[0])
|
||||
writePaddedString(&sb, row.namespace, connectorColorizer(utils.GetBoxDrawingString(row.connectNamespaceUp, row.connectNamespaceDown, row.namespace != "", true, 1, 1))+" ", colsWidth[1])
|
||||
writePaddedString(&sb, rscName, "", colsWidth[2])
|
||||
writePaddedString(&sb, rscStatus, "", colsWidth[3])
|
||||
} else {
|
||||
writePaddedString(&sb, "", connectorColorizer(utils.GetBoxDrawingString(row.connectClusterDown, row.connectClusterDown, false, false, 1, 1))+" ", colsWidth[0])
|
||||
writePaddedString(&sb, "", connectorColorizer(utils.GetBoxDrawingString(row.connectNamespaceDown, row.connectNamespaceDown, false, false, 1, 1))+" ", colsWidth[1])
|
||||
writePaddedString(&sb, "", "", colsWidth[2])
|
||||
writePaddedString(&sb, "", "", colsWidth[3])
|
||||
}
|
||||
|
||||
if options.DetailRetriever != nil {
|
||||
if lineIdx != 0 {
|
||||
applyTime = ""
|
||||
}
|
||||
writePaddedString(&sb, applyTime, "", applyTimeWidth)
|
||||
}
|
||||
_, _ = writer.Write([]byte(sb.String() + line + "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (options *ResourceTreePrintOptions) addNonExistingPlacementToRows(placements []v1alpha1.PlacementDecision, rows []*resourceRow) []*resourceRow {
|
||||
existingClusters := map[string]struct{}{}
|
||||
for _, row := range rows {
|
||||
existingClusters[row.mr.Cluster] = struct{}{}
|
||||
}
|
||||
for _, p := range placements {
|
||||
if _, found := existingClusters[p.Cluster]; !found {
|
||||
rows = append(rows, &resourceRow{
|
||||
mr: &v1beta1.ManagedResource{
|
||||
ClusterObjectReference: apicommon.ClusterObjectReference{Cluster: p.Cluster},
|
||||
},
|
||||
status: resourceRowStatusNotDeployed,
|
||||
})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// PrintResourceTree print resource tree to writer
|
||||
func (options *ResourceTreePrintOptions) PrintResourceTree(writer io.Writer, currentPlacements []v1alpha1.PlacementDecision, currentRT *v1beta1.ResourceTracker, historyRT []*v1beta1.ResourceTracker) {
|
||||
rows := options.loadResourceRows(currentRT, historyRT)
|
||||
rows = options.addNonExistingPlacementToRows(currentPlacements, rows)
|
||||
options.sortRows(rows)
|
||||
|
||||
colsWidth := make([]int, 4)
|
||||
options.fillResourceRows(rows, colsWidth)
|
||||
|
||||
options.writeResourceTree(writer, rows, colsWidth)
|
||||
}
|
||||
|
||||
type tableRoundTripper struct {
|
||||
rt http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip mutate the request header to let apiserver return table data
|
||||
func (rt tableRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("Accept", strings.Join([]string{
|
||||
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
|
||||
"application/json",
|
||||
}, ","))
|
||||
return rt.rt.RoundTrip(req)
|
||||
}
|
||||
|
||||
// RetrieveKubeCtlGetMessageGenerator get details like kubectl get
|
||||
func RetrieveKubeCtlGetMessageGenerator(cfg *rest.Config, format string) (ResourceDetailRetriever, error) {
|
||||
cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return tableRoundTripper{rt: rt}
|
||||
})
|
||||
cli, err := client.New(cfg, client.Options{Scheme: common.Scheme})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(row *resourceRow) error {
|
||||
mr := row.mr
|
||||
un := &unstructured.Unstructured{}
|
||||
un.SetAPIVersion(mr.APIVersion)
|
||||
un.SetKind(mr.Kind)
|
||||
if err = cli.Get(multicluster.ContextWithClusterName(context.Background(), mr.Cluster), mr.NamespacedName(), un); err != nil {
|
||||
return err
|
||||
}
|
||||
un.SetAPIVersion(metav1.SchemeGroupVersion.String())
|
||||
un.SetKind("Table")
|
||||
table := &metav1.Table{}
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, table); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
if err := json.Unmarshal(table.Rows[0].Object.Raw, obj); err == nil {
|
||||
row.applyTime = oam.GetLastAppliedTime(obj).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "raw":
|
||||
raw := table.Rows[0].Object.Raw
|
||||
if annotations := obj.GetAnnotations(); annotations != nil && annotations[oam.AnnotationLastAppliedConfig] != "" {
|
||||
raw = []byte(annotations[oam.AnnotationLastAppliedConfig])
|
||||
}
|
||||
bs, err := yaml.JSONToYAML(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row.details = string(bs)
|
||||
case "table":
|
||||
tab := uitable.New()
|
||||
var tabHeaders, tabValues []interface{}
|
||||
for cid, column := range table.ColumnDefinitions {
|
||||
if column.Name == "Name" || column.Name == "Created At" || column.Priority != 0 {
|
||||
continue
|
||||
}
|
||||
tabHeaders = append(tabHeaders, column.Name)
|
||||
tabValues = append(tabValues, table.Rows[0].Cells[cid])
|
||||
}
|
||||
tab.AddRow(tabHeaders...)
|
||||
tab.AddRow(tabValues...)
|
||||
row.details = tab.String()
|
||||
default: // inline / wide / list
|
||||
var entries []string
|
||||
for cid, column := range table.ColumnDefinitions {
|
||||
if column.Name == "Name" || column.Name == "Created At" || (format == "inline" && column.Priority != 0) {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, fmt.Sprintf("%s: %v", column.Name, table.Rows[0].Cells[cid]))
|
||||
}
|
||||
if format == "inline" || format == "wide" {
|
||||
row.details = strings.Join(entries, " ")
|
||||
} else {
|
||||
row.details = strings.Join(entries, "\n")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package apply
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
@@ -147,6 +148,7 @@ func getModifiedConfiguration(obj runtime.Object, updateAnnotation bool) ([]byte
|
||||
|
||||
// restore original annotations back to the object
|
||||
annots[oam.AnnotationLastAppliedConfig] = original
|
||||
annots[oam.AnnotationLastAppliedTime] = time.Now().Format(time.RFC3339)
|
||||
_ = metadataAccessor.SetAnnotations(obj, annots)
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package utils
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringsContain strings contain
|
||||
@@ -85,3 +86,57 @@ func MapKey2Array(source map[string]string) []string {
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// GetBoxDrawingString get line drawing string, see https://en.wikipedia.org/wiki/Box-drawing_character
|
||||
// nolint:gocyclo
|
||||
func GetBoxDrawingString(up bool, down bool, left bool, right bool, padLeft int, padRight int) string {
|
||||
var c rune
|
||||
switch {
|
||||
case up && down && left && right:
|
||||
c = '┼'
|
||||
case up && down && left && !right:
|
||||
c = '┤'
|
||||
case up && down && !left && right:
|
||||
c = '├'
|
||||
case up && down && !left && !right:
|
||||
c = '│'
|
||||
case up && !down && left && right:
|
||||
c = '┴'
|
||||
case up && !down && left && !right:
|
||||
c = '┘'
|
||||
case up && !down && !left && right:
|
||||
c = '└'
|
||||
case up && !down && !left && !right:
|
||||
c = '╵'
|
||||
case !up && down && left && right:
|
||||
c = '┬'
|
||||
case !up && down && left && !right:
|
||||
c = '┐'
|
||||
case !up && down && !left && right:
|
||||
c = '┌'
|
||||
case !up && down && !left && !right:
|
||||
c = '╷'
|
||||
case !up && !down && left && right:
|
||||
c = '─'
|
||||
case !up && !down && left && !right:
|
||||
c = '╴'
|
||||
case !up && !down && !left && right:
|
||||
c = '╶'
|
||||
case !up && !down && !left && !right:
|
||||
c = ' '
|
||||
}
|
||||
sb := strings.Builder{}
|
||||
writePadding := func(connect bool, width int) {
|
||||
for i := 0; i < width; i++ {
|
||||
if connect {
|
||||
sb.WriteRune('─')
|
||||
} else {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
writePadding(left, padLeft)
|
||||
sb.WriteRune(c)
|
||||
writePadding(right, padRight)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -168,58 +168,13 @@ func (p *provider) ExpandTopology(ctx wfContext.Context, v *value.Value, act wfT
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies := &[]*v1beta1.AppPolicy{}
|
||||
policies := &[]v1beta1.AppPolicy{}
|
||||
if err = policiesRaw.UnmarshalTo(policies); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse policies")
|
||||
}
|
||||
var placements []v1alpha1.PlacementDecision
|
||||
placementMap := map[string]struct{}{}
|
||||
addCluster := func(cluster string, ns string, validateCluster bool) error {
|
||||
if validateCluster {
|
||||
if _, e := multicluster.GetVirtualCluster(context.Background(), p, cluster); e != nil {
|
||||
return errors.Wrapf(e, "failed to get cluster %s", cluster)
|
||||
}
|
||||
}
|
||||
if !resourcekeeper.AllowCrossNamespaceResource && (ns != p.app.GetNamespace() && ns != "") {
|
||||
return errors.Errorf("cannot cross namespace")
|
||||
}
|
||||
placement := v1alpha1.PlacementDecision{Cluster: cluster, Namespace: ns}
|
||||
name := placement.String()
|
||||
if _, found := placementMap[name]; !found {
|
||||
placementMap[name] = struct{}{}
|
||||
placements = append(placements, placement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, policy := range *policies {
|
||||
if policy.Type == v1alpha1.TopologyPolicyType {
|
||||
topologySpec := &v1alpha1.TopologyPolicySpec{}
|
||||
if err := utils.StrictUnmarshal(policy.Properties.Raw, topologySpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse topology policy %s", policy.Name)
|
||||
}
|
||||
clusterLabelSelector := pkgpolicy.GetClusterLabelSelectorInTopology(topologySpec)
|
||||
switch {
|
||||
case topologySpec.Clusters != nil:
|
||||
for _, cluster := range topologySpec.Clusters {
|
||||
if err := addCluster(cluster, topologySpec.Namespace, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case clusterLabelSelector != nil:
|
||||
clusters, err := multicluster.FindVirtualClustersByLabels(context.Background(), p, clusterLabelSelector)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to find clusters in topology %s", policy.Name)
|
||||
}
|
||||
if len(clusters) == 0 {
|
||||
return errors.Errorf("failed to find any cluster matches given labels")
|
||||
}
|
||||
for _, cluster := range clusters {
|
||||
if err = addCluster(cluster.Name, topologySpec.Namespace, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(context.Background(), p, p.app, *policies, resourcekeeper.AllowCrossNamespaceResource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.FillObject(placements, "outputs", "decisions")
|
||||
}
|
||||
|
||||
@@ -30,9 +30,15 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
pkgappfile "github.com/oam-dev/kubevela/pkg/appfile"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||||
"github.com/oam-dev/kubevela/pkg/policy"
|
||||
"github.com/oam-dev/kubevela/pkg/resourcetracker"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/appfile"
|
||||
@@ -103,6 +109,9 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if printTree, err := cmd.Flags().GetBool("tree"); err == nil && printTree {
|
||||
return printApplicationTree(c, cmd, appName, namespace)
|
||||
}
|
||||
newClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -125,8 +134,10 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream
|
||||
cmd.Flags().StringP("svc", "s", "", "service name")
|
||||
cmd.Flags().BoolP("endpoint", "p", false, "show all service endpoints of the application")
|
||||
cmd.Flags().StringP("component", "c", "", "filter service endpoints by component name")
|
||||
cmd.Flags().BoolP("tree", "t", false, "display the application resources into tree structure")
|
||||
cmd.Flags().BoolP("detail", "d", false, "display the realtime details of application resources")
|
||||
cmd.Flags().StringP("detail-format", "", "inline", "the format for displaying details. Can be one of inline (default), wide, list, table, raw.")
|
||||
addNamespaceAndEnvArg(cmd)
|
||||
cmd.SetOut(ioStreams.Out)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -344,3 +355,57 @@ func getAppPhaseColor(appPhase commontypes.ApplicationPhase) *color.Color {
|
||||
}
|
||||
return yellow
|
||||
}
|
||||
|
||||
func printApplicationTree(c common.Args, cmd *cobra.Command, appName string, appNs string) error {
|
||||
config, err := c.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper)
|
||||
cli, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pd, err := c.GetPackageDiscover()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dm, err := discoverymapper.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app, err := loadRemoteApplication(cli, appNs, appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, cli, app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := multicluster.GetClusterGatewayService(context.Background(), cli)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get cluster secret namespace, please ensure cluster gateway is correctly deployed")
|
||||
}
|
||||
multicluster.ClusterGatewaySecretNamespace = svc.Namespace
|
||||
|
||||
var placements []v1alpha1.PlacementDecision
|
||||
af, err := pkgappfile.NewApplicationParser(cli, dm, pd).GenerateAppFile(context.Background(), app)
|
||||
if err == nil {
|
||||
placements, _ = policy.GetPlacementsFromTopologyPolicies(context.Background(), cli, app, af.Policies, true)
|
||||
}
|
||||
options := resourcetracker.ResourceTreePrintOptions{}
|
||||
printDetails, _ := cmd.Flags().GetBool("detail")
|
||||
format, _ := cmd.Flags().GetString("detail-format")
|
||||
if printDetails {
|
||||
msgRetriever, err := resourcetracker.RetrieveKubeCtlGetMessageGenerator(config, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.DetailRetriever = msgRetriever
|
||||
}
|
||||
options.PrintResourceTree(cmd.OutOrStdout(), placements, currentRT, historyRTs)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,17 @@ var _ = Describe("Test multicluster CLI commands", func() {
|
||||
Expect(string(bs)).Should(ContainSubstring("Hello World"))
|
||||
})
|
||||
|
||||
It("Test vela status --tree", func() {
|
||||
for _, format := range []string{"inline", "wide", "table", "list"} {
|
||||
outputs, err := execCommand("status", app.Name, "-n", namespace, "--tree", "--detail", "--detail-format", format)
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(string(outputs)).Should(SatisfyAll(
|
||||
ContainSubstring("Deployment/exec-podinfo"),
|
||||
ContainSubstring("updated"),
|
||||
ContainSubstring("1/1"),
|
||||
))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user