github.com/adityamillind98/nomad@v0.11.8/nomad/job_endpoint_hook_expose_check_test.go (about)

     1  package nomad
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/hashicorp/nomad/nomad/structs"
     7  	"github.com/stretchr/testify/require"
     8  )
     9  
    10  func TestJobExposeCheckHook_Name(t *testing.T) {
    11  	t.Parallel()
    12  
    13  	require.Equal(t, "expose-check", new(jobExposeCheckHook).Name())
    14  }
    15  
    16  func TestJobExposeCheckHook_serviceUsesConnectEnvoy(t *testing.T) {
    17  	t.Parallel()
    18  
    19  	t.Run("connect is nil", func(t *testing.T) {
    20  		require.False(t, serviceUsesConnectEnvoy(&structs.Service{
    21  			Connect: nil,
    22  		}))
    23  	})
    24  
    25  	t.Run("sidecar-task is overridden", func(t *testing.T) {
    26  		require.False(t, serviceUsesConnectEnvoy(&structs.Service{
    27  			Connect: &structs.ConsulConnect{
    28  				SidecarTask: &structs.SidecarTask{
    29  					Name: "my-sidecar",
    30  				},
    31  			},
    32  		}))
    33  	})
    34  
    35  	t.Run("sidecar-task is nil", func(t *testing.T) {
    36  		require.True(t, serviceUsesConnectEnvoy(&structs.Service{
    37  			Connect: &structs.ConsulConnect{
    38  				SidecarTask: nil,
    39  			},
    40  		}))
    41  	})
    42  }
    43  
    44  func TestJobExposeCheckHook_tgUsesExposeCheck(t *testing.T) {
    45  	t.Parallel()
    46  
    47  	t.Run("no check.expose", func(t *testing.T) {
    48  		require.False(t, tgUsesExposeCheck(&structs.TaskGroup{
    49  			Services: []*structs.Service{{
    50  				Checks: []*structs.ServiceCheck{{
    51  					Expose: false,
    52  				}},
    53  			}},
    54  		}))
    55  	})
    56  
    57  	t.Run("with check.expose", func(t *testing.T) {
    58  		require.True(t, tgUsesExposeCheck(&structs.TaskGroup{
    59  			Services: []*structs.Service{{
    60  				Checks: []*structs.ServiceCheck{{
    61  					Expose: false,
    62  				}, {
    63  					Expose: true,
    64  				}},
    65  			}},
    66  		}))
    67  	})
    68  }
    69  
    70  func TestJobExposeCheckHook_tgValidateUseOfBridgeMode(t *testing.T) {
    71  	t.Parallel()
    72  
    73  	s1 := &structs.Service{
    74  		Name: "s1",
    75  		Checks: []*structs.ServiceCheck{{
    76  			Name:      "s1-check1",
    77  			Type:      "http",
    78  			PortLabel: "health",
    79  			Expose:    true,
    80  		}},
    81  	}
    82  
    83  	t.Run("no networks but no use of expose", func(t *testing.T) {
    84  		require.Nil(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{
    85  			Networks: make(structs.Networks, 0),
    86  		}))
    87  	})
    88  
    89  	t.Run("no networks and uses expose", func(t *testing.T) {
    90  		require.EqualError(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{
    91  			Name:     "g1",
    92  			Networks: make(structs.Networks, 0),
    93  			Services: []*structs.Service{s1},
    94  		}), `group "g1" must specify one bridge network for exposing service check(s)`)
    95  	})
    96  
    97  	t.Run("non-bridge network and uses expose", func(t *testing.T) {
    98  		require.EqualError(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{
    99  			Name: "g1",
   100  			Networks: structs.Networks{{
   101  				Mode: "host",
   102  			}},
   103  			Services: []*structs.Service{s1},
   104  		}), `group "g1" must use bridge network for exposing service check(s)`)
   105  	})
   106  
   107  	t.Run("bridge network uses expose", func(t *testing.T) {
   108  		require.Nil(t, tgValidateUseOfBridgeMode(&structs.TaskGroup{
   109  			Name: "g1",
   110  			Networks: structs.Networks{{
   111  				Mode: "bridge",
   112  			}},
   113  			Services: []*structs.Service{s1},
   114  		}))
   115  	})
   116  }
   117  
   118  func TestJobExposeCheckHook_tgValidateUseOfCheckExpose(t *testing.T) {
   119  	t.Parallel()
   120  
   121  	withCustomProxyTask := &structs.Service{
   122  		Name: "s1",
   123  		Connect: &structs.ConsulConnect{
   124  			SidecarTask: &structs.SidecarTask{Name: "custom"},
   125  		},
   126  		Checks: []*structs.ServiceCheck{{
   127  			Name:      "s1-check1",
   128  			Type:      "http",
   129  			PortLabel: "health",
   130  			Expose:    true,
   131  		}},
   132  	}
   133  
   134  	t.Run("group-service uses custom proxy", func(t *testing.T) {
   135  		require.EqualError(t, tgValidateUseOfCheckExpose(&structs.TaskGroup{
   136  			Name:     "g1",
   137  			Services: []*structs.Service{withCustomProxyTask},
   138  		}), `exposed service check g1->s1->s1-check1 requires use of Nomad's builtin Connect proxy`)
   139  	})
   140  
   141  	t.Run("group-service uses custom proxy but no expose", func(t *testing.T) {
   142  		withCustomProxyTaskNoExpose := &(*withCustomProxyTask)
   143  		withCustomProxyTask.Checks[0].Expose = false
   144  		require.Nil(t, tgValidateUseOfCheckExpose(&structs.TaskGroup{
   145  			Name:     "g1",
   146  			Services: []*structs.Service{withCustomProxyTaskNoExpose},
   147  		}))
   148  	})
   149  
   150  	t.Run("task-service sets expose", func(t *testing.T) {
   151  		require.EqualError(t, tgValidateUseOfCheckExpose(&structs.TaskGroup{
   152  			Name: "g1",
   153  			Tasks: []*structs.Task{{
   154  				Name: "t1",
   155  				Services: []*structs.Service{{
   156  					Name: "s2",
   157  					Checks: []*structs.ServiceCheck{{
   158  						Name:   "check1",
   159  						Type:   "http",
   160  						Expose: true,
   161  					}},
   162  				}},
   163  			}},
   164  		}), `exposed service check g1[t1]->s2->check1 is not a task-group service`)
   165  	})
   166  }
   167  
   168  func TestJobExposeCheckHook_Validate(t *testing.T) {
   169  	s1 := &structs.Service{
   170  		Name: "s1",
   171  		Checks: []*structs.ServiceCheck{{
   172  			Name:   "s1-check1",
   173  			Type:   "http",
   174  			Expose: true,
   175  		}},
   176  	}
   177  
   178  	t.Run("double network", func(t *testing.T) {
   179  		warnings, err := new(jobExposeCheckHook).Validate(&structs.Job{
   180  			TaskGroups: []*structs.TaskGroup{{
   181  				Name: "g1",
   182  				Networks: structs.Networks{{
   183  					Mode: "bridge",
   184  				}, {
   185  					Mode: "bridge",
   186  				}},
   187  				Services: []*structs.Service{s1},
   188  			}},
   189  		})
   190  		require.Empty(t, warnings)
   191  		require.EqualError(t, err, `group "g1" must specify one bridge network for exposing service check(s)`)
   192  	})
   193  
   194  	t.Run("expose in service check", func(t *testing.T) {
   195  		warnings, err := new(jobExposeCheckHook).Validate(&structs.Job{
   196  			TaskGroups: []*structs.TaskGroup{{
   197  				Name: "g1",
   198  				Networks: structs.Networks{{
   199  					Mode: "bridge",
   200  				}},
   201  				Tasks: []*structs.Task{{
   202  					Name: "t1",
   203  					Services: []*structs.Service{{
   204  						Name: "s2",
   205  						Checks: []*structs.ServiceCheck{{
   206  							Name:   "s2-check1",
   207  							Type:   "http",
   208  							Expose: true,
   209  						}},
   210  					}},
   211  				}},
   212  			}},
   213  		})
   214  		require.Empty(t, warnings)
   215  		require.EqualError(t, err, `exposed service check g1[t1]->s2->s2-check1 is not a task-group service`)
   216  	})
   217  
   218  	t.Run("ok", func(t *testing.T) {
   219  		warnings, err := new(jobExposeCheckHook).Validate(&structs.Job{
   220  			TaskGroups: []*structs.TaskGroup{{
   221  				Name: "g1",
   222  				Networks: structs.Networks{{
   223  					Mode: "bridge",
   224  				}},
   225  				Services: []*structs.Service{{
   226  					Name:    "s1",
   227  					Connect: &structs.ConsulConnect{},
   228  					Checks: []*structs.ServiceCheck{{
   229  						Name:   "check1",
   230  						Type:   "http",
   231  						Expose: true,
   232  					}},
   233  				}},
   234  				Tasks: []*structs.Task{{
   235  					Name: "t1",
   236  					Services: []*structs.Service{{
   237  						Name: "s2",
   238  						Checks: []*structs.ServiceCheck{{
   239  							Name:   "s2-check1",
   240  							Type:   "http",
   241  							Expose: false,
   242  						}},
   243  					}},
   244  				}},
   245  			}},
   246  		})
   247  		require.Empty(t, warnings)
   248  		require.Nil(t, err)
   249  	})
   250  }
   251  
   252  func TestJobExposeCheckHook_exposePathForCheck(t *testing.T) {
   253  	t.Parallel()
   254  
   255  	t.Run("not expose compatible", func(t *testing.T) {
   256  		c := &structs.ServiceCheck{
   257  			Type: "tcp", // not expose compatible
   258  		}
   259  		s := &structs.Service{
   260  			Checks: []*structs.ServiceCheck{c},
   261  		}
   262  		ePath, err := exposePathForCheck(&structs.TaskGroup{
   263  			Services: []*structs.Service{s},
   264  		}, s, c)
   265  		require.NoError(t, err)
   266  		require.Nil(t, ePath)
   267  	})
   268  
   269  	t.Run("direct port", func(t *testing.T) {
   270  		c := &structs.ServiceCheck{
   271  			Name:      "check1",
   272  			Type:      "http",
   273  			Path:      "/health",
   274  			PortLabel: "hcPort",
   275  		}
   276  		s := &structs.Service{
   277  			Name:      "service1",
   278  			PortLabel: "4000",
   279  			Checks:    []*structs.ServiceCheck{c},
   280  		}
   281  		ePath, err := exposePathForCheck(&structs.TaskGroup{
   282  			Name:     "group1",
   283  			Services: []*structs.Service{s},
   284  		}, s, c)
   285  		require.NoError(t, err)
   286  		require.Equal(t, &structs.ConsulExposePath{
   287  			Path:          "/health",
   288  			Protocol:      "", // often blank, consul does the Right Thing
   289  			LocalPathPort: 4000,
   290  			ListenerPort:  "hcPort",
   291  		}, ePath)
   292  	})
   293  
   294  	t.Run("labeled port", func(t *testing.T) {
   295  		c := &structs.ServiceCheck{
   296  			Name:      "check1",
   297  			Type:      "http",
   298  			Path:      "/health",
   299  			PortLabel: "hcPort",
   300  		}
   301  		s := &structs.Service{
   302  			Name:      "service1",
   303  			PortLabel: "sPort", // port label indirection
   304  			Checks:    []*structs.ServiceCheck{c},
   305  		}
   306  		ePath, err := exposePathForCheck(&structs.TaskGroup{
   307  			Name:     "group1",
   308  			Services: []*structs.Service{s},
   309  			Networks: structs.Networks{{
   310  				Mode: "bridge",
   311  				DynamicPorts: []structs.Port{
   312  					{Label: "sPort", Value: 4000},
   313  				},
   314  			}},
   315  		}, s, c)
   316  		require.NoError(t, err)
   317  		require.Equal(t, &structs.ConsulExposePath{
   318  			Path:          "/health",
   319  			Protocol:      "",
   320  			LocalPathPort: 4000,
   321  			ListenerPort:  "hcPort",
   322  		}, ePath)
   323  	})
   324  
   325  	t.Run("missing port", func(t *testing.T) {
   326  		c := &structs.ServiceCheck{
   327  			Name:      "check1",
   328  			Type:      "http",
   329  			Path:      "/health",
   330  			PortLabel: "hcPort",
   331  		}
   332  		s := &structs.Service{
   333  			Name:      "service1",
   334  			PortLabel: "sPort", // port label indirection
   335  			Checks:    []*structs.ServiceCheck{c},
   336  		}
   337  		_, err := exposePathForCheck(&structs.TaskGroup{
   338  			Name:     "group1",
   339  			Services: []*structs.Service{s},
   340  			Networks: structs.Networks{{
   341  				Mode:         "bridge",
   342  				DynamicPorts: []structs.Port{
   343  					// service declares "sPort", but does not exist
   344  				},
   345  			}},
   346  		}, s, c)
   347  		require.EqualError(t, err, `unable to determine local service port for service check group1->service1->check1`)
   348  	})
   349  
   350  	t.Run("empty check port", func(t *testing.T) {
   351  		c := &structs.ServiceCheck{
   352  			Name: "check1",
   353  			Type: "http",
   354  			Path: "/health",
   355  		}
   356  		s := &structs.Service{
   357  			Name:      "service1",
   358  			PortLabel: "9999",
   359  			Checks:    []*structs.ServiceCheck{c},
   360  		}
   361  		tg := &structs.TaskGroup{
   362  			Name:     "group1",
   363  			Services: []*structs.Service{s},
   364  			Networks: structs.Networks{{
   365  				Mode:         "bridge",
   366  				DynamicPorts: []structs.Port{},
   367  			}},
   368  		}
   369  		ePath, err := exposePathForCheck(tg, s, c)
   370  		require.NoError(t, err)
   371  		require.Len(t, tg.Networks[0].DynamicPorts, 1)
   372  		require.Equal(t, &structs.ConsulExposePath{
   373  			Path:          "/health",
   374  			Protocol:      "",
   375  			LocalPathPort: 9999,
   376  			ListenerPort:  tg.Networks[0].DynamicPorts[0].Label,
   377  		}, ePath)
   378  	})
   379  }
   380  
   381  func TestJobExposeCheckHook_containsExposePath(t *testing.T) {
   382  	t.Parallel()
   383  
   384  	t.Run("contains path", func(t *testing.T) {
   385  		require.True(t, containsExposePath([]structs.ConsulExposePath{{
   386  			Path:          "/v2/health",
   387  			Protocol:      "grpc",
   388  			LocalPathPort: 8080,
   389  			ListenerPort:  "v2Port",
   390  		}, {
   391  			Path:          "/health",
   392  			Protocol:      "http",
   393  			LocalPathPort: 8080,
   394  			ListenerPort:  "hcPort",
   395  		}}, structs.ConsulExposePath{
   396  			Path:          "/health",
   397  			Protocol:      "http",
   398  			LocalPathPort: 8080,
   399  			ListenerPort:  "hcPort",
   400  		}))
   401  	})
   402  
   403  	t.Run("no such path", func(t *testing.T) {
   404  		require.False(t, containsExposePath([]structs.ConsulExposePath{{
   405  			Path:          "/v2/health",
   406  			Protocol:      "grpc",
   407  			LocalPathPort: 8080,
   408  			ListenerPort:  "v2Port",
   409  		}, {
   410  			Path:          "/health",
   411  			Protocol:      "http",
   412  			LocalPathPort: 8080,
   413  			ListenerPort:  "hcPort",
   414  		}}, structs.ConsulExposePath{
   415  			Path:          "/v3/health",
   416  			Protocol:      "http",
   417  			LocalPathPort: 8080,
   418  			ListenerPort:  "hcPort",
   419  		}))
   420  	})
   421  }
   422  
   423  func TestJobExposeCheckHook_serviceExposeConfig(t *testing.T) {
   424  	t.Parallel()
   425  
   426  	t.Run("proxy is nil", func(t *testing.T) {
   427  		require.NotNil(t, serviceExposeConfig(&structs.Service{
   428  			Connect: &structs.ConsulConnect{
   429  				SidecarService: &structs.ConsulSidecarService{},
   430  			},
   431  		}))
   432  	})
   433  
   434  	t.Run("expose is nil", func(t *testing.T) {
   435  		require.NotNil(t, serviceExposeConfig(&structs.Service{
   436  			Connect: &structs.ConsulConnect{
   437  				SidecarService: &structs.ConsulSidecarService{
   438  					Proxy: &structs.ConsulProxy{},
   439  				},
   440  			},
   441  		}))
   442  	})
   443  
   444  	t.Run("expose pre-existing", func(t *testing.T) {
   445  		exposeConfig := serviceExposeConfig(&structs.Service{
   446  			Connect: &structs.ConsulConnect{
   447  				SidecarService: &structs.ConsulSidecarService{
   448  					Proxy: &structs.ConsulProxy{
   449  						Expose: &structs.ConsulExposeConfig{
   450  							Paths: []structs.ConsulExposePath{{
   451  								Path: "/health",
   452  							}},
   453  						},
   454  					},
   455  				},
   456  			},
   457  		})
   458  		require.NotNil(t, exposeConfig)
   459  		require.Equal(t, []structs.ConsulExposePath{{
   460  			Path: "/health",
   461  		}}, exposeConfig.Paths)
   462  	})
   463  
   464  	t.Run("append to paths is safe", func(t *testing.T) {
   465  		// double check that serviceExposeConfig(s).Paths can be appended to
   466  		// from a derived pointer without fear of the original underlying array
   467  		// pointer being lost
   468  
   469  		s := &structs.Service{
   470  			Connect: &structs.ConsulConnect{
   471  				SidecarService: &structs.ConsulSidecarService{
   472  					Proxy: &structs.ConsulProxy{
   473  						Expose: &structs.ConsulExposeConfig{
   474  							Paths: []structs.ConsulExposePath{{
   475  								Path: "/one",
   476  							}},
   477  						},
   478  					},
   479  				},
   480  			},
   481  		}
   482  
   483  		exposeConfig := serviceExposeConfig(s)
   484  		exposeConfig.Paths = append(exposeConfig.Paths,
   485  			structs.ConsulExposePath{Path: "/two"},
   486  			structs.ConsulExposePath{Path: "/three"},
   487  			structs.ConsulExposePath{Path: "/four"},
   488  			structs.ConsulExposePath{Path: "/five"},
   489  			structs.ConsulExposePath{Path: "/six"},
   490  			structs.ConsulExposePath{Path: "/seven"},
   491  			structs.ConsulExposePath{Path: "/eight"},
   492  			structs.ConsulExposePath{Path: "/nine"},
   493  		)
   494  
   495  		// works, because exposeConfig.Paths gets re-assigned into exposeConfig
   496  		// which is a pointer, meaning the field is modified also from the
   497  		// service struct's perspective
   498  		require.Equal(t, 9, len(s.Connect.SidecarService.Proxy.Expose.Paths))
   499  	})
   500  }
   501  
   502  func TestJobExposeCheckHook_checkIsExposable(t *testing.T) {
   503  	t.Parallel()
   504  
   505  	t.Run("grpc", func(t *testing.T) {
   506  		require.True(t, checkIsExposable(&structs.ServiceCheck{
   507  			Type: "grpc",
   508  			Path: "/health",
   509  		}))
   510  		require.True(t, checkIsExposable(&structs.ServiceCheck{
   511  			Type: "gRPC",
   512  			Path: "/health",
   513  		}))
   514  	})
   515  
   516  	t.Run("http", func(t *testing.T) {
   517  		require.True(t, checkIsExposable(&structs.ServiceCheck{
   518  			Type: "http",
   519  			Path: "/health",
   520  		}))
   521  		require.True(t, checkIsExposable(&structs.ServiceCheck{
   522  			Type: "HTTP",
   523  			Path: "/health",
   524  		}))
   525  	})
   526  
   527  	t.Run("tcp", func(t *testing.T) {
   528  		require.False(t, checkIsExposable(&structs.ServiceCheck{
   529  			Type: "tcp",
   530  			Path: "/health",
   531  		}))
   532  	})
   533  
   534  	t.Run("no path slash prefix", func(t *testing.T) {
   535  		require.False(t, checkIsExposable(&structs.ServiceCheck{
   536  			Type: "http",
   537  			Path: "health",
   538  		}))
   539  	})
   540  }
   541  
   542  func TestJobExposeCheckHook_Mutate(t *testing.T) {
   543  	t.Parallel()
   544  
   545  	t.Run("typical", func(t *testing.T) {
   546  		result, warnings, err := new(jobExposeCheckHook).Mutate(&structs.Job{
   547  			TaskGroups: []*structs.TaskGroup{{
   548  				Name: "group0",
   549  				Networks: structs.Networks{{
   550  					Mode: "host",
   551  				}},
   552  			}, {
   553  				Name: "group1",
   554  				Networks: structs.Networks{{
   555  					Mode: "bridge",
   556  				}},
   557  				Services: []*structs.Service{{
   558  					Name:      "service1",
   559  					PortLabel: "8000",
   560  					Checks: []*structs.ServiceCheck{{
   561  						Name:      "check1",
   562  						Type:      "tcp",
   563  						PortLabel: "8100",
   564  					}, {
   565  						Name:      "check2",
   566  						Type:      "http",
   567  						PortLabel: "health",
   568  						Path:      "/health",
   569  						Expose:    true,
   570  					}, {
   571  						Name:      "check3",
   572  						Type:      "grpc",
   573  						PortLabel: "health",
   574  						Path:      "/v2/health",
   575  						Expose:    true,
   576  					}},
   577  					Connect: &structs.ConsulConnect{
   578  						SidecarService: &structs.ConsulSidecarService{
   579  							Proxy: &structs.ConsulProxy{
   580  								Expose: &structs.ConsulExposeConfig{
   581  									Paths: []structs.ConsulExposePath{{
   582  										Path:          "/pre-existing",
   583  										Protocol:      "http",
   584  										LocalPathPort: 9000,
   585  										ListenerPort:  "otherPort",
   586  									}}}}}}}, {
   587  					Name:      "service2",
   588  					PortLabel: "3000",
   589  					Checks: []*structs.ServiceCheck{{
   590  						Name:      "check1",
   591  						Type:      "grpc",
   592  						Protocol:  "http2",
   593  						Path:      "/ok",
   594  						PortLabel: "health",
   595  						Expose:    true,
   596  					}},
   597  					Connect: &structs.ConsulConnect{
   598  						SidecarService: &structs.ConsulSidecarService{
   599  							Proxy: &structs.ConsulProxy{},
   600  						},
   601  					},
   602  				}}}},
   603  		})
   604  
   605  		require.NoError(t, err)
   606  		require.Empty(t, warnings)
   607  		require.Equal(t, []structs.ConsulExposePath{{
   608  			Path:          "/pre-existing",
   609  			LocalPathPort: 9000,
   610  			Protocol:      "http",
   611  			ListenerPort:  "otherPort",
   612  		}, {
   613  			Path:          "/health",
   614  			LocalPathPort: 8000,
   615  			ListenerPort:  "health",
   616  		}, {
   617  			Path:          "/v2/health",
   618  			LocalPathPort: 8000,
   619  			ListenerPort:  "health",
   620  		}}, result.TaskGroups[1].Services[0].Connect.SidecarService.Proxy.Expose.Paths)
   621  		require.Equal(t, []structs.ConsulExposePath{{
   622  			Path:          "/ok",
   623  			LocalPathPort: 3000,
   624  			Protocol:      "http2",
   625  			ListenerPort:  "health",
   626  		}}, result.TaskGroups[1].Services[1].Connect.SidecarService.Proxy.Expose.Paths)
   627  	})
   628  }