769 lines
23 KiB
Go
769 lines
23 KiB
Go
/*
|
|
Copyright 2024 The Kruise 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 describe
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
|
kruiseappsv1beta1 "github.com/openkruise/kruise-api/apps/v1beta1"
|
|
rolloutsapi "github.com/openkruise/kruise-rollout-api/client/clientset/versioned"
|
|
rolloutv1alpha1types "github.com/openkruise/kruise-rollout-api/client/clientset/versioned/typed/rollouts/v1alpha1"
|
|
rolloutsv1beta1types "github.com/openkruise/kruise-rollout-api/client/clientset/versioned/typed/rollouts/v1beta1"
|
|
rolloutsapiv1alpha1 "github.com/openkruise/kruise-rollout-api/rollouts/v1alpha1"
|
|
rolloutsapiv1beta1 "github.com/openkruise/kruise-rollout-api/rollouts/v1beta1"
|
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
|
internalpolymorphichelpers "github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
|
"github.com/spf13/cobra"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/duration"
|
|
"k8s.io/apimachinery/pkg/watch"
|
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
|
"k8s.io/cli-runtime/pkg/resource"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"k8s.io/kubectl/pkg/util/i18n"
|
|
"k8s.io/kubectl/pkg/util/templates"
|
|
)
|
|
|
|
const (
|
|
tableFormat = "%-19s%v\n"
|
|
)
|
|
|
|
var (
|
|
rolloutLong = templates.LongDesc(i18n.T(`
|
|
Get details about and visual representation of a rollout.`))
|
|
|
|
rolloutExample = templates.Examples(`
|
|
# Describe the rollout named rollout-demo within namespace default
|
|
kubectl-kruise describe rollout rollout-demo/default
|
|
|
|
# Watch for changes to the rollout named rollout-demo
|
|
kubectl-kruise describe rollout rollout-demo/default -w`)
|
|
)
|
|
|
|
type DescribeRolloutOptions struct {
|
|
genericclioptions.IOStreams
|
|
Builder func() *resource.Builder
|
|
Namespace string
|
|
EnforceNamespace bool
|
|
Resources []string
|
|
RolloutViewerFn func(runtime.Object) (interface{}, error)
|
|
Watch bool
|
|
NoColor bool
|
|
All bool
|
|
TimeoutSeconds int
|
|
RolloutsV1beta1Client rolloutsv1beta1types.RolloutInterface
|
|
RolloutsV1alpha1Client rolloutv1alpha1types.RolloutInterface
|
|
}
|
|
|
|
type WorkloadInfo struct {
|
|
Name string
|
|
Kind string
|
|
Images []string
|
|
Replicas struct {
|
|
Desired int32
|
|
Current int32
|
|
Updated int32
|
|
Ready int32
|
|
Available int32
|
|
}
|
|
Pod []struct {
|
|
Name string
|
|
BatchID string
|
|
Status string
|
|
Ready string
|
|
Age string
|
|
Restarts string
|
|
Revision string
|
|
}
|
|
CurrentRevision string
|
|
UpdateRevision string
|
|
}
|
|
|
|
type RolloutInfo struct {
|
|
Name string
|
|
Namespace string
|
|
Phase string
|
|
Message string
|
|
ObservedGeneration int64
|
|
Generation int64
|
|
CurrentStepIndex int32
|
|
CurrentStepState string
|
|
Strategy rolloutsapiv1beta1.CanaryStrategy
|
|
TrafficRoutingRef string
|
|
WorkloadRef RolloutWorkloadRef
|
|
}
|
|
|
|
func NewCmdDescribeRollout(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
|
o := &DescribeRolloutOptions{IOStreams: streams}
|
|
cmd := &cobra.Command{
|
|
Use: "rollout SUBCOMMAND",
|
|
DisableFlagsInUseLine: true,
|
|
Short: i18n.T("Get details about a rollout"),
|
|
Long: rolloutLong,
|
|
Example: rolloutExample,
|
|
Aliases: []string{"rollouts", "ro"},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cmdutil.CheckErr(o.Complete(f, args))
|
|
cmdutil.CheckErr(o.Run())
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", false, "Watch for changes to the rollout")
|
|
cmd.Flags().BoolVar(&o.NoColor, "no-color", false, "If true, print output without color")
|
|
cmd.Flags().IntVar(&o.TimeoutSeconds, "timeout", 0, "Timeout after specified seconds")
|
|
cmd.Flags().BoolVar(&o.All, "all", false, "Show all pods in the rollout")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) Complete(f cmdutil.Factory, args []string) error {
|
|
var err error
|
|
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("required rollout name not specified")
|
|
}
|
|
|
|
o.Resources = args
|
|
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parts := strings.Split(args[0], "/")
|
|
if len(parts) == 2 {
|
|
o.Resources = []string{parts[0]}
|
|
o.Namespace = parts[1]
|
|
}
|
|
|
|
o.RolloutViewerFn = internalpolymorphichelpers.RolloutViewerFn
|
|
o.Builder = f.NewBuilder
|
|
|
|
config, err := f.ToRESTConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rolloutsClientset, err := rolloutsapi.NewForConfig(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.RolloutsV1beta1Client = rolloutsClientset.RolloutsV1beta1().Rollouts(o.Namespace)
|
|
|
|
o.RolloutsV1alpha1Client = rolloutsClientset.RolloutsV1alpha1().Rollouts(o.Namespace)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) Run() error {
|
|
rolloutName := o.Resources[0]
|
|
|
|
r := o.Builder().
|
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
|
ResourceNames("rollouts.rollouts.kruise.io", rolloutName).
|
|
ContinueOnError().
|
|
Latest().
|
|
Flatten().
|
|
Do()
|
|
|
|
if err := r.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !o.Watch {
|
|
return r.Visit(o.describeRollout)
|
|
}
|
|
|
|
return o.watchRollout(r)
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) describeRollout(info *resource.Info, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rollout, err := o.RolloutViewerFn(info.Object)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.printRolloutInfo(rollout)
|
|
return nil
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) watchRollout(r *resource.Result) error {
|
|
infos, err := r.Infos()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(infos) != 1 {
|
|
return fmt.Errorf("watch is only supported on a single rollout")
|
|
}
|
|
info := infos[0]
|
|
|
|
var watcher watch.Interface
|
|
|
|
switch info.Object.(type) {
|
|
case *rolloutsapiv1beta1.Rollout:
|
|
watcher, err = o.RolloutsV1beta1Client.Watch(context.TODO(), metav1.ListOptions{
|
|
FieldSelector: "metadata.name=" + info.Name,
|
|
})
|
|
case *rolloutsapiv1alpha1.Rollout:
|
|
watcher, err = o.RolloutsV1alpha1Client.Watch(context.TODO(), metav1.ListOptions{
|
|
FieldSelector: "metadata.name=" + info.Name,
|
|
})
|
|
default:
|
|
return fmt.Errorf("unsupported rollout type %T", info.Object)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer watcher.Stop()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
if o.TimeoutSeconds > 0 {
|
|
ctx, cancel = context.WithTimeout(ctx, time.Duration(o.TimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
}
|
|
|
|
return o.watchRolloutUpdates(ctx, watcher)
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) watchRolloutUpdates(ctx context.Context, watcher watch.Interface) error {
|
|
for {
|
|
select {
|
|
case event, ok := <-watcher.ResultChan():
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if event.Type == watch.Added || event.Type == watch.Modified {
|
|
var rollout interface{}
|
|
switch obj := event.Object.(type) {
|
|
case *rolloutsapiv1beta1.Rollout:
|
|
rollout = obj
|
|
case *rolloutsapiv1alpha1.Rollout:
|
|
rollout = obj
|
|
default:
|
|
continue
|
|
}
|
|
o.clearScreen()
|
|
o.printRolloutInfo(rollout)
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) clearScreen() {
|
|
fmt.Fprint(o.Out, "\033[2J\033[H")
|
|
}
|
|
|
|
type RolloutWorkloadRef struct {
|
|
Kind string
|
|
Name string
|
|
StableRevision string
|
|
CanaryRevision string
|
|
PodTemplateHash string
|
|
CurrentStepIndex int32
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) GetResources(rollout RolloutWorkloadRef) (*WorkloadInfo, error) {
|
|
resources := []string{rollout.Kind + "/" + rollout.Name}
|
|
r := o.Builder().
|
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
|
ResourceTypeOrNameArgs(true, resources...).
|
|
ContinueOnError().
|
|
Latest().
|
|
Flatten().
|
|
Do()
|
|
|
|
if err := r.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
obj, err := r.Object()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
workloadInfo := &WorkloadInfo{}
|
|
objValue := reflect.ValueOf(obj).Elem()
|
|
workloadInfo.Name = objValue.FieldByName("Name").String()
|
|
workloadInfo.Kind = objValue.Type().Name()
|
|
|
|
podTemplateSpec := objValue.FieldByName("Spec").FieldByName("Template").FieldByName("Spec")
|
|
containers := podTemplateSpec.FieldByName("Containers")
|
|
for i := 0; i < containers.Len(); i++ {
|
|
container := containers.Index(i)
|
|
workloadInfo.Images = append(workloadInfo.Images, container.FieldByName("Image").String())
|
|
}
|
|
|
|
// Deployment,StatefulSet,CloneSet,Advanced StatefulSet,Advanced DaemonSet
|
|
|
|
switch o := obj.(type) {
|
|
case *appsv1.Deployment:
|
|
workloadInfo.Replicas.Desired = *o.Spec.Replicas
|
|
workloadInfo.Replicas.Current = o.Status.Replicas
|
|
workloadInfo.Replicas.Updated = o.Status.UpdatedReplicas
|
|
workloadInfo.Replicas.Ready = o.Status.ReadyReplicas
|
|
workloadInfo.Replicas.Available = o.Status.AvailableReplicas
|
|
case *appsv1.StatefulSet:
|
|
workloadInfo.Replicas.Desired = *o.Spec.Replicas
|
|
workloadInfo.Replicas.Current = o.Status.Replicas
|
|
workloadInfo.Replicas.Updated = o.Status.UpdatedReplicas
|
|
workloadInfo.Replicas.Ready = o.Status.ReadyReplicas
|
|
workloadInfo.Replicas.Available = o.Status.AvailableReplicas
|
|
case *kruiseappsv1alpha1.DaemonSet:
|
|
workloadInfo.Replicas.Desired = o.Spec.BurstReplicas.IntVal
|
|
workloadInfo.Replicas.Current = o.Status.CurrentNumberScheduled
|
|
workloadInfo.Replicas.Updated = o.Status.UpdatedNumberScheduled
|
|
workloadInfo.Replicas.Ready = o.Status.NumberReady
|
|
workloadInfo.Replicas.Available = o.Status.NumberAvailable
|
|
case *kruiseappsv1beta1.StatefulSet:
|
|
workloadInfo.Replicas.Desired = *o.Spec.Replicas
|
|
workloadInfo.Replicas.Current = o.Status.Replicas
|
|
workloadInfo.Replicas.Updated = o.Status.UpdatedReplicas
|
|
workloadInfo.Replicas.Ready = o.Status.ReadyReplicas
|
|
workloadInfo.Replicas.Available = o.Status.AvailableReplicas
|
|
case *kruiseappsv1alpha1.CloneSet:
|
|
workloadInfo.Replicas.Desired = *o.Spec.Replicas
|
|
workloadInfo.Replicas.Current = o.Status.Replicas
|
|
workloadInfo.Replicas.Updated = o.Status.UpdatedReplicas
|
|
workloadInfo.Replicas.Ready = o.Status.ReadyReplicas
|
|
workloadInfo.Replicas.Available = o.Status.AvailableReplicas
|
|
default:
|
|
return nil, fmt.Errorf("unsupported workload kind %T", obj)
|
|
}
|
|
|
|
workloadInfo.CurrentRevision = rollout.StableRevision
|
|
workloadInfo.UpdateRevision = rollout.CanaryRevision
|
|
|
|
var labelSelectorParam string
|
|
switch obj.(type) {
|
|
case *appsv1.Deployment:
|
|
labelSelectorParam = "pod-template-hash"
|
|
default:
|
|
labelSelectorParam = "controller-revision-hash"
|
|
}
|
|
|
|
selectorParam := rollout.PodTemplateHash
|
|
if selectorParam == "" {
|
|
selectorParam = rollout.StableRevision
|
|
}
|
|
|
|
labelSelectors := []string{
|
|
fmt.Sprintf("%s=%s", labelSelectorParam, selectorParam),
|
|
}
|
|
|
|
if !o.All && rollout.CurrentStepIndex != 0 {
|
|
labelSelectors = append(labelSelectors,
|
|
fmt.Sprintf("rollouts.kruise.io/rollout-batch-id=%v",
|
|
rollout.CurrentStepIndex))
|
|
}
|
|
|
|
// Fetch pods
|
|
r2 := o.Builder().
|
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
|
ResourceTypes("pods").
|
|
LabelSelectorParam(strings.Join(labelSelectors, ",")).
|
|
Latest().
|
|
Flatten().
|
|
Do()
|
|
|
|
if err := r2.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = r2.Visit(func(info *resource.Info, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pod, ok := info.Object.(*corev1.Pod)
|
|
if !ok {
|
|
return fmt.Errorf("expected *corev1.Pod, got %T", info.Object)
|
|
}
|
|
|
|
podInfo := struct {
|
|
Name string
|
|
BatchID string
|
|
Status string
|
|
Ready string
|
|
Age string
|
|
Restarts string
|
|
Revision string
|
|
}{
|
|
Name: pod.Name,
|
|
BatchID: pod.Labels["rollouts.kruise.io/rollout-batch-id"],
|
|
Status: string(pod.Status.Phase),
|
|
Age: duration.HumanDuration(time.Since(pod.CreationTimestamp.Time)),
|
|
Restarts: "0",
|
|
}
|
|
|
|
if pod.DeletionTimestamp != nil {
|
|
podInfo.Status = "Terminating"
|
|
}
|
|
|
|
if len(pod.Status.ContainerStatuses) > 0 {
|
|
restartCount := 0
|
|
for _, containerStatus := range pod.Status.ContainerStatuses {
|
|
restartCount += int(containerStatus.RestartCount)
|
|
}
|
|
|
|
podInfo.Restarts = strconv.Itoa(restartCount)
|
|
}
|
|
|
|
// Calculate ready status
|
|
readyContainers := 0
|
|
for _, containerStatus := range pod.Status.ContainerStatuses {
|
|
if containerStatus.Ready {
|
|
readyContainers++
|
|
}
|
|
}
|
|
podInfo.Ready = fmt.Sprintf("%d/%d", readyContainers, len(pod.Spec.Containers))
|
|
|
|
// Calculate revision
|
|
if pod.Labels["pod-template-hash"] != "" {
|
|
podInfo.Revision = pod.Labels["pod-template-hash"]
|
|
} else if pod.Labels["controller-revision-hash"] != "" {
|
|
podInfo.Revision = pod.Labels["controller-revision-hash"]
|
|
}
|
|
|
|
workloadInfo.Pod = append(workloadInfo.Pod, podInfo)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sort pods by batch ID and ready count
|
|
sort.Slice(workloadInfo.Pod, func(i, j int) bool {
|
|
if workloadInfo.Pod[i].BatchID != workloadInfo.Pod[j].BatchID {
|
|
return workloadInfo.Pod[i].BatchID < workloadInfo.Pod[j].BatchID
|
|
}
|
|
|
|
iReady := strings.Split(workloadInfo.Pod[i].Ready, "/")
|
|
jReady := strings.Split(workloadInfo.Pod[j].Ready, "/")
|
|
|
|
iReadyCount, _ := strconv.Atoi(iReady[0])
|
|
jReadyCount, _ := strconv.Atoi(jReady[0])
|
|
|
|
return iReadyCount > jReadyCount
|
|
})
|
|
|
|
return workloadInfo, nil
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) colorizeIcon(phase string) string {
|
|
if o.NoColor || phase == "" {
|
|
return ""
|
|
}
|
|
switch phase {
|
|
case string(rolloutsapiv1beta1.RolloutPhaseHealthy), string(corev1.PodRunning):
|
|
return "\033[32m✔\033[0m"
|
|
case string(rolloutsapiv1beta1.RolloutPhaseProgressing), string(corev1.PodPending):
|
|
return "\033[33m⚠\033[0m"
|
|
case string(rolloutsapiv1beta1.RolloutPhaseDisabled), string(corev1.PodUnknown), string(rolloutsapiv1beta1.RolloutPhaseTerminating), string(rolloutsapiv1beta1.RolloutPhaseDisabling), string(corev1.PodFailed):
|
|
return "\033[31m✘\033[0m"
|
|
case string(rolloutsapiv1beta1.RolloutPhaseInitial):
|
|
return "\033[33m⚠\033[0m"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) printTrafficRouting(trafficRouting []rolloutsapiv1beta1.TrafficRoutingRef) {
|
|
fmt.Fprint(o.Out, "Traffic Routings:\n")
|
|
for _, trafficRouting := range trafficRouting {
|
|
fmt.Fprintf(o.Out, tableFormat, " - Service: ", trafficRouting.Service)
|
|
if trafficRouting.Ingress != nil {
|
|
fmt.Fprintln(o.Out, ` Ingress: `)
|
|
fmt.Fprintf(o.Out, tableFormat, " classType: ", trafficRouting.Ingress.ClassType)
|
|
fmt.Fprintf(o.Out, tableFormat, " name: ", trafficRouting.Ingress.Name)
|
|
}
|
|
if trafficRouting.Gateway != nil {
|
|
fmt.Fprintln(o.Out, ` Gateway: `)
|
|
fmt.Fprintf(o.Out, tableFormat, " HttpRouteName: ", trafficRouting.Gateway.HTTPRouteName)
|
|
}
|
|
if trafficRouting.CustomNetworkRefs != nil {
|
|
fmt.Fprintln(o.Out, ` CustomNetworkRefs: `)
|
|
for _, customNetworkRef := range trafficRouting.CustomNetworkRefs {
|
|
fmt.Fprintf(o.Out, tableFormat, " name: ", customNetworkRef.Name)
|
|
fmt.Fprintf(o.Out, tableFormat, " kind: ", customNetworkRef.Kind)
|
|
fmt.Fprintf(o.Out, tableFormat, " apiVersion: ", customNetworkRef.APIVersion)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func convertCustomNetworkRefs(refs []rolloutsapiv1alpha1.CustomNetworkRef) []rolloutsapiv1beta1.ObjectRef {
|
|
var result []rolloutsapiv1beta1.ObjectRef
|
|
for _, ref := range refs {
|
|
result = append(result, rolloutsapiv1beta1.ObjectRef{
|
|
Name: ref.Name,
|
|
Kind: ref.Kind,
|
|
APIVersion: ref.APIVersion,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func extractRolloutInfo(obj interface{}) *RolloutInfo {
|
|
info := &RolloutInfo{}
|
|
|
|
switch r := obj.(type) {
|
|
case *rolloutsapiv1beta1.Rollout:
|
|
info.Name = r.Name
|
|
info.Namespace = r.Namespace
|
|
info.Phase = string(r.Status.Phase)
|
|
info.Message = r.Status.Message
|
|
info.ObservedGeneration = r.Status.ObservedGeneration
|
|
info.Generation = r.GetObjectMeta().GetGeneration()
|
|
info.CurrentStepIndex = r.Status.CanaryStatus.CurrentStepIndex
|
|
info.CurrentStepState = string(r.Status.CanaryStatus.CurrentStepState)
|
|
info.WorkloadRef = RolloutWorkloadRef{
|
|
Kind: r.Spec.WorkloadRef.Kind,
|
|
Name: r.Spec.WorkloadRef.Name,
|
|
StableRevision: r.Status.CanaryStatus.StableRevision,
|
|
CanaryRevision: r.Status.CanaryStatus.CanaryRevision,
|
|
PodTemplateHash: r.Status.CanaryStatus.PodTemplateHash,
|
|
CurrentStepIndex: r.Status.CanaryStatus.CurrentStepIndex,
|
|
}
|
|
|
|
if r.Spec.Strategy.Canary != nil {
|
|
info.TrafficRoutingRef = r.Spec.Strategy.Canary.TrafficRoutingRef
|
|
}
|
|
|
|
case *rolloutsapiv1alpha1.Rollout:
|
|
info.Name = r.Name
|
|
info.Namespace = r.Namespace
|
|
info.Phase = string(r.Status.Phase)
|
|
info.Message = r.Status.Message
|
|
info.ObservedGeneration = r.Status.ObservedGeneration
|
|
info.Generation = r.GetObjectMeta().GetGeneration()
|
|
info.CurrentStepIndex = r.Status.CanaryStatus.CurrentStepIndex
|
|
info.CurrentStepState = string(r.Status.CanaryStatus.CurrentStepState)
|
|
info.WorkloadRef = RolloutWorkloadRef{
|
|
Kind: r.Spec.ObjectRef.WorkloadRef.Kind,
|
|
Name: r.Spec.ObjectRef.WorkloadRef.Name,
|
|
StableRevision: r.Status.CanaryStatus.StableRevision,
|
|
CanaryRevision: r.Status.CanaryStatus.CanaryRevision,
|
|
PodTemplateHash: r.Status.CanaryStatus.PodTemplateHash,
|
|
CurrentStepIndex: r.Status.CanaryStatus.CurrentStepIndex,
|
|
}
|
|
|
|
if r.Spec.Strategy.Canary != nil {
|
|
info.TrafficRoutingRef = r.ObjectMeta.Annotations["rollouts.kruise.io/trafficrouting"]
|
|
}
|
|
}
|
|
|
|
if obj.(*rolloutsapiv1beta1.Rollout).Spec.Strategy.Canary != nil {
|
|
info.Strategy = *obj.(*rolloutsapiv1beta1.Rollout).Spec.Strategy.Canary
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) fetchAndPrintTrafficRoutingRef(ref string) {
|
|
r := o.Builder().
|
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
|
ResourceNames("trafficroutings.rollouts.kruise.io", ref).
|
|
ContinueOnError().
|
|
Latest().
|
|
Flatten().
|
|
Do()
|
|
|
|
if err := r.Err(); err != nil {
|
|
fmt.Fprintf(o.Out, "Error getting TrafficRoutingRef: %v\n", err)
|
|
return
|
|
}
|
|
|
|
err := r.Visit(func(info *resource.Info, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
trafficRouting, ok := info.Object.(*rolloutsapiv1alpha1.TrafficRouting)
|
|
if !ok {
|
|
return fmt.Errorf("expected *rolloutsapiv1alpha1.TrafficRouting")
|
|
}
|
|
|
|
var trafficRoutingRef []rolloutsapiv1beta1.TrafficRoutingRef
|
|
for _, ref := range trafficRouting.Spec.ObjectRef {
|
|
trafficRoutingRef = append(trafficRoutingRef, rolloutsapiv1beta1.TrafficRoutingRef{
|
|
Service: ref.Service,
|
|
Ingress: (*rolloutsapiv1beta1.IngressTrafficRouting)(ref.Ingress),
|
|
Gateway: (*rolloutsapiv1beta1.GatewayTrafficRouting)(ref.Gateway),
|
|
CustomNetworkRefs: convertCustomNetworkRefs(ref.CustomNetworkRefs),
|
|
})
|
|
}
|
|
|
|
o.printTrafficRouting(trafficRoutingRef)
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) printRolloutInfo(rollout interface{}) {
|
|
info := extractRolloutInfo(rollout)
|
|
|
|
// Print basic info
|
|
fmt.Fprintf(o.Out, tableFormat, "Name:", info.Name)
|
|
fmt.Fprintf(o.Out, tableFormat, "Namespace:", info.Namespace)
|
|
|
|
if info.ObservedGeneration == info.Generation {
|
|
fmt.Fprintf(o.Out, tableFormat, "Status:", o.colorizeIcon(info.Phase)+" "+info.Phase)
|
|
if info.Message != "" {
|
|
fmt.Fprintf(o.Out, tableFormat, "Message:", info.Message)
|
|
}
|
|
}
|
|
|
|
// Print strategy
|
|
fmt.Fprintf(o.Out, tableFormat, "Strategy:", "Canary")
|
|
fmt.Fprintf(o.Out, tableFormat, " Step:", strconv.Itoa(int(info.CurrentStepIndex))+"/"+strconv.Itoa(len(info.Strategy.Steps)))
|
|
|
|
// Print steps
|
|
fmt.Fprint(o.Out, " Steps:\n")
|
|
o.printSteps(info)
|
|
|
|
// Print traffic routing if available
|
|
if len(info.Strategy.TrafficRoutings) > 0 {
|
|
o.printTrafficRouting(info.Strategy.TrafficRoutings)
|
|
}
|
|
|
|
if info.TrafficRoutingRef != "" {
|
|
o.fetchAndPrintTrafficRoutingRef(info.TrafficRoutingRef)
|
|
}
|
|
|
|
// Print workload info
|
|
workloadInfo, err := o.GetResources(info.WorkloadRef)
|
|
if err != nil {
|
|
fmt.Fprintf(o.Out, "Error getting resources: %v\n", err)
|
|
return
|
|
}
|
|
|
|
// Print images
|
|
for i, image := range workloadInfo.Images {
|
|
if i == 0 {
|
|
fmt.Fprintf(o.Out, tableFormat, "Images:", image)
|
|
} else {
|
|
fmt.Fprintf(o.Out, tableFormat, "", image)
|
|
}
|
|
}
|
|
|
|
// Print revisions
|
|
fmt.Fprintf(o.Out, tableFormat, "Current Revision:", workloadInfo.CurrentRevision)
|
|
fmt.Fprintf(o.Out, tableFormat, "Update Revision:", workloadInfo.UpdateRevision)
|
|
|
|
// Print replicas
|
|
if info.ObservedGeneration == info.Generation {
|
|
o.printReplicas(workloadInfo)
|
|
}
|
|
|
|
// Print pods
|
|
if len(workloadInfo.Pod) > 0 {
|
|
o.printPods(workloadInfo)
|
|
}
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) printSteps(info *RolloutInfo) {
|
|
currentStepIndex := int(info.CurrentStepIndex)
|
|
|
|
for i, step := range info.Strategy.Steps {
|
|
isCurrentStep := (i + 1) == currentStepIndex
|
|
if isCurrentStep {
|
|
fmt.Fprint(o.Out, "\033[32m")
|
|
}
|
|
|
|
if step.Replicas != nil {
|
|
fmt.Fprintf(o.Out, tableFormat, " - Replicas: ", step.Replicas)
|
|
}
|
|
if step.Traffic != nil {
|
|
fmt.Fprintf(o.Out, tableFormat, " Traffic: ", *step.Traffic)
|
|
}
|
|
|
|
if len(step.Matches) > 0 {
|
|
fmt.Fprintln(o.Out, " Matches: ")
|
|
for _, match := range step.Matches {
|
|
fmt.Fprintln(o.Out, " - Headers: ")
|
|
for _, header := range match.Headers {
|
|
fmt.Fprintf(o.Out, tableFormat, " - Name:", header.Name)
|
|
fmt.Fprintf(o.Out, tableFormat, " Value:", header.Value)
|
|
fmt.Fprintf(o.Out, tableFormat, " Type:", *header.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
if isCurrentStep {
|
|
fmt.Fprintf(o.Out, tableFormat, " State:", info.CurrentStepState)
|
|
fmt.Fprint(o.Out, "\033[0m")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) printReplicas(info *WorkloadInfo) {
|
|
fmt.Fprint(o.Out, "Replicas:\n")
|
|
fmt.Fprintf(o.Out, tableFormat, " Desired:", info.Replicas.Desired)
|
|
fmt.Fprintf(o.Out, tableFormat, " Updated:", info.Replicas.Updated)
|
|
fmt.Fprintf(o.Out, tableFormat, " Current:", info.Replicas.Current)
|
|
fmt.Fprintf(o.Out, tableFormat, " Ready:", info.Replicas.Ready)
|
|
fmt.Fprintf(o.Out, tableFormat, " Available:", info.Replicas.Available)
|
|
}
|
|
|
|
func (o *DescribeRolloutOptions) printPods(info *WorkloadInfo) {
|
|
w := tabwriter.NewWriter(o.Out, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "NAME\tREADY\tBATCH ID\tREVISION\tAGE\tRESTARTS\tSTATUS")
|
|
|
|
for _, pod := range info.Pod {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s %s\n",
|
|
pod.Name,
|
|
pod.Ready,
|
|
pod.BatchID,
|
|
pod.Revision,
|
|
pod.Age,
|
|
pod.Restarts,
|
|
o.colorizeIcon(pod.Status),
|
|
pod.Status,
|
|
)
|
|
}
|
|
w.Flush()
|
|
}
|