istioctl/cmd/revision.go (1,000 lines of code) (raw):

// Copyright Istio 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 cmd import ( "context" "encoding/json" "fmt" "io" "strings" "text/tabwriter" "time" ) import ( "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "istio.io/api/label" "istio.io/api/operator/v1alpha1" admit_v1 "k8s.io/api/admissionregistration/v1" v1 "k8s.io/api/core/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" apimachinery_schema "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/validation" ) import ( "github.com/apache/dubbo-go-pixiu/istioctl/pkg/tag" "github.com/apache/dubbo-go-pixiu/operator/cmd/mesh" operator_istio "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio" iopv1alpha1 "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1" "github.com/apache/dubbo-go-pixiu/operator/pkg/manifest" "github.com/apache/dubbo-go-pixiu/operator/pkg/util" "github.com/apache/dubbo-go-pixiu/operator/pkg/util/clog" "github.com/apache/dubbo-go-pixiu/pkg/config" "github.com/apache/dubbo-go-pixiu/pkg/kube" ) type revisionArgs struct { manifestsPath string name string verbose bool output string } const ( istioOperatorCRSection = "ISTIO-OPERATOR-CR" controlPlaneSection = "CONTROL-PLANE" gatewaysSection = "GATEWAYS" webhooksSection = "MUTATING-WEBHOOKS" podsSection = "PODS" jsonFormat = "json" tableFormat = "table" ) var ( validFormats = map[string]bool{ tableFormat: true, jsonFormat: true, } defaultSections = []string{ istioOperatorCRSection, webhooksSection, controlPlaneSection, gatewaysSection, } verboseSections = []string{ istioOperatorCRSection, webhooksSection, controlPlaneSection, gatewaysSection, podsSection, } ) var ( istioOperatorGVR = apimachinery_schema.GroupVersionResource{ Group: iopv1alpha1.SchemeGroupVersion.Group, Version: iopv1alpha1.SchemeGroupVersion.Version, Resource: "istiooperators", } revArgs = revisionArgs{} ) func revisionCommand() *cobra.Command { revisionCmd := &cobra.Command{ Use: "revision", Long: "The revision command provides a revision centric view of istio deployments. " + "It provides insight into IstioOperator CRs defining the revision, istiod and gateway pods " + "which are part of deployment of a particular revision.", Short: "Provide insight into various revisions (istiod, gateways) installed in the cluster", Aliases: []string{"rev"}, } revisionCmd.PersistentFlags().StringVarP(&revArgs.manifestsPath, "manifests", "d", "", mesh.ManifestsFlagHelpStr) revisionCmd.PersistentFlags().BoolVarP(&revArgs.verbose, "verbose", "v", false, "Enable verbose output") revisionCmd.PersistentFlags().StringVarP(&revArgs.output, "output", "o", tableFormat, "Output format for revision description "+ "(available formats: table,json)") revisionCmd.AddCommand(revisionListCommand()) revisionCmd.AddCommand(revisionDescribeCommand()) revisionCmd.AddCommand(tagCommand()) return revisionCmd } func revisionDescribeCommand() *cobra.Command { describeCmd := &cobra.Command{ Use: "describe", Example: ` # View the details of a revision named 'canary' istioctl x revision describe canary # View the details of a revision named 'canary' and also the pods # under that particular revision istioctl x revision describe canary -v # Get details about a revision in json format (default format is human-friendly table format) istioctl x revision describe canary -v -o json `, Short: "Show information about a revision, including customizations, " + "istiod version and which pods/gateways are using it.", PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("revision must be specified") } if len(args) != 1 { return fmt.Errorf("exactly 1 revision should be specified") } revArgs.name = args[0] if !validFormats[revArgs.output] { return fmt.Errorf("unknown format %s. It should be %#v", revArgs.output, validFormats) } if errs := validation.IsDNS1123Label(revArgs.name); len(errs) > 0 { return fmt.Errorf("%s - invalid revision format: %v", revArgs.name, errs) } return nil }, RunE: func(cmd *cobra.Command, args []string) error { logger := clog.NewConsoleLogger(cmd.OutOrStdout(), cmd.ErrOrStderr(), scope) return printRevisionDescription(cmd.OutOrStdout(), &revArgs, logger) }, } describeCmd.Flags().BoolVarP(&revArgs.verbose, "verbose", "v", false, "Enable verbose output") return describeCmd } func revisionListCommand() *cobra.Command { listCmd := &cobra.Command{ Use: "list", Short: "Show list of control plane and gateway revisions that are currently installed in cluster", Example: ` # View summary of revisions installed in the current cluster # which can be overridden with --context parameter. istioctl x revision list # View list of revisions including customizations, istiod and gateway pods istioctl x revision list -v `, PreRunE: func(cmd *cobra.Command, args []string) error { if !revArgs.verbose && revArgs.manifestsPath != "" { return fmt.Errorf("manifest path should only be specified with -v") } return nil }, RunE: func(cmd *cobra.Command, args []string) error { logger := clog.NewConsoleLogger(cmd.OutOrStdout(), cmd.ErrOrStderr(), scope) return revisionList(cmd.OutOrStdout(), &revArgs, logger) }, } return listCmd } // PodFilteredInfo represents a small subset of fields from // Pod object in Kubernetes. Exposed for integration test type PodFilteredInfo struct { Namespace string `json:"namespace"` Name string `json:"name"` Address string `json:"address"` Status v1.PodPhase `json:"status"` Age string `json:"age"` } // IstioOperatorCRInfo represents a tiny subset of fields from // IstioOperator CR. This structure is used for displaying data. // Exposed for integration test type IstioOperatorCRInfo struct { IOP *iopv1alpha1.IstioOperator `json:"-"` Namespace string `json:"namespace"` Name string `json:"name"` Profile string `json:"profile"` Components []string `json:"components,omitempty"` Customizations []iopDiff `json:"customizations,omitempty"` } // MutatingWebhookConfigInfo represents a tiny subset of fields from // MutatingWebhookConfiguration kubernetes object. This is exposed for // integration tests only type MutatingWebhookConfigInfo struct { Name string `json:"name"` Revision string `json:"revision"` Tag string `json:"tag,omitempty"` } // NsInfo represents namespace related information like pods running there. // It is used to display data and is exposed for integration tests. type NsInfo struct { Name string `json:"name,omitempty"` Pods []*PodFilteredInfo `json:"pods,omitempty"` } // RevisionDescription is used to display revision related information. // This is exposed for integration tests. type RevisionDescription struct { IstioOperatorCRs []*IstioOperatorCRInfo `json:"istio_operator_crs,omitempty"` Webhooks []*MutatingWebhookConfigInfo `json:"webhooks,omitempty"` ControlPlanePods []*PodFilteredInfo `json:"control_plane_pods,omitempty"` IngressGatewayPods []*PodFilteredInfo `json:"ingess_gateways,omitempty"` EgressGatewayPods []*PodFilteredInfo `json:"egress_gateways,omitempty"` NamespaceSummary map[string]*NsInfo `json:"namespace_summary,omitempty"` } func revisionList(writer io.Writer, args *revisionArgs, logger clog.Logger) error { client, err := newKubeClient(kubeconfig, configContext) if err != nil { return fmt.Errorf("cannot create kubeclient for kubeconfig=%s, context=%s: %v", kubeconfig, configContext, err) } revisions := map[string]*RevisionDescription{} // Get a list of control planes which are installed in remote clusters // In this case, it is possible that they only have webhooks installed. webhooks, err := getWebhooks(context.Background(), client) if err != nil { return fmt.Errorf("error while listing mutating webhooks: %v", err) } for _, hook := range webhooks { rev := renderWithDefault(hook.GetLabels()[label.IoIstioRev.Name], "default") tag := hook.GetLabels()[tag.IstioTagLabel] ri, revPresent := revisions[rev] if revPresent { if tag != "" { ri.Webhooks = append(ri.Webhooks, &MutatingWebhookConfigInfo{ Name: hook.Name, Revision: rev, Tag: tag, }) } } else { revisions[rev] = &RevisionDescription{ IstioOperatorCRs: []*IstioOperatorCRInfo{}, Webhooks: []*MutatingWebhookConfigInfo{{Name: hook.Name, Revision: rev, Tag: tag}}, } } } // Get a list of all CRs which are installed in this cluster iopcrs, err := getAllIstioOperatorCRs(client) if err != nil { return fmt.Errorf("error while listing IstioOperator CRs: %v", err) } for _, iop := range iopcrs { rev := renderWithDefault(iop.Spec.GetRevision(), "default") if ri := revisions[rev]; ri == nil { revisions[rev] = &RevisionDescription{} } iopInfo := &IstioOperatorCRInfo{ IOP: iop, Namespace: iop.GetNamespace(), Name: iop.GetName(), Profile: iop.Spec.GetProfile(), Components: getEnabledComponents(iop.Spec), Customizations: nil, } if args.verbose { iopInfo.Customizations, err = getDiffs(iop, revArgs.manifestsPath, profileWithDefault(iop.Spec.GetProfile()), logger) if err != nil { return fmt.Errorf("error while finding customizations: %v", err) } } revisions[rev].IstioOperatorCRs = append(revisions[rev].IstioOperatorCRs, iopInfo) } if args.verbose { for rev, desc := range revisions { revClient, err := newKubeClientWithRevision(kubeconfig, configContext, rev) if err != nil { return fmt.Errorf("failed to get revision based kubeclient for revision: %s", rev) } if err = annotateWithControlPlanePodInfo(desc, revClient); err != nil { return fmt.Errorf("failed to get control plane pods for revision: %s", rev) } if err = annotateWithGatewayInfo(desc, revClient); err != nil { return fmt.Errorf("failed to get gateway pods for revision: %s", rev) } } } switch revArgs.output { case jsonFormat: return printJSON(writer, revisions) case tableFormat: if len(revisions) == 0 { _, err = fmt.Fprintln(writer, "No Istio installation found.\n"+ "No IstioOperator CR or sidecar injectors found") return err } return printRevisionInfoTable(writer, args.verbose, revisions) default: return fmt.Errorf("unknown format: %s", revArgs.output) } } func printRevisionInfoTable(writer io.Writer, verbose bool, revisions map[string]*RevisionDescription) error { if err := printSummaryTable(writer, verbose, revisions); err != nil { return fmt.Errorf("failed to print summary table: %v", err) } if verbose { if err := printControlPlaneSummaryTable(writer, revisions); err != nil { return fmt.Errorf("failed to print control plane table: %v", err) } if err := printGatewaySummaryTable(writer, revisions); err != nil { return fmt.Errorf("failed to print gateway summary table: %v", err) } } return nil } func printControlPlaneSummaryTable(w io.Writer, revisions map[string]*RevisionDescription) error { fmt.Fprintf(w, "\nCONTROL PLANE:\n") tw := new(tabwriter.Writer).Init(w, 0, 8, 1, ' ', 0) fmt.Fprintf(tw, "REVISION\tISTIOD-ENABLED\tCONTROL-PLANE-PODS\n") for rev, rd := range revisions { isIstiodEnabled := false outer: for _, iop := range rd.IstioOperatorCRs { for _, c := range iop.Components { if c == "istiod" { isIstiodEnabled = true break outer } } } maxRows := max(1, len(rd.ControlPlanePods)) for i := 0; i < maxRows; i++ { rowRev, rowEnabled, rowPod := "", "NO", "" if i == 0 { rowRev = rev if isIstiodEnabled { rowEnabled = "YES" } } switch { case i < len(rd.ControlPlanePods): rowPod = fmt.Sprintf("%s/%s", rd.ControlPlanePods[i].Namespace, rd.ControlPlanePods[i].Name) case i == 0 && len(rd.ControlPlanePods) == 0: rowPod = "<no-istiod>" } fmt.Fprintf(tw, "%s\t%s\t%s\n", rowRev, rowEnabled, rowPod) } } return tw.Flush() } func printGatewaySummaryTable(w io.Writer, revisions map[string]*RevisionDescription) error { if err := printIngressGatewaySummaryTable(w, revisions); err != nil { return fmt.Errorf("error while printing ingress gateway summary: %v", err) } if err := printEgressGatewaySummaryTable(w, revisions); err != nil { return fmt.Errorf("error while printing egress gateway summary: %v", err) } return nil } func printIngressGatewaySummaryTable(w io.Writer, revisions map[string]*RevisionDescription) error { fmt.Fprintf(w, "\nINGRESS GATEWAYS:\n") tw := new(tabwriter.Writer).Init(w, 0, 8, 1, ' ', 0) fmt.Fprintf(tw, "REVISION\tDECLARED-GATEWAYS\tGATEWAY-POD\n") for rev, rd := range revisions { enabledIngressGateways := []string{} for _, iop := range rd.IstioOperatorCRs { for _, c := range iop.Components { if strings.HasPrefix(c, "ingress") { enabledIngressGateways = append(enabledIngressGateways, c) } } } maxRows := max(max(1, len(enabledIngressGateways)), len(rd.IngressGatewayPods)) for i := 0; i < maxRows; i++ { var rowRev, rowEnabled, rowPod string if i == 0 { rowRev = rev } if i == 0 && len(enabledIngressGateways) == 0 { rowEnabled = "<no-ingress-enabled>" } else if i < len(enabledIngressGateways) { rowEnabled = enabledIngressGateways[i] } if i == 0 && len(rd.IngressGatewayPods) == 0 { rowPod = "<no-ingress-pod>" } else if i < len(rd.IngressGatewayPods) { rowPod = fmt.Sprintf("%s/%s", rd.IngressGatewayPods[i].Namespace, rd.IngressGatewayPods[i].Name) } fmt.Fprintf(tw, "%s\t%s\t%s\n", rowRev, rowEnabled, rowPod) } } return tw.Flush() } // TODO(su225): This is a copy paste of corresponding function of ingress. Refactor these parts! func printEgressGatewaySummaryTable(w io.Writer, revisions map[string]*RevisionDescription) error { fmt.Fprintf(w, "\nEGRESS GATEWAYS:\n") tw := new(tabwriter.Writer).Init(w, 0, 8, 1, ' ', 0) fmt.Fprintf(tw, "REVISION\tDECLARED-GATEWAYS\tGATEWAY-POD\n") for rev, rd := range revisions { enabledEgressGateways := []string{} for _, iop := range rd.IstioOperatorCRs { for _, c := range iop.Components { if strings.HasPrefix(c, "egress") { enabledEgressGateways = append(enabledEgressGateways, c) } } } maxRows := max(max(1, len(enabledEgressGateways)), len(rd.EgressGatewayPods)) for i := 0; i < maxRows; i++ { var rowRev, rowEnabled, rowPod string if i == 0 { rowRev = rev } if i == 0 && len(enabledEgressGateways) == 0 { rowEnabled = "<no-egress-enabled>" } else if i < len(enabledEgressGateways) { rowEnabled = enabledEgressGateways[i] } if i == 0 && len(rd.EgressGatewayPods) == 0 { rowPod = "<no-egress-pod>" } else if i < len(rd.EgressGatewayPods) { rowPod = fmt.Sprintf("%s/%s", rd.EgressGatewayPods[i].Namespace, rd.EgressGatewayPods[i].Name) } fmt.Fprintf(tw, "%s\t%s\t%s\n", rowRev, rowEnabled, rowPod) } } return tw.Flush() } //nolint:errcheck func printSummaryTable(writer io.Writer, verbose bool, revisions map[string]*RevisionDescription) error { tw := new(tabwriter.Writer).Init(writer, 0, 8, 1, ' ', 0) if verbose { tw.Write([]byte("REVISION\tTAG\tISTIO-OPERATOR-CR\tPROFILE\tREQD-COMPONENTS\tCUSTOMIZATIONS\n")) } else { tw.Write([]byte("REVISION\tTAG\tISTIO-OPERATOR-CR\tPROFILE\tREQD-COMPONENTS\n")) } for r, ri := range revisions { rowID, tags := 0, []string{} for _, wh := range ri.Webhooks { if wh.Tag != "" { tags = append(tags, wh.Tag) } } if len(tags) == 0 { tags = append(tags, "<no-tag>") } for _, iop := range ri.IstioOperatorCRs { profile := profileWithDefault(iop.Profile) components := iop.Components qualifiedName := fmt.Sprintf("%s/%s", iop.Namespace, iop.Name) customizations := []string{} for _, c := range iop.Customizations { customizations = append(customizations, fmt.Sprintf("%s=%s", c.Path, c.Value)) } if len(customizations) == 0 { customizations = append(customizations, "<no-customization>") } maxIopRows := max(max(1, len(components)), len(customizations)) for i := 0; i < maxIopRows; i++ { var rowTag, rowRev string var rowIopName, rowProfile, rowComp, rowCust string if i == 0 { rowIopName = qualifiedName rowProfile = profile } if i < len(components) { rowComp = components[i] } if i < len(customizations) { rowCust = customizations[i] } if rowID < len(tags) { rowTag = tags[rowID] } if rowID == 0 { rowRev = r } if verbose { fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", rowRev, rowTag, rowIopName, rowProfile, rowComp, rowCust) } else { fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", rowRev, rowTag, rowIopName, rowProfile, rowComp) } rowID++ } } for rowID < len(tags) { var rowRev, rowTag, rowIopName string if rowID == 0 { rowRev = r } if rowID == 0 { rowIopName = "<no-iop>" } rowTag = tags[rowID] if verbose { fmt.Fprintf(tw, "%s\t%s\t%s\t \t \t \n", rowRev, rowTag, rowIopName) } else { fmt.Fprintf(tw, "%s\t%s\t%s\t \t \n", rowRev, rowTag, rowIopName) } rowID++ } } return tw.Flush() } func getAllIstioOperatorCRs(client kube.ExtendedClient) ([]*iopv1alpha1.IstioOperator, error) { ucrs, err := client.Dynamic().Resource(istioOperatorGVR). List(context.Background(), meta_v1.ListOptions{}) if err != nil { return []*iopv1alpha1.IstioOperator{}, fmt.Errorf("cannot retrieve IstioOperator CRs: %v", err) } iopCRs := []*iopv1alpha1.IstioOperator{} for _, u := range ucrs.Items { u.SetCreationTimestamp(meta_v1.Time{}) u.SetManagedFields([]meta_v1.ManagedFieldsEntry{}) iop, err := operator_istio.UnmarshalIstioOperator(util.ToYAML(u.Object), true) if err != nil { return []*iopv1alpha1.IstioOperator{}, fmt.Errorf("error while converting to IstioOperator CR - %s/%s: %v", u.GetNamespace(), u.GetName(), err) } iopCRs = append(iopCRs, iop) } return iopCRs, nil } func printRevisionDescription(w io.Writer, args *revisionArgs, logger clog.Logger) error { revision := args.name client, err := newKubeClientWithRevision(kubeconfig, configContext, revision) if err != nil { return fmt.Errorf("cannot create kubeclient for kubeconfig=%s, context=%s: %v", kubeconfig, configContext, err) } allIops, err := getAllIstioOperatorCRs(client) if err != nil { return fmt.Errorf("error while fetching IstioOperator CR: %v", err) } iopsInCluster := getIOPWithRevision(allIops, revision) allWebhooks, err := getWebhooks(context.Background(), client) if err != nil { return fmt.Errorf("error while fetching mutating webhook configurations: %v", err) } webhooks := filterWebhooksWithRevision(allWebhooks, revision) revDescription := getBasicRevisionDescription(iopsInCluster, webhooks) if err = annotateWithIOPCustomization(revDescription, args.manifestsPath, logger); err != nil { return err } if err = annotateWithControlPlanePodInfo(revDescription, client); err != nil { return err } if err = annotateWithGatewayInfo(revDescription, client); err != nil { return err } if !revisionExists(revDescription) { return fmt.Errorf("revision %s is not present", revision) } if args.verbose { revAliases := []string{revision} for _, wh := range revDescription.Webhooks { revAliases = append(revAliases, wh.Tag) } if err = annotateWithNamespaceAndPodInfo(revDescription, revAliases); err != nil { return err } } switch revArgs.output { case jsonFormat: return printJSON(w, revDescription) case tableFormat: sections := defaultSections if args.verbose { sections = verboseSections } return printTable(w, sections, revDescription) default: return fmt.Errorf("unknown format %s", revArgs.output) } } func revisionExists(revDescription *RevisionDescription) bool { return len(revDescription.IstioOperatorCRs) != 0 || len(revDescription.Webhooks) != 0 || len(revDescription.ControlPlanePods) != 0 || len(revDescription.IngressGatewayPods) != 0 || len(revDescription.EgressGatewayPods) != 0 } func annotateWithNamespaceAndPodInfo(revDescription *RevisionDescription, revisionAliases []string) error { client, err := newKubeClient(kubeconfig, configContext) if err != nil { return fmt.Errorf("failed to create kubeclient: %v", err) } nsMap := make(map[string]*NsInfo) for _, ra := range revisionAliases { pods, err := getPodsWithSelector(client, "", &meta_v1.LabelSelector{ MatchLabels: map[string]string{ label.IoIstioRev.Name: ra, }, }) if err != nil { return fmt.Errorf("failed to fetch pods for rev/tag: %s: %v", ra, err) } for _, po := range pods { fpo := getFilteredPodInfo(&po) _, ok := nsMap[po.Namespace] if !ok { nsMap[po.Namespace] = &NsInfo{ Name: po.Namespace, Pods: []*PodFilteredInfo{fpo}, } } else { nsMap[po.Namespace].Pods = append(nsMap[po.Namespace].Pods, fpo) } } } revDescription.NamespaceSummary = nsMap return nil } func annotateWithGatewayInfo(revDescription *RevisionDescription, client kube.ExtendedClient) error { ingressPods, err := getPodsForComponent(client, "IngressGateways") if err != nil { return fmt.Errorf("error while fetching ingress gateway pods: %v", err) } revDescription.IngressGatewayPods = transformToFilteredPodInfo(ingressPods) egressPods, err := getPodsForComponent(client, "EgressGateways") if err != nil { return fmt.Errorf("error while fetching egress gateway pods: %v", err) } revDescription.EgressGatewayPods = transformToFilteredPodInfo(egressPods) return nil } func annotateWithControlPlanePodInfo(revDescription *RevisionDescription, client kube.ExtendedClient) error { controlPlanePods, err := getPodsForComponent(client, "Pilot") if err != nil { return fmt.Errorf("error while fetching control plane pods: %v", err) } revDescription.ControlPlanePods = transformToFilteredPodInfo(controlPlanePods) return nil } func annotateWithIOPCustomization(revDesc *RevisionDescription, manifestsPath string, logger clog.Logger) error { for _, cr := range revDesc.IstioOperatorCRs { cust, err := getDiffs(cr.IOP, manifestsPath, profileWithDefault(cr.IOP.Spec.GetProfile()), logger) if err != nil { return fmt.Errorf("error while computing customization for %s/%s: %v", cr.Name, cr.Namespace, err) } cr.Customizations = cust } return nil } func getBasicRevisionDescription(iopCRs []*iopv1alpha1.IstioOperator, mutatingWebhooks []admit_v1.MutatingWebhookConfiguration) *RevisionDescription { revDescription := &RevisionDescription{ IstioOperatorCRs: []*IstioOperatorCRInfo{}, Webhooks: []*MutatingWebhookConfigInfo{}, } for _, iop := range iopCRs { revDescription.IstioOperatorCRs = append(revDescription.IstioOperatorCRs, &IstioOperatorCRInfo{ IOP: iop, Namespace: iop.Namespace, Name: iop.Name, Profile: renderWithDefault(iop.Spec.Profile, "default"), Components: getEnabledComponents(iop.Spec), Customizations: nil, }) } for _, mwh := range mutatingWebhooks { revDescription.Webhooks = append(revDescription.Webhooks, &MutatingWebhookConfigInfo{ Name: mwh.Name, Revision: renderWithDefault(mwh.Labels[label.IoIstioRev.Name], "default"), Tag: mwh.Labels[tag.IstioTagLabel], }) } return revDescription } func printJSON(w io.Writer, res interface{}) error { out, err := json.MarshalIndent(res, "", "\t") if err != nil { return fmt.Errorf("error while marshaling to JSON: %v", err) } fmt.Fprintln(w, string(out)) return nil } func filterWebhooksWithRevision(webhooks []admit_v1.MutatingWebhookConfiguration, revision string) []admit_v1.MutatingWebhookConfiguration { whFiltered := []admit_v1.MutatingWebhookConfiguration{} for _, wh := range webhooks { if wh.GetLabels()[label.IoIstioRev.Name] == revision { whFiltered = append(whFiltered, wh) } } return whFiltered } func transformToFilteredPodInfo(pods []v1.Pod) []*PodFilteredInfo { pfilInfo := []*PodFilteredInfo{} for _, p := range pods { pfilInfo = append(pfilInfo, getFilteredPodInfo(&p)) } return pfilInfo } func getFilteredPodInfo(pod *v1.Pod) *PodFilteredInfo { return &PodFilteredInfo{ Namespace: pod.Namespace, Name: pod.Name, Address: pod.Status.PodIP, Status: pod.Status.Phase, Age: translateTimestampSince(pod.CreationTimestamp), } } func printTable(w io.Writer, sections []string, desc *RevisionDescription) error { errs := &multierror.Error{} tablePrintFuncs := map[string]func(io.Writer, *RevisionDescription) error{ istioOperatorCRSection: printIstioOperatorCRInfo, webhooksSection: printWebhookInfo, controlPlaneSection: printControlPlane, gatewaysSection: printGateways, podsSection: printPodsInfo, } for _, s := range sections { f := tablePrintFuncs[s] if f == nil { errs = multierror.Append(fmt.Errorf("unknown section: %s", s), errs.Errors...) continue } err := f(w, desc) if err != nil { errs = multierror.Append(fmt.Errorf("error in section %s: %v", s, err)) } } return errs.ErrorOrNil() } func printPodsInfo(w io.Writer, desc *RevisionDescription) error { fmt.Fprintf(w, "\nPODS:\n") if len(desc.NamespaceSummary) == 0 { fmt.Fprintln(w, "No pods for this revision") return nil } for ns, nsi := range desc.NamespaceSummary { fmt.Fprintf(w, "NAMESPACE %s: (%d)\n", ns, len(nsi.Pods)) if err := printPodTable(w, nsi.Pods); err != nil { return fmt.Errorf("error while printing pod table: %v", err) } fmt.Fprintln(w) } return nil } //nolint:unparam func printIstioOperatorCRInfo(w io.Writer, desc *RevisionDescription) error { fmt.Fprintf(w, "\nISTIO-OPERATOR CUSTOM RESOURCE: (%d)", len(desc.IstioOperatorCRs)) if len(desc.IstioOperatorCRs) == 0 { if len(desc.Webhooks) > 0 { fmt.Fprintln(w, "\nThere are webhooks and Istiod could be external to the cluster") } else { // Ideally, it should not come here fmt.Fprintln(w, "\nNo CRs found.") } return nil } for i, iop := range desc.IstioOperatorCRs { fmt.Fprintf(w, "\n%d. %s/%s\n", i+1, iop.Namespace, iop.Name) fmt.Fprintf(w, " COMPONENTS:\n") if len(iop.Components) > 0 { for _, c := range iop.Components { fmt.Fprintf(w, " - %s\n", c) } } else { fmt.Fprintf(w, " no enabled components\n") } // For each IOP, print all customizations for it fmt.Fprintf(w, " CUSTOMIZATIONS:\n") if len(iop.Customizations) > 0 { for _, customization := range iop.Customizations { fmt.Fprintf(w, " - %s=%s\n", customization.Path, customization.Value) } } else { fmt.Fprintf(w, " <no-customizations>\n") } } return nil } //nolint:errcheck func printWebhookInfo(w io.Writer, desc *RevisionDescription) error { fmt.Fprintf(w, "\nMUTATING WEBHOOK CONFIGURATIONS: (%d)\n", len(desc.Webhooks)) if len(desc.Webhooks) == 0 { fmt.Fprintln(w, "No mutating webhook found for this revision. Something could be wrong with installation") return nil } tw := new(tabwriter.Writer).Init(w, 0, 0, 1, ' ', 0) tw.Write([]byte("WEBHOOK\tTAG\n")) for _, wh := range desc.Webhooks { tw.Write([]byte(fmt.Sprintf("%s\t%s\n", wh.Name, renderWithDefault(wh.Tag, "<no-tag>")))) } return tw.Flush() } func printControlPlane(w io.Writer, desc *RevisionDescription) error { fmt.Fprintf(w, "\nCONTROL PLANE PODS (ISTIOD): (%d)\n", len(desc.ControlPlanePods)) if len(desc.ControlPlanePods) == 0 { if len(desc.Webhooks) > 0 { fmt.Fprintln(w, "No Istiod found in this cluster for the revision. "+ "However there are webhooks. It is possible that Istiod is external to this cluster or "+ "perhaps it is not uninstalled properly") } else { fmt.Fprintln(w, "No Istiod or the webhook found in this cluster for the revision. Something could be wrong") } return nil } return printPodTable(w, desc.ControlPlanePods) } func printGateways(w io.Writer, desc *RevisionDescription) error { if err := printIngressGateways(w, desc); err != nil { return fmt.Errorf("error while printing ingress-gateway info: %v", err) } if err := printEgressGateways(w, desc); err != nil { return fmt.Errorf("error while printing egress-gateway info: %v", err) } return nil } func printIngressGateways(w io.Writer, desc *RevisionDescription) error { fmt.Fprintf(w, "\nINGRESS GATEWAYS: (%d)\n", len(desc.IngressGatewayPods)) if len(desc.IngressGatewayPods) == 0 { if ingressGatewayEnabled(desc) { fmt.Fprintln(w, "Ingress gateway is enabled for this revision. However there are no such pods. "+ "It could be that it is replaced by ingress-gateway from another revision (as it is still upgraded in-place) "+ "or it could be some issue with installation") } else { fmt.Fprintln(w, "Ingress gateway is disabled for this revision") } return nil } if !ingressGatewayEnabled(desc) { fmt.Fprintln(w, "WARNING: Ingress gateway is not enabled for this revision.") } return printPodTable(w, desc.IngressGatewayPods) } func printEgressGateways(w io.Writer, desc *RevisionDescription) error { fmt.Fprintf(w, "\nEGRESS GATEWAYS: (%d)\n", len(desc.IngressGatewayPods)) if len(desc.EgressGatewayPods) == 0 { if egressGatewayEnabled(desc) { fmt.Fprintln(w, "Egress gateway is enabled for this revision. However there are no such pods. "+ "It could be that it is replaced by egress-gateway from another revision (as it is still upgraded in-place) "+ "or it could be some issue with installation") } else { fmt.Fprintln(w, "Egress gateway is disabled for this revision") } return nil } if !egressGatewayEnabled(desc) { fmt.Fprintln(w, "WARNING: Egress gateway is not enabled for this revision.") } return printPodTable(w, desc.EgressGatewayPods) } type istioGatewayType = string const ( ingress istioGatewayType = "ingress" egress istioGatewayType = "egress" ) func ingressGatewayEnabled(desc *RevisionDescription) bool { return gatewayTypeEnabled(desc, ingress) } func egressGatewayEnabled(desc *RevisionDescription) bool { return gatewayTypeEnabled(desc, egress) } func gatewayTypeEnabled(desc *RevisionDescription, gwType istioGatewayType) bool { for _, iopdesc := range desc.IstioOperatorCRs { for _, comp := range iopdesc.Components { if strings.HasPrefix(comp, gwType) { return true } } } return false } func getIOPWithRevision(iops []*iopv1alpha1.IstioOperator, revision string) []*iopv1alpha1.IstioOperator { filteredIOPs := []*iopv1alpha1.IstioOperator{} for _, iop := range iops { if iop.Spec == nil { continue } if iop.Spec.Revision == revision || (revision == "default" && iop.Spec.Revision == "") { filteredIOPs = append(filteredIOPs, iop) } } return filteredIOPs } func printPodTable(w io.Writer, pods []*PodFilteredInfo) error { podTableW := new(tabwriter.Writer).Init(w, 0, 0, 1, ' ', 0) fmt.Fprintln(podTableW, "NAMESPACE\tNAME\tADDRESS\tSTATUS\tAGE") for _, pod := range pods { fmt.Fprintf(podTableW, "%s\t%s\t%s\t%s\t%s\n", pod.Namespace, pod.Name, pod.Address, pod.Status, pod.Age) } return podTableW.Flush() } func getEnabledComponents(iops *v1alpha1.IstioOperatorSpec) []string { if iops == nil || iops.Components == nil { return []string{} } enabledComponents := []string{} if iops.Components.Base != nil && iops.Components.Base.Enabled.GetValue() { enabledComponents = append(enabledComponents, "base") } if iops.Components.Cni != nil && iops.Components.Cni.Enabled.GetValue() { enabledComponents = append(enabledComponents, "cni") } if iops.Components.Pilot != nil && iops.Components.Pilot.Enabled.GetValue() { enabledComponents = append(enabledComponents, "istiod") } for _, gw := range iops.Components.IngressGateways { if gw.Enabled.GetValue() { enabledComponents = append(enabledComponents, fmt.Sprintf("ingress:%s", gw.GetName())) } } for _, gw := range iops.Components.EgressGateways { if gw.Enabled.GetValue() { enabledComponents = append(enabledComponents, fmt.Sprintf("egress:%s", gw.GetName())) } } return enabledComponents } func getPodsForComponent(client kube.ExtendedClient, component string) ([]v1.Pod, error) { return getPodsWithSelector(client, istioNamespace, &meta_v1.LabelSelector{ MatchLabels: map[string]string{ label.IoIstioRev.Name: client.Revision(), label.OperatorComponent.Name: component, }, }) } func getPodsWithSelector(client kube.ExtendedClient, ns string, selector *meta_v1.LabelSelector) ([]v1.Pod, error) { labelSelector, err := meta_v1.LabelSelectorAsSelector(selector) if err != nil { return []v1.Pod{}, err } podList, err := client.CoreV1().Pods(ns).List(context.TODO(), meta_v1.ListOptions{LabelSelector: labelSelector.String()}) if err != nil { return []v1.Pod{}, err } return podList.Items, nil } type iopDiff struct { Path string `json:"path"` Value string `json:"value"` } func getDiffs(installed *iopv1alpha1.IstioOperator, manifestsPath, profile string, l clog.Logger) ([]iopDiff, error) { setFlags := []string{"profile=" + profile} if manifestsPath != "" { setFlags = append(setFlags, fmt.Sprintf("installPackagePath=%s", manifestsPath)) } _, base, err := manifest.GenerateConfig([]string{}, setFlags, true, nil, l) if err != nil { return []iopDiff{}, err } mapInstalled, err := config.ToMap(installed.Spec) if err != nil { return []iopDiff{}, err } mapBase, err := config.ToMap(base.Spec) if err != nil { return []iopDiff{}, err } return diffWalk("", "", mapInstalled, mapBase) } // TODO(su225): Improve this and write tests for it. func diffWalk(path, separator string, installed interface{}, base interface{}) ([]iopDiff, error) { switch v := installed.(type) { case map[string]interface{}: accum := make([]iopDiff, 0) typedOrig, ok := base.(map[string]interface{}) if ok { for key, vv := range v { childwalk, err := diffWalk(fmt.Sprintf("%s%s%s", path, separator, pathComponent(key)), ".", vv, typedOrig[key]) if err != nil { return accum, err } accum = append(accum, childwalk...) } } return accum, nil case []interface{}: accum := make([]iopDiff, 0) typedOrig, ok := base.([]interface{}) if ok { for idx, vv := range v { var baseMap interface{} if idx < len(typedOrig) { baseMap = typedOrig[idx] } indexwalk, err := diffWalk(fmt.Sprintf("%s[%d]", path, idx), ".", vv, baseMap) if err != nil { return accum, err } accum = append(accum, indexwalk...) } } return accum, nil case string: if v != base && base != nil { return []iopDiff{{Path: path, Value: fmt.Sprintf("%v", v)}}, nil } default: if v != base && base != nil { return []iopDiff{{Path: path, Value: fmt.Sprintf("%v", v)}}, nil } } return []iopDiff{}, nil } func renderWithDefault(s, def string) string { if s != "" { return s } return def } func profileWithDefault(profile string) string { if profile != "" { return profile } return "default" } func pathComponent(component string) string { if !strings.Contains(component, util.PathSeparator) { return component } return strings.ReplaceAll(component, util.PathSeparator, util.EscapedPathSeparator) } // Human-readable age. (This is from kubectl pkg/describe/describe.go) func translateTimestampSince(timestamp meta_v1.Time) string { if timestamp.IsZero() { return "<unknown>" } return duration.HumanDuration(time.Since(timestamp.Time)) } func max(x, y int) int { if x > y { return x } return y }