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
This commit is contained in:
Anton Ovchinnikov
2025-06-09 22:10:53 +02:00
committed by GitHub
parent 971b21d6c1
commit 2f55af0cab
11 changed files with 196 additions and 19 deletions
+1
View File
@@ -30,6 +30,7 @@ go.work.sum
/data/ /data/
/dist/ /dist/
/uc /uc
/tmp
# Web # Web
node_modules/ node_modules/
+2 -5
View File
@@ -92,11 +92,8 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
fmt.Println("Not building services as requested.") fmt.Println("Not building services as requested.")
} else { } else {
buildOpts := cli.BuildOptions{ buildOpts := cli.BuildOptions{
Files: opts.files, Push: true,
Profiles: opts.profiles, NoCache: false,
Services: opts.services,
Push: true,
NoCache: false,
} }
if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil { if err := cli.BuildServices(ctx, servicesToBuild, buildOpts); err != nil {
+5 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
@@ -30,6 +31,8 @@ type Cluster struct {
type CreateClusterOptions struct { type CreateClusterOptions struct {
Machines int Machines int
// Ports to forward from the cluster machines to the host.
PortMap nat.PortMap
} }
func (c *Cluster) PopulateMachineIDs(ctx context.Context) error { 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. // Create machines (containers) in the created cluster network.
for i := 1; i < opts.Machines+1; i++ { for i := 1; i < opts.Machines+1; i++ {
mopts := CreateMachineOptions{ mopts := CreateMachineOptions{
Name: fmt.Sprintf("machine-%d", i), Name: fmt.Sprintf("machine-%d", i),
PortMap: opts.PortMap,
} }
m, err := p.CreateMachine(ctx, name, mopts) m, err := p.CreateMachine(ctx, name, mopts)
if err != nil { if err != nil {
+12
View File
@@ -42,6 +42,8 @@ func (m *Machine) Connect(ctx context.Context) (*client.Client, error) {
type CreateMachineOptions struct { type CreateMachineOptions struct {
Name string Name string
Image 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) { 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, 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 { if _, err := p.createContainerWithImagePull(ctx, containerName, config, hostConfig); err != nil {
return m, err return m, err
+163
View File
@@ -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")
})
}
+1 -1
View File
@@ -16,7 +16,7 @@ import (
func TestComposeDeployment(t *testing.T) { func TestComposeDeployment(t *testing.T) {
t.Parallel() t.Parallel()
clusterName := "ucind-test.compose" clusterName := "ucind-test.compose-deploy"
ctx := context.Background() ctx := context.Background()
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true) c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true)
@@ -1,3 +0,0 @@
FROM busybox:1.37.0-musl
ENV SERVICE_NAME=busybox-first
@@ -1,3 +0,0 @@
FROM busybox:1.37.0-musl
ENV SERVICE_NAME=busybox-second
@@ -2,13 +2,13 @@ services:
service-no-build: service-no-build:
image: portainer/pause:3.9 image: portainer/pause:3.9
busybox-first: service-first:
image: localhost:5000/busybox-first image: 127.0.0.1:${TEST_REGISTRY_PORT}/service-first
build: build:
context: busybox-first/ context: service-first-dir/
busybox-second: service-second:
image: localhost:5000/busybox-second image: 127.0.0.1:${TEST_REGISTRY_PORT}/service-second:version2
build: build:
context: busybox-second/ context: service-second-dir/
dockerfile: Dockerfile.alt dockerfile: Dockerfile.alt
@@ -0,0 +1,3 @@
FROM portainer/pause:3.9
ENV SERVICE_NAME=service-first
@@ -0,0 +1,3 @@
FROM portainer/pause:3.9
ENV SERVICE_NAME=service-second