chore: combine some e2e tests to reduce the number of ucind clusters hence used system resources

This commit is contained in:
Pasha Sviderski
2026-03-09 19:07:00 +10:00
parent a67f936cc1
commit d5687cd713
3 changed files with 211 additions and 290 deletions
-215
View File
@@ -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 <machine-id>.m.<service-name>.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)
}
})
}
+61 -75
View File
@@ -12,10 +12,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMachineRename(t *testing.T) { func TestMachineOperations(t *testing.T) {
t.Parallel() t.Parallel()
name := "ucind-test.machine-rename" name := "ucind-test.machine-ops"
ctx := context.Background() ctx := context.Background()
c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true) c, _ := createTestCluster(t, name, ucind.CreateClusterOptions{Machines: 3}, true)
@@ -23,59 +23,61 @@ func TestMachineRename(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer cli.Close() defer cli.Close()
// RenameMachine subtests.
t.Run("rename machine by name", func(t *testing.T) { t.Run("rename machine by name", func(t *testing.T) {
// Get initial machine state // Get initial machine state.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Select the second machine to rename // Select the second machine to rename.
originalMachine := machines[1] originalMachine := machines[1]
originalName := originalMachine.Machine.Name originalName := originalMachine.Machine.Name
newName := "renamed-machine-1" newName := "renamed-machine-1"
// Rename the machine // Rename the machine.
updatedMachine, err := cli.RenameMachine(ctx, originalName, newName) updatedMachine, err := cli.RenameMachine(ctx, originalName, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, originalMachine.Machine.Id, updatedMachine.Id) 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) machines, err = cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Find the renamed machine // Find the renamed machine.
var found bool var found bool
for _, m := range machines { for _, m := range machines {
if m.Machine.Id == originalMachine.Machine.Id { if m.Machine.Id == originalMachine.Machine.Id {
assert.Equal(t, newName, m.Machine.Name) assert.Equal(t, newName, m.Machine.Name)
found = true found = true
} else { } else {
// Ensure other machines are unaffected // Ensure other machines are unaffected.
assert.NotEqual(t, newName, m.Machine.Name) assert.NotEqual(t, newName, m.Machine.Name)
} }
} }
assert.True(t, found, "Renamed machine should be in the list") 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) inspectedMachine, err := cli.InspectMachine(ctx, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspectedMachine.Machine.Name) assert.Equal(t, newName, inspectedMachine.Machine.Name)
assert.Equal(t, originalMachine.Machine.Id, inspectedMachine.Machine.Id) 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) _, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("rename machine by ID", func(t *testing.T) { t.Run("rename machine by ID", func(t *testing.T) {
// Get the third machine // Get the third machine.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) 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 var targetMachine *pb.MachineMember
for _, m := range machines { for _, m := range machines {
if m.Machine.Name != "renamed-machine-1" { if m.Machine.Name != "renamed-machine-1" {
@@ -89,67 +91,67 @@ func TestMachineRename(t *testing.T) {
machineID := targetMachine.Machine.Id machineID := targetMachine.Machine.Id
newName := "renamed-machine-2" newName := "renamed-machine-2"
// Rename using ID instead of name // Rename using ID instead of name.
updatedMachine, err := cli.RenameMachine(ctx, machineID, newName) updatedMachine, err := cli.RenameMachine(ctx, machineID, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, machineID, updatedMachine.Id) assert.Equal(t, machineID, updatedMachine.Id)
// Verify the rename was successful // Verify the rename was successful.
inspectedMachine, err := cli.InspectMachine(ctx, newName) inspectedMachine, err := cli.InspectMachine(ctx, newName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspectedMachine.Machine.Name) assert.Equal(t, newName, inspectedMachine.Machine.Name)
assert.Equal(t, machineID, inspectedMachine.Machine.Id) 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) _, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("rename non-existent machine", func(t *testing.T) { 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") _, err := cli.RenameMachine(ctx, "non-existent-machine", "new-name")
assert.ErrorIs(t, err, api.ErrNotFound) 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") _, err = cli.RenameMachine(ctx, "non-existent-id-12345", "new-name")
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("rename to existing name", func(t *testing.T) { t.Run("rename to existing name", func(t *testing.T) {
// Get current machines // Get current machines.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) 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 machine0Name := machines[0].Machine.Name
machine1Name := machines[1].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) _, err = cli.RenameMachine(ctx, machine0Name, machine1Name)
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("rename with empty name", func(t *testing.T) { 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) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
machineName := machines[0].Machine.Name machineName := machines[0].Machine.Name
// Try to rename with empty string // Try to rename with empty string.
_, err = cli.RenameMachine(ctx, machineName, "") _, err = cli.RenameMachine(ctx, machineName, "")
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("service continuity after rename", func(t *testing.T) { 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) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) 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 var targetMachine *pb.MachineMember
for _, m := range machines { for _, m := range machines {
if m.Machine.Name != "renamed-machine-1" && m.Machine.Name != "renamed-machine-2" { 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 originalMachineName := targetMachine.Machine.Name
serviceName := "test-service-rename-continuity" serviceName := "test-service-rename-continuity"
// Create a service on the specific machine // Create a service on the specific machine.
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Name: serviceName, Name: serviceName,
Mode: api.ServiceModeGlobal, 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) svc, err := cli.InspectService(ctx, serviceName)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, svc.Containers, 1) assert.Len(t, svc.Containers, 1)
assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID) assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID)
// Rename the machine // Rename the machine.
newMachineName := "renamed-for-service-test" newMachineName := "renamed-for-service-test"
_, err = cli.RenameMachine(ctx, originalMachineName, newMachineName) _, err = cli.RenameMachine(ctx, originalMachineName, newMachineName)
require.NoError(t, err) 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) svc, err = cli.InspectService(ctx, serviceName)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, svc.Containers, 1) assert.Len(t, svc.Containers, 1)
assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID) assert.Equal(t, targetMachine.Machine.Id, svc.Containers[0].MachineID)
// The service spec's placement still references the old name, // 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) { // UpdateMachine subtests.
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()
t.Run("update machine name", func(t *testing.T) { t.Run("update machine name", func(t *testing.T) {
// Get initial machine state // Get initial machine state.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
// Select a machine to update // Select a machine to update.
targetMachine := machines[1] targetMachine := machines[1]
originalName := targetMachine.Machine.Name originalName := targetMachine.Machine.Name
newName := "updated-machine-name" newName := "updated-machine-name"
// Update the machine name using UpdateMachine directly // Update the machine name using UpdateMachine directly.
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, MachineId: targetMachine.Machine.Id,
Name: &newName, Name: &newName,
@@ -238,22 +230,22 @@ func TestUpdateMachine(t *testing.T) {
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, targetMachine.Machine.Id, updatedMachine.Id) assert.Equal(t, targetMachine.Machine.Id, updatedMachine.Id)
// Verify the change persisted // Verify the change persisted.
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspected.Machine.Name) assert.Equal(t, newName, inspected.Machine.Name)
// Verify old name no longer works // Verify old name no longer works.
_, err = cli.InspectMachine(ctx, originalName) _, err = cli.InspectMachine(ctx, originalName)
assert.ErrorIs(t, err, api.ErrNotFound) assert.ErrorIs(t, err, api.ErrNotFound)
}) })
t.Run("update machine public IP", func(t *testing.T) { 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) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
// Find a machine that hasn't been renamed // Find a machine that hasn't been renamed.
var targetMachine *pb.MachineMember var targetMachine *pb.MachineMember
for _, m := range machines { for _, m := range machines {
if m.Machine.Name != "updated-machine-name" { if m.Machine.Name != "updated-machine-name" {
@@ -263,12 +255,12 @@ func TestUpdateMachine(t *testing.T) {
} }
require.NotNil(t, targetMachine) 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{ newPublicIP := &pb.IP{
Ip: []byte{8, 8, 8, 8}, Ip: []byte{8, 8, 8, 8},
} }
// Update the public IP // Update the public IP.
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, MachineId: targetMachine.Machine.Id,
PublicIp: newPublicIP, PublicIp: newPublicIP,
@@ -277,31 +269,31 @@ func TestUpdateMachine(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip) assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip)
// Verify the change persisted // Verify the change persisted.
inspected, err := cli.InspectMachine(ctx, targetMachine.Machine.Id) inspected, err := cli.InspectMachine(ctx, targetMachine.Machine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newPublicIP.Ip, inspected.Machine.PublicIp.Ip) assert.Equal(t, newPublicIP.Ip, inspected.Machine.PublicIp.Ip)
}) })
t.Run("remove machine public IP", func(t *testing.T) { t.Run("remove machine public IP", func(t *testing.T) {
// Get machines // Get machines.
machines, err := cli.ListMachines(ctx, nil) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.True(t, len(machines) > 0, "Need at least one machine") 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] targetMachine := machines[0]
setIPReq := &pb.UpdateMachineRequest{ setIPReq := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, 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) updatedMachine, err := cli.UpdateMachine(ctx, setIPReq)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, updatedMachine.PublicIp) 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{} emptyIP := &pb.IP{}
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: updatedMachine.Id, MachineId: updatedMachine.Id,
@@ -311,23 +303,17 @@ func TestUpdateMachine(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, removedIPMachine.PublicIp) assert.Nil(t, removedIPMachine.PublicIp)
// Verify the change persisted // Verify the change persisted.
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Nil(t, inspected.Machine.PublicIp) assert.Nil(t, inspected.Machine.PublicIp)
}) })
t.Run("update machine endpoints", func(t *testing.T) { 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) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
targetMachine := machines[0]
var targetMachine *pb.MachineMember
for _, m := range machines {
targetMachine = m
break
}
require.NotNil(t, targetMachine)
newEndpoints := []*pb.IPPort{ newEndpoints := []*pb.IPPort{
{ {
@@ -348,13 +334,13 @@ func TestUpdateMachine(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, len(newEndpoints), len(updatedMachine.Network.Endpoints)) assert.Equal(t, len(newEndpoints), len(updatedMachine.Network.Endpoints))
// Verify endpoints were updated // Verify endpoints were updated.
for i, endpoint := range updatedMachine.Network.Endpoints { for i, endpoint := range updatedMachine.Network.Endpoints {
assert.Equal(t, newEndpoints[i].Ip.Ip, endpoint.Ip.Ip) assert.Equal(t, newEndpoints[i].Ip.Ip, endpoint.Ip.Ip)
assert.Equal(t, newEndpoints[i].Port, endpoint.Port) 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.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.Subnet.Bits, updatedMachine.Network.Subnet.Bits)
assert.Equal(t, targetMachine.Machine.Network.ManagementIp.Ip, updatedMachine.Network.ManagementIp.Ip) 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) { 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) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
@@ -375,7 +361,7 @@ func TestUpdateMachine(t *testing.T) {
} }
require.NotNil(t, targetMachine) require.NotNil(t, targetMachine)
// Update both name and public IP // Update both name and public IP.
newName := "multi-update-machine" newName := "multi-update-machine"
newPublicIP := &pb.IP{ newPublicIP := &pb.IP{
Ip: []byte{1, 1, 1, 1}, Ip: []byte{1, 1, 1, 1},
@@ -391,7 +377,7 @@ func TestUpdateMachine(t *testing.T) {
assert.Equal(t, newName, updatedMachine.Name) assert.Equal(t, newName, updatedMachine.Name)
assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip) assert.Equal(t, newPublicIP.Ip, updatedMachine.PublicIp.Ip)
// Verify both changes persisted // Verify both changes persisted.
inspected, err := cli.InspectMachine(ctx, updatedMachine.Id) inspected, err := cli.InspectMachine(ctx, updatedMachine.Id)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newName, inspected.Machine.Name) 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) { 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" nonExistentName := "should-be-updated"
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: "non-existent-machine-id", MachineId: "non-existent-machine-id",
@@ -410,7 +396,7 @@ func TestUpdateMachine(t *testing.T) {
}) })
t.Run("update to duplicate name", func(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) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, machines, 3) require.Len(t, machines, 3)
@@ -418,7 +404,7 @@ func TestUpdateMachine(t *testing.T) {
machine1 := machines[0] machine1 := machines[0]
machine2 := machines[1] machine2 := machines[1]
// Try to update machine2 with machine1's name // Try to update machine2 with machine1's name.
req := &pb.UpdateMachineRequest{ req := &pb.UpdateMachineRequest{
MachineId: machine2.Machine.Id, MachineId: machine2.Machine.Id,
Name: &machine1.Machine.Name, Name: &machine1.Machine.Name,
@@ -428,20 +414,20 @@ func TestUpdateMachine(t *testing.T) {
}) })
t.Run("update with empty request", func(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) machines, err := cli.ListMachines(ctx, nil)
require.NoError(t, err) require.NoError(t, err)
targetMachine := machines[0] 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{ req := &pb.UpdateMachineRequest{
MachineId: targetMachine.Machine.Id, MachineId: targetMachine.Machine.Id,
} }
updatedMachine, err := cli.UpdateMachine(ctx, req) updatedMachine, err := cli.UpdateMachine(ctx, req)
require.NoError(t, err) require.NoError(t, err)
// Machine should remain unchanged // Machine should remain unchanged.
assert.Equal(t, targetMachine.Machine.Name, updatedMachine.Name) assert.Equal(t, targetMachine.Machine.Name, updatedMachine.Name)
if targetMachine.Machine.PublicIp != nil && updatedMachine.PublicIp != nil { if targetMachine.Machine.PublicIp != nil && updatedMachine.PublicIp != nil {
assert.Equal(t, targetMachine.Machine.PublicIp.Ip, updatedMachine.PublicIp.Ip) assert.Equal(t, targetMachine.Machine.PublicIp.Ip, updatedMachine.PublicIp.Ip)
+150
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"regexp"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -2022,4 +2023,153 @@ func TestServiceLifecycle(t *testing.T) {
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "machines not found") 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)
}
})
})
} }