blob: 9f34593e40a12c80e16ed2f6519eddcebf7e45bf [file]
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 cmd
import (
"context"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"text/tabwriter"
"github.com/apache/dubbo-kubernetes/cli/pkg/cli"
"github.com/apache/dubbo-kubernetes/pkg/kube"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const dubbodMonitoringPort = "8080"
// proxySyncStatus mirrors the JSON emitted by dubbod's /debug/syncz endpoint.
type proxySyncStatus struct {
ProxyID string `json:"proxy"`
ClusterID string `json:"cluster_id,omitempty"`
WatchedTypes []struct {
TypeURL string `json:"type_url"`
NonceSent string `json:"nonce_sent,omitempty"`
NonceAcked string `json:"nonce_acked,omitempty"`
Synced bool `json:"synced"`
ResourceSize int `json:"resources"`
LastError string `json:"last_error,omitempty"`
} `json:"watched_types"`
}
// ProxyStatusCmd reports the xDS synchronization status of every proxy
// connected to dubbod, based on the /debug/syncz endpoint.
func ProxyStatusCmd(ctx cli.Context) *cobra.Command {
command := &cobra.Command{
Use: "proxy-status",
Aliases: []string{"ps"},
Short: "Retrieves the xDS sync status of each proxy in the mesh",
Long: `Retrieves the last sent and last acknowledged xDS resource versions for each proxy
connected to dubbod. A resource type is SYNCED when the proxy has acknowledged
the most recent configuration pushed by dubbod.`,
Example: ` # Show the sync status of all proxies
dubboctl proxy-status`,
RunE: func(cmd *cobra.Command, _ []string) error {
client, err := ctx.CLIClient()
if err != nil {
return fmt.Errorf("failed to create Kubernetes client: %v", err)
}
statuses, err := fetchSyncStatuses(cmd.Context(), client, ctx.Namespace())
if err != nil {
return err
}
writeProxyStatusTable(cmd.OutOrStdout(), statuses)
return nil
},
}
return command
}
// fetchSyncStatuses queries /debug/syncz on every dubbod pod through the
// Kubernetes API server proxy and merges the results.
func fetchSyncStatuses(ctx context.Context, client kube.CLIClient, namespace string) ([]proxySyncStatus, error) {
pods, err := client.Kube().CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: "app=dubbod",
FieldSelector: "status.phase=Running",
})
if err != nil {
return nil, fmt.Errorf("failed to list dubbod pods in namespace %q: %v", namespace, err)
}
if len(pods.Items) == 0 {
return nil, fmt.Errorf("no running dubbod pods found in namespace %q; is the control plane installed?", namespace)
}
seen := map[string]bool{}
all := []proxySyncStatus{}
var errs []string
for _, pod := range pods.Items {
data, err := proxyGetDebugEndpoint(ctx, client, pod, "/debug/syncz")
if err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", pod.Name, err))
continue
}
statuses, err := parseSyncStatuses(data)
if err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", pod.Name, err))
continue
}
for _, s := range statuses {
if seen[s.ProxyID] {
continue
}
seen[s.ProxyID] = true
all = append(all, s)
}
}
if len(all) == 0 && len(errs) > 0 {
return nil, fmt.Errorf("failed to fetch sync status from dubbod: %s", strings.Join(errs, "; "))
}
sort.Slice(all, func(i, j int) bool { return all[i].ProxyID < all[j].ProxyID })
return all, nil
}
func proxyGetDebugEndpoint(ctx context.Context, client kube.CLIClient, pod corev1.Pod, path string) ([]byte, error) {
return client.Kube().CoreV1().Pods(pod.Namespace).
ProxyGet("http", pod.Name, dubbodMonitoringPort, path, nil).
DoRaw(ctx)
}
func writeProxyStatusTable(out io.Writer, statuses []proxySyncStatus) {
if len(statuses) == 0 {
fmt.Fprintln(out, "No proxies are connected to dubbod.")
return
}
w := tabwriter.NewWriter(out, 0, 8, 4, ' ', 0)
fmt.Fprintln(w, "NAME\tCLUSTER\tCDS\tLDS\tEDS\tRDS")
for _, s := range statuses {
byType := map[string]string{"CDS": "NOT WATCHED", "LDS": "NOT WATCHED", "EDS": "NOT WATCHED", "RDS": "NOT WATCHED"}
for _, wt := range s.WatchedTypes {
short := shortTypeName(wt.TypeURL)
if _, ok := byType[short]; !ok {
continue
}
switch {
case wt.LastError != "":
byType[short] = "ERROR"
case wt.Synced:
byType[short] = "SYNCED"
case wt.NonceSent == "":
byType[short] = "NOT SENT"
default:
byType[short] = "STALE"
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", s.ProxyID, s.ClusterID, byType["CDS"], byType["LDS"], byType["EDS"], byType["RDS"])
}
w.Flush()
}
func shortTypeName(typeURL string) string {
switch {
case strings.Contains(typeURL, "ClusterLoadAssignment"), strings.Contains(typeURL, "Endpoint"):
return "EDS"
case strings.Contains(typeURL, "Cluster"):
return "CDS"
case strings.Contains(typeURL, "Listener"):
return "LDS"
case strings.Contains(typeURL, "Route"):
return "RDS"
default:
return typeURL
}
}
func parseSyncStatuses(data []byte) ([]proxySyncStatus, error) {
statuses := []proxySyncStatus{}
if err := json.Unmarshal(data, &statuses); err != nil {
return nil, fmt.Errorf("failed to parse dubbod sync status: %v", err)
}
return statuses, nil
}