Refactor: Enhance container management and UI components
- Add TraefikEntryPoint field to ContainerCommandParams struct - Update fetch command to handle Traefik entry point - Refactor container list and creation UI components - Improve error handling and state management in hyperscript - Update _hyperscript and htmx libraries to latest versions
- commit
- e9b201ce14aef662a38096a3f5e46431b713009f
- author
- Brice · · 2024-09-16 10:46 UTC
- parent
- 11bec3a
- signature
- unsigned
13 files · +184 -160
M internal/cli/cmd/deploy.go +1 -1
| @@ -67,7 +67,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 67 | 67 | }, | |
| 68 | 68 | } | |
| 69 | 69 | ||
| 70 | - | deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container") | |
| 70 | + | deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container and Traefik entry point") | |
| 71 | 71 | deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik") | |
| 72 | 72 | ||
| 73 | 73 | return deployCmd | |
| @@ -67,7 +67,7 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||||
| 67 | }, | 67 | }, | ||
| 68 | } | 68 | } | ||
| 69 | 69 | ||||
| 70 | - | deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container") | 70 | + | deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container and Traefik entry point") |
| 71 | deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik") | 71 | deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik") | ||
| 72 | 72 | ||||
| 73 | return deployCmd | 73 | return deployCmd | ||
M internal/common/initconfig.go +56 -9
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "os" | |
| 8 | 8 | "path/filepath" | |
| 9 | 9 | "runtime" | |
| 10 | + | "strings" | |
| 10 | 11 | ||
| 11 | 12 | "github.com/bnema/gordon/pkg/docker" | |
| 12 | 13 | "github.com/bnema/gordon/pkg/parser" | |
| @@ -68,21 +69,50 @@ var ( | |||
| 68 | 69 | ) | |
| 69 | 70 | ||
| 70 | 71 | func getConfigDir() (string, error) { | |
| 72 | + | var configDir string | |
| 73 | + | ||
| 74 | + | if isWSL() { | |
| 75 | + | // Use XDG_CONFIG_HOME for WSL | |
| 76 | + | if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { | |
| 77 | + | configDir = filepath.Join(xdgConfigHome, "Gordon") | |
| 78 | + | } else { | |
| 79 | + | // If XDG_CONFIG_HOME is not set, fall back to default locations | |
| 80 | + | homeDir, err := os.UserHomeDir() | |
| 81 | + | if err != nil { | |
| 82 | + | // If we can't get the home directory, use a fallback | |
| 83 | + | homeDir = os.TempDir() // Use the system's temp directory as a fallback | |
| 84 | + | fmt.Printf("Warning: Unable to determine home directory. Using temp directory: %s\n", homeDir) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | configDir = filepath.Join(homeDir, ".config", "Gordon") | |
| 88 | + | ||
| 89 | + | } | |
| 90 | + | ||
| 91 | + | return configDir, nil | |
| 92 | + | } | |
| 71 | 93 | ||
| 72 | 94 | if docker.IsRunningInContainer() { | |
| 73 | 95 | return "/.", nil | |
| 74 | 96 | } | |
| 75 | 97 | ||
| 76 | - | // Get the user's home directory for non-container environments | |
| 77 | - | homeDir, err := os.UserHomeDir() | |
| 78 | - | if err != nil { | |
| 79 | - | return "", fmt.Errorf("error getting user's home directory: %w", err) | |
| 80 | - | } | |
| 98 | + | // Check for XDG_CONFIG_HOME first | |
| 99 | + | if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { | |
| 100 | + | configDir = filepath.Join(xdgConfigHome, "Gordon") | |
| 101 | + | } else { | |
| 102 | + | // If XDG_CONFIG_HOME is not set, fall back to default locations | |
| 103 | + | homeDir, err := os.UserHomeDir() | |
| 104 | + | if err != nil { | |
| 105 | + | // If we can't get the home directory, use a fallback | |
| 106 | + | homeDir = os.TempDir() // Use the system's temp directory as a fallback | |
| 107 | + | fmt.Printf("Warning: Unable to determine home directory. Using temp directory: %s\n", homeDir) | |
| 108 | + | } | |
| 81 | 109 | ||
| 82 | - | // Select the configuration directory based on the OS | |
| 83 | - | configDir := filepath.Join(homeDir, ".config", "Gordon") | |
| 84 | - | if runtime.GOOS == "windows" { | |
| 85 | - | configDir = filepath.Join(homeDir, "AppData", "Roaming", "Gordon") | |
| 110 | + | // Select the configuration directory based on the OS | |
| 111 | + | if runtime.GOOS == "windows" { | |
| 112 | + | configDir = filepath.Join(homeDir, "AppData", "Roaming", "Gordon") | |
| 113 | + | } else { | |
| 114 | + | configDir = filepath.Join(homeDir, ".config", "Gordon") | |
| 115 | + | } | |
| 86 | 116 | } | |
| 87 | 117 | ||
| 88 | 118 | return configDir, nil | |
| @@ -201,3 +231,20 @@ func (c *Config) GetBackendURL() string { | |||
| 201 | 231 | func (c *Config) SetToken(token string) { | |
| 202 | 232 | c.General.Token = token | |
| 203 | 233 | } | |
| 234 | + | ||
| 235 | + | func isWSL() bool { | |
| 236 | + | // Check for /proc/version file | |
| 237 | + | if _, err := os.Stat("/proc/version"); err == nil { | |
| 238 | + | content, err := os.ReadFile("/proc/version") | |
| 239 | + | if err == nil && strings.Contains(strings.ToLower(string(content)), "microsoft") { | |
| 240 | + | return true | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | // Check for WSL-specific environment variable | |
| 245 | + | if os.Getenv("WSL_DISTRO_NAME") != "" { | |
| 246 | + | return true | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | return false | |
| 250 | + | } | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "os" | 7 | "os" | ||
| 8 | "path/filepath" | 8 | "path/filepath" | ||
| 9 | "runtime" | 9 | "runtime" | ||
| 10 | + | "strings" | |||
| 10 | 11 | ||||
| 11 | "github.com/bnema/gordon/pkg/docker" | 12 | "github.com/bnema/gordon/pkg/docker" | ||
| 12 | "github.com/bnema/gordon/pkg/parser" | 13 | "github.com/bnema/gordon/pkg/parser" | ||
| @@ -68,21 +69,50 @@ var ( | |||||
| 68 | ) | 69 | ) | ||
| 69 | 70 | ||||
| 70 | func getConfigDir() (string, error) { | 71 | func getConfigDir() (string, error) { | ||
| 72 | + | var configDir string | |||
| 73 | + | ||||
| 74 | + | if isWSL() { | |||
| 75 | + | // Use XDG_CONFIG_HOME for WSL | |||
| 76 | + | if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { | |||
| 77 | + | configDir = filepath.Join(xdgConfigHome, "Gordon") | |||
| 78 | + | } else { | |||
| 79 | + | // If XDG_CONFIG_HOME is not set, fall back to default locations | |||
| 80 | + | homeDir, err := os.UserHomeDir() | |||
| 81 | + | if err != nil { | |||
| 82 | + | // If we can't get the home directory, use a fallback | |||
| 83 | + | homeDir = os.TempDir() // Use the system's temp directory as a fallback | |||
| 84 | + | fmt.Printf("Warning: Unable to determine home directory. Using temp directory: %s\n", homeDir) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | configDir = filepath.Join(homeDir, ".config", "Gordon") | |||
| 88 | + | ||||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | return configDir, nil | |||
| 92 | + | } | |||
| 71 | 93 | ||||
| 72 | if docker.IsRunningInContainer() { | 94 | if docker.IsRunningInContainer() { | ||
| 73 | return "/.", nil | 95 | return "/.", nil | ||
| 74 | } | 96 | } | ||
| 75 | 97 | ||||
| 76 | - | // Get the user's home directory for non-container environments | 98 | + | // Check for XDG_CONFIG_HOME first |
| 77 | - | homeDir, err := os.UserHomeDir() | 99 | + | if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { |
| 78 | - | if err != nil { | 100 | + | configDir = filepath.Join(xdgConfigHome, "Gordon") |
| 79 | - | return "", fmt.Errorf("error getting user's home directory: %w", err) | 101 | + | } else { |
| 80 | - | } | 102 | + | // If XDG_CONFIG_HOME is not set, fall back to default locations |
| 103 | + | homeDir, err := os.UserHomeDir() | |||
| 104 | + | if err != nil { | |||
| 105 | + | // If we can't get the home directory, use a fallback | |||
| 106 | + | homeDir = os.TempDir() // Use the system's temp directory as a fallback | |||
| 107 | + | fmt.Printf("Warning: Unable to determine home directory. Using temp directory: %s\n", homeDir) | |||
| 108 | + | } | |||
| 81 | 109 | ||||
| 82 | - | // Select the configuration directory based on the OS | 110 | + | // Select the configuration directory based on the OS |
| 83 | - | configDir := filepath.Join(homeDir, ".config", "Gordon") | 111 | + | if runtime.GOOS == "windows" { |
| 84 | - | if runtime.GOOS == "windows" { | 112 | + | configDir = filepath.Join(homeDir, "AppData", "Roaming", "Gordon") |
| 85 | - | configDir = filepath.Join(homeDir, "AppData", "Roaming", "Gordon") | 113 | + | } else { |
| 114 | + | configDir = filepath.Join(homeDir, ".config", "Gordon") | |||
| 115 | + | } | |||
| 86 | } | 116 | } | ||
| 87 | 117 | ||||
| 88 | return configDir, nil | 118 | return configDir, nil | ||
| @@ -201,3 +231,20 @@ func (c *Config) GetBackendURL() string { | |||||
| 201 | func (c *Config) SetToken(token string) { | 231 | func (c *Config) SetToken(token string) { | ||
| 202 | c.General.Token = token | 232 | c.General.Token = token | ||
| 203 | } | 233 | } | ||
| 234 | + | ||||
| 235 | + | func isWSL() bool { | |||
| 236 | + | // Check for /proc/version file | |||
| 237 | + | if _, err := os.Stat("/proc/version"); err == nil { | |||
| 238 | + | content, err := os.ReadFile("/proc/version") | |||
| 239 | + | if err == nil && strings.Contains(strings.ToLower(string(content)), "microsoft") { | |||
| 240 | + | return true | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | // Check for WSL-specific environment variable | |||
| 245 | + | if os.Getenv("WSL_DISTRO_NAME") != "" { | |||
| 246 | + | return true | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | return false | |||
| 250 | + | } | |||
M internal/httpserve/handler/containers.go +30 -15
| @@ -3,6 +3,7 @@ package handler | |||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | 5 | "net/http" | |
| 6 | + | "strings" | |
| 6 | 7 | "sync" | |
| 7 | 8 | "time" | |
| 8 | 9 | ||
| @@ -61,13 +62,14 @@ type HumanReadableContainerImage struct { | |||
| 61 | 62 | ||
| 62 | 63 | type HumanReadableContainer struct { | |
| 63 | 64 | *types.Container | |
| 64 | - | Name string | |
| 65 | - | Ports []string | |
| 66 | - | ShortID string | |
| 67 | - | CreatedStr string | |
| 68 | - | SizeStr string | |
| 69 | - | UpSince string | |
| 70 | - | StateColor string | |
| 65 | + | Name string | |
| 66 | + | Ports []string | |
| 67 | + | ShortID string | |
| 68 | + | CreatedStr string | |
| 69 | + | SizeStr string | |
| 70 | + | UpSince string | |
| 71 | + | StateColor string | |
| 72 | + | TraefikEntryPoint string | |
| 71 | 73 | } | |
| 72 | 74 | ||
| 73 | 75 | // renderHTML is a generalized function to render HTML | |
| @@ -162,9 +164,11 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||
| 162 | 164 | for _, container := range containers { | |
| 163 | 165 | localContainer := container // Make a local copy | |
| 164 | 166 | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | |
| 165 | - | stateColor := "green" | |
| 167 | + | stateColor := "blue" | |
| 166 | 168 | ||
| 167 | - | if container.State == "exited" { | |
| 169 | + | if container.State == "running" { | |
| 170 | + | stateColor = "green" | |
| 171 | + | } else if container.State == "exited" { | |
| 168 | 172 | stateColor = "red" | |
| 169 | 173 | } | |
| 170 | 174 | var ports []string | |
| @@ -176,12 +180,14 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||
| 176 | 180 | for _, name := range container.Names { | |
| 177 | 181 | name = name[1:] | |
| 178 | 182 | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | |
| 179 | - | Container: &localContainer, | |
| 180 | - | SizeStr: sizeStr, | |
| 181 | - | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | |
| 182 | - | StateColor: stateColor, | |
| 183 | - | Name: name, | |
| 184 | - | Ports: ports, | |
| 183 | + | Container: &localContainer, | |
| 184 | + | SizeStr: sizeStr, | |
| 185 | + | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | |
| 186 | + | StateColor: stateColor, | |
| 187 | + | Name: name, | |
| 188 | + | Ports: ports, | |
| 189 | + | TraefikEntryPoint: extractTraefikEntryPoint(container.Labels), | |
| 190 | + | CreatedStr: humanize.TimeAgo(time.Unix(container.Created, 0)), | |
| 185 | 191 | }) | |
| 186 | 192 | } | |
| 187 | 193 | } | |
| @@ -197,6 +203,15 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||
| 197 | 203 | return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data) | |
| 198 | 204 | } | |
| 199 | 205 | ||
| 206 | + | func extractTraefikEntryPoint(labels map[string]string) string { | |
| 207 | + | for key, value := range labels { | |
| 208 | + | if strings.HasSuffix(key, ".loadbalancer.server.port") { | |
| 209 | + | return value | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | return "" | |
| 213 | + | } | |
| 214 | + | ||
| 200 | 215 | // ContainerManagerDelete handles the /container-manager/delete route | |
| 201 | 216 | func ContainerManagerDelete(c echo.Context, a *server.App) error { | |
| 202 | 217 | err := docker.RemoveContainer(c.Param("ID")) | |
| @@ -3,6 +3,7 @@ package handler | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | "net/http" | 5 | "net/http" | ||
| 6 | + | "strings" | |||
| 6 | "sync" | 7 | "sync" | ||
| 7 | "time" | 8 | "time" | ||
| 8 | 9 | ||||
| @@ -61,13 +62,14 @@ type HumanReadableContainerImage struct { | |||||
| 61 | 62 | ||||
| 62 | type HumanReadableContainer struct { | 63 | type HumanReadableContainer struct { | ||
| 63 | *types.Container | 64 | *types.Container | ||
| 64 | - | Name string | 65 | + | Name string |
| 65 | - | Ports []string | 66 | + | Ports []string |
| 66 | - | ShortID string | 67 | + | ShortID string |
| 67 | - | CreatedStr string | 68 | + | CreatedStr string |
| 68 | - | SizeStr string | 69 | + | SizeStr string |
| 69 | - | UpSince string | 70 | + | UpSince string |
| 70 | - | StateColor string | 71 | + | StateColor string |
| 72 | + | TraefikEntryPoint string | |||
| 71 | } | 73 | } | ||
| 72 | 74 | ||||
| 73 | // renderHTML is a generalized function to render HTML | 75 | // renderHTML is a generalized function to render HTML | ||
| @@ -162,9 +164,11 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||||
| 162 | for _, container := range containers { | 164 | for _, container := range containers { | ||
| 163 | localContainer := container // Make a local copy | 165 | localContainer := container // Make a local copy | ||
| 164 | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | 166 | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | ||
| 165 | - | stateColor := "green" | 167 | + | stateColor := "blue" |
| 166 | 168 | ||||
| 167 | - | if container.State == "exited" { | 169 | + | if container.State == "running" { |
| 170 | + | stateColor = "green" | |||
| 171 | + | } else if container.State == "exited" { | |||
| 168 | stateColor = "red" | 172 | stateColor = "red" | ||
| 169 | } | 173 | } | ||
| 170 | var ports []string | 174 | var ports []string | ||
| @@ -176,12 +180,14 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||||
| 176 | for _, name := range container.Names { | 180 | for _, name := range container.Names { | ||
| 177 | name = name[1:] | 181 | name = name[1:] | ||
| 178 | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | 182 | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | ||
| 179 | - | Container: &localContainer, | 183 | + | Container: &localContainer, |
| 180 | - | SizeStr: sizeStr, | 184 | + | SizeStr: sizeStr, |
| 181 | - | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | 185 | + | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), |
| 182 | - | StateColor: stateColor, | 186 | + | StateColor: stateColor, |
| 183 | - | Name: name, | 187 | + | Name: name, |
| 184 | - | Ports: ports, | 188 | + | Ports: ports, |
| 189 | + | TraefikEntryPoint: extractTraefikEntryPoint(container.Labels), | |||
| 190 | + | CreatedStr: humanize.TimeAgo(time.Unix(container.Created, 0)), | |||
| 185 | }) | 191 | }) | ||
| 186 | } | 192 | } | ||
| 187 | } | 193 | } | ||
| @@ -197,6 +203,15 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||||
| 197 | return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data) | 203 | return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data) | ||
| 198 | } | 204 | } | ||
| 199 | 205 | ||||
| 206 | + | func extractTraefikEntryPoint(labels map[string]string) string { | |||
| 207 | + | for key, value := range labels { | |||
| 208 | + | if strings.HasSuffix(key, ".loadbalancer.server.port") { | |||
| 209 | + | return value | |||
| 210 | + | } | |||
| 211 | + | } | |||
| 212 | + | return "" | |||
| 213 | + | } | |||
| 214 | + | ||||
| 200 | // ContainerManagerDelete handles the /container-manager/delete route | 215 | // ContainerManagerDelete handles the /container-manager/delete route | ||
| 201 | func ContainerManagerDelete(c echo.Context, a *server.App) error { | 216 | func ContainerManagerDelete(c echo.Context, a *server.App) error { | ||
| 202 | err := docker.RemoveContainer(c.Param("ID")) | 217 | err := docker.RemoveContainer(c.Param("ID")) | ||
M internal/templating/cmdparams/fromcli.go +12 -11
| @@ -37,19 +37,20 @@ func FromPayloadStructToCmdParams(ppl *common.DeployPayload, a *server.App, imag | |||
| 37 | 37 | containerDomain := strings.Join(hostParts[1:], ".") | |
| 38 | 38 | ||
| 39 | 39 | params := docker.ContainerCommandParams{ | |
| 40 | - | IsSSL: protocol == "https", | |
| 41 | - | ContainerName: containerSubdomain, | |
| 42 | - | ServiceName: containerSubdomain, | |
| 43 | - | Domain: containerDomain, | |
| 44 | - | ImageName: ppl.ImageName, | |
| 45 | - | ImageID: imageID, | |
| 46 | - | Restart: "always", | |
| 47 | - | Volumes: volumeSlice, | |
| 48 | - | Environment: environmentSlice, | |
| 49 | - | Network: a.Config.ContainerEngine.Network, | |
| 40 | + | IsSSL: protocol == "https", | |
| 41 | + | ContainerName: containerSubdomain, | |
| 42 | + | ServiceName: containerSubdomain, | |
| 43 | + | Domain: containerDomain, | |
| 44 | + | ImageName: ppl.ImageName, | |
| 45 | + | ImageID: imageID, | |
| 46 | + | Restart: "always", | |
| 47 | + | Volumes: volumeSlice, | |
| 48 | + | Environment: environmentSlice, | |
| 49 | + | Network: a.Config.ContainerEngine.Network, | |
| 50 | + | TraefikEntryPoint: ppl.Port, | |
| 50 | 51 | } | |
| 51 | 52 | ||
| 52 | - | err = CreateTraefikLabels(¶ms, ppl.Port, a) | |
| 53 | + | err = CreateTraefikLabels(¶ms, params.TraefikEntryPoint, a) | |
| 53 | 54 | if err != nil { | |
| 54 | 55 | return docker.ContainerCommandParams{}, fmt.Errorf("error creating Traefik labels: %w", err) | |
| 55 | 56 | } | |
| @@ -37,19 +37,20 @@ func FromPayloadStructToCmdParams(ppl *common.DeployPayload, a *server.App, imag | |||||
| 37 | containerDomain := strings.Join(hostParts[1:], ".") | 37 | containerDomain := strings.Join(hostParts[1:], ".") | ||
| 38 | 38 | ||||
| 39 | params := docker.ContainerCommandParams{ | 39 | params := docker.ContainerCommandParams{ | ||
| 40 | - | IsSSL: protocol == "https", | 40 | + | IsSSL: protocol == "https", |
| 41 | - | ContainerName: containerSubdomain, | 41 | + | ContainerName: containerSubdomain, |
| 42 | - | ServiceName: containerSubdomain, | 42 | + | ServiceName: containerSubdomain, |
| 43 | - | Domain: containerDomain, | 43 | + | Domain: containerDomain, |
| 44 | - | ImageName: ppl.ImageName, | 44 | + | ImageName: ppl.ImageName, |
| 45 | - | ImageID: imageID, | 45 | + | ImageID: imageID, |
| 46 | - | Restart: "always", | 46 | + | Restart: "always", |
| 47 | - | Volumes: volumeSlice, | 47 | + | Volumes: volumeSlice, |
| 48 | - | Environment: environmentSlice, | 48 | + | Environment: environmentSlice, |
| 49 | - | Network: a.Config.ContainerEngine.Network, | 49 | + | Network: a.Config.ContainerEngine.Network, |
| 50 | + | TraefikEntryPoint: ppl.Port, | |||
| 50 | } | 51 | } | ||
| 51 | 52 | ||||
| 52 | - | err = CreateTraefikLabels(¶ms, ppl.Port, a) | 53 | + | err = CreateTraefikLabels(¶ms, params.TraefikEntryPoint, a) |
| 53 | if err != nil { | 54 | if err != nil { | ||
| 54 | return docker.ContainerCommandParams{}, fmt.Errorf("error creating Traefik labels: %w", err) | 55 | return docker.ContainerCommandParams{}, fmt.Errorf("error creating Traefik labels: %w", err) | ||
| 55 | } | 56 | } | ||
M internal/templating/cmdparams/frominputs.go +21 -17
| @@ -11,30 +11,34 @@ import ( | |||
| 11 | 11 | func FromInputsToCmdParams(inputs map[string]string, a *server.App) (docker.ContainerCommandParams, error) { | |
| 12 | 12 | volumeSlice := ParseVolumeSlice(inputs["volumes"]) | |
| 13 | 13 | environmentSlice := ParseEnvironmentSlice(inputs["environment_variables"]) | |
| 14 | - | portMappings, err := ParsePortMappingsSlice(inputs["ports"]) | |
| 15 | 14 | ||
| 16 | - | // Extract the container port (right side of the mapping) | |
| 17 | - | containerPort := extractContainerPort(portMappings) | |
| 15 | + | var portMappings []docker.PortMapping | |
| 16 | + | var err error | |
| 18 | 17 | ||
| 19 | - | if err != nil { | |
| 20 | - | return docker.ContainerCommandParams{}, err | |
| 18 | + | // Parse port mappings if provided | |
| 19 | + | if inputs["ports"] != "" { | |
| 20 | + | portMappings, err = ParsePortMappingsSlice(inputs["ports"]) | |
| 21 | + | if err != nil { | |
| 22 | + | return docker.ContainerCommandParams{}, err | |
| 23 | + | } | |
| 21 | 24 | } | |
| 22 | 25 | ||
| 23 | 26 | params := docker.ContainerCommandParams{ | |
| 24 | - | IsSSL: inputs["container_protocol"] == "https", | |
| 25 | - | ContainerName: inputs["container_name"], | |
| 26 | - | ServiceName: inputs["container_subdomain"], | |
| 27 | - | Domain: inputs["container_domain"], | |
| 28 | - | ImageName: inputs["image_name"], | |
| 29 | - | ImageID: inputs["image_id"], | |
| 30 | - | Restart: inputs["restart"], | |
| 31 | - | Volumes: volumeSlice, | |
| 32 | - | Environment: environmentSlice, | |
| 33 | - | Network: a.Config.ContainerEngine.Network, | |
| 34 | - | PortMappings: portMappings, | |
| 27 | + | IsSSL: inputs["container_protocol"] == "https", | |
| 28 | + | ContainerName: inputs["container_name"], | |
| 29 | + | ServiceName: inputs["container_subdomain"], | |
| 30 | + | Domain: inputs["container_domain"], | |
| 31 | + | ImageName: inputs["image_name"], | |
| 32 | + | ImageID: inputs["image_id"], | |
| 33 | + | Restart: inputs["restart"], | |
| 34 | + | TraefikEntryPoint: inputs["traefik_entry_point"], | |
| 35 | + | Volumes: volumeSlice, | |
| 36 | + | Environment: environmentSlice, | |
| 37 | + | Network: a.Config.ContainerEngine.Network, | |
| 38 | + | PortMappings: portMappings, | |
| 35 | 39 | } | |
| 36 | 40 | ||
| 37 | - | err = CreateTraefikLabels(¶ms, containerPort, a) | |
| 41 | + | err = CreateTraefikLabels(¶ms, params.TraefikEntryPoint, a) | |
| 38 | 42 | if err != nil { | |
| 39 | 43 | return docker.ContainerCommandParams{}, fmt.Errorf("error creating Traefik labels: %w", err) | |
| 40 | 44 | } | |
| @@ -11,30 +11,34 @@ import ( | |||||
| 11 | func FromInputsToCmdParams(inputs map[string]string, a *server.App) (docker.ContainerCommandParams, error) { | 11 | func FromInputsToCmdParams(inputs map[string]string, a *server.App) (docker.ContainerCommandParams, error) { | ||
| 12 | volumeSlice := ParseVolumeSlice(inputs["volumes"]) | 12 | volumeSlice := ParseVolumeSlice(inputs["volumes"]) | ||
| 13 | environmentSlice := ParseEnvironmentSlice(inputs["environment_variables"]) | 13 | environmentSlice := ParseEnvironmentSlice(inputs["environment_variables"]) | ||
| 14 | - | portMappings, err := ParsePortMappingsSlice(inputs["ports"]) | |||
| 15 | 14 | ||||
| 16 | - | // Extract the container port (right side of the mapping) | 15 | + | var portMappings []docker.PortMapping |
| 17 | - | containerPort := extractContainerPort(portMappings) | 16 | + | var err error |
| 18 | 17 | ||||
| 19 | - | if err != nil { | 18 | + | // Parse port mappings if provided |
| 20 | - | return docker.ContainerCommandParams{}, err | 19 | + | if inputs["ports"] != "" { |
| 20 | + | portMappings, err = ParsePortMappingsSlice(inputs["ports"]) | |||
| 21 | + | if err != nil { | |||
| 22 | + | return docker.ContainerCommandParams{}, err | |||
| 23 | + | } | |||
| 21 | } | 24 | } | ||
| 22 | 25 | ||||
| 23 | params := docker.ContainerCommandParams{ | 26 | params := docker.ContainerCommandParams{ | ||
| 24 | - | IsSSL: inputs["container_protocol"] == "https", | 27 | + | IsSSL: inputs["container_protocol"] == "https", |
| 25 | - | ContainerName: inputs["container_name"], | 28 | + | ContainerName: inputs["container_name"], |
| 26 | - | ServiceName: inputs["container_subdomain"], | 29 | + | ServiceName: inputs["container_subdomain"], |
| 27 | - | Domain: inputs["container_domain"], | 30 | + | Domain: inputs["container_domain"], |
| 28 | - | ImageName: inputs["image_name"], | 31 | + | ImageName: inputs["image_name"], |
| 29 | - | ImageID: inputs["image_id"], | 32 | + | ImageID: inputs["image_id"], |
| 30 | - | Restart: inputs["restart"], | 33 | + | Restart: inputs["restart"], |
| 31 | - | Volumes: volumeSlice, | 34 | + | TraefikEntryPoint: inputs["traefik_entry_point"], |
| 32 | - | Environment: environmentSlice, | 35 | + | Volumes: volumeSlice, |
| 33 | - | Network: a.Config.ContainerEngine.Network, | 36 | + | Environment: environmentSlice, |
| 34 | - | PortMappings: portMappings, | 37 | + | Network: a.Config.ContainerEngine.Network, |
| 38 | + | PortMappings: portMappings, | |||
| 35 | } | 39 | } | ||
| 36 | 40 | ||||
| 37 | - | err = CreateTraefikLabels(¶ms, containerPort, a) | 41 | + | err = CreateTraefikLabels(¶ms, params.TraefikEntryPoint, a) |
| 38 | if err != nil { | 42 | if err != nil { | ||
| 39 | return docker.ContainerCommandParams{}, fmt.Errorf("error creating Traefik labels: %w", err) | 43 | return docker.ContainerCommandParams{}, fmt.Errorf("error creating Traefik labels: %w", err) | ||
| 40 | } | 44 | } | ||
M internal/templating/cmdparams/parsing.go +4 -0
| @@ -26,6 +26,10 @@ func ParseEnvironmentSlice(environmentStr string) []string { | |||
| 26 | 26 | ||
| 27 | 27 | // ParsePortMappingsSlice parses the port mappings string into a slice and a struct. | |
| 28 | 28 | func ParsePortMappingsSlice(portMappingsStr string) ([]docker.PortMapping, error) { | |
| 29 | + | if portMappingsStr == "" { | |
| 30 | + | return []docker.PortMapping{}, nil | |
| 31 | + | } | |
| 32 | + | ||
| 29 | 33 | portMappingsSliceRaw := strings.Split(portMappingsStr, ",") | |
| 30 | 34 | portMappingsSlice := make([]string, len(portMappingsSliceRaw)) | |
| 31 | 35 | for i, spec := range portMappingsSliceRaw { | |
| @@ -26,6 +26,10 @@ func ParseEnvironmentSlice(environmentStr string) []string { | |||||
| 26 | 26 | ||||
| 27 | // ParsePortMappingsSlice parses the port mappings string into a slice and a struct. | 27 | // ParsePortMappingsSlice parses the port mappings string into a slice and a struct. | ||
| 28 | func ParsePortMappingsSlice(portMappingsStr string) ([]docker.PortMapping, error) { | 28 | func ParsePortMappingsSlice(portMappingsStr string) ([]docker.PortMapping, error) { | ||
| 29 | + | if portMappingsStr == "" { | |||
| 30 | + | return []docker.PortMapping{}, nil | |||
| 31 | + | } | |||
| 32 | + | ||||
| 29 | portMappingsSliceRaw := strings.Split(portMappingsStr, ",") | 33 | portMappingsSliceRaw := strings.Split(portMappingsStr, ",") | ||
| 30 | portMappingsSlice := make([]string, len(portMappingsSliceRaw)) | 34 | portMappingsSlice := make([]string, len(portMappingsSliceRaw)) | ||
| 31 | for i, spec := range portMappingsSliceRaw { | 35 | for i, spec := range portMappingsSliceRaw { | ||
M internal/templating/cmdparams/traefik.go +3 -3
| @@ -8,7 +8,7 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | // CreateTraefikLabels creates Traefik labels based on the provided parameters. | |
| 11 | - | func CreateTraefikLabels(params *docker.ContainerCommandParams, portEntryPoint string, a *server.App) error { | |
| 11 | + | func CreateTraefikLabels(params *docker.ContainerCommandParams, containerPort string, a *server.App) error { | |
| 12 | 12 | entryPoint := a.Config.Traefik.EntryPoint | |
| 13 | 13 | if params.IsSSL { | |
| 14 | 14 | entryPoint = a.Config.Traefik.SecureEntryPoint | |
| @@ -21,12 +21,12 @@ func CreateTraefikLabels(params *docker.ContainerCommandParams, portEntryPoint s | |||
| 21 | 21 | "traefik.enable=true", | |
| 22 | 22 | fmt.Sprintf("%s.rule=Host(`%s.%s`)", baseRouter, params.ServiceName, params.Domain), | |
| 23 | 23 | fmt.Sprintf("%s.entrypoints=%s", baseRouter, entryPoint), | |
| 24 | - | fmt.Sprintf("%s.loadbalancer.server.port=%s", baseService, portEntryPoint), | |
| 24 | + | fmt.Sprintf("%s.loadbalancer.server.port=%s", baseService, containerPort), | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | 27 | resolver := a.Config.Traefik.Resolver | |
| 28 | 28 | if resolver == "" { | |
| 29 | - | fmt.Errorf("resolver not set in config") | |
| 29 | + | return fmt.Errorf("resolver not set in config") | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | if params.IsSSL { | |
| @@ -8,7 +8,7 @@ import ( | |||||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | // CreateTraefikLabels creates Traefik labels based on the provided parameters. | 10 | // CreateTraefikLabels creates Traefik labels based on the provided parameters. | ||
| 11 | - | func CreateTraefikLabels(params *docker.ContainerCommandParams, portEntryPoint string, a *server.App) error { | 11 | + | func CreateTraefikLabels(params *docker.ContainerCommandParams, containerPort string, a *server.App) error { |
| 12 | entryPoint := a.Config.Traefik.EntryPoint | 12 | entryPoint := a.Config.Traefik.EntryPoint | ||
| 13 | if params.IsSSL { | 13 | if params.IsSSL { | ||
| 14 | entryPoint = a.Config.Traefik.SecureEntryPoint | 14 | entryPoint = a.Config.Traefik.SecureEntryPoint | ||
| @@ -21,12 +21,12 @@ func CreateTraefikLabels(params *docker.ContainerCommandParams, portEntryPoint s | |||||
| 21 | "traefik.enable=true", | 21 | "traefik.enable=true", | ||
| 22 | fmt.Sprintf("%s.rule=Host(`%s.%s`)", baseRouter, params.ServiceName, params.Domain), | 22 | fmt.Sprintf("%s.rule=Host(`%s.%s`)", baseRouter, params.ServiceName, params.Domain), | ||
| 23 | fmt.Sprintf("%s.entrypoints=%s", baseRouter, entryPoint), | 23 | fmt.Sprintf("%s.entrypoints=%s", baseRouter, entryPoint), | ||
| 24 | - | fmt.Sprintf("%s.loadbalancer.server.port=%s", baseService, portEntryPoint), | 24 | + | fmt.Sprintf("%s.loadbalancer.server.port=%s", baseService, containerPort), |
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | resolver := a.Config.Traefik.Resolver | 27 | resolver := a.Config.Traefik.Resolver | ||
| 28 | if resolver == "" { | 28 | if resolver == "" { | ||
| 29 | - | fmt.Errorf("resolver not set in config") | 29 | + | return fmt.Errorf("resolver not set in config") |
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | if params.IsSSL { | 32 | if params.IsSSL { | ||
M internal/templating/models/html/fragments/containerlist.gohtml +2 -0
| @@ -7,6 +7,7 @@ | |||
| 7 | 7 | <tr class="text-center text-gray-400"> | |
| 8 | 8 | <th>{{index .Lang "manager" "content" "thName"}}</th> | |
| 9 | 9 | <th>{{index .Lang "manager" "content" "thPorts"}}</th> | |
| 10 | + | <th>{{index .Lang "manager" "content" "thEntryPoint"}}</th> | |
| 10 | 11 | <th>{{index .Lang "manager" "content" "thStatus"}}</th> | |
| 11 | 12 | <th>{{index .Lang "manager" "content" "thState"}}</th> | |
| 12 | 13 | <th>{{index .Lang "manager" "content" "thActions"}}</th> | |
| @@ -26,6 +27,7 @@ | |||
| 26 | 27 | > | |
| 27 | 28 | </td> | |
| 28 | 29 | <td class="port-container">{{ .Ports }}</td> | |
| 30 | + | <td class="entrypoint-container">{{.TraefikEntryPoint}}</td> | |
| 29 | 31 | <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td> | |
| 30 | 32 | <td style="color: {{.StateColor}};">{{.State}}</td> | |
| 31 | 33 | <td> | |
| @@ -7,6 +7,7 @@ | |||||
| 7 | <tr class="text-center text-gray-400"> | 7 | <tr class="text-center text-gray-400"> | ||
| 8 | <th>{{index .Lang "manager" "content" "thName"}}</th> | 8 | <th>{{index .Lang "manager" "content" "thName"}}</th> | ||
| 9 | <th>{{index .Lang "manager" "content" "thPorts"}}</th> | 9 | <th>{{index .Lang "manager" "content" "thPorts"}}</th> | ||
| 10 | + | <th>{{index .Lang "manager" "content" "thEntryPoint"}}</th> | |||
| 10 | <th>{{index .Lang "manager" "content" "thStatus"}}</th> | 11 | <th>{{index .Lang "manager" "content" "thStatus"}}</th> | ||
| 11 | <th>{{index .Lang "manager" "content" "thState"}}</th> | 12 | <th>{{index .Lang "manager" "content" "thState"}}</th> | ||
| 12 | <th>{{index .Lang "manager" "content" "thActions"}}</th> | 13 | <th>{{index .Lang "manager" "content" "thActions"}}</th> | ||
| @@ -26,6 +27,7 @@ | |||||
| 26 | > | 27 | > | ||
| 27 | </td> | 28 | </td> | ||
| 28 | <td class="port-container">{{ .Ports }}</td> | 29 | <td class="port-container">{{ .Ports }}</td> | ||
| 30 | + | <td class="entrypoint-container">{{.TraefikEntryPoint}}</td> | |||
| 29 | <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td> | 31 | <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td> | ||
| 30 | <td style="color: {{.StateColor}};">{{.State}}</td> | 32 | <td style="color: {{.StateColor}};">{{.State}}</td> | ||
| 31 | <td> | 33 | <td> | ||
M internal/templating/models/html/fragments/createcontainer.gohtml +34 -86
| @@ -1,64 +1,38 @@ | |||
| 1 | - | <div | |
| 2 | - | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | |
| 3 | - | hx-ext="response-targets" | |
| 4 | - | > | |
| 1 | + | <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets"> | |
| 5 | 2 | <!-- Left Side: Docker Run Options --> | |
| 6 | 3 | <div class="flex-grow bg-slate-100 p-4 rounded-md"> | |
| 7 | 4 | <h2 class="font-bold pb-4">Deploy a container based on {{.ImageName}}</h2> | |
| 8 | 5 | {{if .ErrorMessage}} | |
| 9 | 6 | <div class="text-red-500">{{.ErrorMessage}}</div> | |
| 10 | 7 | {{ end }} | |
| 11 | - | <form | |
| 12 | - | action="/create-container/{{.ShortID}}" | |
| 13 | - | method="post" | |
| 14 | - | hx-post="/htmx/create-container/{{.ShortID}}" | |
| 15 | - | hx-swap="innerHTML" | |
| 16 | - | hx-target-*="#error-message-create-container-{{.ShortID}}" | |
| 17 | - | > | |
| 8 | + | <form action="/create-container/{{.ShortID}}" method="post" hx-post="/htmx/create-container/{{.ShortID}}" | |
| 9 | + | hx-swap="innerHTML" hx-target-*="#error-message-create-container-{{.ShortID}}"> | |
| 18 | 10 | <!-- Image Name --> | |
| 19 | 11 | <div class="flex space-x-4 pb-4"> | |
| 20 | 12 | <div class="w-1/3"> | |
| 21 | 13 | <label class="block">Image name</label> | |
| 22 | - | <input | |
| 23 | - | type="text" | |
| 24 | - | name="image_name" | |
| 25 | - | class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |
| 26 | - | value="{{.ImageName}}" | |
| 27 | - | readonly | |
| 28 | - | /> | |
| 14 | + | <input type="text" name="image_name" class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |
| 15 | + | value="{{.ImageName}}" readonly /> | |
| 29 | 16 | </div> | |
| 30 | 17 | ||
| 31 | 18 | <div class="w-2/3"> | |
| 32 | 19 | <label class="block">Image ID</label> | |
| 33 | - | <input | |
| 34 | - | type="text" | |
| 35 | - | name="image_id" | |
| 36 | - | class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |
| 37 | - | value="{{.ImageID}}" | |
| 38 | - | readonly | |
| 39 | - | /> | |
| 20 | + | <input type="text" name="image_id" class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |
| 21 | + | value="{{.ImageID}}" readonly /> | |
| 40 | 22 | </div> | |
| 41 | 23 | </div> | |
| 42 | 24 | ||
| 43 | 25 | <!-- Container Name --> | |
| 44 | 26 | <div class="pb-4"> | |
| 45 | 27 | <label class="block">Container name</label> | |
| 46 | - | <input | |
| 47 | - | type="text" | |
| 48 | - | name="container_name" | |
| 49 | - | class="w-full p-4 rounded-md" | |
| 50 | - | value="{{.ContainerName}}" | |
| 51 | - | /> | |
| 28 | + | <input type="text" name="container_name" class="w-full p-4 rounded-md" value="{{.ContainerName}}" /> | |
| 52 | 29 | </div> | |
| 53 | 30 | <!-- Http(s) / Subdomain / Domain --> | |
| 54 | 31 | <div class="flex pb-4 space-x-4"> | |
| 55 | 32 | <!-- Protocol --> | |
| 56 | 33 | <div class="w-1/4"> | |
| 57 | 34 | <label class="block">Protocol</label> | |
| 58 | - | <select | |
| 59 | - | name="container_protocol" | |
| 60 | - | class="w-full p-4 rounded-md border-2 bg-slate-200 border-gray-300" | |
| 61 | - | > | |
| 35 | + | <select name="container_protocol" class="w-full p-4 rounded-md border-2 bg-slate-200 border-gray-300"> | |
| 62 | 36 | <option value="https">https</option> | |
| 63 | 37 | <option value="http">http</option> | |
| 64 | 38 | </select> | |
| @@ -67,69 +41,50 @@ | |||
| 67 | 41 | <!-- Subdomain --> | |
| 68 | 42 | <div class="w-1/3"> | |
| 69 | 43 | <label class="block">Subdomain</label> | |
| 70 | - | <input | |
| 71 | - | type="text" | |
| 72 | - | name="container_subdomain" | |
| 73 | - | class="w-full p-4 rounded-md" | |
| 74 | - | value="{{.ContainerSubdomain}}" | |
| 75 | - | /> | |
| 44 | + | <input type="text" name="container_subdomain" class="w-full p-4 rounded-md" value="{{.ContainerSubdomain}}" /> | |
| 76 | 45 | </div> | |
| 77 | 46 | ||
| 78 | 47 | <!-- Domain --> | |
| 79 | 48 | <div class="w-5/12"> | |
| 80 | 49 | <label class="block">Domain</label> | |
| 81 | - | <input | |
| 82 | - | type="text" | |
| 83 | - | name="container_domain" | |
| 84 | - | class="w-full p-4 rounded-md" | |
| 85 | - | value="{{.ContainerDomain}}" | |
| 86 | - | /> | |
| 50 | + | <input type="text" name="container_domain" class="w-full p-4 rounded-md" value="{{.ContainerDomain}}" /> | |
| 87 | 51 | </div> | |
| 88 | 52 | </div> | |
| 89 | - | <!-- Ports --> | |
| 53 | + | <!-- Exposed Ports --> | |
| 90 | 54 | <div class="pb-4"> | |
| 91 | - | <label class="block">Ports</label> | |
| 92 | - | <span class="text-gray-500" | |
| 93 | - | >eg: 80:80/tcp (You can specify multiple ports by separating them with | |
| 94 | - | a comma)</span | |
| 95 | - | > | |
| 96 | - | <input | |
| 97 | - | type="text" | |
| 98 | - | name="ports" | |
| 99 | - | class="w-full p-4 rounded-md" | |
| 100 | - | value="{{.Ports}}" | |
| 101 | - | /> | |
| 55 | + | <label class="block">Exposed Ports (Optional)</label> | |
| 56 | + | <span class="text-gray-500"> | |
| 57 | + | eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma) | |
| 58 | + | </span> | |
| 59 | + | <input type="text" name="ports" class="w-full p-4 rounded-md" value="{{.Ports}}" /> | |
| 102 | 60 | </div> | |
| 103 | 61 | ||
| 62 | + | <!-- Traefik Entry Point --> | |
| 63 | + | <div class="pb-4"> | |
| 64 | + | <label class="block">Traefik Entry Point (Required)</label> | |
| 65 | + | <span class="text-gray-500"> | |
| 66 | + | Specify the internal container port used in Traefik's loadbalancer labels | |
| 67 | + | </span> | |
| 68 | + | <input type="text" name="traefik_entry_point" class="w-full p-4 rounded-md" value="{{.TraefikEntryPoint}}" required /> | |
| 69 | + | </div> | |
| 70 | + | ||
| 104 | 71 | <!-- Volumes --> | |
| 105 | 72 | <div class="pb-4"> | |
| 106 | 73 | <label class="block">Volumes</label> | |
| 107 | 74 | <span class="text-gray-500">e.g. ./data:/container/data </span> | |
| 108 | - | <input | |
| 109 | - | type="text" | |
| 110 | - | name="volumes" | |
| 111 | - | class="w-full p-4 rounded-md" | |
| 112 | - | value="{{.Volumes}}" | |
| 113 | - | /> | |
| 75 | + | <input type="text" name="volumes" class="w-full p-4 rounded-md" value="{{.Volumes}}" /> | |
| 114 | 76 | </div> | |
| 115 | 77 | ||
| 116 | 78 | <div class="pb-4"> | |
| 117 | 79 | <label class="block">Environment variables</label> | |
| 118 | 80 | <span class="text-gray-500">e.g. KEY=VALUE </span> | |
| 119 | - | <textarea | |
| 120 | - | name="environment_variables" | |
| 121 | - | class="w-full p-4 rounded-md" | |
| 122 | - | rows="4" | |
| 123 | - | >{{.EnvironmentVariables}}</textarea | |
| 124 | - | > | |
| 81 | + | <textarea name="environment_variables" class="w-full p-4 rounded-md" | |
| 82 | + | rows="4">{{.EnvironmentVariables}}</textarea> | |
| 125 | 83 | </div> | |
| 126 | 84 | <!-- Restart: always/on-failure/only when stopped/never --> | |
| 127 | 85 | <div class="pb-4"> | |
| 128 | 86 | <label class="block">Restart</label> | |
| 129 | - | <select | |
| 130 | - | name="restart" | |
| 131 | - | class="w-full p-4 rounded-md border-2 bg-gray-100 border-gray-300" | |
| 132 | - | > | |
| 87 | + | <select name="restart" class="w-full p-4 rounded-md border-2 bg-gray-100 border-gray-300"> | |
| 133 | 88 | <option value="always">Always</option> | |
| 134 | 89 | <option value="on-failure">On failure</option> | |
| 135 | 90 | <option value="unless-stopped">Unless stopped</option> | |
| @@ -137,16 +92,9 @@ | |||
| 137 | 92 | </select> | |
| 138 | 93 | </div> | |
| 139 | 94 | ||
| 140 | - | <input | |
| 141 | - | type="submit" | |
| 142 | - | value="Create" | |
| 143 | - | id="create-container-submit" | |
| 144 | - | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |
| 145 | - | /> | |
| 95 | + | <input type="submit" value="Create" id="create-container-submit" | |
| 96 | + | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" /> | |
| 146 | 97 | </form> | |
| 147 | - | <div | |
| 148 | - | id="error-message-create-container-{{.ShortID}}" | |
| 149 | - | class="text-red-500" | |
| 150 | - | ></div> | |
| 98 | + | <div id="error-message-create-container-{{.ShortID}}" class="text-red-500"></div> | |
| 151 | 99 | </div> | |
| 152 | - | </div> | |
| 100 | + | </div>⏎̸ | |
| @@ -1,64 +1,38 @@ | |||||
| 1 | - | <div | 1 | + | <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets"> |
| 2 | - | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" | |||
| 3 | - | hx-ext="response-targets" | |||
| 4 | - | > | |||
| 5 | <!-- Left Side: Docker Run Options --> | 2 | <!-- Left Side: Docker Run Options --> | ||
| 6 | <div class="flex-grow bg-slate-100 p-4 rounded-md"> | 3 | <div class="flex-grow bg-slate-100 p-4 rounded-md"> | ||
| 7 | <h2 class="font-bold pb-4">Deploy a container based on {{.ImageName}}</h2> | 4 | <h2 class="font-bold pb-4">Deploy a container based on {{.ImageName}}</h2> | ||
| 8 | {{if .ErrorMessage}} | 5 | {{if .ErrorMessage}} | ||
| 9 | <div class="text-red-500">{{.ErrorMessage}}</div> | 6 | <div class="text-red-500">{{.ErrorMessage}}</div> | ||
| 10 | {{ end }} | 7 | {{ end }} | ||
| 11 | - | <form | 8 | + | <form action="/create-container/{{.ShortID}}" method="post" hx-post="/htmx/create-container/{{.ShortID}}" |
| 12 | - | action="/create-container/{{.ShortID}}" | 9 | + | hx-swap="innerHTML" hx-target-*="#error-message-create-container-{{.ShortID}}"> |
| 13 | - | method="post" | |||
| 14 | - | hx-post="/htmx/create-container/{{.ShortID}}" | |||
| 15 | - | hx-swap="innerHTML" | |||
| 16 | - | hx-target-*="#error-message-create-container-{{.ShortID}}" | |||
| 17 | - | > | |||
| 18 | <!-- Image Name --> | 10 | <!-- Image Name --> | ||
| 19 | <div class="flex space-x-4 pb-4"> | 11 | <div class="flex space-x-4 pb-4"> | ||
| 20 | <div class="w-1/3"> | 12 | <div class="w-1/3"> | ||
| 21 | <label class="block">Image name</label> | 13 | <label class="block">Image name</label> | ||
| 22 | - | <input | 14 | + | <input type="text" name="image_name" class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" |
| 23 | - | type="text" | 15 | + | value="{{.ImageName}}" readonly /> |
| 24 | - | name="image_name" | |||
| 25 | - | class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |||
| 26 | - | value="{{.ImageName}}" | |||
| 27 | - | readonly | |||
| 28 | - | /> | |||
| 29 | </div> | 16 | </div> | ||
| 30 | 17 | ||||
| 31 | <div class="w-2/3"> | 18 | <div class="w-2/3"> | ||
| 32 | <label class="block">Image ID</label> | 19 | <label class="block">Image ID</label> | ||
| 33 | - | <input | 20 | + | <input type="text" name="image_id" class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" |
| 34 | - | type="text" | 21 | + | value="{{.ImageID}}" readonly /> |
| 35 | - | name="image_id" | |||
| 36 | - | class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2" | |||
| 37 | - | value="{{.ImageID}}" | |||
| 38 | - | readonly | |||
| 39 | - | /> | |||
| 40 | </div> | 22 | </div> | ||
| 41 | </div> | 23 | </div> | ||
| 42 | 24 | ||||
| 43 | <!-- Container Name --> | 25 | <!-- Container Name --> | ||
| 44 | <div class="pb-4"> | 26 | <div class="pb-4"> | ||
| 45 | <label class="block">Container name</label> | 27 | <label class="block">Container name</label> | ||
| 46 | - | <input | 28 | + | <input type="text" name="container_name" class="w-full p-4 rounded-md" value="{{.ContainerName}}" /> |
| 47 | - | type="text" | |||
| 48 | - | name="container_name" | |||
| 49 | - | class="w-full p-4 rounded-md" | |||
| 50 | - | value="{{.ContainerName}}" | |||
| 51 | - | /> | |||
| 52 | </div> | 29 | </div> | ||
| 53 | <!-- Http(s) / Subdomain / Domain --> | 30 | <!-- Http(s) / Subdomain / Domain --> | ||
| 54 | <div class="flex pb-4 space-x-4"> | 31 | <div class="flex pb-4 space-x-4"> | ||
| 55 | <!-- Protocol --> | 32 | <!-- Protocol --> | ||
| 56 | <div class="w-1/4"> | 33 | <div class="w-1/4"> | ||
| 57 | <label class="block">Protocol</label> | 34 | <label class="block">Protocol</label> | ||
| 58 | - | <select | 35 | + | <select name="container_protocol" class="w-full p-4 rounded-md border-2 bg-slate-200 border-gray-300"> |
| 59 | - | name="container_protocol" | |||
| 60 | - | class="w-full p-4 rounded-md border-2 bg-slate-200 border-gray-300" | |||
| 61 | - | > | |||
| 62 | <option value="https">https</option> | 36 | <option value="https">https</option> | ||
| 63 | <option value="http">http</option> | 37 | <option value="http">http</option> | ||
| 64 | </select> | 38 | </select> | ||
| @@ -67,69 +41,50 @@ | |||||
| 67 | <!-- Subdomain --> | 41 | <!-- Subdomain --> | ||
| 68 | <div class="w-1/3"> | 42 | <div class="w-1/3"> | ||
| 69 | <label class="block">Subdomain</label> | 43 | <label class="block">Subdomain</label> | ||
| 70 | - | <input | 44 | + | <input type="text" name="container_subdomain" class="w-full p-4 rounded-md" value="{{.ContainerSubdomain}}" /> |
| 71 | - | type="text" | |||
| 72 | - | name="container_subdomain" | |||
| 73 | - | class="w-full p-4 rounded-md" | |||
| 74 | - | value="{{.ContainerSubdomain}}" | |||
| 75 | - | /> | |||
| 76 | </div> | 45 | </div> | ||
| 77 | 46 | ||||
| 78 | <!-- Domain --> | 47 | <!-- Domain --> | ||
| 79 | <div class="w-5/12"> | 48 | <div class="w-5/12"> | ||
| 80 | <label class="block">Domain</label> | 49 | <label class="block">Domain</label> | ||
| 81 | - | <input | 50 | + | <input type="text" name="container_domain" class="w-full p-4 rounded-md" value="{{.ContainerDomain}}" /> |
| 82 | - | type="text" | |||
| 83 | - | name="container_domain" | |||
| 84 | - | class="w-full p-4 rounded-md" | |||
| 85 | - | value="{{.ContainerDomain}}" | |||
| 86 | - | /> | |||
| 87 | </div> | 51 | </div> | ||
| 88 | </div> | 52 | </div> | ||
| 89 | - | <!-- Ports --> | 53 | + | <!-- Exposed Ports --> |
| 90 | <div class="pb-4"> | 54 | <div class="pb-4"> | ||
| 91 | - | <label class="block">Ports</label> | 55 | + | <label class="block">Exposed Ports (Optional)</label> |
| 92 | - | <span class="text-gray-500" | 56 | + | <span class="text-gray-500"> |
| 93 | - | >eg: 80:80/tcp (You can specify multiple ports by separating them with | 57 | + | eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma) |
| 94 | - | a comma)</span | 58 | + | </span> |
| 95 | - | > | 59 | + | <input type="text" name="ports" class="w-full p-4 rounded-md" value="{{.Ports}}" /> |
| 96 | - | <input | |||
| 97 | - | type="text" | |||
| 98 | - | name="ports" | |||
| 99 | - | class="w-full p-4 rounded-md" | |||
| 100 | - | value="{{.Ports}}" | |||
| 101 | - | /> | |||
| 102 | </div> | 60 | </div> | ||
| 103 | 61 | ||||
| 62 | + | <!-- Traefik Entry Point --> | |||
| 63 | + | <div class="pb-4"> | |||
| 64 | + | <label class="block">Traefik Entry Point (Required)</label> | |||
| 65 | + | <span class="text-gray-500"> | |||
| 66 | + | Specify the internal container port used in Traefik's loadbalancer labels | |||
| 67 | + | </span> | |||
| 68 | + | <input type="text" name="traefik_entry_point" class="w-full p-4 rounded-md" value="{{.TraefikEntryPoint}}" required /> | |||
| 69 | + | </div> | |||
| 70 | + | ||||
| 104 | <!-- Volumes --> | 71 | <!-- Volumes --> | ||
| 105 | <div class="pb-4"> | 72 | <div class="pb-4"> | ||
| 106 | <label class="block">Volumes</label> | 73 | <label class="block">Volumes</label> | ||
| 107 | <span class="text-gray-500">e.g. ./data:/container/data </span> | 74 | <span class="text-gray-500">e.g. ./data:/container/data </span> | ||
| 108 | - | <input | 75 | + | <input type="text" name="volumes" class="w-full p-4 rounded-md" value="{{.Volumes}}" /> |
| 109 | - | type="text" | |||
| 110 | - | name="volumes" | |||
| 111 | - | class="w-full p-4 rounded-md" | |||
| 112 | - | value="{{.Volumes}}" | |||
| 113 | - | /> | |||
| 114 | </div> | 76 | </div> | ||
| 115 | 77 | ||||
| 116 | <div class="pb-4"> | 78 | <div class="pb-4"> | ||
| 117 | <label class="block">Environment variables</label> | 79 | <label class="block">Environment variables</label> | ||
| 118 | <span class="text-gray-500">e.g. KEY=VALUE </span> | 80 | <span class="text-gray-500">e.g. KEY=VALUE </span> | ||
| 119 | - | <textarea | 81 | + | <textarea name="environment_variables" class="w-full p-4 rounded-md" |
| 120 | - | name="environment_variables" | 82 | + | rows="4">{{.EnvironmentVariables}}</textarea> |
| 121 | - | class="w-full p-4 rounded-md" | |||
| 122 | - | rows="4" | |||
| 123 | - | >{{.EnvironmentVariables}}</textarea | |||
| 124 | - | > | |||
| 125 | </div> | 83 | </div> | ||
| 126 | <!-- Restart: always/on-failure/only when stopped/never --> | 84 | <!-- Restart: always/on-failure/only when stopped/never --> | ||
| 127 | <div class="pb-4"> | 85 | <div class="pb-4"> | ||
| 128 | <label class="block">Restart</label> | 86 | <label class="block">Restart</label> | ||
| 129 | - | <select | 87 | + | <select name="restart" class="w-full p-4 rounded-md border-2 bg-gray-100 border-gray-300"> |
| 130 | - | name="restart" | |||
| 131 | - | class="w-full p-4 rounded-md border-2 bg-gray-100 border-gray-300" | |||
| 132 | - | > | |||
| 133 | <option value="always">Always</option> | 88 | <option value="always">Always</option> | ||
| 134 | <option value="on-failure">On failure</option> | 89 | <option value="on-failure">On failure</option> | ||
| 135 | <option value="unless-stopped">Unless stopped</option> | 90 | <option value="unless-stopped">Unless stopped</option> | ||
| @@ -137,16 +92,9 @@ | |||||
| 137 | </select> | 92 | </select> | ||
| 138 | </div> | 93 | </div> | ||
| 139 | 94 | ||||
| 140 | - | <input | 95 | + | <input type="submit" value="Create" id="create-container-submit" |
| 141 | - | type="submit" | 96 | + | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" /> |
| 142 | - | value="Create" | |||
| 143 | - | id="create-container-submit" | |||
| 144 | - | class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |||
| 145 | - | /> | |||
| 146 | </form> | 97 | </form> | ||
| 147 | - | <div | 98 | + | <div id="error-message-create-container-{{.ShortID}}" class="text-red-500"></div> |
| 148 | - | id="error-message-create-container-{{.ShortID}}" | |||
| 149 | - | class="text-red-500" | |||
| 150 | - | ></div> | |||
| 151 | </div> | 99 | </div> | ||
| 152 | - | </div> | 100 | + | </div>⏎̸ |
M internal/templating/models/txt/locstrings.yml +4 -2
| @@ -27,10 +27,11 @@ en: | |||
| 27 | 27 | confirm-container-stop: Are you sure you want to stop this container? | |
| 28 | 28 | no-containers: No available containers. | |
| 29 | 29 | thName: Name | |
| 30 | - | thPorts: Ports | |
| 30 | + | thPorts: Exposed Ports | |
| 31 | 31 | thStatus: Status | |
| 32 | 32 | thState: State | |
| 33 | 33 | thActions: Actions | |
| 34 | + | thEntryPoint: "Entry Point" | |
| 34 | 35 | settings: | |
| 35 | 36 | meta: | |
| 36 | 37 | title: Gordon - Settings | |
| @@ -66,10 +67,11 @@ fr: | |||
| 66 | 67 | confirm-container-stop: Êtes-vous sûr de vouloir arrêter ce conteneur? | |
| 67 | 68 | no-containers: Aucun conteneur disponible. | |
| 68 | 69 | thName: Nom | |
| 69 | - | thPorts: Ports | |
| 70 | + | thPorts: Ports exposés | |
| 70 | 71 | thStatus: Statut | |
| 71 | 72 | thState: État | |
| 72 | 73 | thActions: Actions | |
| 74 | + | thEntryPoint: "Point d'entrée" | |
| 73 | 75 | settings: | |
| 74 | 76 | meta: | |
| 75 | 77 | title: Gordon - Paramètres | |
| @@ -27,10 +27,11 @@ en: | |||||
| 27 | confirm-container-stop: Are you sure you want to stop this container? | 27 | confirm-container-stop: Are you sure you want to stop this container? | ||
| 28 | no-containers: No available containers. | 28 | no-containers: No available containers. | ||
| 29 | thName: Name | 29 | thName: Name | ||
| 30 | - | thPorts: Ports | 30 | + | thPorts: Exposed Ports |
| 31 | thStatus: Status | 31 | thStatus: Status | ||
| 32 | thState: State | 32 | thState: State | ||
| 33 | thActions: Actions | 33 | thActions: Actions | ||
| 34 | + | thEntryPoint: "Entry Point" | |||
| 34 | settings: | 35 | settings: | ||
| 35 | meta: | 36 | meta: | ||
| 36 | title: Gordon - Settings | 37 | title: Gordon - Settings | ||
| @@ -66,10 +67,11 @@ fr: | |||||
| 66 | confirm-container-stop: Êtes-vous sûr de vouloir arrêter ce conteneur? | 67 | confirm-container-stop: Êtes-vous sûr de vouloir arrêter ce conteneur? | ||
| 67 | no-containers: Aucun conteneur disponible. | 68 | no-containers: Aucun conteneur disponible. | ||
| 68 | thName: Nom | 69 | thName: Nom | ||
| 69 | - | thPorts: Ports | 70 | + | thPorts: Ports exposés |
| 70 | thStatus: Statut | 71 | thStatus: Statut | ||
| 71 | thState: État | 72 | thState: État | ||
| 72 | thActions: Actions | 73 | thActions: Actions | ||
| 74 | + | thEntryPoint: "Point d'entrée" | |||
| 73 | settings: | 75 | settings: | ||
| 74 | meta: | 76 | meta: | ||
| 75 | title: Gordon - Paramètres | 77 | title: Gordon - Paramètres | ||
M internal/webui/public/libs/htmx.min.js +1 -1 generated file: collapsed
M internal/webui/public/libs/hyperscript.min.js +1 -1 generated file: collapsed
M pkg/docker/containers.go +15 -14
| @@ -15,20 +15,21 @@ import ( | |||
| 15 | 15 | ) | |
| 16 | 16 | ||
| 17 | 17 | type ContainerCommandParams struct { | |
| 18 | - | ContainerName string | |
| 19 | - | ContainerHost string | |
| 20 | - | Domain string | |
| 21 | - | ServiceName string | |
| 22 | - | IsSSL bool | |
| 23 | - | EnvVar string | |
| 24 | - | ImageName string | |
| 25 | - | ImageID string | |
| 26 | - | PortMappings []PortMapping | |
| 27 | - | Volumes []string | |
| 28 | - | Labels []string | |
| 29 | - | Network string | |
| 30 | - | Restart string | |
| 31 | - | Environment []string | |
| 18 | + | ContainerName string | |
| 19 | + | ContainerHost string | |
| 20 | + | Domain string | |
| 21 | + | ServiceName string | |
| 22 | + | IsSSL bool | |
| 23 | + | EnvVar string | |
| 24 | + | ImageName string | |
| 25 | + | ImageID string | |
| 26 | + | PortMappings []PortMapping | |
| 27 | + | Volumes []string | |
| 28 | + | Labels []string | |
| 29 | + | Network string | |
| 30 | + | Restart string | |
| 31 | + | Environment []string | |
| 32 | + | TraefikEntryPoint string | |
| 32 | 33 | } | |
| 33 | 34 | ||
| 34 | 35 | type PortMapping struct { | |
| @@ -15,20 +15,21 @@ import ( | |||||
| 15 | ) | 15 | ) | ||
| 16 | 16 | ||||
| 17 | type ContainerCommandParams struct { | 17 | type ContainerCommandParams struct { | ||
| 18 | - | ContainerName string | 18 | + | ContainerName string |
| 19 | - | ContainerHost string | 19 | + | ContainerHost string |
| 20 | - | Domain string | 20 | + | Domain string |
| 21 | - | ServiceName string | 21 | + | ServiceName string |
| 22 | - | IsSSL bool | 22 | + | IsSSL bool |
| 23 | - | EnvVar string | 23 | + | EnvVar string |
| 24 | - | ImageName string | 24 | + | ImageName string |
| 25 | - | ImageID string | 25 | + | ImageID string |
| 26 | - | PortMappings []PortMapping | 26 | + | PortMappings []PortMapping |
| 27 | - | Volumes []string | 27 | + | Volumes []string |
| 28 | - | Labels []string | 28 | + | Labels []string |
| 29 | - | Network string | 29 | + | Network string |
| 30 | - | Restart string | 30 | + | Restart string |
| 31 | - | Environment []string | 31 | + | Environment []string |
| 32 | + | TraefikEntryPoint string | |||
| 32 | } | 33 | } | ||
| 33 | 34 | ||||
| 34 | type PortMapping struct { | 35 | type PortMapping struct { | ||