-
Notifications
You must be signed in to change notification settings - Fork 20
More docker control #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
79d3f74
5760618
742f97c
550bab5
bd2add0
d0a4407
6be04bc
020a98c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,6 @@ | ||
| # Generated by buf. DO NOT EDIT. | ||
| version: v2 | ||
| deps: | ||
| - name: buf.build/googleapis/googleapis | ||
| commit: 004180b77378443887d3b55cabc00384 | ||
| digest: b5:e8f475fe3330f31f5fd86ac689093bcd274e19611a09db91f41d637cb9197881ce89882b94d13a58738e53c91c6e4bae7dc1feba85f590164c975a89e25115dc | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |
| "maps" | ||
| "net/http" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "reflect" | ||
| "strings" | ||
|
|
@@ -333,18 +334,78 @@ func ApplyOverrides(overrides *v1.DockerOverrides, config *container.Config, hos | |
| } | ||
| } | ||
|
|
||
| // generateInitWrapper creates a bash wrapper script that runs init commands before the original entrypoint | ||
| func (c *Client) generateInitWrapper(ctx context.Context, initCommands []string, originalEntrypoint []string) (string, error) { | ||
Wolfhound905 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if len(initCommands) == 0 { | ||
| return "", nil | ||
| } | ||
|
|
||
| c.log.Info("Generating init wrapper script with %d commands", len(initCommands)) | ||
|
|
||
| // Create wrapper script content | ||
| script := "#!/bin/bash\n" | ||
|
||
| script += "set -e # Exit on first error\n" | ||
|
||
| script += "set -o pipefail # Catch errors in pipes\n\n" | ||
| script += "echo '[DiscoPanel] Starting init commands...'\n\n" | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Add each init command with logging | ||
| for i, cmd := range initCommands { | ||
| script += fmt.Sprintf("echo '[DiscoPanel] Init command %d/%d: Running...'\n", i+1, len(initCommands)) | ||
| script += fmt.Sprintf("%s\n", cmd) | ||
| script += fmt.Sprintf("echo '[DiscoPanel] Init command %d/%d: SUCCESS'\n\n", i+1, len(initCommands)) | ||
| } | ||
|
|
||
| script += "echo '[DiscoPanel] All init commands completed successfully'\n" | ||
| script += "echo '[DiscoPanel] Starting original entrypoint...'\n\n" | ||
|
|
||
| // Exec original entrypoint (replaces shell process) | ||
| if len(originalEntrypoint) > 0 { | ||
| // Properly quote and escape arguments | ||
| entrypointCmd := "exec" | ||
| for _, part := range originalEntrypoint { | ||
| // Escape single quotes in the argument | ||
| escaped := strings.ReplaceAll(part, "'", "'\"'\"'") | ||
| entrypointCmd += fmt.Sprintf(" '%s'", escaped) | ||
| } | ||
| script += entrypointCmd + "\n" | ||
| } else { | ||
| script += "# No original entrypoint specified\n" | ||
| script += "exec /bin/bash\n" | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return script, nil | ||
| } | ||
|
|
||
| func (c *Client) CreateContainer(ctx context.Context, server *models.Server, serverConfig *models.ServerConfig) (string, error) { | ||
| // Use server's DockerImage if specified, otherwise determine based on version and loader | ||
| var imageName string | ||
| var isLocalImage bool | ||
| if server.DockerImage != "" { | ||
| imageName = "itzg/minecraft-server:" + server.DockerImage | ||
| // Check if DockerImage is a full image reference (contains "/") or a local image | ||
| if strings.Contains(server.DockerImage, "/") { | ||
| // It's a full image reference (e.g., "my-registry.com/image:tag"), use as-is | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| imageName = server.DockerImage | ||
| c.log.Debug("Using full image reference: %s", imageName) | ||
| } else if c.imageExistsLocally(ctx, server.DockerImage) { | ||
| // It's a local image (e.g., "minecraft-with-git:latest"), use as-is | ||
| imageName = server.DockerImage | ||
| isLocalImage = true | ||
| c.log.Info("Using local image: %s", imageName) | ||
| } else { | ||
| // It's just a tag (e.g., "java21"), prepend the default itzg image | ||
| imageName = "itzg/minecraft-server:" + server.DockerImage | ||
| c.log.Debug("Using itzg image with tag: %s", imageName) | ||
| } | ||
| } else { | ||
| imageName = getDockerImage(server.ModLoader, server.MCVersion) | ||
| c.log.Debug("Using optimal docker tag: %s", imageName) | ||
| } | ||
|
|
||
| // Try pulling latest | ||
| if err := c.pullImage(ctx, imageName); err != nil { | ||
| return "", fmt.Errorf("failed to pull image: %w", err) | ||
| // Only pull if it's not a local image | ||
| if !isLocalImage { | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if err := c.pullImage(ctx, imageName); err != nil { | ||
| return "", fmt.Errorf("failed to pull image: %w", err) | ||
| } | ||
| } | ||
|
|
||
| // Build environment variables | ||
|
|
@@ -456,6 +517,55 @@ func (c *Client) CreateContainer(ctx context.Context, server *models.Server, ser | |
| // Apply docker overrides | ||
| ApplyOverrides(server.DockerOverrides, config, hostConfig) | ||
|
|
||
| // Handle init commands wrapper - if init_commands are present | ||
| if server.DockerOverrides != nil && len(server.DockerOverrides.InitCommands) > 0 { | ||
| c.log.Info("Server %s has init commands, generating wrapper script", server.ID) | ||
|
|
||
| // Determine original entrypoint | ||
| originalEntrypoint := config.Entrypoint | ||
| if len(originalEntrypoint) == 0 { | ||
| // If no entrypoint specified, Docker will use image default | ||
| // Try to get it from the image inspection | ||
| imageInspect, err := c.docker.ImageInspect(ctx, imageName) | ||
| if err == nil && len(imageInspect.Config.Entrypoint) > 0 { | ||
| originalEntrypoint = imageInspect.Config.Entrypoint | ||
| c.log.Debug("Retrieved image entrypoint: %v", originalEntrypoint) | ||
| } else { | ||
| // Fallback for itzg/minecraft-server (known default) | ||
| originalEntrypoint = []string{"/start"} | ||
| c.log.Debug("Using fallback entrypoint for itzg/minecraft-server: /start") | ||
| } | ||
| } | ||
|
|
||
| // Generate wrapper script | ||
| scriptContent, err := c.generateInitWrapper(ctx, server.DockerOverrides.InitCommands, originalEntrypoint) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to generate init wrapper: %w", err) | ||
| } | ||
|
|
||
| // Create script file in server's data directory | ||
| scriptPath := filepath.Join(server.DataPath, ".discopanel-init.sh") | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { | ||
| return "", fmt.Errorf("failed to write init wrapper script: %w", err) | ||
| } | ||
|
|
||
| c.log.Info("Wrote init wrapper script to %s", scriptPath) | ||
|
|
||
| // Mount the script into the container (read-only) | ||
| hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ | ||
| Type: mount.TypeBind, | ||
| Source: scriptPath, | ||
| Target: "/discopanel-init.sh", | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ReadOnly: true, | ||
| }) | ||
|
|
||
| // Override entrypoint to use our wrapper | ||
| config.Entrypoint = []string{"/bin/bash", "/discopanel-init.sh"} | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| c.log.Info("[AUDIT] Generated init wrapper for server %s with %d commands", | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| server.ID, len(server.DockerOverrides.InitCommands)) | ||
| } | ||
|
|
||
| // Network configuration | ||
| networkConfig := &network.NetworkingConfig{} | ||
| if c.config.NetworkName != "" && hostConfig.NetworkMode == "" { | ||
|
|
@@ -765,6 +875,83 @@ func (c *Client) GetDockerImages() []DockerImageTag { | |
| return activeImages | ||
| } | ||
|
|
||
| // parseImageReference splits an image reference into repository and tag | ||
| // Returns normalized image name with tag (defaults to "latest" if not specified) | ||
| func parseImageReference(imageStr string) (string, error) { | ||
| imageStr = strings.TrimSpace(imageStr) | ||
| if imageStr == "" { | ||
| return "", fmt.Errorf("image name cannot be empty") | ||
| } | ||
|
|
||
| // Check for invalid characters | ||
| if strings.ContainsAny(imageStr, " \t\n") { | ||
| return "", fmt.Errorf("image name contains invalid whitespace") | ||
| } | ||
|
|
||
| // If no tag specified, add :latest | ||
| if !strings.Contains(imageStr, ":") { | ||
| return imageStr + ":latest", nil | ||
| } | ||
|
|
||
| return imageStr, nil | ||
| } | ||
|
|
||
| // imageExistsLocally checks if a Docker image exists in the local Docker daemon | ||
| func (c *Client) imageExistsLocally(ctx context.Context, imageName string) bool { | ||
| // Use the Docker API client for more reliable local image detection | ||
| _, err := c.docker.ImageInspect(ctx, imageName) | ||
| exists := err == nil | ||
| c.log.Debug("imageExistsLocally check for '%s': exists=%v, err=%v", imageName, exists, err) | ||
| return exists | ||
| } | ||
|
|
||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So purpose of this method aside, this can cause problems for a couple reasons - but there are ways around it, stylistically and functionally. func (c *Client) imageExistsLocally(ctx context.Context, imageName string) (bool, error) {
// Get all existing docker images on host
images, err := c.docker..ImageList(ctx, image.ListOptions{})
if err != nil {
return false, err // Returning error intently to the caller
}
// Loop through all found images,
for _, img := range images {
for _, tag := range img.RepoTags {
if tag == target { // Check for image matching our target
return true, nil
}
}
}
return false, nil
}In your implementation, we assume that all errors returned by the docker image inspection call are inferring only that the image does not exist. We assert that opinion on all future callers of this method, forever masking what could be a legitimate underlying docker error. |
||
| // ValidateImageExists checks if a Docker image exists locally or on accessible registries | ||
| // First checks for local images using docker image inspect, then falls back to docker manifest inspect for remote images | ||
| func (c *Client) ValidateImageExists(ctx context.Context, imageName string) error { | ||
| // Parse and normalize the image reference | ||
| normalizedImage, err := parseImageReference(imageName) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // First try to check if it's a local image using docker image inspect | ||
| cmd := exec.CommandContext(ctx, "docker", "image", "inspect", normalizedImage) | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var stdout, stderr bytes.Buffer | ||
| cmd.Stdout = &stdout | ||
| cmd.Stderr = &stderr | ||
|
|
||
| err = cmd.Run() | ||
| if err == nil { | ||
| // Image exists locally | ||
| return nil | ||
| } | ||
|
|
||
| // Not found locally, try remote registries using docker manifest inspect | ||
| cmd = exec.CommandContext(ctx, "docker", "manifest", "inspect", normalizedImage) | ||
Wolfhound905 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| stdout.Reset() | ||
| stderr.Reset() | ||
| cmd.Stdout = &stdout | ||
| cmd.Stderr = &stderr | ||
|
|
||
| err = cmd.Run() | ||
| if err != nil { | ||
| // More descriptive error messages based on error output | ||
| errStr := stderr.String() + " " + stdout.String() | ||
| if strings.Contains(errStr, "no such manifest") || strings.Contains(errStr, "not found") { | ||
| return fmt.Errorf("image '%s' not found locally or on Docker Hub or accessible registries", normalizedImage) | ||
| } | ||
| if strings.Contains(errStr, "unauthorized") || strings.Contains(errStr, "forbidden") { | ||
| return fmt.Errorf("access denied to image '%s' - may require authentication", normalizedImage) | ||
| } | ||
| if strings.Contains(errStr, "connection refused") || strings.Contains(errStr, "network") { | ||
| return fmt.Errorf("cannot reach Docker daemon or registry: %w", err) | ||
| } | ||
| return fmt.Errorf("failed to validate image '%s': %w", normalizedImage, err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func getDockerImage(loader models.ModLoader, mcVersion string) string { | ||
| _ = loader | ||
| // itzg/minecraft-server supports all mod loaders through environment variables | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.