mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-28 03:53:33 +00:00
ref: Rewrite config tests with Exec
This commit is contained in:
@@ -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,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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user