| // 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 |
| } |