From 2f55af0cabef2d4c2b5380a03d94dcc1495a58f3 Mon Sep 17 00:00:00 2001 From: Anton Ovchinnikov Date: Mon, 9 Jun 2025 22:10:53 +0200 Subject: [PATCH] test: Add an e2e test for build&push (#69) * test: Add an e2e test for build&push * test: Add image cleanup * test: Switch to assert.NoError --- .gitignore | 1 + cmd/uncloud/deploy.go | 7 +- internal/ucind/cluster.go | 6 +- internal/ucind/machine.go | 12 ++ test/e2e/compose_build_test.go | 163 ++++++++++++++++++ test/e2e/compose_deploy_test.go | 2 +- .../busybox-first/Dockerfile | 3 - .../busybox-second/Dockerfile.alt | 3 - .../fixtures/compose-build-basic/compose.yaml | 12 +- .../service-first-dir/Dockerfile | 3 + .../service-second-dir/Dockerfile.alt | 3 + 11 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 test/e2e/compose_build_test.go delete mode 100644 test/e2e/fixtures/compose-build-basic/busybox-first/Dockerfile delete mode 100644 test/e2e/fixtures/compose-build-basic/busybox-second/Dockerfile.alt create mode 100644 test/e2e/fixtures/compose-build-basic/service-first-dir/Dockerfile create mode 100644 test/e2e/fixtures/compose-build-basic/service-second-dir/Dockerfile.alt diff --git a/.gitignore b/.gitignore index 41b49dde..c813b569 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ go.work.sum /data/ /dist/ /uc +/tmp # Web node_modules/ diff --git a/cmd/uncloud/deploy.go b/cmd/uncloud/deploy.go index fb4264ba..4cc40e2b 100644 --- a/cmd/uncloud/deploy.go +++ b/cmd/uncloud/deploy.go @@ -92,11 +92,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { fmt.Println("Not building services as requested.") } else { buildOpts := cli.BuildOptions{ - Files: opts.files, - Profiles: opts.profiles, - Services: opts.services, - Push: true, - NoCache: false, + Push: true, + NoCache: false, } if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil { diff --git a/internal/ucind/cluster.go b/internal/ucind/cluster.go index 1e7ed20f..ad890117 100644 --- a/internal/ucind/cluster.go +++ b/internal/ucind/cluster.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" dockerclient "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" "github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/cluster" @@ -30,6 +31,8 @@ type Cluster struct { type CreateClusterOptions struct { Machines int + // Ports to forward from the cluster machines to the host. + PortMap nat.PortMap } func (c *Cluster) PopulateMachineIDs(ctx context.Context) error { @@ -81,7 +84,8 @@ func (p *Provisioner) CreateCluster(ctx context.Context, name string, opts Creat // Create machines (containers) in the created cluster network. for i := 1; i < opts.Machines+1; i++ { mopts := CreateMachineOptions{ - Name: fmt.Sprintf("machine-%d", i), + Name: fmt.Sprintf("machine-%d", i), + PortMap: opts.PortMap, } m, err := p.CreateMachine(ctx, name, mopts) if err != nil { diff --git a/internal/ucind/machine.go b/internal/ucind/machine.go index c98fac25..7685195b 100644 --- a/internal/ucind/machine.go +++ b/internal/ucind/machine.go @@ -42,6 +42,8 @@ func (m *Machine) Connect(ctx context.Context) (*client.Client, error) { type CreateMachineOptions struct { Name string Image string + // Ports to forward from the machine to the host. + PortMap nat.PortMap } func (p *Provisioner) CreateMachine(ctx context.Context, clusterName string, opts CreateMachineOptions) (Machine, error) { @@ -88,6 +90,16 @@ func (p *Provisioner) CreateMachine(ctx context.Context, clusterName string, opt Name: container.RestartPolicyAlways, }, } + // Forward ports, if requested + if opts.PortMap != nil { + for port, bindings := range opts.PortMap { + if len(bindings) == 0 { + continue + } + config.ExposedPorts[port] = struct{}{} + hostConfig.PortBindings[port] = bindings + } + } if _, err := p.createContainerWithImagePull(ctx, containerName, config, hostConfig); err != nil { return m, err diff --git a/test/e2e/compose_build_test.go b/test/e2e/compose_build_test.go new file mode 100644 index 00000000..78cb043d --- /dev/null +++ b/test/e2e/compose_build_test.go @@ -0,0 +1,163 @@ +package e2e + +import ( + "context" + "fmt" + "path" + "strconv" + "testing" + + composecli "github.com/compose-spec/compose-go/v2/cli" + "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/docker/api/types/image" + dockerclient "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/psviderski/uncloud/internal/cli" + cliInternal "github.com/psviderski/uncloud/internal/cli" + "github.com/psviderski/uncloud/internal/ucind" + "github.com/psviderski/uncloud/pkg/api" + "github.com/psviderski/uncloud/pkg/client/compose" + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func TestComposeBuild(t *testing.T) { + t.Parallel() + + clusterName := "ucind-test.compose-build" + ctx := context.Background() + + registryInternalPort := "5000/tcp" + clusterOpts := ucind.CreateClusterOptions{ + Machines: 1, + PortMap: nat.PortMap{ + // Forwarding the registry port to the host. + nat.Port(registryInternalPort): []nat.PortBinding{ + { + HostIP: "127.0.0.1", + // Host port is a random available port. + }, + }, + }, + } + c, _ := createTestCluster(t, clusterName, clusterOpts, true) + + machine := c.Machines[0] + client, err := machine.Connect(ctx) + require.NoError(t, err) + + // Get the (random) assigned host port for the future registry service + dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) + require.NoError(t, err) + containerInfo, err := dockerCli.ContainerInspect(ctx, machine.ContainerName) + require.NoError(t, err) + portMap := containerInfo.NetworkSettings.Ports + registryHostPortStr := portMap[nat.Port(registryInternalPort)][0].HostPort + registryHostPort, err := strconv.Atoi(registryHostPortStr) + require.NoError(t, err) + assert.True(t, registryHostPort > 0, "Expected host port to be a valid number") + + t.Run("build and push images for a basic project", func(t *testing.T) { + t.Parallel() + + registryServiceName := "registry" + t.Cleanup(func() { + removeServices(t, client, registryServiceName) + }) + + // Start registry + resp, err := client.RunService(ctx, api.ServiceSpec{ + Name: registryServiceName, + Mode: api.ServiceModeReplicated, + Container: api.ContainerSpec{ + Image: "registry:2", + }, + Ports: []api.PortSpec{ + { + ContainerPort: 5000, + PublishedPort: 5000, + Mode: api.PortModeHost, + Protocol: api.ProtocolTCP, + }, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, resp.ID) + assert.Equal(t, "registry", resp.Name) + + project, err := compose.LoadProject( + ctx, + []string{"fixtures/compose-build-basic/compose.yaml"}, + composecli.WithEnv([]string{ + fmt.Sprintf("TEST_REGISTRY_PORT=%s", registryHostPortStr), + }), + ) + require.NoError(t, err) + servicesToBuild := cliInternal.GetServicesThatNeedBuild(project) + serviceImage1 := fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort) + serviceImage2 := fmt.Sprintf("127.0.0.1:%d/service-second:version2", registryHostPort) + t.Cleanup(func() { + // Remove the images after the test + removeOptions := image.RemoveOptions{Force: true, PruneChildren: true} + _, err := dockerCli.ImageRemove(ctx, serviceImage1, removeOptions) + assert.NoErrorf(t, err, "failed to remove image %s on test cleanup: %w", serviceImage1, err) + _, err = dockerCli.ImageRemove(ctx, serviceImage2, removeOptions) + assert.NoErrorf(t, err, "failed to remove image %s on test cleanup: %w", serviceImage2, err) + }) + + servicesToBuildExpected := map[string]types.ServiceConfig{ + "service-first": { + Name: "service-first", + Build: &types.BuildConfig{ + Context: path.Join(project.WorkingDir, "service-first-dir"), + Dockerfile: "Dockerfile", + }, + Image: serviceImage1, + Environment: types.NewMappingWithEquals([]string{}), + Networks: map[string]*types.ServiceNetworkConfig{ + "default": nil, + }, + }, + "service-second": { + Name: "service-second", + Build: &types.BuildConfig{ + Context: path.Join(project.WorkingDir, "service-second-dir"), + Dockerfile: "Dockerfile.alt", + }, + Image: serviceImage2, + Environment: types.NewMappingWithEquals([]string{}), + Networks: map[string]*types.ServiceNetworkConfig{ + "default": nil, + }, + }, + } + assert.Equal(t, servicesToBuildExpected, servicesToBuild) + + // Build and push the images + buildOpts := cli.BuildOptions{ + Push: true, + NoCache: false, + } + cli.BuildServices(context.Background(), servicesToBuild, buildOpts) + + // Check the image of the first service + ref1, err := name.NewRepository(fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort)) + require.NoError(t, err) + tags, err := remote.List(ref1) + require.NoError(t, err) + + assert.Equal(t, tags, []string{"latest"}, "Tags for service service-first do not match") + + // Check the image of the second service + ref2, err := name.NewRepository(fmt.Sprintf("127.0.0.1:%d/service-second", registryHostPort)) + require.NoError(t, err) + tags, err = remote.List(ref2) + require.NoError(t, err) + + assert.Equal(t, tags, []string{"version2"}, "Tags for service service-second do not match") + + }) +} diff --git a/test/e2e/compose_deploy_test.go b/test/e2e/compose_deploy_test.go index 27b89d69..ac203c83 100644 --- a/test/e2e/compose_deploy_test.go +++ b/test/e2e/compose_deploy_test.go @@ -16,7 +16,7 @@ import ( func TestComposeDeployment(t *testing.T) { t.Parallel() - clusterName := "ucind-test.compose" + clusterName := "ucind-test.compose-deploy" ctx := context.Background() c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true) diff --git a/test/e2e/fixtures/compose-build-basic/busybox-first/Dockerfile b/test/e2e/fixtures/compose-build-basic/busybox-first/Dockerfile deleted file mode 100644 index 6044ff34..00000000 --- a/test/e2e/fixtures/compose-build-basic/busybox-first/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM busybox:1.37.0-musl - -ENV SERVICE_NAME=busybox-first diff --git a/test/e2e/fixtures/compose-build-basic/busybox-second/Dockerfile.alt b/test/e2e/fixtures/compose-build-basic/busybox-second/Dockerfile.alt deleted file mode 100644 index ffed44cf..00000000 --- a/test/e2e/fixtures/compose-build-basic/busybox-second/Dockerfile.alt +++ /dev/null @@ -1,3 +0,0 @@ -FROM busybox:1.37.0-musl - -ENV SERVICE_NAME=busybox-second diff --git a/test/e2e/fixtures/compose-build-basic/compose.yaml b/test/e2e/fixtures/compose-build-basic/compose.yaml index db03f2b5..b99dd7c4 100644 --- a/test/e2e/fixtures/compose-build-basic/compose.yaml +++ b/test/e2e/fixtures/compose-build-basic/compose.yaml @@ -2,13 +2,13 @@ services: service-no-build: image: portainer/pause:3.9 - busybox-first: - image: localhost:5000/busybox-first + service-first: + image: 127.0.0.1:${TEST_REGISTRY_PORT}/service-first build: - context: busybox-first/ + context: service-first-dir/ - busybox-second: - image: localhost:5000/busybox-second + service-second: + image: 127.0.0.1:${TEST_REGISTRY_PORT}/service-second:version2 build: - context: busybox-second/ + context: service-second-dir/ dockerfile: Dockerfile.alt diff --git a/test/e2e/fixtures/compose-build-basic/service-first-dir/Dockerfile b/test/e2e/fixtures/compose-build-basic/service-first-dir/Dockerfile new file mode 100644 index 00000000..9dae1148 --- /dev/null +++ b/test/e2e/fixtures/compose-build-basic/service-first-dir/Dockerfile @@ -0,0 +1,3 @@ +FROM portainer/pause:3.9 + +ENV SERVICE_NAME=service-first diff --git a/test/e2e/fixtures/compose-build-basic/service-second-dir/Dockerfile.alt b/test/e2e/fixtures/compose-build-basic/service-second-dir/Dockerfile.alt new file mode 100644 index 00000000..ea5543a9 --- /dev/null +++ b/test/e2e/fixtures/compose-build-basic/service-second-dir/Dockerfile.alt @@ -0,0 +1,3 @@ +FROM portainer/pause:3.9 + +ENV SERVICE_NAME=service-second