diff --git a/test/e2e/dns_test.go b/test/e2e/dns_test.go deleted file mode 100644 index d175c401..00000000 --- a/test/e2e/dns_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package e2e - -import ( - "context" - "regexp" - "strings" - "testing" - "time" - - "github.com/psviderski/uncloud/internal/ucind" - "github.com/psviderski/uncloud/pkg/api" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestInternalDNS tests the internal DNS functionality including the new machine-specific service lookups -func TestInternalDNS(t *testing.T) { - t.Parallel() - - clusterName := "ucind-test.dns" - ctx := context.Background() - c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true) - - cli, err := c.Machines[0].Connect(ctx) - require.NoError(t, err) - - // Create a test service with multiple replicas across machines - serviceName := "test-dns-service" - t.Cleanup(func() { - err := cli.RemoveService(ctx, serviceName) - if err != nil && !strings.Contains(err.Error(), "not found") { - require.NoError(t, err) - } - }) - - // Deploy a service across all machines in global mode using pause container - spec := api.ServiceSpec{ - Name: serviceName, - Mode: api.ServiceModeGlobal, - Container: api.ContainerSpec{ - Image: "portainer/pause:latest", - }, - } - - deployment := cli.NewDeployment(spec, nil) - _, err = deployment.Run(ctx) - require.NoError(t, err) - - // Wait for the service to be deployed - var svc api.Service - require.Eventually(t, func() bool { - svc, err = cli.InspectService(ctx, serviceName) - if err != nil { - return false - } - // Should have 3 containers (one per machine) and all should be running - if len(svc.Containers) != 3 { - return false - } - for _, ctr := range svc.Containers { - if ctr.Container.State.Status != "running" { - return false - } - } - return true - }, 30*time.Second, 1*time.Second, "Service should be deployed and running on all machines") - - // Deploy a single "network-multitool" service to be used for all DNS queries - queryServiceName := "dns-query-service" - t.Cleanup(func() { - err := cli.RemoveService(ctx, queryServiceName) - if err != nil && !strings.Contains(err.Error(), "not found") { - require.NoError(t, err) - } - }) - querySvcSpec := api.ServiceSpec{ - Name: queryServiceName, - Mode: api.ServiceModeReplicated, - Replicas: 1, - Placement: api.Placement{ - Machines: []string{c.Machines[0].Name}, - }, - Container: api.ContainerSpec{ - Image: "wbitt/network-multitool", - Command: []string{"sleep", "infinity"}, - }, - } - _, err = cli.RunService(ctx, querySvcSpec) - require.NoError(t, err) - - // Wait for the query service to be deployed - var querySvc api.Service - require.Eventually(t, func() bool { - querySvc, err = cli.InspectService(ctx, queryServiceName) - if err != nil { - return false - } - // Should have 1 container and it should be running - if len(querySvc.Containers) != 1 { - return false - } - return querySvc.Containers[0].Container.State.Status == "running" - }, 30*time.Second, 1*time.Second, "Query service should be deployed and running") - queryContainer := querySvc.Containers[0] - - // Run nslookup for given query - runNslookup := func(t *testing.T, dnsQuery string) string { - dnsOutput, err := execInContainerAndReadOutput( - t, ctx, cli, queryServiceName, queryContainer.Container.ID, - []string{"nslookup", dnsQuery}, - ) - require.NoError(t, err) - return dnsOutput - } - - // Helper function to verify DNS output doesn't contain errors - assertNoDNSErrors := func(t *testing.T, dnsOutput string) { - assert.NotContains(t, dnsOutput, "can't resolve", "DNS query should not contain resolution errors") - assert.NotContains(t, dnsOutput, "Name or service not known", - "DNS query should not contain unknown service errors") - } - - t.Run("service name resolves to all container IPs", func(t *testing.T) { - dnsOutput := runNslookup(t, serviceName+".internal") - t.Logf("DNS query output:\n%s", dnsOutput) - - // Verify that all service container IPs are in the DNS response - for _, ctr := range svc.Containers { - containerIP := ctr.Container.UncloudNetworkIP().String() - assert.Contains(t, dnsOutput, containerIP, - "Service DNS should resolve to container IP %s", containerIP) - } - - assertNoDNSErrors(t, dnsOutput) - }) - - t.Run("machine-specific service DNS lookups", func(t *testing.T) { - // Test the new .m..internal DNS feature - for _, targetContainer := range svc.Containers { - targetMachineID := targetContainer.MachineID - targetContainerIP := targetContainer.Container.UncloudNetworkIP().String() - - // Construct the machine-specific DNS name - machineSpecificDNS := targetMachineID + ".m." + serviceName + ".internal" - - dnsOutput := runNslookup(t, machineSpecificDNS) - t.Logf("Machine-specific DNS query output for %s:\n%s", machineSpecificDNS, dnsOutput) - - // Verify that the specific container IP is returned - assert.Contains(t, dnsOutput, targetContainerIP, - "Machine-specific DNS %s should resolve to container IP %s", - machineSpecificDNS, targetContainerIP) - - // Verify that other container IPs are not returned (machine-specific should return only one IP) - for _, ctr := range svc.Containers { - if ctr.MachineID != targetMachineID { - otherContainerIP := ctr.Container.UncloudNetworkIP().String() - assert.NotContains(t, dnsOutput, otherContainerIP, - "Machine-specific DNS %s should not resolve to other container IP %s", - machineSpecificDNS, otherContainerIP) - } - } - - assertNoDNSErrors(t, dnsOutput) - } - }) - - t.Run("service ID DNS lookup", func(t *testing.T) { - // Test that service ID also resolves (existing functionality) - dnsOutput := runNslookup(t, svc.ID+".internal") - t.Logf("Service ID DNS query output:\n%s", dnsOutput) - - // Verify that all service container IPs are in the DNS response - for _, ctr := range svc.Containers { - containerIP := ctr.Container.UncloudNetworkIP().String() - assert.Contains(t, dnsOutput, containerIP, - "Service ID DNS should resolve to container IP %s", containerIP) - } - - assertNoDNSErrors(t, dnsOutput) - }) - - t.Run("nearest mode prioritizes local subnet IPs", func(t *testing.T) { - // Find the service container on the same machine as the query container. - var localIP string - for _, ctr := range svc.Containers { - if ctr.MachineID == queryContainer.MachineID { - localIP = ctr.Container.UncloudNetworkIP().String() - break - } - } - require.NotEmpty(t, localIP, "Should find local container IP on query machine %s", queryContainer.MachineID) - - // We will extract the first IP address from the DNS output using a regex. - // Pattern matches "Name: nearest.test-dns-service.internal" followed by "Address: X.X.X.X". - re := regexp.MustCompile(`(?m)Name:\s+[\w\.\-]+\s+Address:\s+([\d\.]+)`) - - // Test the "nearest" mode which should sort local subnet IPs first. - // The default behavior randomizes the order, so run it a few times - // to reduce the chance we're just getting lucky with the order. - for range 5 { - dnsOutput := runNslookup(t, "nearest."+serviceName+".internal") - t.Logf("Nearest mode DNS query output:\n%s", dnsOutput) - - matches := re.FindStringSubmatch(dnsOutput) - require.Len(t, matches, 2, "Should find Name's Address in DNS output") - firstIP := matches[1] - assert.Equal(t, localIP, firstIP, - "Nearest mode should return local subnet IP first (query machine: %s, local IP: %s, first DNS result: %s)", - queryContainer.MachineID, localIP, firstIP) - - assertNoDNSErrors(t, dnsOutput) - } - }) -} diff --git a/test/e2e/machine_test.go b/test/e2e/machine_test.go index 62184fe8..e5f945d5 100644 --- a/test/e2e/machine_test.go +++ b/test/e2e/machine_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/require" ) -func TestMachineRename(t *testing.T) { +func TestMachineOperations(t *testing.T) { t.Parallel() - name := "ucind-test.machine-rename" + name := "ucind-test.machine-ops" ctx := context.Background() c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true) @@ -23,59 +23,61 @@ func TestMachineRename(t *testing.T) { require.NoError(t, err) defer cli.Close() + // RenameMachine subtests. + t.Run("rename machine by name", func(t *testing.T) { - // Get initial machine state + // Get initial machine state. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) require.Len(t, machines, 3) - // Select the second machine to rename + // Select the second machine to rename. originalMachine := machines[1] originalName := originalMachine.Machine.Name newName := "renamed-machine-1" - // Rename the machine + // Rename the machine. updatedMachine, err := cli.RenameMachine(ctx, originalName, newName) require.NoError(t, err) assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, originalMachine.Machine.Id, updatedMachine.Id) - // Verify the machine list reflects the change + // Verify the machine list reflects the change. machines, err = cli.ListMachines(ctx, nil) require.NoError(t, err) require.Len(t, machines, 3) - // Find the renamed machine + // Find the renamed machine. var found bool for _, m := range machines { if m.Machine.Id == originalMachine.Machine.Id { assert.Equal(t, newName, m.Machine.Name) found = true } else { - // Ensure other machines are unaffected + // Ensure other machines are unaffected. assert.NotEqual(t, newName, m.Machine.Name) } } assert.True(t, found, "Renamed machine should be in the list") - // Verify we can inspect the machine by its new name + // Verify we can inspect the machine by its new name. inspectedMachine, err := cli.InspectMachine(ctx, newName) require.NoError(t, err) assert.Equal(t, newName, inspectedMachine.Machine.Name) assert.Equal(t, originalMachine.Machine.Id, inspectedMachine.Machine.Id) - // Verify the old name no longer works + // Verify the old name no longer works. _, err = cli.InspectMachine(ctx, originalName) assert.ErrorIs(t, err, api.ErrNotFound) }) t.Run("rename machine by ID", func(t *testing.T) { - // Get the third machine + // Get the third machine. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) require.Len(t, machines, 3) - // Find a machine that hasn't been renamed yet + // Find a machine that hasn't been renamed yet. var targetMachine *pb.MachineMember for _, m := range machines { if m.Machine.Name != "renamed-machine-1" { @@ -89,67 +91,67 @@ func TestMachineRename(t *testing.T) { machineID := targetMachine.Machine.Id newName := "renamed-machine-2" - // Rename using ID instead of name + // Rename using ID instead of name. updatedMachine, err := cli.RenameMachine(ctx, machineID, newName) require.NoError(t, err) assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, machineID, updatedMachine.Id) - // Verify the rename was successful + // Verify the rename was successful. inspectedMachine, err := cli.InspectMachine(ctx, newName) require.NoError(t, err) assert.Equal(t, newName, inspectedMachine.Machine.Name) assert.Equal(t, machineID, inspectedMachine.Machine.Id) - // Verify the old name no longer works + // Verify the old name no longer works. _, err = cli.InspectMachine(ctx, originalName) assert.ErrorIs(t, err, api.ErrNotFound) }) t.Run("rename non-existent machine", func(t *testing.T) { - // Try to rename a machine that doesn't exist + // Try to rename a machine that doesn't exist. _, err := cli.RenameMachine(ctx, "non-existent-machine", "new-name") assert.ErrorIs(t, err, api.ErrNotFound) - // Try with a non-existent ID + // Try with a non-existent ID. _, err = cli.RenameMachine(ctx, "non-existent-id-12345", "new-name") assert.ErrorIs(t, err, api.ErrNotFound) }) t.Run("rename to existing name", func(t *testing.T) { - // Get current machines + // Get current machines. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) require.Len(t, machines, 3) - // Try to rename machine 0 to the name of machine 1 + // Try to rename machine 0 to the name of machine 1. machine0Name := machines[0].Machine.Name machine1Name := machines[1].Machine.Name - // This should fail because the name is already taken + // This should fail because the name is already taken. _, err = cli.RenameMachine(ctx, machine0Name, machine1Name) assert.Error(t, err) }) t.Run("rename with empty name", func(t *testing.T) { - // Get a machine to rename + // Get a machine to rename. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) require.Len(t, machines, 3) machineName := machines[0].Machine.Name - // Try to rename with empty string + // Try to rename with empty string. _, err = cli.RenameMachine(ctx, machineName, "") assert.Error(t, err) }) t.Run("service continuity after rename", func(t *testing.T) { - // Deploy a service on a specific machine + // Deploy a service on a specific machine. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) - // Find a machine that hasn't been renamed to test with + // Find a machine that hasn't been renamed to test with. var targetMachine *pb.MachineMember for _, m := range machines { if m.Machine.Name != "renamed-machine-1" && m.Machine.Name != "renamed-machine-2" { @@ -162,7 +164,7 @@ func TestMachineRename(t *testing.T) { originalMachineName := targetMachine.Machine.Name serviceName := "test-service-rename-continuity" - // Create a service on the specific machine + // Create a service on the specific machine. spec := api.ServiceSpec{ Name: serviceName, Mode: api.ServiceModeGlobal, @@ -184,51 +186,41 @@ func TestMachineRename(t *testing.T) { } }) - // Verify service is running on the machine + // Verify service is running on the machine. svc, err := cli.InspectService(ctx, serviceName) require.NoError(t, err) assert.Len(t, svc.Containers, 1) assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID) - // Rename the machine + // Rename the machine. newMachineName := "renamed-for-service-test" _, err = cli.RenameMachine(ctx, originalMachineName, newMachineName) require.NoError(t, err) - // Verify service is still running on the renamed machine + // Verify service is still running on the renamed machine. svc, err = cli.InspectService(ctx, serviceName) require.NoError(t, err) assert.Len(t, svc.Containers, 1) assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID) // The service spec's placement still references the old name, - // but the service should continue to run on the same machine id + // but the service should continue to run on the same machine id. }) -} -func TestUpdateMachine(t *testing.T) { - t.Parallel() - - name := "ucind-test.machine-update" - ctx := context.Background() - c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true) - - cli, err := c.Machines[0].Connect(ctx) - require.NoError(t, err) - defer cli.Close() + // UpdateMachine subtests. t.Run("update machine name", func(t *testing.T) { - // Get initial machine state + // Get initial machine state. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) require.Len(t, machines, 3) - // Select a machine to update + // Select a machine to update. targetMachine := machines[1] originalName := targetMachine.Machine.Name newName := "updated-machine-name" - // Update the machine name using UpdateMachine directly + // Update the machine name using UpdateMachine directly. req := &pb.UpdateMachineRequest{ MachineId: targetMachine.Machine.Id, Name: &newName, @@ -238,22 +230,22 @@ func TestUpdateMachine(t *testing.T) { assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, targetMachine.Machine.Id, updatedMachine.Id) - // Verify the change persisted + // Verify the change persisted. inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) require.NoError(t, err) assert.Equal(t, newName, inspected.Machine.Name) - // Verify old name no longer works + // Verify old name no longer works. _, err = cli.InspectMachine(ctx, originalName) assert.ErrorIs(t, err, api.ErrNotFound) }) t.Run("update machine public IP", func(t *testing.T) { - // Get a machine to update + // Get a machine to update. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) - // Find a machine that hasn't been renamed + // Find a machine that hasn't been renamed. var targetMachine *pb.MachineMember for _, m := range machines { if m.Machine.Name != "updated-machine-name" { @@ -263,12 +255,12 @@ func TestUpdateMachine(t *testing.T) { } require.NotNil(t, targetMachine) - // Create a new public IP (must be a valid public IP address) + // Create a new public IP (must be a valid public IP address). newPublicIP := &pb.IP{ Ip: []byte{8, 8, 8, 8}, } - // Update the public IP + // Update the public IP. req := &pb.UpdateMachineRequest{ MachineId: targetMachine.Machine.Id, PublicIp: newPublicIP, @@ -277,31 +269,31 @@ func TestUpdateMachine(t *testing.T) { require.NoError(t, err) assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip) - // Verify the change persisted + // Verify the change persisted. inspected, err := cli.InspectMachine(ctx, targetMachine.Machine.Id) require.NoError(t, err) assert.Equal(t, newPublicIP.Ip, inspected.Machine.PublicIp.Ip) }) t.Run("remove machine public IP", func(t *testing.T) { - // Get machines + // Get machines. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) require.True(t, len(machines) > 0, "Need at least one machine") - // First, set a public IP on a machine + // First, set a public IP on a machine. targetMachine := machines[0] setIPReq := &pb.UpdateMachineRequest{ MachineId: targetMachine.Machine.Id, - PublicIp: &pb.IP{Ip: []byte{192, 0, 2, 1}}, // TEST-NET-1 address + PublicIp: &pb.IP{Ip: []byte{192, 0, 2, 1}}, // TEST-NET-1 address. } updatedMachine, err := cli.UpdateMachine(ctx, setIPReq) require.NoError(t, err) require.NotNil(t, updatedMachine.PublicIp) - // Now test removing the public IP + // Now test removing the public IP. - // Remove the public IP by setting it to empty + // Remove the public IP by setting it to empty. emptyIP := &pb.IP{} req := &pb.UpdateMachineRequest{ MachineId: updatedMachine.Id, @@ -311,23 +303,17 @@ func TestUpdateMachine(t *testing.T) { require.NoError(t, err) assert.Nil(t, removedIPMachine.PublicIp) - // Verify the change persisted + // Verify the change persisted. inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) require.NoError(t, err) assert.Nil(t, inspected.Machine.PublicIp) }) t.Run("update machine endpoints", func(t *testing.T) { - // Get a machine to update + // Get a machine to update. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) - - var targetMachine *pb.MachineMember - for _, m := range machines { - targetMachine = m - break - } - require.NotNil(t, targetMachine) + targetMachine := machines[0] newEndpoints := []*pb.IPPort{ { @@ -348,13 +334,13 @@ func TestUpdateMachine(t *testing.T) { require.NoError(t, err) assert.Equal(t, len(newEndpoints), len(updatedMachine.Network.Endpoints)) - // Verify endpoints were updated + // Verify endpoints were updated. for i, endpoint := range updatedMachine.Network.Endpoints { assert.Equal(t, newEndpoints[i].Ip.Ip, endpoint.Ip.Ip) assert.Equal(t, newEndpoints[i].Port, endpoint.Port) } - // Verify other network fields remain unchanged + // Verify other network fields remain unchanged. assert.Equal(t, targetMachine.Machine.Network.Subnet.Ip.Ip, updatedMachine.Network.Subnet.Ip.Ip) assert.Equal(t, targetMachine.Machine.Network.Subnet.Bits, updatedMachine.Network.Subnet.Bits) assert.Equal(t, targetMachine.Machine.Network.ManagementIp.Ip, updatedMachine.Network.ManagementIp.Ip) @@ -362,7 +348,7 @@ func TestUpdateMachine(t *testing.T) { }) t.Run("update multiple fields simultaneously", func(t *testing.T) { - // Get a machine to update + // Get a machine to update. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) @@ -375,7 +361,7 @@ func TestUpdateMachine(t *testing.T) { } require.NotNil(t, targetMachine) - // Update both name and public IP + // Update both name and public IP. newName := "multi-update-machine" newPublicIP := &pb.IP{ Ip: []byte{1, 1, 1, 1}, @@ -391,7 +377,7 @@ func TestUpdateMachine(t *testing.T) { assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip) - // Verify both changes persisted + // Verify both changes persisted. inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) require.NoError(t, err) assert.Equal(t, newName, inspected.Machine.Name) @@ -399,7 +385,7 @@ func TestUpdateMachine(t *testing.T) { }) t.Run("update non-existent machine", func(t *testing.T) { - // Try to update properties on a machine that doesn't exist + // Try to update properties on a machine that doesn't exist. nonExistentName := "should-be-updated" req := &pb.UpdateMachineRequest{ MachineId: "non-existent-machine-id", @@ -410,7 +396,7 @@ func TestUpdateMachine(t *testing.T) { }) t.Run("update to duplicate name", func(t *testing.T) { - // Get two machines + // Get two machines. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) require.Len(t, machines, 3) @@ -418,7 +404,7 @@ func TestUpdateMachine(t *testing.T) { machine1 := machines[0] machine2 := machines[1] - // Try to update machine2 with machine1's name + // Try to update machine2 with machine1's name. req := &pb.UpdateMachineRequest{ MachineId: machine2.Machine.Id, Name: &machine1.Machine.Name, @@ -428,20 +414,20 @@ func TestUpdateMachine(t *testing.T) { }) t.Run("update with empty request", func(t *testing.T) { - // Get a machine + // Get a machine. machines, err := cli.ListMachines(ctx, nil) require.NoError(t, err) targetMachine := machines[0] - // Update with no fields set (should be a no-op) + // Update with no fields set (should be a no-op). req := &pb.UpdateMachineRequest{ MachineId: targetMachine.Machine.Id, } updatedMachine, err := cli.UpdateMachine(ctx, req) require.NoError(t, err) - // Machine should remain unchanged + // Machine should remain unchanged. assert.Equal(t, targetMachine.Machine.Name, updatedMachine.Name) if targetMachine.Machine.PublicIp != nil && updatedMachine.PublicIp != nil { assert.Equal(t, targetMachine.Machine.PublicIp.Ip, updatedMachine.PublicIp.Ip) diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go index fbc91937..cbfdd9b6 100644 --- a/test/e2e/service_test.go +++ b/test/e2e/service_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/netip" + "regexp" "slices" "strings" "testing" @@ -2022,4 +2023,153 @@ func TestServiceLifecycle(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "machines not found") }) + + t.Run("internal DNS", func(t *testing.T) { + t.Parallel() + + // Deploy a global service so each machine gets one container. + serviceName := "test-dns-service" + t.Cleanup(func() { + err := cli.RemoveService(ctx, serviceName) + if err != nil && !errors.Is(err, api.ErrNotFound) { + require.NoError(t, err) + } + }) + + spec := api.ServiceSpec{ + Name: serviceName, + Mode: api.ServiceModeGlobal, + Container: api.ContainerSpec{ + Image: "portainer/pause:latest", + }, + } + _, err := cli.RunService(ctx, spec) + require.NoError(t, err) + + svc, err := cli.InspectService(ctx, serviceName) + require.NoError(t, err) + + // Deploy a test service to run DNS queries from. + queryServiceName := "test-dns-query-service" + t.Cleanup(func() { + err := cli.RemoveService(ctx, queryServiceName) + if err != nil && !errors.Is(err, api.ErrNotFound) { + require.NoError(t, err) + } + }) + querySvcSpec := api.ServiceSpec{ + Name: queryServiceName, + Mode: api.ServiceModeReplicated, + Replicas: 1, + Placement: api.Placement{ + Machines: []string{c.Machines[0].Name}, + }, + Container: api.ContainerSpec{ + Image: "alpine:3.20", + Command: []string{"sleep", "infinity"}, + }, + } + _, err = cli.RunService(ctx, querySvcSpec) + require.NoError(t, err) + + querySvc, err := cli.InspectService(ctx, queryServiceName) + require.NoError(t, err) + queryContainer := querySvc.Containers[0] + + runNslookup := func(t *testing.T, dnsQuery string) string { + dnsOutput, err := execInContainerAndReadOutput( + t, ctx, cli, queryServiceName, queryContainer.Container.ID, + []string{"nslookup", dnsQuery}, + ) + require.NoError(t, err) + return dnsOutput + } + + assertNoDNSErrors := func(t *testing.T, dnsOutput string) { + assert.NotContains(t, dnsOutput, "server can't find", "DNS query should not contain NXDOMAIN/SERVFAIL errors") + } + + t.Run("service name resolves to all container IPs", func(t *testing.T) { + dnsOutput := runNslookup(t, serviceName+".internal") + t.Logf("DNS query output:\n%s", dnsOutput) + + for _, ctr := range svc.Containers { + containerIP := ctr.Container.UncloudNetworkIP().String() + assert.Contains(t, dnsOutput, containerIP, + "Service DNS should resolve to container IP %s", containerIP) + } + + assertNoDNSErrors(t, dnsOutput) + }) + + t.Run("machine-specific service DNS lookups", func(t *testing.T) { + for _, targetContainer := range svc.Containers { + targetMachineID := targetContainer.MachineID + targetContainerIP := targetContainer.Container.UncloudNetworkIP().String() + + machineSpecificDNS := targetMachineID + ".m." + serviceName + ".internal" + + dnsOutput := runNslookup(t, machineSpecificDNS) + t.Logf("Machine-specific DNS query output for %s:\n%s", machineSpecificDNS, dnsOutput) + + assert.Contains(t, dnsOutput, targetContainerIP, + "Machine-specific DNS %s should resolve to container IP %s", + machineSpecificDNS, targetContainerIP) + + // Machine-specific lookup should return only the container on that machine. + for _, ctr := range svc.Containers { + if ctr.MachineID != targetMachineID { + otherContainerIP := ctr.Container.UncloudNetworkIP().String() + assert.NotContains(t, dnsOutput, otherContainerIP, + "Machine-specific DNS %s should not resolve to other container IP %s", + machineSpecificDNS, otherContainerIP) + } + } + + assertNoDNSErrors(t, dnsOutput) + } + }) + + t.Run("service ID DNS lookup", func(t *testing.T) { + dnsOutput := runNslookup(t, svc.ID+".internal") + t.Logf("Service ID DNS query output:\n%s", dnsOutput) + + for _, ctr := range svc.Containers { + containerIP := ctr.Container.UncloudNetworkIP().String() + assert.Contains(t, dnsOutput, containerIP, + "Service ID DNS should resolve to container IP %s", containerIP) + } + + assertNoDNSErrors(t, dnsOutput) + }) + + t.Run("nearest mode prioritizes local subnet IPs", func(t *testing.T) { + var localIP string + for _, ctr := range svc.Containers { + if ctr.MachineID == queryContainer.MachineID { + localIP = ctr.Container.UncloudNetworkIP().String() + break + } + } + require.NotEmpty(t, localIP, "Should find local container IP on query machine %s", queryContainer.MachineID) + + // Extracts the first IP from nslookup output after the "Name:" line. + re := regexp.MustCompile(`(?m)Name:\s+[\w\.\-]+\s+Address:\s+([\d\.]+)`) + + // The default mode randomizes IP order, so run multiple times to reduce the chance of passing by coincidence. + for range 5 { + dnsOutput := runNslookup(t, "nearest."+serviceName+".internal") + t.Logf("Nearest mode DNS query output:\n%s", dnsOutput) + + matches := re.FindStringSubmatch(dnsOutput) + require.Len(t, matches, 2, "Should find Name's Address in DNS output") + firstIP := matches[1] + assert.Equal(t, localIP, firstIP, + "Nearest mode should return local subnet IP first (query machine: %s, local IP: %s, first DNS result: %s)", + queryContainer.MachineID, localIP, firstIP) + + assertNoDNSErrors(t, dnsOutput) + } + }) + }) }