ref: Rewrite config tests with Exec

This commit is contained in:
Anton Ovchinnikov
2025-11-09 17:37:07 +01:00
parent b0cd752619
commit 7bff706aa0
3 changed files with 71 additions and 75 deletions
+7 -3
View File
@@ -51,7 +51,7 @@ func TestComposeConfigs(t *testing.T) {
Name: name, Name: name,
Mode: api.ServiceModeReplicated, Mode: api.ServiceModeReplicated,
Container: api.ContainerSpec{ Container: api.ContainerSpec{
Command: []string{"true"}, Command: []string{"sleep", "600"},
Image: "busybox:1.37.0-uclibc", Image: "busybox:1.37.0-uclibc",
ConfigMounts: []api.ConfigMount{ ConfigMounts: []api.ConfigMount{
{ {
@@ -93,18 +93,22 @@ func TestComposeConfigs(t *testing.T) {
// Verify the config files are actually created in the container and contain expected content // Verify the config files are actually created in the container and contain expected content
containerName := svc.Containers[0].Container.Name containerName := svc.Containers[0].Container.Name
configContentFirst, err := readFileInfoInContainer(t, &machine, containerName, "/etc/config-from-file.conf") configContentFirst, err := readFileInfoInContainer(t, cli, name, containerName, "/etc/config-from-file.conf")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, fileInfo{ assert.Equal(t, fileInfo{
permissions: 0o644, permissions: 0o644,
content: "this is file config\n", content: "this is file config\n",
userId: 0,
groupId: 0,
}, configContentFirst) }, configContentFirst)
configContentSecond, err := readFileInfoInContainer(t, &machine, containerName, "/etc/config-inline.conf") configContentSecond, err := readFileInfoInContainer(t, cli, name, containerName, "/etc/config-inline.conf")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, fileInfo{ assert.Equal(t, fileInfo{
permissions: 0o600, permissions: 0o600,
content: "this is inline config\n", content: "this is inline config\n",
userId: 1000,
groupId: 1000,
}, configContentSecond) }, configContentSecond)
}) })
} }
+1 -1
View File
@@ -1,7 +1,7 @@
services: services:
web: web:
image: busybox:1.37.0-uclibc image: busybox:1.37.0-uclibc
command: ["true"] command: ["sleep", "600"]
configs: configs:
- source: from-file - source: from-file
target: /etc/config-from-file.conf target: /etc/config-from-file.conf
+63 -71
View File
@@ -1,106 +1,98 @@
package e2e package e2e
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strconv"
"strings" "strings"
"testing" "testing"
"github.com/docker/docker/api/types/container" "github.com/psviderski/uncloud/pkg/api"
dockerclient "github.com/docker/docker/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/psviderski/uncloud/internal/ucind"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type fileInfo struct { type fileInfo struct {
permissions os.FileMode permissions os.FileMode
content string content string
userId int
groupId int
} }
// a helper function that takes a machine, container name inside it and file path, and returns the file contents along with metadata // execInContainerAndReadOutput is a helper that executes a command in a container and returns stdout output.
// // It validates the exit code is 0 and that there's no stderr output.
// Uncloud API does not currently expose exec functionality to run commands inside containers. func execInContainerAndReadOutput(
// Instead this helper function uses "docker cp" inside the ucind container to copy the file from the target container t *testing.T,
// to a temporary location (also inside the ucind container), and then inspect its content and permissions. ctx context.Context,
// TODO: update when exec functionality is implemented cli *client.Client,
func readFileInfoInContainer(t *testing.T, machine *ucind.Machine, containerName, filePath string) (fileInfo, error) { serviceNameOrID, containerNameOrID string,
command []string,
) (string, error) {
t.Helper()
var stdout, stderr bytes.Buffer
execOpts := api.ExecOptions{
Command: command,
AttachStdout: true,
AttachStderr: true,
Stdout: &stdout,
Stderr: &stderr,
}
commandName := command[0]
exitCode, err := cli.ExecContainer(ctx, serviceNameOrID, containerNameOrID, execOpts)
if err != nil {
return "", fmt.Errorf("exec %s: %w", commandName, err)
}
assert.Equal(t, 0, exitCode, "Expected exit code 0 from %s command", commandName)
if stderr.Len() > 0 {
return "", fmt.Errorf("%s stderr output: %s", commandName, stderr.String())
}
return stdout.String(), nil
}
// readFileInfoInContainer reads file information from a container using the Uncloud client ExecContainer API.
// Information read: permissions, uid, gid, content.
func readFileInfoInContainer(t *testing.T, cli *client.Client, serviceNameOrID, containerNameOrID, filePath string) (fileInfo, error) {
t.Helper() t.Helper()
ctx := context.Background() ctx := context.Background()
dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) // Get file permissions
if err != nil { permOutput, err := execInContainerAndReadOutput(t, ctx, cli, serviceNameOrID, containerNameOrID,
return fileInfo{}, err []string{"stat", "-c", "%a %u %g", filePath})
}
defer dockerCli.Close()
machineContainerName := fmt.Sprintf("%s-%s", machine.ClusterName, machine.Name)
// 1. Create an exec instance
fileLocation := fmt.Sprintf("%s:%s", containerName, filePath)
tmpFileLocation := fmt.Sprintf("/tmp/%s-%s", containerName, filepath.Base(filePath))
cmdDocker := fmt.Sprintf("docker cp %s %s", fileLocation, tmpFileLocation)
cmdPermissions := fmt.Sprintf("stat -c %%a %s", tmpFileLocation)
cmdCat := fmt.Sprintf("cat %s", tmpFileLocation)
cmdCombined := fmt.Sprintf("%s; %s; %s", cmdDocker, cmdPermissions, cmdCat)
execConfig := container.ExecOptions{
Cmd: []string{"sh", "-ec", cmdCombined},
AttachStdout: true,
AttachStderr: true,
}
resp, err := dockerCli.ContainerExecCreate(ctx, machineContainerName, execConfig)
if err != nil { if err != nil {
return fileInfo{}, err return fileInfo{}, err
} }
// 2. Attach to the exec session // Parse permissions
hijackResp, err := dockerCli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{}) permissions := strings.TrimSpace(permOutput)
fmt.Printf("Permissions output: %s\n", permissions)
// Parse three numbers: permissions, uid, gid
var permissionsOctal, uid, gid int
_, err = fmt.Sscanf(permissions, "%o %d %d", &permissionsOctal, &uid, &gid)
if err != nil { if err != nil {
return fileInfo{}, err return fileInfo{}, fmt.Errorf("parse stat output: %w", err)
}
defer hijackResp.Close()
// 3. Inspect result
inspectResp, err := dockerCli.ContainerExecInspect(ctx, resp.ID)
if err != nil {
return fileInfo{}, err
} }
assert.Equal(t, 0, inspectResp.ExitCode, "Expected exit code 0 from exec command")
// 4. Read output to string using stdcopy to demultiplex the Docker stream
var stdout, stderr strings.Builder
_, err = stdcopy.StdCopy(&stdout, &stderr, hijackResp.Reader)
if err != nil {
return fileInfo{}, err
}
if stderr.Len() > 0 {
return fileInfo{}, fmt.Errorf("stderr output: %s", stderr.String())
}
// 5. Parse output: permissions, content
outputLines := strings.SplitN(stdout.String(), "\n", 2)
if len(outputLines) < 2 {
return fileInfo{}, fmt.Errorf("unexpected output format, expected at least 2 lines, got %d", len(outputLines))
}
permissions := outputLines[0]
// convert to octal number
permissionsOctal, err := strconv.ParseInt(permissions, 8, 32)
if err != nil {
return fileInfo{}, fmt.Errorf("parse file permissions: %w", err)
}
mode := os.FileMode(permissionsOctal) mode := os.FileMode(permissionsOctal)
fileContent := outputLines[1] // Get file content
fileContent, err := execInContainerAndReadOutput(t, ctx, cli, serviceNameOrID, containerNameOrID,
[]string{"cat", filePath})
if err != nil {
return fileInfo{}, err
}
return fileInfo{ return fileInfo{
permissions: mode, permissions: mode,
userId: uid,
groupId: gid,
content: fileContent, content: fileContent,
}, nil }, nil
} }