istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/describe/describe_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package describe
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"os"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	corev1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	klabels "k8s.io/apimachinery/pkg/labels"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/util/intstr"
    31  
    32  	apiannotation "istio.io/api/annotation"
    33  	v1alpha32 "istio.io/api/networking/v1alpha3"
    34  	"istio.io/client-go/pkg/apis/networking/v1alpha3"
    35  	"istio.io/istio/istioctl/pkg/cli"
    36  	"istio.io/istio/istioctl/pkg/util/configdump"
    37  	"istio.io/istio/pilot/test/util"
    38  	"istio.io/istio/pkg/test/util/assert"
    39  )
    40  
    41  // execAndK8sConfigTestCase lets a test case hold some Envoy, Istio, and Kubernetes configuration
    42  type execAndK8sConfigTestCase struct {
    43  	k8sConfigs     []runtime.Object // Canned K8s configuration
    44  	istioConfigs   []runtime.Object // Canned Istio configuration
    45  	configDumps    map[string][]byte
    46  	namespace      string
    47  	istioNamespace string
    48  
    49  	args []string
    50  
    51  	// Typically use one of the three
    52  	expectedOutput string // Expected constant output
    53  	expectedString string // String output is expected to contain
    54  
    55  	wantException bool
    56  }
    57  
    58  // Tests Pilot /debug
    59  func TestDescribe(t *testing.T) {
    60  	productPageConfigPath := "testdata/describe/http_config.json"
    61  	config, err := os.ReadFile(productPageConfigPath)
    62  	if err != nil {
    63  		t.Fatalf("failed to read %s: %v", productPageConfigPath, err)
    64  	}
    65  	cases := []execAndK8sConfigTestCase{
    66  		{ // case 0
    67  			args:           []string{},
    68  			expectedString: "Describe resource and related Istio configuration",
    69  		},
    70  		{ // case 2 no pod
    71  			args:           strings.Split("pod", " "),
    72  			expectedString: "Error: expecting pod name",
    73  			wantException:  true, // "istioctl experimental inspect pod" should fail
    74  		},
    75  		{ // case 3 unknown pod
    76  			args:           strings.Split("po not-a-pod", " "),
    77  			expectedString: "pods \"not-a-pod\" not found",
    78  			wantException:  true, // "istioctl experimental describe pod not-a-pod" should fail
    79  		},
    80  		{ // case 8 unknown service
    81  			args:           strings.Split("service not-a-service", " "),
    82  			expectedString: "services \"not-a-service\" not found",
    83  			wantException:  true, // "istioctl experimental describe service not-a-service" should fail
    84  		},
    85  		{
    86  			k8sConfigs: []runtime.Object{
    87  				&corev1.Service{
    88  					ObjectMeta: metav1.ObjectMeta{
    89  						Name:      "productpage",
    90  						Namespace: "default",
    91  					},
    92  					Spec: corev1.ServiceSpec{
    93  						Selector: map[string]string{
    94  							"app": "productpage",
    95  						},
    96  						Ports: []corev1.ServicePort{
    97  							{
    98  								Name:       "http",
    99  								Port:       9080,
   100  								TargetPort: intstr.FromInt32(9080),
   101  							},
   102  						},
   103  					},
   104  				},
   105  				&corev1.Service{
   106  					ObjectMeta: metav1.ObjectMeta{
   107  						Name:      "ingress",
   108  						Namespace: "default",
   109  						Labels: map[string]string{
   110  							"istio": "ingressgateway",
   111  						},
   112  					},
   113  					Spec: corev1.ServiceSpec{
   114  						Selector: map[string]string{
   115  							"istio": "ingressgateway",
   116  						},
   117  						Ports: []corev1.ServicePort{
   118  							{
   119  								Name:       "http",
   120  								Port:       80,
   121  								TargetPort: intstr.FromInt32(80),
   122  							},
   123  						},
   124  					},
   125  				},
   126  				&corev1.Pod{
   127  					ObjectMeta: metav1.ObjectMeta{
   128  						Name:      "productpage-v1-1234567890",
   129  						Namespace: "default",
   130  						Labels: map[string]string{
   131  							"app": "productpage",
   132  						},
   133  					},
   134  					Spec: corev1.PodSpec{
   135  						Containers: []corev1.Container{
   136  							{
   137  								Name: "productpage",
   138  								Ports: []corev1.ContainerPort{
   139  									{
   140  										Name:          "http",
   141  										ContainerPort: 9080,
   142  									},
   143  								},
   144  							},
   145  							{
   146  								Name: "istio-proxy",
   147  							},
   148  						},
   149  					},
   150  					Status: corev1.PodStatus{
   151  						Phase: corev1.PodRunning,
   152  						ContainerStatuses: []corev1.ContainerStatus{
   153  							{
   154  								Name:  "istio-proxy",
   155  								Ready: true,
   156  							},
   157  						},
   158  					},
   159  				},
   160  				&corev1.Pod{
   161  					ObjectMeta: metav1.ObjectMeta{
   162  						Name:      "ingress",
   163  						Namespace: "default",
   164  						Labels: map[string]string{
   165  							"istio": "ingressgateway",
   166  						},
   167  					},
   168  					Spec: corev1.PodSpec{
   169  						Containers: []corev1.Container{
   170  							{
   171  								Name: "istio-proxy",
   172  							},
   173  						},
   174  					},
   175  					Status: corev1.PodStatus{
   176  						Phase: corev1.PodRunning,
   177  						ContainerStatuses: []corev1.ContainerStatus{
   178  							{
   179  								Name:  "istio-proxy",
   180  								Ready: true,
   181  							},
   182  						},
   183  					},
   184  				},
   185  			},
   186  			istioConfigs: []runtime.Object{
   187  				&v1alpha3.VirtualService{
   188  					ObjectMeta: metav1.ObjectMeta{
   189  						Name:      "bookinfo",
   190  						Namespace: "default",
   191  					},
   192  					Spec: v1alpha32.VirtualService{
   193  						Hosts:    []string{"productpage"},
   194  						Gateways: []string{"fake-gw"},
   195  						Http: []*v1alpha32.HTTPRoute{
   196  							{
   197  								Match: []*v1alpha32.HTTPMatchRequest{
   198  									{
   199  										Uri: &v1alpha32.StringMatch{
   200  											MatchType: &v1alpha32.StringMatch_Prefix{
   201  												Prefix: "/prefix",
   202  											},
   203  										},
   204  									},
   205  								},
   206  								Route: []*v1alpha32.HTTPRouteDestination{
   207  									{
   208  										Destination: &v1alpha32.Destination{
   209  											Host: "productpage",
   210  										},
   211  										Weight: 30,
   212  									},
   213  									{
   214  										Destination: &v1alpha32.Destination{
   215  											Host: "productpage2",
   216  										},
   217  										Weight: 20,
   218  									},
   219  									{
   220  										Destination: &v1alpha32.Destination{
   221  											Host: "productpage3",
   222  										},
   223  										Weight: 50,
   224  									},
   225  								},
   226  							},
   227  						},
   228  					},
   229  				},
   230  				&v1alpha3.DestinationRule{
   231  					ObjectMeta: metav1.ObjectMeta{
   232  						Name:      "productpage",
   233  						Namespace: "default",
   234  					},
   235  					Spec: v1alpha32.DestinationRule{
   236  						Host: "productpage",
   237  						Subsets: []*v1alpha32.Subset{
   238  							{
   239  								Name:   "v1",
   240  								Labels: map[string]string{"version": "v1"},
   241  							},
   242  						},
   243  					},
   244  				},
   245  			},
   246  			configDumps: map[string][]byte{
   247  				"productpage-v1-1234567890": config,
   248  				"ingress":                   []byte("{}"),
   249  			},
   250  			namespace:      "default",
   251  			istioNamespace: "default",
   252  			// case 9, vs route to multiple hosts
   253  			args: strings.Split("service productpage", " "),
   254  			expectedOutput: `Service: productpage
   255  DestinationRule: productpage for "productpage"
   256     WARNING POD DOES NOT MATCH ANY SUBSETS.  (Non matching subsets v1)
   257     Matching subsets: 
   258        (Non-matching subsets v1)
   259     No Traffic Policy
   260  VirtualService: bookinfo
   261     Route to host "productpage" with weight 30%
   262     Route to host "productpage2" with weight 20%
   263     Route to host "productpage3" with weight 50%
   264     Match: /prefix*
   265  `,
   266  		},
   267  		{
   268  			k8sConfigs: []runtime.Object{
   269  				&corev1.Service{
   270  					ObjectMeta: metav1.ObjectMeta{
   271  						Name:      "productpage",
   272  						Namespace: "default",
   273  					},
   274  					Spec: corev1.ServiceSpec{
   275  						Selector: map[string]string{
   276  							"app": "productpage",
   277  						},
   278  						Ports: []corev1.ServicePort{
   279  							{
   280  								Name:       "http",
   281  								Port:       9080,
   282  								TargetPort: intstr.FromInt(9080),
   283  							},
   284  						},
   285  					},
   286  				},
   287  				&corev1.Service{
   288  					ObjectMeta: metav1.ObjectMeta{
   289  						Name:      "istio-ingressgateway",
   290  						Namespace: "default",
   291  					},
   292  					Spec: corev1.ServiceSpec{
   293  						Selector: map[string]string{
   294  							"istio": "ingressgateway",
   295  						},
   296  						Ports: []corev1.ServicePort{
   297  							{
   298  								Name:       "http",
   299  								Port:       80,
   300  								TargetPort: intstr.FromInt(80),
   301  							},
   302  						},
   303  					},
   304  				},
   305  				&corev1.Pod{
   306  					ObjectMeta: metav1.ObjectMeta{
   307  						Name:      "productpage-v1-1234567890",
   308  						Namespace: "default",
   309  						Labels: map[string]string{
   310  							"app": "productpage",
   311  						},
   312  					},
   313  					Spec: corev1.PodSpec{
   314  						Containers: []corev1.Container{
   315  							{
   316  								Name: "productpage",
   317  								Ports: []corev1.ContainerPort{
   318  									{
   319  										Name:          "http",
   320  										ContainerPort: 9080,
   321  									},
   322  								},
   323  							},
   324  							{
   325  								Name: "istio-proxy",
   326  							},
   327  						},
   328  					},
   329  					Status: corev1.PodStatus{
   330  						Phase: corev1.PodRunning,
   331  						ContainerStatuses: []corev1.ContainerStatus{
   332  							{
   333  								Name:  "istio-proxy",
   334  								Ready: true,
   335  							},
   336  						},
   337  					},
   338  				},
   339  				&corev1.Pod{
   340  					ObjectMeta: metav1.ObjectMeta{
   341  						Name:      "ingress",
   342  						Namespace: "default",
   343  						Labels: map[string]string{
   344  							"istio": "ingressgateway",
   345  						},
   346  					},
   347  					Spec: corev1.PodSpec{
   348  						Containers: []corev1.Container{
   349  							{
   350  								Name: "istio-proxy",
   351  							},
   352  						},
   353  					},
   354  					Status: corev1.PodStatus{
   355  						Phase: corev1.PodRunning,
   356  						ContainerStatuses: []corev1.ContainerStatus{
   357  							{
   358  								Name:  "istio-proxy",
   359  								Ready: true,
   360  							},
   361  						},
   362  					},
   363  				},
   364  			},
   365  			istioConfigs: []runtime.Object{
   366  				&v1alpha3.VirtualService{
   367  					ObjectMeta: metav1.ObjectMeta{
   368  						Name:      "bookinfo",
   369  						Namespace: "default",
   370  					},
   371  					Spec: v1alpha32.VirtualService{
   372  						Hosts:    []string{"productpage"},
   373  						Gateways: []string{"fake-gw"},
   374  						Http: []*v1alpha32.HTTPRoute{
   375  							{
   376  								Match: []*v1alpha32.HTTPMatchRequest{
   377  									{
   378  										Uri: &v1alpha32.StringMatch{
   379  											MatchType: &v1alpha32.StringMatch_Prefix{
   380  												Prefix: "/prefix",
   381  											},
   382  										},
   383  									},
   384  								},
   385  								Route: []*v1alpha32.HTTPRouteDestination{
   386  									{
   387  										Destination: &v1alpha32.Destination{
   388  											Host: "productpage",
   389  										},
   390  										Weight: 30,
   391  									},
   392  									{
   393  										Destination: &v1alpha32.Destination{
   394  											Host: "productpage2",
   395  										},
   396  										Weight: 20,
   397  									},
   398  									{
   399  										Destination: &v1alpha32.Destination{
   400  											Host: "productpage3",
   401  										},
   402  										Weight: 50,
   403  									},
   404  								},
   405  							},
   406  						},
   407  					},
   408  				},
   409  				&v1alpha3.DestinationRule{
   410  					ObjectMeta: metav1.ObjectMeta{
   411  						Name:      "productpage",
   412  						Namespace: "default",
   413  					},
   414  					Spec: v1alpha32.DestinationRule{
   415  						Host: "productpage",
   416  						Subsets: []*v1alpha32.Subset{
   417  							{
   418  								Name:   "v1",
   419  								Labels: map[string]string{"version": "v1"},
   420  							},
   421  						},
   422  					},
   423  				},
   424  			},
   425  			configDumps: map[string][]byte{
   426  				"productpage-v1-1234567890": config,
   427  				"ingress":                   []byte("{}"),
   428  			},
   429  			namespace:      "default",
   430  			istioNamespace: "default",
   431  			// case 9, vs route to multiple hosts
   432  			args: strings.Split("service productpage", " "),
   433  			expectedOutput: `Service: productpage
   434  DestinationRule: productpage for "productpage"
   435     WARNING POD DOES NOT MATCH ANY SUBSETS.  (Non matching subsets v1)
   436     Matching subsets: 
   437        (Non-matching subsets v1)
   438     No Traffic Policy
   439  VirtualService: bookinfo
   440     Route to host "productpage" with weight 30%
   441     Route to host "productpage2" with weight 20%
   442     Route to host "productpage3" with weight 50%
   443     Match: /prefix*
   444  `,
   445  		},
   446  		// have traffic policy
   447  		{
   448  			k8sConfigs: []runtime.Object{
   449  				&corev1.Service{
   450  					ObjectMeta: metav1.ObjectMeta{
   451  						Name:      "productpage",
   452  						Namespace: "default",
   453  					},
   454  					Spec: corev1.ServiceSpec{
   455  						Selector: map[string]string{
   456  							"app": "productpage",
   457  						},
   458  						Ports: []corev1.ServicePort{
   459  							{
   460  								Name:       "http",
   461  								Port:       9080,
   462  								TargetPort: intstr.FromInt32(9080),
   463  							},
   464  						},
   465  					},
   466  				},
   467  				&corev1.Service{
   468  					ObjectMeta: metav1.ObjectMeta{
   469  						Name:      "ingress",
   470  						Namespace: "default",
   471  						Labels: map[string]string{
   472  							"istio": "ingressgateway",
   473  						},
   474  					},
   475  					Spec: corev1.ServiceSpec{
   476  						Selector: map[string]string{
   477  							"istio": "ingressgateway",
   478  						},
   479  						Ports: []corev1.ServicePort{
   480  							{
   481  								Name:       "http",
   482  								Port:       80,
   483  								TargetPort: intstr.FromInt32(80),
   484  							},
   485  						},
   486  					},
   487  				},
   488  				&corev1.Pod{
   489  					ObjectMeta: metav1.ObjectMeta{
   490  						Name:      "productpage-v1-1234567890",
   491  						Namespace: "default",
   492  						Labels: map[string]string{
   493  							"app": "productpage",
   494  						},
   495  					},
   496  					Spec: corev1.PodSpec{
   497  						Containers: []corev1.Container{
   498  							{
   499  								Name: "productpage",
   500  								Ports: []corev1.ContainerPort{
   501  									{
   502  										Name:          "http",
   503  										ContainerPort: 9080,
   504  									},
   505  								},
   506  							},
   507  							{
   508  								Name: "istio-proxy",
   509  							},
   510  						},
   511  					},
   512  					Status: corev1.PodStatus{
   513  						Phase: corev1.PodRunning,
   514  						ContainerStatuses: []corev1.ContainerStatus{
   515  							{
   516  								Name:  "istio-proxy",
   517  								Ready: true,
   518  							},
   519  						},
   520  					},
   521  				},
   522  				&corev1.Pod{
   523  					ObjectMeta: metav1.ObjectMeta{
   524  						Name:      "ingress",
   525  						Namespace: "default",
   526  						Labels: map[string]string{
   527  							"istio": "ingressgateway",
   528  						},
   529  					},
   530  					Spec: corev1.PodSpec{
   531  						Containers: []corev1.Container{
   532  							{
   533  								Name: "istio-proxy",
   534  							},
   535  						},
   536  					},
   537  					Status: corev1.PodStatus{
   538  						Phase: corev1.PodRunning,
   539  						ContainerStatuses: []corev1.ContainerStatus{
   540  							{
   541  								Name:  "istio-proxy",
   542  								Ready: true,
   543  							},
   544  						},
   545  					},
   546  				},
   547  			},
   548  			istioConfigs: []runtime.Object{
   549  				&v1alpha3.VirtualService{
   550  					ObjectMeta: metav1.ObjectMeta{
   551  						Name:      "bookinfo",
   552  						Namespace: "default",
   553  					},
   554  					Spec: v1alpha32.VirtualService{
   555  						Hosts:    []string{"productpage"},
   556  						Gateways: []string{"fake-gw"},
   557  						Http: []*v1alpha32.HTTPRoute{
   558  							{
   559  								Match: []*v1alpha32.HTTPMatchRequest{
   560  									{
   561  										Uri: &v1alpha32.StringMatch{
   562  											MatchType: &v1alpha32.StringMatch_Prefix{
   563  												Prefix: "/prefix",
   564  											},
   565  										},
   566  									},
   567  								},
   568  								Route: []*v1alpha32.HTTPRouteDestination{
   569  									{
   570  										Destination: &v1alpha32.Destination{
   571  											Host: "productpage",
   572  										},
   573  										Weight: 30,
   574  									},
   575  									{
   576  										Destination: &v1alpha32.Destination{
   577  											Host: "productpage2",
   578  										},
   579  										Weight: 20,
   580  									},
   581  									{
   582  										Destination: &v1alpha32.Destination{
   583  											Host: "productpage3",
   584  										},
   585  										Weight: 50,
   586  									},
   587  								},
   588  							},
   589  						},
   590  					},
   591  				},
   592  				&v1alpha3.DestinationRule{
   593  					ObjectMeta: metav1.ObjectMeta{
   594  						Name:      "productpage",
   595  						Namespace: "default",
   596  					},
   597  					Spec: v1alpha32.DestinationRule{
   598  						Host: "productpage",
   599  						Subsets: []*v1alpha32.Subset{
   600  							{
   601  								Name:   "v1",
   602  								Labels: map[string]string{"version": "v1"},
   603  							},
   604  						},
   605  						TrafficPolicy: &v1alpha32.TrafficPolicy{
   606  							LoadBalancer: &v1alpha32.LoadBalancerSettings{
   607  								LbPolicy: &v1alpha32.LoadBalancerSettings_Simple{Simple: v1alpha32.LoadBalancerSettings_LEAST_REQUEST},
   608  							},
   609  							ConnectionPool:   &v1alpha32.ConnectionPoolSettings{Tcp: &v1alpha32.ConnectionPoolSettings_TCPSettings{MaxConnections: 10}},
   610  							OutlierDetection: &v1alpha32.OutlierDetection{MinHealthPercent: 10},
   611  							Tls:              &v1alpha32.ClientTLSSettings{Mode: v1alpha32.ClientTLSSettings_ISTIO_MUTUAL},
   612  							PortLevelSettings: []*v1alpha32.TrafficPolicy_PortTrafficPolicy{
   613  								{
   614  									LoadBalancer: &v1alpha32.LoadBalancerSettings{
   615  										LbPolicy: &v1alpha32.LoadBalancerSettings_Simple{Simple: v1alpha32.LoadBalancerSettings_LEAST_REQUEST},
   616  									},
   617  									Port:             &v1alpha32.PortSelector{Number: 8080},
   618  									Tls:              &v1alpha32.ClientTLSSettings{Mode: v1alpha32.ClientTLSSettings_DISABLE},
   619  									ConnectionPool:   &v1alpha32.ConnectionPoolSettings{Tcp: &v1alpha32.ConnectionPoolSettings_TCPSettings{MaxConnections: 10}},
   620  									OutlierDetection: &v1alpha32.OutlierDetection{MinHealthPercent: 10},
   621  								},
   622  							},
   623  							Tunnel:        nil,
   624  							ProxyProtocol: nil,
   625  						},
   626  					},
   627  				},
   628  			},
   629  			configDumps: map[string][]byte{
   630  				"productpage-v1-1234567890": config,
   631  				"ingress":                   []byte("{}"),
   632  			},
   633  			namespace:      "default",
   634  			istioNamespace: "default",
   635  			// case 9, vs route to multiple hosts
   636  			args: strings.Split("service productpage", " "),
   637  			expectedOutput: `Service: productpage
   638  DestinationRule: productpage for "productpage"
   639     WARNING POD DOES NOT MATCH ANY SUBSETS.  (Non matching subsets v1)
   640     Matching subsets: 
   641        (Non-matching subsets v1)
   642     Traffic Policy TLS Mode: ISTIO_MUTUAL
   643     Policies: load balancer/connection pool/outlier detection
   644     Port Level Settings:
   645      8080:
   646        TLS Mode: DISABLE
   647        Policies: load balancer/connection pool/outlier detection
   648  VirtualService: bookinfo
   649     Route to host "productpage" with weight 30%
   650     Route to host "productpage2" with weight 20%
   651     Route to host "productpage3" with weight 50%
   652     Match: /prefix*
   653  `,
   654  		},
   655  		// have ingress gateway
   656  		{
   657  			k8sConfigs: []runtime.Object{
   658  				&corev1.Service{
   659  					ObjectMeta: metav1.ObjectMeta{
   660  						Name:      "productpage",
   661  						Namespace: "default",
   662  					},
   663  					Spec: corev1.ServiceSpec{
   664  						Selector: map[string]string{
   665  							"app": "productpage",
   666  						},
   667  						Ports: []corev1.ServicePort{
   668  							{
   669  								Name:       "http",
   670  								Port:       9080,
   671  								TargetPort: intstr.FromInt32(9080),
   672  							},
   673  						},
   674  					},
   675  				},
   676  				&corev1.Service{
   677  					ObjectMeta: metav1.ObjectMeta{
   678  						Name:      "ingress",
   679  						Namespace: "default",
   680  						Labels: map[string]string{
   681  							"istio": "ingressgateway",
   682  							"app":   "ingress",
   683  						},
   684  					},
   685  					Spec: corev1.ServiceSpec{
   686  						Selector: map[string]string{
   687  							"istio": "ingressgateway",
   688  						},
   689  						Ports: []corev1.ServicePort{
   690  							{
   691  								Name:       "http",
   692  								Port:       80,
   693  								TargetPort: intstr.FromInt32(80),
   694  								Protocol:   corev1.ProtocolTCP,
   695  							},
   696  						},
   697  					},
   698  					Status: corev1.ServiceStatus{
   699  						LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{
   700  							{
   701  								IP: "2.2.2.2",
   702  							},
   703  						}},
   704  					},
   705  				},
   706  				&corev1.Service{
   707  					ObjectMeta: metav1.ObjectMeta{
   708  						Name:      "ingress",
   709  						Namespace: "istio-ingress",
   710  						Labels: map[string]string{
   711  							"istio": "ingressgateway",
   712  						},
   713  					},
   714  					Spec: corev1.ServiceSpec{
   715  						Selector: map[string]string{
   716  							"istio": "ingressgateway",
   717  						},
   718  						Ports: []corev1.ServicePort{
   719  							{
   720  								Name:       "http",
   721  								Port:       80,
   722  								TargetPort: intstr.FromInt32(80),
   723  								Protocol:   corev1.ProtocolTCP,
   724  							},
   725  						},
   726  					},
   727  					Status: corev1.ServiceStatus{
   728  						LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{
   729  							{
   730  								IP: "1.1.1.1",
   731  							},
   732  						}},
   733  					},
   734  				},
   735  				&corev1.Pod{
   736  					ObjectMeta: metav1.ObjectMeta{
   737  						Name:      "productpage-v1-1234567890",
   738  						Namespace: "default",
   739  						Labels: map[string]string{
   740  							"app": "productpage",
   741  						},
   742  					},
   743  					Spec: corev1.PodSpec{
   744  						Containers: []corev1.Container{
   745  							{
   746  								Name: "productpage",
   747  								Ports: []corev1.ContainerPort{
   748  									{
   749  										Name:          "http",
   750  										ContainerPort: 9080,
   751  									},
   752  								},
   753  							},
   754  							{
   755  								Name: "istio-proxy",
   756  							},
   757  						},
   758  					},
   759  					Status: corev1.PodStatus{
   760  						Phase: corev1.PodRunning,
   761  						ContainerStatuses: []corev1.ContainerStatus{
   762  							{
   763  								Name:  "istio-proxy",
   764  								Ready: true,
   765  							},
   766  						},
   767  					},
   768  				},
   769  				&corev1.Pod{
   770  					ObjectMeta: metav1.ObjectMeta{
   771  						Name:      "ingress",
   772  						Namespace: "default",
   773  						Labels: map[string]string{
   774  							"istio": "ingressgateway",
   775  						},
   776  					},
   777  					Spec: corev1.PodSpec{
   778  						Containers: []corev1.Container{
   779  							{
   780  								Name: "istio-proxy",
   781  							},
   782  						},
   783  					},
   784  					Status: corev1.PodStatus{
   785  						Phase: corev1.PodRunning,
   786  						ContainerStatuses: []corev1.ContainerStatus{
   787  							{
   788  								Name:  "istio-proxy",
   789  								Ready: true,
   790  							},
   791  						},
   792  					},
   793  				},
   794  				&corev1.Pod{
   795  					ObjectMeta: metav1.ObjectMeta{
   796  						Name:      "ingress",
   797  						Namespace: "istio-ingress",
   798  						Labels: map[string]string{
   799  							"istio": "ingressgateway",
   800  						},
   801  					},
   802  					Spec: corev1.PodSpec{
   803  						Containers: []corev1.Container{
   804  							{
   805  								Name: "istio-proxy",
   806  							},
   807  						},
   808  					},
   809  					Status: corev1.PodStatus{
   810  						Phase: corev1.PodRunning,
   811  						ContainerStatuses: []corev1.ContainerStatus{
   812  							{
   813  								Name:  "istio-proxy",
   814  								Ready: true,
   815  							},
   816  						},
   817  					},
   818  				},
   819  			},
   820  			istioConfigs: []runtime.Object{
   821  				&v1alpha3.VirtualService{
   822  					ObjectMeta: metav1.ObjectMeta{
   823  						Name:      "bookinfo",
   824  						Namespace: "default",
   825  					},
   826  					Spec: v1alpha32.VirtualService{
   827  						Hosts: []string{
   828  							"productpage",
   829  							"productpage.exapple.com",
   830  						},
   831  						Gateways: []string{"fake-gw"},
   832  						Http: []*v1alpha32.HTTPRoute{
   833  							{
   834  								Match: []*v1alpha32.HTTPMatchRequest{
   835  									{
   836  										Uri: &v1alpha32.StringMatch{
   837  											MatchType: &v1alpha32.StringMatch_Prefix{
   838  												Prefix: "/prefix",
   839  											},
   840  										},
   841  									},
   842  								},
   843  								Route: []*v1alpha32.HTTPRouteDestination{
   844  									{
   845  										Destination: &v1alpha32.Destination{
   846  											Host: "productpage",
   847  										},
   848  										Weight: 30,
   849  									},
   850  									{
   851  										Destination: &v1alpha32.Destination{
   852  											Host: "productpage2",
   853  										},
   854  										Weight: 20,
   855  									},
   856  									{
   857  										Destination: &v1alpha32.Destination{
   858  											Host: "productpage3",
   859  										},
   860  										Weight: 50,
   861  									},
   862  								},
   863  							},
   864  						},
   865  					},
   866  				},
   867  				&v1alpha3.DestinationRule{
   868  					ObjectMeta: metav1.ObjectMeta{
   869  						Name:      "productpage",
   870  						Namespace: "default",
   871  					},
   872  					Spec: v1alpha32.DestinationRule{
   873  						Host: "productpage",
   874  						Subsets: []*v1alpha32.Subset{
   875  							{
   876  								Name:   "v1",
   877  								Labels: map[string]string{"version": "v1"},
   878  							},
   879  						},
   880  						TrafficPolicy: &v1alpha32.TrafficPolicy{
   881  							LoadBalancer: &v1alpha32.LoadBalancerSettings{
   882  								LbPolicy: &v1alpha32.LoadBalancerSettings_Simple{Simple: v1alpha32.LoadBalancerSettings_LEAST_REQUEST},
   883  							},
   884  							ConnectionPool:   &v1alpha32.ConnectionPoolSettings{Tcp: &v1alpha32.ConnectionPoolSettings_TCPSettings{MaxConnections: 10}},
   885  							OutlierDetection: &v1alpha32.OutlierDetection{MinHealthPercent: 10},
   886  							Tls:              &v1alpha32.ClientTLSSettings{Mode: v1alpha32.ClientTLSSettings_ISTIO_MUTUAL},
   887  							PortLevelSettings: []*v1alpha32.TrafficPolicy_PortTrafficPolicy{
   888  								{
   889  									LoadBalancer: &v1alpha32.LoadBalancerSettings{
   890  										LbPolicy: &v1alpha32.LoadBalancerSettings_Simple{Simple: v1alpha32.LoadBalancerSettings_LEAST_REQUEST},
   891  									},
   892  									Port:             &v1alpha32.PortSelector{Number: 8080},
   893  									Tls:              &v1alpha32.ClientTLSSettings{Mode: v1alpha32.ClientTLSSettings_DISABLE},
   894  									ConnectionPool:   &v1alpha32.ConnectionPoolSettings{Tcp: &v1alpha32.ConnectionPoolSettings_TCPSettings{MaxConnections: 10}},
   895  									OutlierDetection: &v1alpha32.OutlierDetection{MinHealthPercent: 10},
   896  								},
   897  							},
   898  							Tunnel:        nil,
   899  							ProxyProtocol: nil,
   900  						},
   901  					},
   902  				},
   903  				&v1alpha3.Gateway{
   904  					ObjectMeta: metav1.ObjectMeta{
   905  						Name:      "fake-gw",
   906  						Namespace: "default",
   907  					},
   908  					Spec: v1alpha32.Gateway{
   909  						Servers: []*v1alpha32.Server{
   910  							{
   911  								Port: &v1alpha32.Port{
   912  									Number:   80,
   913  									Name:     "default",
   914  									Protocol: "HTTP",
   915  								},
   916  								Hosts: []string{
   917  									"productpage.exapple.com",
   918  								},
   919  							},
   920  						},
   921  						Selector: map[string]string{
   922  							"istio": "ingressgateway",
   923  						},
   924  					},
   925  				},
   926  			},
   927  			configDumps: map[string][]byte{
   928  				"productpage-v1-1234567890": config,
   929  				"ingress":                   config,
   930  			},
   931  			namespace:      "default",
   932  			istioNamespace: "default",
   933  			// case 9, vs route to multiple hosts
   934  			args: strings.Split("service productpage", " "),
   935  			expectedOutput: `Service: productpage
   936  DestinationRule: productpage for "productpage"
   937     WARNING POD DOES NOT MATCH ANY SUBSETS.  (Non matching subsets v1)
   938     Matching subsets: 
   939        (Non-matching subsets v1)
   940     Traffic Policy TLS Mode: ISTIO_MUTUAL
   941     Policies: load balancer/connection pool/outlier detection
   942     Port Level Settings:
   943      8080:
   944        TLS Mode: DISABLE
   945        Policies: load balancer/connection pool/outlier detection
   946  VirtualService: bookinfo
   947     Route to host "productpage" with weight 30%
   948     Route to host "productpage2" with weight 20%
   949     Route to host "productpage3" with weight 50%
   950     Match: /prefix*
   951  --------------------
   952  Exposed on Ingress Gateway http://1.1.1.1
   953  Exposed on Ingress Gateway http://2.2.2.2
   954  VirtualService: bookinfo
   955     Route to host "productpage" with weight 30%
   956     Route to host "productpage2" with weight 20%
   957     Route to host "productpage3" with weight 50%
   958     Match: /prefix*
   959  `,
   960  		},
   961  	}
   962  
   963  	for i, c := range cases {
   964  		t.Run(fmt.Sprintf("case %d %s", i, strings.Join(c.args, " ")), func(t *testing.T) {
   965  			verifyExecAndK8sConfigTestCaseTestOutput(t, c)
   966  		})
   967  	}
   968  }
   969  
   970  func TestGetRevisionFromPodAnnotation(t *testing.T) {
   971  	cases := []struct {
   972  		anno klabels.Set
   973  
   974  		expected string
   975  	}{
   976  		{
   977  			anno: klabels.Set{
   978  				apiannotation.SidecarStatus.Name: "",
   979  			},
   980  			expected: "",
   981  		},
   982  		{
   983  			anno:     klabels.Set{},
   984  			expected: "",
   985  		},
   986  		{
   987  			anno: klabels.Set{
   988  				apiannotation.SidecarStatus.Name: `
   989  				{
   990  					"initContainers": [
   991  						"istio-init"
   992  					],
   993  					"containers": [
   994  						"istio-proxy"
   995  					],
   996  					"volumes": [
   997  						"istio-envoy",
   998  						"istio-data",
   999  						"istio-podinfo",
  1000  						"istio-token",
  1001  						"istiod-ca-cert"
  1002  					],
  1003  					"imagePullSecrets": null,
  1004  					"revision": "1-13-2"
  1005  				}`,
  1006  			},
  1007  			expected: "1-13-2",
  1008  		},
  1009  	}
  1010  
  1011  	for _, tc := range cases {
  1012  		t.Run("", func(t *testing.T) {
  1013  			got := GetRevisionFromPodAnnotation(tc.anno)
  1014  			assert.Equal(t, tc.expected, got)
  1015  		})
  1016  	}
  1017  }
  1018  
  1019  func TestFindProtocolForPort(t *testing.T) {
  1020  	http := "HTTP"
  1021  	cases := []struct {
  1022  		port             corev1.ServicePort
  1023  		expectedProtocol string
  1024  	}{
  1025  		{
  1026  			port: corev1.ServicePort{
  1027  				Name:     "http",
  1028  				Protocol: corev1.ProtocolTCP,
  1029  			},
  1030  			expectedProtocol: "HTTP",
  1031  		},
  1032  		{
  1033  			port: corev1.ServicePort{
  1034  				Name:     "GRPC-port",
  1035  				Protocol: corev1.ProtocolTCP,
  1036  			},
  1037  			expectedProtocol: "GRPC",
  1038  		},
  1039  		{
  1040  			port: corev1.ServicePort{
  1041  				AppProtocol: &http,
  1042  				Protocol:    corev1.ProtocolTCP,
  1043  			},
  1044  			expectedProtocol: "HTTP",
  1045  		},
  1046  		{
  1047  			port: corev1.ServicePort{
  1048  				Protocol: corev1.ProtocolTCP,
  1049  				Port:     80,
  1050  			},
  1051  			expectedProtocol: "auto-detect",
  1052  		},
  1053  		{
  1054  			port: corev1.ServicePort{
  1055  				Protocol: corev1.ProtocolUDP,
  1056  				Port:     80,
  1057  			},
  1058  			expectedProtocol: "UDP",
  1059  		},
  1060  	}
  1061  
  1062  	for _, tc := range cases {
  1063  		protocol := findProtocolForPort(&tc.port)
  1064  		if protocol != tc.expectedProtocol {
  1065  			t.Fatalf("Output didn't match for the port protocol: got %s want %s", protocol, tc.expectedProtocol)
  1066  		}
  1067  	}
  1068  }
  1069  
  1070  func verifyExecAndK8sConfigTestCaseTestOutput(t *testing.T, c execAndK8sConfigTestCase) {
  1071  	t.Helper()
  1072  
  1073  	ctx := cli.NewFakeContext(&cli.NewFakeContextOption{
  1074  		Namespace:      c.namespace,
  1075  		IstioNamespace: c.istioNamespace,
  1076  		Results:        c.configDumps,
  1077  	})
  1078  	client, err := ctx.CLIClient()
  1079  	assert.NoError(t, err)
  1080  	// Override the Istio config factory
  1081  	for i := range c.istioConfigs {
  1082  		switch t := c.istioConfigs[i].(type) {
  1083  		case *v1alpha3.DestinationRule:
  1084  			client.Istio().NetworkingV1alpha3().DestinationRules(t.Namespace).Create(context.TODO(), t, metav1.CreateOptions{})
  1085  		case *v1alpha3.Gateway:
  1086  			client.Istio().NetworkingV1alpha3().Gateways(t.Namespace).Create(context.TODO(), t, metav1.CreateOptions{})
  1087  		case *v1alpha3.VirtualService:
  1088  			client.Istio().NetworkingV1alpha3().VirtualServices(t.Namespace).Create(context.TODO(), t, metav1.CreateOptions{})
  1089  		}
  1090  	}
  1091  	for i := range c.k8sConfigs {
  1092  		switch t := c.k8sConfigs[i].(type) {
  1093  		case *corev1.Service:
  1094  			client.Kube().CoreV1().Services(t.Namespace).Create(context.TODO(), t, metav1.CreateOptions{})
  1095  		case *corev1.Pod:
  1096  			client.Kube().CoreV1().Pods(t.Namespace).Create(context.TODO(), t, metav1.CreateOptions{})
  1097  		}
  1098  	}
  1099  
  1100  	if c.configDumps == nil {
  1101  		c.configDumps = map[string][]byte{}
  1102  	}
  1103  
  1104  	var out bytes.Buffer
  1105  	rootCmd := Cmd(ctx)
  1106  	rootCmd.SetArgs(c.args)
  1107  
  1108  	rootCmd.SetOut(&out)
  1109  	rootCmd.SetErr(&out)
  1110  
  1111  	if c.namespace != "" {
  1112  		describeNamespace = c.namespace
  1113  	}
  1114  
  1115  	fErr := rootCmd.Execute()
  1116  	output := out.String()
  1117  
  1118  	if c.expectedOutput != "" && c.expectedOutput != output {
  1119  		t.Fatalf("Unexpected output for 'istioctl %s'\n got: %q\nwant: %q", strings.Join(c.args, " "), output, c.expectedOutput)
  1120  	}
  1121  
  1122  	if c.expectedString != "" && !strings.Contains(output, c.expectedString) {
  1123  		t.Fatalf("Output didn't match for 'istioctl %s'\n got %v\nwant: %v", strings.Join(c.args, " "), output, c.expectedString)
  1124  	}
  1125  
  1126  	if c.wantException {
  1127  		if fErr == nil {
  1128  			t.Fatalf("Wanted an exception for 'istioctl %s', didn't get one, output was %q",
  1129  				strings.Join(c.args, " "), output)
  1130  		}
  1131  	} else {
  1132  		if fErr != nil {
  1133  			t.Fatalf("Unwanted exception for 'istioctl %s': %v", strings.Join(c.args, " "), fErr)
  1134  		}
  1135  	}
  1136  }
  1137  
  1138  func TestGetIstioVirtualServicePathForSvcFromRoute(t *testing.T) {
  1139  	tests := []struct {
  1140  		name         string
  1141  		inputConfig  string
  1142  		inputService corev1.Service
  1143  		inputPort    int32
  1144  		expected     string
  1145  	}{
  1146  		{
  1147  			name:        "test tls config",
  1148  			inputConfig: "testdata/describe/tls_config.json",
  1149  			inputService: corev1.Service{
  1150  				TypeMeta: metav1.TypeMeta{},
  1151  				ObjectMeta: metav1.ObjectMeta{
  1152  					Name:      "productpage",
  1153  					Namespace: "default",
  1154  				},
  1155  				Spec:   corev1.ServiceSpec{},
  1156  				Status: corev1.ServiceStatus{},
  1157  			},
  1158  			inputPort: int32(9080),
  1159  			expected:  "/apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/bookinfo",
  1160  		},
  1161  		{
  1162  			name:        "test http config",
  1163  			inputConfig: "testdata/describe/http_config.json",
  1164  			inputService: corev1.Service{
  1165  				TypeMeta: metav1.TypeMeta{},
  1166  				ObjectMeta: metav1.ObjectMeta{
  1167  					Name:      "productpage",
  1168  					Namespace: "default",
  1169  				},
  1170  				Spec:   corev1.ServiceSpec{},
  1171  				Status: corev1.ServiceStatus{},
  1172  			},
  1173  			inputPort: int32(9080),
  1174  			expected:  "/apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/bookinfo",
  1175  		},
  1176  	}
  1177  
  1178  	for _, test := range tests {
  1179  		t.Run(test.name, func(t *testing.T) {
  1180  			ic := util.ReadFile(t, test.inputConfig)
  1181  			cd := configdump.Wrapper{}
  1182  			err := cd.UnmarshalJSON(ic)
  1183  			if err != nil {
  1184  				t.Fatal(err)
  1185  			}
  1186  			out, err := getIstioVirtualServicePathForSvcFromRoute(&cd, test.inputService, test.inputPort)
  1187  			if err != nil {
  1188  				t.Fatal(err)
  1189  			}
  1190  			if diff := cmp.Diff(test.expected, out); diff != "" {
  1191  				t.Fatalf("Diff:\n%s", diff)
  1192  			}
  1193  		})
  1194  	}
  1195  }