From 7bff706aa0c85ae23a5fce3ed8e6c4ef55e75447 Mon Sep 17 00:00:00 2001 From: Anton Ovchinnikov Date: Sun, 9 Nov 2025 17:37:07 +0100 Subject: [PATCH] ref: Rewrite config tests with Exec --- test/e2e/compose_configs_test.go | 10 +- test/e2e/fixtures/compose-configs.yaml | 2 +- test/e2e/helpers.go | 134 ++++++++++++------------- 3 files changed, 71 insertions(+), 75 deletions(-) diff --git a/test/e2e/compose_configs_test.go b/test/e2e/compose_configs_test.go index dffc167a..76456219 100644 --- a/test/e2e/compose_configs_test.go +++ b/test/e2e/compose_configs_test.go @@ -51,7 +51,7 @@ func TestComposeConfigs(t *testing.T) { Name: name, Mode: api.ServiceModeReplicated, Container: api.ContainerSpec{ - Command: []string{"true"}, + Command: []string{"sleep", "600"}, Image: "busybox:1.37.0-uclibc", 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 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) assert.Equal(t, fileInfo{ permissions: 0o644, content: "this is file config\n", + userId: 0, + groupId: 0, }, 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) assert.Equal(t, fileInfo{ permissions: 0o600, content: "this is inline config\n", + userId: 1000, + groupId: 1000, }, configContentSecond) }) } diff --git a/test/e2e/fixtures/compose-configs.yaml b/test/e2e/fixtures/compose-configs.yaml index d2e59857..b420deab 100644 --- a/test/e2e/fixtures/compose-configs.yaml +++ b/test/e2e/fixtures/compose-configs.yaml @@ -1,7 +1,7 @@ services: web: image: busybox:1.37.0-uclibc - command: ["true"] + command: ["sleep", "600"] configs: - source: from-file target: /etc/config-from-file.conf diff --git a/test/e2e/helpers.go b/test/e2e/helpers.go index f8d3b7ab..07ad5b60 100644 --- a/test/e2e/helpers.go +++ b/test/e2e/helpers.go @@ -1,106 +1,98 @@ package e2e import ( + "bytes" "context" "fmt" "os" - "path/filepath" - "strconv" "strings" "testing" - "github.com/docker/docker/api/types/container" - dockerclient "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - "github.com/psviderski/uncloud/internal/ucind" + "github.com/psviderski/uncloud/pkg/api" + "github.com/psviderski/uncloud/pkg/client" "github.com/stretchr/testify/assert" ) type fileInfo struct { permissions os.FileMode 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 -// -// Uncloud API does not currently expose exec functionality to run commands inside containers. -// Instead this helper function uses "docker cp" inside the ucind container to copy the file from the target container -// to a temporary location (also inside the ucind container), and then inspect its content and permissions. -// TODO: update when exec functionality is implemented -func readFileInfoInContainer(t *testing.T, machine *ucind.Machine, containerName, filePath string) (fileInfo, error) { +// 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. +func execInContainerAndReadOutput( + t *testing.T, + ctx context.Context, + cli *client.Client, + 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() ctx := context.Background() - dockerCli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) - if err != nil { - return fileInfo{}, err - } - 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) + // Get file permissions + permOutput, err := execInContainerAndReadOutput(t, ctx, cli, serviceNameOrID, containerNameOrID, + []string{"stat", "-c", "%a %u %g", filePath}) if err != nil { return fileInfo{}, err } - // 2. Attach to the exec session - hijackResp, err := dockerCli.ContainerExecAttach(ctx, resp.ID, container.ExecAttachOptions{}) + // Parse permissions + 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 { - return fileInfo{}, err - } - defer hijackResp.Close() - - // 3. Inspect result - inspectResp, err := dockerCli.ContainerExecInspect(ctx, resp.ID) - if err != nil { - return fileInfo{}, err + return fileInfo{}, fmt.Errorf("parse stat output: %w", 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) - 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{ permissions: mode, + userId: uid, + groupId: gid, content: fileContent, }, nil }