mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
fix(configs): Create non-existent parent directories automatically (#233)
This commit is contained in:
@@ -7,6 +7,8 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
/uncloud
|
||||||
|
/uncloudd
|
||||||
|
|
||||||
# OS X
|
# OS X
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -801,14 +801,17 @@ func (s *Server) injectConfigs(ctx context.Context, containerID string, configs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
|
// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
|
||||||
|
// It will create any intermediate directories in the target path that don't exist.
|
||||||
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
|
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
|
||||||
// Create a tar archive containing the file
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
tw := tar.NewWriter(&buf)
|
tw := tar.NewWriter(&buf)
|
||||||
|
|
||||||
// Create tar header
|
// Trim leading slash(es) to avoid double slashes in the tar path
|
||||||
|
tarPath := strings.TrimPrefix(targetPath, "/")
|
||||||
|
|
||||||
|
// Create tar header with full path
|
||||||
header := &tar.Header{
|
header := &tar.Header{
|
||||||
Name: filepath.Base(targetPath),
|
Name: tarPath,
|
||||||
Size: int64(len(content)),
|
Size: int64(len(content)),
|
||||||
Mode: int64(fileMode),
|
Mode: int64(fileMode),
|
||||||
ModTime: time.Now(),
|
ModTime: time.Now(),
|
||||||
@@ -834,16 +837,12 @@ func (s *Server) copyContentToContainer(ctx context.Context, containerID string,
|
|||||||
return fmt.Errorf("close tar writer: %w", err)
|
return fmt.Errorf("close tar writer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the tar archive to the container
|
// Always extract to root. The tar archive contains the full path, so tar will
|
||||||
targetDir := filepath.Dir(targetPath)
|
// automatically create any intermediate directories that don't exist in the container.
|
||||||
if targetDir == "." {
|
|
||||||
targetDir = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.client.CopyToContainer(
|
if err := s.client.CopyToContainer(
|
||||||
ctx,
|
ctx,
|
||||||
containerID,
|
containerID,
|
||||||
targetDir,
|
"/",
|
||||||
&buf,
|
&buf,
|
||||||
container.CopyToContainerOptions{CopyUIDGID: true},
|
container.CopyToContainerOptions{CopyUIDGID: true},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
@@ -13,18 +13,27 @@ import (
|
|||||||
func configSpecsFromCompose(
|
func configSpecsFromCompose(
|
||||||
configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string,
|
configs types.Configs, serviceConfigs []types.ServiceConfigObjConfig, workingDir string,
|
||||||
) ([]api.ConfigSpec, []api.ConfigMount, error) {
|
) ([]api.ConfigSpec, []api.ConfigMount, error) {
|
||||||
var configSpecs []api.ConfigSpec
|
|
||||||
var configMounts []api.ConfigMount
|
var configMounts []api.ConfigMount
|
||||||
|
|
||||||
|
// Temporary map to hold config specs
|
||||||
|
configSpecsMap := make(map[string]api.ConfigSpec)
|
||||||
|
|
||||||
|
// We iterate over all service config objects (config mounts)
|
||||||
for _, serviceConfig := range serviceConfigs {
|
for _, serviceConfig := range serviceConfigs {
|
||||||
var spec api.ConfigSpec
|
var spec api.ConfigSpec
|
||||||
|
|
||||||
if projectConfig, exists := configs[serviceConfig.Source]; exists {
|
projectConfig, exists := configs[serviceConfig.Source]
|
||||||
|
if !exists {
|
||||||
|
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
|
||||||
|
}
|
||||||
|
|
||||||
if projectConfig.External {
|
if projectConfig.External {
|
||||||
return nil, nil, fmt.Errorf("external configs are not supported: %s",
|
return nil, nil, fmt.Errorf("external configs are not supported: %s",
|
||||||
serviceConfig.Source)
|
serviceConfig.Source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spec, exists = configSpecsMap[serviceConfig.Source]
|
||||||
|
if !exists {
|
||||||
spec = api.ConfigSpec{
|
spec = api.ConfigSpec{
|
||||||
Name: serviceConfig.Source,
|
Name: serviceConfig.Source,
|
||||||
Content: []byte(projectConfig.Content),
|
Content: []byte(projectConfig.Content),
|
||||||
@@ -44,11 +53,9 @@ func configSpecsFromCompose(
|
|||||||
}
|
}
|
||||||
spec.Content = fileContent
|
spec.Content = fileContent
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return nil, nil, fmt.Errorf("config '%s' not found in project configs", serviceConfig.Source)
|
|
||||||
}
|
|
||||||
|
|
||||||
configSpecs = append(configSpecs, spec)
|
configSpecsMap[serviceConfig.Source] = spec
|
||||||
|
}
|
||||||
|
|
||||||
// Create config mount
|
// Create config mount
|
||||||
target := serviceConfig.Target
|
target := serviceConfig.Target
|
||||||
@@ -71,5 +78,11 @@ func configSpecsFromCompose(
|
|||||||
configMounts = append(configMounts, mount)
|
configMounts = append(configMounts, mount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configSpecs []api.ConfigSpec
|
||||||
|
// Convert config spec map to slice
|
||||||
|
for _, spec := range configSpecsMap {
|
||||||
|
configSpecs = append(configSpecs, spec)
|
||||||
|
}
|
||||||
|
|
||||||
return configSpecs, configMounts, nil
|
return configSpecs, configMounts, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,147 @@ func TestConfigSpecsFromCompose(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "same source mounted to different targets",
|
||||||
|
configs: types.Configs{
|
||||||
|
"shared-config": types.ConfigObjConfig{
|
||||||
|
File: "testdata/config1.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "shared-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
UID: "1000",
|
||||||
|
GID: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Source: "shared-config",
|
||||||
|
Target: "/backup/config.json",
|
||||||
|
UID: "1001",
|
||||||
|
GID: "1001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "shared-config",
|
||||||
|
Content: []byte("test config content\n"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "shared-config",
|
||||||
|
ContainerPath: "/app/config.json",
|
||||||
|
Uid: "1000",
|
||||||
|
Gid: "1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ConfigName: "shared-config",
|
||||||
|
ContainerPath: "/backup/config.json",
|
||||||
|
Uid: "1001",
|
||||||
|
Gid: "1001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with default target path",
|
||||||
|
configs: types.Configs{
|
||||||
|
"default-config": types.ConfigObjConfig{
|
||||||
|
Content: "inline config content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "default-config",
|
||||||
|
// No Target specified - should use default
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "default-config",
|
||||||
|
Content: []byte("inline config content"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "default-config",
|
||||||
|
ContainerPath: "/default-config",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config with inline content",
|
||||||
|
configs: types.Configs{
|
||||||
|
"inline-config": types.ConfigObjConfig{
|
||||||
|
Content: "server {\n listen 80;\n}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "inline-config",
|
||||||
|
Target: "/etc/nginx/sites-available/default",
|
||||||
|
Mode: func() *types.FileMode { m := types.FileMode(0o755); return &m }(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSpecs: []api.ConfigSpec{
|
||||||
|
{
|
||||||
|
Name: "inline-config",
|
||||||
|
Content: []byte("server {\n listen 80;\n}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMounts: []api.ConfigMount{
|
||||||
|
{
|
||||||
|
ConfigName: "inline-config",
|
||||||
|
ContainerPath: "/etc/nginx/sites-available/default",
|
||||||
|
Mode: func() *os.FileMode { m := os.FileMode(0o755); return &m }(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config not found error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"existing-config": types.ConfigObjConfig{
|
||||||
|
Content: "some content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "missing-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "external config error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"external-config": types.ConfigObjConfig{
|
||||||
|
External: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "external-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file not found error",
|
||||||
|
configs: types.Configs{
|
||||||
|
"missing-file-config": types.ConfigObjConfig{
|
||||||
|
File: "testdata/nonexistent.txt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serviceConfigs: []types.ServiceConfigObjConfig{
|
||||||
|
{
|
||||||
|
Source: "missing-file-config",
|
||||||
|
Target: "/app/config.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ func TestComposeConfigs(t *testing.T) {
|
|||||||
Gid: "1000",
|
Gid: "1000",
|
||||||
Mode: func() *os.FileMode { m := os.FileMode(0o600); return &m }(),
|
Mode: func() *os.FileMode { m := os.FileMode(0o600); return &m }(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ConfigName: "from-file",
|
||||||
|
ContainerPath: "/etc/new-dir/config-from-file.conf",
|
||||||
|
Mode: func() *os.FileMode { m := os.FileMode(0o644); return &m }(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Configs: []api.ConfigSpec{
|
Configs: []api.ConfigSpec{
|
||||||
@@ -110,5 +115,14 @@ func TestComposeConfigs(t *testing.T) {
|
|||||||
userId: 1000,
|
userId: 1000,
|
||||||
groupId: 1000,
|
groupId: 1000,
|
||||||
}, configContentSecond)
|
}, configContentSecond)
|
||||||
|
|
||||||
|
configContentThird, err := readFileInfoInContainer(t, cli, name, containerName, "/etc/new-dir/config-from-file.conf")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, fileInfo{
|
||||||
|
permissions: 0o644,
|
||||||
|
content: "this is file config\n",
|
||||||
|
userId: 0,
|
||||||
|
groupId: 0,
|
||||||
|
}, configContentThird, "Same config should be mountable to multiple paths, including nested directories")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ services:
|
|||||||
uid: "1000"
|
uid: "1000"
|
||||||
gid: "1000"
|
gid: "1000"
|
||||||
mode: 0600
|
mode: 0600
|
||||||
|
# Writing a config to a new, non-existent directory
|
||||||
|
- source: from-file
|
||||||
|
target: /etc/new-dir/config-from-file.conf
|
||||||
|
mode: 0644
|
||||||
deploy:
|
deploy:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
configs:
|
configs:
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ func readFileInfoInContainer(t *testing.T, cli *client.Client, serviceNameOrID,
|
|||||||
|
|
||||||
// Parse permissions
|
// Parse permissions
|
||||||
permissions := strings.TrimSpace(permOutput)
|
permissions := strings.TrimSpace(permOutput)
|
||||||
fmt.Printf("Permissions output: %s\n", permissions)
|
|
||||||
|
|
||||||
// Parse three numbers: permissions, uid, gid
|
// Parse three numbers: permissions, uid, gid
|
||||||
var permissionsOctal, uid, gid int
|
var permissionsOctal, uid, gid int
|
||||||
|
|||||||
Reference in New Issue
Block a user