istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/ambient/workloads_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 ambient
    16  
    17  import (
    18  	"fmt"
    19  	"net/netip"
    20  	"testing"
    21  
    22  	v1 "k8s.io/api/core/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  
    25  	meshapi "istio.io/api/mesh/v1alpha1"
    26  	networking "istio.io/api/networking/v1alpha3"
    27  	networkingclient "istio.io/client-go/pkg/apis/networking/v1alpha3"
    28  	securityclient "istio.io/client-go/pkg/apis/security/v1beta1"
    29  	"istio.io/istio/pilot/pkg/model"
    30  	"istio.io/istio/pkg/config/labels"
    31  	"istio.io/istio/pkg/config/schema/kind"
    32  	"istio.io/istio/pkg/kube/krt"
    33  	"istio.io/istio/pkg/network"
    34  	"istio.io/istio/pkg/slices"
    35  	"istio.io/istio/pkg/test/util/assert"
    36  	"istio.io/istio/pkg/workloadapi"
    37  )
    38  
    39  func TestPodWorkloads(t *testing.T) {
    40  	cases := []struct {
    41  		name   string
    42  		inputs []any
    43  		pod    *v1.Pod
    44  		result *workloadapi.Workload
    45  	}{
    46  		{
    47  			name:   "simple pod not running and not have podIP",
    48  			inputs: []any{},
    49  			pod: &v1.Pod{
    50  				TypeMeta: metav1.TypeMeta{},
    51  				ObjectMeta: metav1.ObjectMeta{
    52  					Name:      "name",
    53  					Namespace: "ns",
    54  				},
    55  				Spec: v1.PodSpec{},
    56  				Status: v1.PodStatus{
    57  					Phase: v1.PodPending,
    58  				},
    59  			},
    60  			result: nil,
    61  		},
    62  		{
    63  			name:   "simple pod not running but have podIP",
    64  			inputs: []any{},
    65  			pod: &v1.Pod{
    66  				TypeMeta: metav1.TypeMeta{},
    67  				ObjectMeta: metav1.ObjectMeta{
    68  					Name:      "name",
    69  					Namespace: "ns",
    70  				},
    71  				Spec: v1.PodSpec{},
    72  				Status: v1.PodStatus{
    73  					Phase: v1.PodPending,
    74  					PodIP: "1.2.3.4",
    75  				},
    76  			},
    77  			result: &workloadapi.Workload{
    78  				Uid:               "cluster0//Pod/ns/name",
    79  				Name:              "name",
    80  				Namespace:         "ns",
    81  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
    82  				Network:           testNW,
    83  				CanonicalName:     "name",
    84  				CanonicalRevision: "latest",
    85  				WorkloadType:      workloadapi.WorkloadType_POD,
    86  				WorkloadName:      "name",
    87  				Status:            workloadapi.WorkloadStatus_UNHEALTHY,
    88  				ClusterId:         testC,
    89  			},
    90  		},
    91  		{
    92  			name:   "simple pod not ready",
    93  			inputs: []any{},
    94  			pod: &v1.Pod{
    95  				TypeMeta: metav1.TypeMeta{},
    96  				ObjectMeta: metav1.ObjectMeta{
    97  					Name:      "name",
    98  					Namespace: "ns",
    99  				},
   100  				Spec: v1.PodSpec{},
   101  				Status: v1.PodStatus{
   102  					Phase: v1.PodRunning,
   103  					PodIP: "1.2.3.4",
   104  				},
   105  			},
   106  			result: &workloadapi.Workload{
   107  				Uid:               "cluster0//Pod/ns/name",
   108  				Name:              "name",
   109  				Namespace:         "ns",
   110  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
   111  				Network:           testNW,
   112  				CanonicalName:     "name",
   113  				CanonicalRevision: "latest",
   114  				WorkloadType:      workloadapi.WorkloadType_POD,
   115  				WorkloadName:      "name",
   116  				Status:            workloadapi.WorkloadStatus_UNHEALTHY,
   117  				ClusterId:         testC,
   118  			},
   119  		},
   120  		{
   121  			name: "pod with service",
   122  			inputs: []any{
   123  				model.ServiceInfo{
   124  					Service: &workloadapi.Service{
   125  						Name:      "svc",
   126  						Namespace: "ns",
   127  						Hostname:  "hostname",
   128  						Ports: []*workloadapi.Port{{
   129  							ServicePort: 80,
   130  							TargetPort:  8080,
   131  						}},
   132  					},
   133  					LabelSelector: model.NewSelector(map[string]string{"app": "foo"}),
   134  				},
   135  			},
   136  			pod: &v1.Pod{
   137  				TypeMeta: metav1.TypeMeta{},
   138  				ObjectMeta: metav1.ObjectMeta{
   139  					Name:      "name",
   140  					Namespace: "ns",
   141  					Labels: map[string]string{
   142  						"app": "foo",
   143  					},
   144  				},
   145  				Spec: v1.PodSpec{},
   146  				Status: v1.PodStatus{
   147  					Phase:      v1.PodRunning,
   148  					Conditions: podReady,
   149  					PodIP:      "1.2.3.4",
   150  				},
   151  			},
   152  			result: &workloadapi.Workload{
   153  				Uid:               "cluster0//Pod/ns/name",
   154  				Name:              "name",
   155  				Namespace:         "ns",
   156  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
   157  				Network:           testNW,
   158  				CanonicalName:     "foo",
   159  				CanonicalRevision: "latest",
   160  				WorkloadType:      workloadapi.WorkloadType_POD,
   161  				WorkloadName:      "name",
   162  				Status:            workloadapi.WorkloadStatus_HEALTHY,
   163  				ClusterId:         testC,
   164  				Services: map[string]*workloadapi.PortList{
   165  					"ns/hostname": {
   166  						Ports: []*workloadapi.Port{{
   167  							ServicePort: 80,
   168  							TargetPort:  8080,
   169  						}},
   170  					},
   171  				},
   172  			},
   173  		},
   174  		{
   175  			name: "pod with service named ports",
   176  			inputs: []any{
   177  				model.ServiceInfo{
   178  					Service: &workloadapi.Service{
   179  						Name:      "svc",
   180  						Namespace: "ns",
   181  						Hostname:  "hostname",
   182  						Ports: []*workloadapi.Port{
   183  							{
   184  								ServicePort: 80,
   185  								TargetPort:  8080,
   186  							},
   187  							{
   188  								ServicePort: 81,
   189  								TargetPort:  0,
   190  							},
   191  							{
   192  								ServicePort: 82,
   193  								TargetPort:  0,
   194  							},
   195  						},
   196  					},
   197  					PortNames: map[int32]model.ServicePortName{
   198  						// Not a named port
   199  						80: {PortName: "80"},
   200  						// Named port found in pod
   201  						81: {PortName: "81", TargetPortName: "81-target"},
   202  						// Named port not found in pod
   203  						82: {PortName: "82", TargetPortName: "82-target"},
   204  					},
   205  					LabelSelector: model.NewSelector(map[string]string{"app": "foo"}),
   206  				},
   207  			},
   208  			pod: &v1.Pod{
   209  				TypeMeta: metav1.TypeMeta{},
   210  				ObjectMeta: metav1.ObjectMeta{
   211  					Name:      "name",
   212  					Namespace: "ns",
   213  					Labels: map[string]string{
   214  						"app": "foo",
   215  					},
   216  				},
   217  				Spec: v1.PodSpec{
   218  					Containers: []v1.Container{{Ports: []v1.ContainerPort{
   219  						{
   220  							Name:          "81-target",
   221  							ContainerPort: 9090,
   222  							Protocol:      v1.ProtocolTCP,
   223  						},
   224  					}}},
   225  				},
   226  				Status: v1.PodStatus{
   227  					Phase:      v1.PodRunning,
   228  					Conditions: podReady,
   229  					PodIP:      "1.2.3.4",
   230  				},
   231  			},
   232  			result: &workloadapi.Workload{
   233  				Uid:               "cluster0//Pod/ns/name",
   234  				Name:              "name",
   235  				Namespace:         "ns",
   236  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
   237  				Network:           testNW,
   238  				CanonicalName:     "foo",
   239  				CanonicalRevision: "latest",
   240  				WorkloadType:      workloadapi.WorkloadType_POD,
   241  				WorkloadName:      "name",
   242  				Status:            workloadapi.WorkloadStatus_HEALTHY,
   243  				ClusterId:         testC,
   244  				Services: map[string]*workloadapi.PortList{
   245  					"ns/hostname": {
   246  						Ports: []*workloadapi.Port{{
   247  							ServicePort: 80,
   248  							TargetPort:  8080,
   249  						}, {
   250  							ServicePort: 81,
   251  							TargetPort:  9090,
   252  						}},
   253  					},
   254  				},
   255  			},
   256  		},
   257  		{
   258  			name: "simple pod with locality",
   259  			inputs: []any{
   260  				&v1.Node{
   261  					ObjectMeta: metav1.ObjectMeta{
   262  						Name: "node",
   263  						Labels: map[string]string{
   264  							v1.LabelTopologyRegion: "region",
   265  							v1.LabelTopologyZone:   "zone",
   266  						},
   267  					},
   268  				},
   269  			},
   270  			pod: &v1.Pod{
   271  				TypeMeta: metav1.TypeMeta{},
   272  				ObjectMeta: metav1.ObjectMeta{
   273  					Name:      "name",
   274  					Namespace: "ns",
   275  				},
   276  				Spec: v1.PodSpec{NodeName: "node"},
   277  				Status: v1.PodStatus{
   278  					Phase: v1.PodPending,
   279  					PodIP: "1.2.3.4",
   280  				},
   281  			},
   282  			result: &workloadapi.Workload{
   283  				Uid:               "cluster0//Pod/ns/name",
   284  				Name:              "name",
   285  				Namespace:         "ns",
   286  				Node:              "node",
   287  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
   288  				Network:           testNW,
   289  				CanonicalName:     "name",
   290  				CanonicalRevision: "latest",
   291  				WorkloadType:      workloadapi.WorkloadType_POD,
   292  				WorkloadName:      "name",
   293  				Status:            workloadapi.WorkloadStatus_UNHEALTHY,
   294  				ClusterId:         testC,
   295  				Locality: &workloadapi.Locality{
   296  					Region: "region",
   297  					Zone:   "zone",
   298  				},
   299  			},
   300  		},
   301  	}
   302  	for _, tt := range cases {
   303  		t.Run(tt.name, func(t *testing.T) {
   304  			inputs := tt.inputs
   305  			a := newAmbientUnitTest()
   306  			AuthorizationPolicies := krt.NewStaticCollection(extractType[model.WorkloadAuthorization](&inputs))
   307  			PeerAuths := krt.NewStaticCollection(extractType[*securityclient.PeerAuthentication](&inputs))
   308  			Waypoints := krt.NewStaticCollection(extractType[Waypoint](&inputs))
   309  			WorkloadServices := krt.NewStaticCollection(extractType[model.ServiceInfo](&inputs))
   310  			MeshConfig := krt.NewStatic(&MeshConfig{slices.First(extractType[meshapi.MeshConfig](&inputs))})
   311  			Namespaces := krt.NewStaticCollection(extractType[*v1.Namespace](&inputs))
   312  			Nodes := krt.NewStaticCollection(extractType[*v1.Node](&inputs))
   313  			assert.Equal(t, len(inputs), 0, fmt.Sprintf("some inputs were not consumed: %v", inputs))
   314  			WorkloadServicesNamespaceIndex := krt.NewNamespaceIndex(WorkloadServices)
   315  			builder := a.podWorkloadBuilder(MeshConfig, AuthorizationPolicies, PeerAuths, Waypoints, WorkloadServices, WorkloadServicesNamespaceIndex, Namespaces, Nodes)
   316  			wrapper := builder(krt.TestingDummyContext{}, tt.pod)
   317  			var res *workloadapi.Workload
   318  			if wrapper != nil {
   319  				res = wrapper.Workload
   320  			}
   321  			assert.Equal(t, res, tt.result)
   322  		})
   323  	}
   324  }
   325  
   326  func TestWorkloadEntryWorkloads(t *testing.T) {
   327  	cases := []struct {
   328  		name   string
   329  		inputs []any
   330  		we     *networkingclient.WorkloadEntry
   331  		result *workloadapi.Workload
   332  	}{
   333  		{
   334  			name: "we with service",
   335  			inputs: []any{
   336  				model.ServiceInfo{
   337  					Service: &workloadapi.Service{
   338  						Name:      "svc",
   339  						Namespace: "ns",
   340  						Hostname:  "hostname",
   341  						Ports: []*workloadapi.Port{{
   342  							ServicePort: 80,
   343  							TargetPort:  8080,
   344  						}},
   345  					},
   346  					LabelSelector: model.NewSelector(map[string]string{"app": "foo"}),
   347  				},
   348  			},
   349  			we: &networkingclient.WorkloadEntry{
   350  				TypeMeta: metav1.TypeMeta{},
   351  				ObjectMeta: metav1.ObjectMeta{
   352  					Name:      "name",
   353  					Namespace: "ns",
   354  					Labels: map[string]string{
   355  						"app": "foo",
   356  					},
   357  				},
   358  				Spec: networking.WorkloadEntry{
   359  					Address: "1.2.3.4",
   360  				},
   361  			},
   362  			result: &workloadapi.Workload{
   363  				Uid:               "cluster0/networking.istio.io/WorkloadEntry/ns/name",
   364  				Name:              "name",
   365  				Namespace:         "ns",
   366  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
   367  				Network:           testNW,
   368  				CanonicalName:     "foo",
   369  				CanonicalRevision: "latest",
   370  				WorkloadType:      workloadapi.WorkloadType_POD,
   371  				WorkloadName:      "name",
   372  				Status:            workloadapi.WorkloadStatus_HEALTHY,
   373  				ClusterId:         testC,
   374  				Services: map[string]*workloadapi.PortList{
   375  					"ns/hostname": {
   376  						Ports: []*workloadapi.Port{{
   377  							ServicePort: 80,
   378  							TargetPort:  8080,
   379  						}},
   380  					},
   381  				},
   382  			},
   383  		},
   384  		{
   385  			name: "pod with service named ports",
   386  			inputs: []any{
   387  				model.ServiceInfo{
   388  					Service: &workloadapi.Service{
   389  						Name:      "svc",
   390  						Namespace: "ns",
   391  						Hostname:  "hostname",
   392  						Ports: []*workloadapi.Port{
   393  							{
   394  								ServicePort: 80,
   395  								TargetPort:  8080,
   396  							},
   397  							{
   398  								ServicePort: 81,
   399  								TargetPort:  0,
   400  							},
   401  							{
   402  								ServicePort: 82,
   403  								TargetPort:  0,
   404  							},
   405  							{
   406  								ServicePort: 83,
   407  								TargetPort:  0,
   408  							},
   409  						},
   410  					},
   411  					PortNames: map[int32]model.ServicePortName{
   412  						// Not a named port
   413  						80: {PortName: "80"},
   414  						// Named port found in WE
   415  						81: {PortName: "81", TargetPortName: "81-target"},
   416  						// Named port target found in WE
   417  						82: {PortName: "82", TargetPortName: "82-target"},
   418  						// Named port not found in WE
   419  						83: {PortName: "83", TargetPortName: "83-target"},
   420  					},
   421  					LabelSelector: model.NewSelector(map[string]string{"app": "foo"}),
   422  					Source:        kind.Service,
   423  				},
   424  			},
   425  			we: &networkingclient.WorkloadEntry{
   426  				TypeMeta: metav1.TypeMeta{},
   427  				ObjectMeta: metav1.ObjectMeta{
   428  					Name:      "name",
   429  					Namespace: "ns",
   430  					Labels: map[string]string{
   431  						"app": "foo",
   432  					},
   433  				},
   434  				Spec: networking.WorkloadEntry{
   435  					Ports: map[string]uint32{
   436  						"81":        8180,
   437  						"82-target": 8280,
   438  					},
   439  					Address: "1.2.3.4",
   440  				},
   441  			},
   442  			result: &workloadapi.Workload{
   443  				Uid:               "cluster0/networking.istio.io/WorkloadEntry/ns/name",
   444  				Name:              "name",
   445  				Namespace:         "ns",
   446  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
   447  				Network:           testNW,
   448  				CanonicalName:     "foo",
   449  				CanonicalRevision: "latest",
   450  				WorkloadType:      workloadapi.WorkloadType_POD,
   451  				WorkloadName:      "name",
   452  				Status:            workloadapi.WorkloadStatus_HEALTHY,
   453  				ClusterId:         testC,
   454  				Services: map[string]*workloadapi.PortList{
   455  					"ns/hostname": {
   456  						Ports: []*workloadapi.Port{
   457  							{
   458  								ServicePort: 80,
   459  								TargetPort:  8080,
   460  							},
   461  							{
   462  								ServicePort: 82,
   463  								TargetPort:  8280,
   464  							},
   465  						},
   466  					},
   467  				},
   468  			},
   469  		},
   470  		{
   471  			name: "pod with serviceentry named ports",
   472  			inputs: []any{
   473  				model.ServiceInfo{
   474  					Service: &workloadapi.Service{
   475  						Name:      "svc",
   476  						Namespace: "ns",
   477  						Hostname:  "hostname",
   478  						Ports: []*workloadapi.Port{
   479  							{
   480  								ServicePort: 80,
   481  								TargetPort:  8080,
   482  							},
   483  							{
   484  								ServicePort: 81,
   485  								TargetPort:  0,
   486  							},
   487  							{
   488  								ServicePort: 82,
   489  								TargetPort:  0,
   490  							},
   491  						},
   492  					},
   493  					PortNames: map[int32]model.ServicePortName{
   494  						// TargetPort explicitly set
   495  						80: {PortName: "80"},
   496  						// Port name found
   497  						81: {PortName: "81"},
   498  						// Port name not found
   499  						82: {PortName: "82"},
   500  					},
   501  					LabelSelector: model.NewSelector(map[string]string{"app": "foo"}),
   502  					Source:        kind.ServiceEntry,
   503  				},
   504  			},
   505  			we: &networkingclient.WorkloadEntry{
   506  				TypeMeta: metav1.TypeMeta{},
   507  				ObjectMeta: metav1.ObjectMeta{
   508  					Name:      "name",
   509  					Namespace: "ns",
   510  					Labels: map[string]string{
   511  						"app": "foo",
   512  					},
   513  				},
   514  				Spec: networking.WorkloadEntry{
   515  					Ports: map[string]uint32{
   516  						"81": 8180,
   517  					},
   518  					Address: "1.2.3.4",
   519  				},
   520  			},
   521  			result: &workloadapi.Workload{
   522  				Uid:               "cluster0/networking.istio.io/WorkloadEntry/ns/name",
   523  				Name:              "name",
   524  				Namespace:         "ns",
   525  				Addresses:         [][]byte{netip.AddrFrom4([4]byte{1, 2, 3, 4}).AsSlice()},
   526  				Network:           testNW,
   527  				CanonicalName:     "foo",
   528  				CanonicalRevision: "latest",
   529  				WorkloadType:      workloadapi.WorkloadType_POD,
   530  				WorkloadName:      "name",
   531  				Status:            workloadapi.WorkloadStatus_HEALTHY,
   532  				ClusterId:         testC,
   533  				Services: map[string]*workloadapi.PortList{
   534  					"ns/hostname": {
   535  						Ports: []*workloadapi.Port{
   536  							{
   537  								ServicePort: 80,
   538  								TargetPort:  8080,
   539  							},
   540  							{
   541  								ServicePort: 81,
   542  								TargetPort:  8180,
   543  							},
   544  							{
   545  								ServicePort: 82,
   546  								TargetPort:  82,
   547  							},
   548  						},
   549  					},
   550  				},
   551  			},
   552  		},
   553  	}
   554  	for _, tt := range cases {
   555  		t.Run(tt.name, func(t *testing.T) {
   556  			inputs := tt.inputs
   557  			a := newAmbientUnitTest()
   558  			AuthorizationPolicies := krt.NewStaticCollection(extractType[model.WorkloadAuthorization](&inputs))
   559  			PeerAuths := krt.NewStaticCollection(extractType[*securityclient.PeerAuthentication](&inputs))
   560  			Waypoints := krt.NewStaticCollection(extractType[Waypoint](&inputs))
   561  			WorkloadServices := krt.NewStaticCollection(extractType[model.ServiceInfo](&inputs))
   562  			Namespaces := krt.NewStaticCollection(extractType[*v1.Namespace](&inputs))
   563  			MeshConfig := krt.NewStatic(&MeshConfig{slices.First(extractType[meshapi.MeshConfig](&inputs))})
   564  			assert.Equal(t, len(inputs), 0, fmt.Sprintf("some inputs were not consumed: %v", inputs))
   565  			WorkloadServicesNamespaceIndex := krt.NewNamespaceIndex(WorkloadServices)
   566  			builder := a.workloadEntryWorkloadBuilder(
   567  				MeshConfig,
   568  				AuthorizationPolicies,
   569  				PeerAuths,
   570  				Waypoints,
   571  				WorkloadServices,
   572  				WorkloadServicesNamespaceIndex,
   573  				Namespaces,
   574  			)
   575  			wrapper := builder(krt.TestingDummyContext{}, tt.we)
   576  			var res *workloadapi.Workload
   577  			if wrapper != nil {
   578  				res = wrapper.Workload
   579  			}
   580  			assert.Equal(t, res, tt.result)
   581  		})
   582  	}
   583  }
   584  
   585  func newAmbientUnitTest() *index {
   586  	return &index{
   587  		networkUpdateTrigger: krt.NewRecomputeTrigger(),
   588  		ClusterID:            testC,
   589  		Network: func(endpointIP string, labels labels.Instance) network.ID {
   590  			return testNW
   591  		},
   592  	}
   593  }
   594  
   595  func extractType[T any](items *[]any) []T {
   596  	var matched []T
   597  	var unmatched []any
   598  	arr := *items
   599  	for _, val := range arr {
   600  		if c, ok := val.(T); ok {
   601  			matched = append(matched, c)
   602  		} else {
   603  			unmatched = append(unmatched, val)
   604  		}
   605  	}
   606  
   607  	*items = unmatched
   608  	return matched
   609  }
   610  
   611  var podReady = []v1.PodCondition{
   612  	{
   613  		Type:               v1.PodReady,
   614  		Status:             v1.ConditionTrue,
   615  		LastTransitionTime: metav1.Now(),
   616  	},
   617  }