feat(wip): docker ui only

This commit is contained in:
Jacky 2025-04-20 22:02:29 +08:00
parent c62fd25b2e
commit d4a4ed1e1c
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
43 changed files with 1269 additions and 372 deletions

View file

@ -45,7 +45,11 @@ func Save(absPath string, content string, cfg *model.Config) (err error) {
return
}
output := nginx.Reload()
output, err := nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) >= nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}

22
internal/docker/docker.go Normal file
View file

@ -0,0 +1,22 @@
package docker
import (
"context"
"github.com/docker/docker/client"
)
// Initialize Docker client from environment variables
func initClient() (cli *client.Client, err error) {
cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return
}
// Optionally ping the server to ensure the connection is valid
_, err = cli.Ping(context.Background())
if err != nil {
return
}
return
}

15
internal/docker/errors.go Normal file
View file

@ -0,0 +1,15 @@
package docker
import "github.com/uozi-tech/cosy"
var (
e = cosy.NewErrorScope("docker")
ErrClientNotInitialized = e.New(500001, "docker client not initialized")
ErrFailedToExec = e.New(500002, "failed to exec command: {0}")
ErrFailedToAttach = e.New(500003, "failed to attach to exec instance: {0}")
ErrReadOutput = e.New(500004, "failed to read output: {0}")
ErrExitUnexpected = e.New(500005, "command exited with unexpected exit code: {0}, error: {1}")
ErrContainerStatusUnknown = e.New(500006, "container status unknown")
ErrInspectContainer = e.New(500007, "failed to inspect container: {0}")
ErrNginxNotRunningInAnotherContainer = e.New(500008, "nginx is not running in another container")
)

80
internal/docker/exec.go Normal file
View file

@ -0,0 +1,80 @@
package docker
import (
"bytes"
"context"
"io"
"strconv"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
)
// Exec executes a command in a specific container and returns the output.
func Exec(ctx context.Context, command []string) (string, error) {
if !settings.NginxSettings.RunningInAnotherContainer() {
return "", ErrNginxNotRunningInAnotherContainer
}
cli, err := initClient()
if err != nil {
return "", cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
}
defer cli.Close()
execConfig := container.ExecOptions{
AttachStdout: true,
AttachStderr: true, // Also attach stderr to capture errors from the command
Cmd: command,
}
// Create the exec instance
execCreateResp, err := cli.ContainerExecCreate(ctx, settings.NginxSettings.ContainerName, execConfig)
if err != nil {
return "", cosy.WrapErrorWithParams(ErrFailedToExec, err.Error())
}
execID := execCreateResp.ID
// Attach to the exec instance
hijackedResp, err := cli.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{})
if err != nil {
return "", cosy.WrapErrorWithParams(ErrFailedToAttach, err.Error())
}
defer hijackedResp.Close()
// Read the output
var outBuf, errBuf bytes.Buffer
outputDone := make(chan error)
go func() {
// stdcopy.StdCopy demultiplexes the stream into two buffers
_, err = stdcopy.StdCopy(&outBuf, &errBuf, hijackedResp.Reader)
outputDone <- err
}()
select {
case err := <-outputDone:
if err != nil && err != io.EOF { // io.EOF is expected when the stream finishes
return "", cosy.WrapErrorWithParams(ErrReadOutput, err.Error())
}
case <-ctx.Done():
return "", cosy.WrapErrorWithParams(ErrReadOutput, ctx.Err().Error())
}
// Optionally inspect the exec process to check the exit code
execInspectResp, err := cli.ContainerExecInspect(ctx, execID)
logger.Debug("docker exec result", outBuf.String(), errBuf.String())
if err != nil {
return "", cosy.WrapErrorWithParams(ErrExitUnexpected, err.Error())
} else if execInspectResp.ExitCode != 0 {
// Command exited with a non-zero status code. Return stderr as part of the error.
return outBuf.String(), cosy.WrapErrorWithParams(ErrExitUnexpected, strconv.Itoa(execInspectResp.ExitCode), errBuf.String())
}
// Return stdout if successful
return outBuf.String(), nil
}

View file

@ -0,0 +1,29 @@
package docker
import (
"context"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/uozi-tech/cosy/logger"
)
// StatPath checks if a path exists in the container
func StatPath(path string) bool {
if !settings.NginxSettings.RunningInAnotherContainer() {
return false
}
cli, err := initClient()
if err != nil {
return false
}
defer cli.Close()
_, err = cli.ContainerStatPath(context.Background(), settings.NginxSettings.ContainerName, path)
if err != nil {
logger.Error("Failed to stat path", "error", err)
return false
}
return true
}

58
internal/docker/status.go Normal file
View file

@ -0,0 +1,58 @@
package docker
import (
"context"
"github.com/docker/docker/client"
"github.com/uozi-tech/cosy"
)
type ContainerStatus int
const (
ContainerStatusCreated ContainerStatus = iota
ContainerStatusRunning
ContainerStatusPaused
ContainerStatusRestarting
ContainerStatusRemoving
ContainerStatusExited
ContainerStatusDead
ContainerStatusUnknown
ContainerStatusNotFound
)
var (
containerStatusMap = map[string]ContainerStatus{
"created": ContainerStatusCreated,
"running": ContainerStatusRunning,
"paused": ContainerStatusPaused,
"restarting": ContainerStatusRestarting,
"removing": ContainerStatusRemoving,
"exited": ContainerStatusExited,
"dead": ContainerStatusDead,
}
)
// GetContainerStatus checks the status of a given container.
func GetContainerStatus(ctx context.Context, containerID string) (ContainerStatus, error) {
cli, err := initClient()
if err != nil {
return ContainerStatusUnknown, cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
}
defer cli.Close()
containerJSON, err := cli.ContainerInspect(ctx, containerID)
if err != nil {
if client.IsErrNotFound(err) {
return ContainerStatusNotFound, nil // Container doesn't exist
}
return ContainerStatusUnknown, cosy.WrapErrorWithParams(ErrInspectContainer, err.Error())
}
// Can be one of "created", "running", "paused", "restarting", "removing", "exited", or "dead"
status, ok := containerStatusMap[containerJSON.State.Status]
if !ok {
return ContainerStatusUnknown, ErrContainerStatusUnknown
}
return status, nil
}

View file

@ -5,4 +5,5 @@ import "github.com/uozi-tech/cosy"
var (
e = cosy.NewErrorScope("nginx")
ErrBlockIsNil = e.New(50001, "block is nil")
ErrReloadFailed = e.New(50002, "reload nginx failed: {0}")
)

28
internal/nginx/exec.go Normal file
View file

@ -0,0 +1,28 @@
package nginx
import (
"context"
"os/exec"
"github.com/0xJacky/Nginx-UI/internal/docker"
"github.com/0xJacky/Nginx-UI/settings"
)
func execShell(cmd string) (stdOut string, stdErr error) {
return execCommand("/bin/sh", "-c", cmd)
}
func execCommand(name string, cmd ...string) (stdOut string, stdErr error) {
switch settings.NginxSettings.RunningInAnotherContainer() {
case true:
cmd = append([]string{name}, cmd...)
stdOut, stdErr = docker.Exec(context.Background(), cmd)
case false:
bytes, err := exec.Command(name, cmd...).CombinedOutput()
stdOut = string(bytes)
if err != nil {
stdErr = err
}
}
return
}

View file

@ -2,46 +2,41 @@ package nginx
import (
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/0xJacky/Nginx-UI/internal/docker"
"github.com/0xJacky/Nginx-UI/settings"
)
var (
mutex sync.Mutex
lastOutput string
lastStdOut string
lastStdErr error
)
func TestConf() (out string) {
// TestConfig tests the nginx config
func TestConfig() (stdOut string, stdErr error) {
mutex.Lock()
defer mutex.Unlock()
if settings.NginxSettings.TestConfigCmd != "" {
out = execShell(settings.NginxSettings.TestConfigCmd)
return
return execShell(settings.NginxSettings.TestConfigCmd)
}
out = execCommand("nginx", "-t")
return
return execCommand("nginx", "-t")
}
func Reload() (out string) {
// Reload reloads the nginx
func Reload() (stdOut string, stdErr error) {
mutex.Lock()
defer mutex.Unlock()
if settings.NginxSettings.ReloadCmd != "" {
out = execShell(settings.NginxSettings.ReloadCmd)
return
return execShell(settings.NginxSettings.ReloadCmd)
}
out = execCommand("nginx", "-s", "reload")
return
return execCommand("nginx", "-s", "reload")
}
// Restart restarts the nginx
func Restart() {
mutex.Lock()
defer mutex.Unlock()
@ -50,41 +45,45 @@ func Restart() {
time.Sleep(500 * time.Millisecond)
if settings.NginxSettings.RestartCmd != "" {
lastOutput = execShell(settings.NginxSettings.RestartCmd)
lastStdOut, lastStdErr = execShell(settings.NginxSettings.RestartCmd)
return
}
pidPath := GetPIDPath()
daemon := GetSbinPath()
lastOutput = execCommand("start-stop-daemon", "--stop", "--quiet", "--oknodo", "--retry=TERM/30/KILL/5", "--pidfile", pidPath)
if daemon == "" {
lastOutput += execCommand("nginx")
lastStdOut, lastStdErr = execCommand("start-stop-daemon", "--stop", "--quiet", "--oknodo", "--retry=TERM/30/KILL/5", "--pidfile", pidPath)
if lastStdErr != nil {
return
}
lastOutput += execCommand("start-stop-daemon", "--start", "--quiet", "--pidfile", pidPath, "--exec", daemon)
if daemon == "" {
lastStdOut, lastStdErr = execCommand("nginx")
return
}
lastStdOut, lastStdErr = execCommand("start-stop-daemon", "--start", "--quiet", "--pidfile", pidPath, "--exec", daemon)
return
}
func GetLastOutput() string {
// GetLastOutput returns the last output of the nginx command
func GetLastOutput() (stdOut string, stdErr error) {
mutex.Lock()
defer mutex.Unlock()
return lastOutput
return lastStdOut, lastStdErr
}
// GetModulesPath returns the nginx modules path
func GetModulesPath() string {
// First try to get from nginx -V output
output := execCommand("nginx", "-V")
if output != "" {
stdOut, stdErr := execCommand("nginx", "-V")
if stdErr != nil {
return ""
}
if stdOut != "" {
// Look for --modules-path in the output
if strings.Contains(output, "--modules-path=") {
parts := strings.Split(output, "--modules-path=")
if strings.Contains(stdOut, "--modules-path=") {
parts := strings.Split(stdOut, "--modules-path=")
if len(parts) > 1 {
// Extract the path
path := strings.Split(parts[1], " ")[0]
@ -99,28 +98,16 @@ func GetModulesPath() string {
return "/usr/lib/nginx/modules"
}
func execShell(cmd string) (out string) {
bytes, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput()
out = string(bytes)
if err != nil {
out += " " + err.Error()
}
return
}
func execCommand(name string, cmd ...string) (out string) {
bytes, err := exec.Command(name, cmd...).CombinedOutput()
out = string(bytes)
if err != nil {
out += " " + err.Error()
}
return
}
func IsNginxRunning() bool {
pidPath := GetPIDPath()
if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
return false
switch settings.NginxSettings.RunningInAnotherContainer() {
case true:
return docker.StatPath(pidPath)
case false:
if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
return false
}
return true
}
return true
return false
}

View file

@ -35,7 +35,10 @@ func Disable(name string) (err error) {
return
}
output := nginx.Reload()
output, err := nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}

View file

@ -35,13 +35,19 @@ func Enable(name string) (err error) {
}
// Test nginx config, if not pass, then disable the site.
output := nginx.TestConf()
output, err := nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
_ = os.Remove(enabledConfigFilePath)
return cosy.WrapErrorWithParams(ErrNginxTestFailed, output)
}
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}

View file

@ -76,7 +76,10 @@ func EnableMaintenance(name string) (err error) {
}
// Test nginx config, if not pass, then restore original configuration
output := nginx.TestConf()
output, err := nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
// Configuration error, cleanup and revert
_ = os.Remove(maintenanceConfigPath)
@ -87,7 +90,10 @@ func EnableMaintenance(name string) (err error) {
}
// Reload nginx
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}
@ -132,7 +138,10 @@ func DisableMaintenance(name string) (err error) {
}
// Test nginx config, if not pass, then revert
output := nginx.TestConf()
output, err := nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
// Configuration error, cleanup and revert
_ = os.Remove(enabledConfigFilePath)
@ -141,7 +150,10 @@ func DisableMaintenance(name string) (err error) {
}
// Reload nginx
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return fmt.Errorf("%s", output)
}

View file

@ -2,16 +2,17 @@ package site
import (
"fmt"
"net/http"
"os"
"runtime"
"sync"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/internal/notification"
"github.com/0xJacky/Nginx-UI/query"
"github.com/go-resty/resty/v2"
"github.com/uozi-tech/cosy/logger"
"net/http"
"os"
"runtime"
"sync"
)
func Rename(oldName string, newName string) (err error) {
@ -47,13 +48,19 @@ func Rename(oldName string, newName string) (err error) {
}
// test nginx configuration
output := nginx.TestConf()
output, err := nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return fmt.Errorf("%s", output)
}
// reload nginx
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return fmt.Errorf("%s", output)
}

View file

@ -38,14 +38,21 @@ func Save(name string, content string, overwrite bool, envGroupId uint64, syncNo
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
if helper.FileExists(enabledConfigFilePath) {
// Test nginx configuration
output := nginx.TestConf()
var output string
output, err = nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxTestFailed, output)
}
if postAction == model.PostSyncActionReloadNginx {
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}

View file

@ -35,7 +35,10 @@ func Disable(name string) (err error) {
return
}
output := nginx.Reload()
output, err := nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}

View file

@ -35,13 +35,19 @@ func Enable(name string) (err error) {
}
// Test nginx config, if not pass, then disable the site.
output := nginx.TestConf()
output, err := nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
_ = os.Remove(enabledConfigFilePath)
return cosy.WrapErrorWithParams(ErrNginxTestFailed, output)
}
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}

View file

@ -49,13 +49,19 @@ func Rename(oldName string, newName string) (err error) {
}
// test nginx configuration
output := nginx.TestConf()
output, err := nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxTestFailed, output)
}
// reload nginx
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}

View file

@ -37,15 +37,22 @@ func Save(name string, content string, overwrite bool, syncNodeIds []uint64, pos
enabledConfigFilePath := nginx.GetConfPath("streams-enabled", name)
if helper.FileExists(enabledConfigFilePath) {
var output string
// Test nginx configuration
output := nginx.TestConf()
output, err = nginx.TestConfig()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxTestFailed, output)
}
if postAction == model.PostSyncActionReloadNginx {
output = nginx.Reload()
output, err = nginx.Reload()
if err != nil {
return
}
if nginx.GetLogLevel(output) > nginx.Warn {
return cosy.WrapErrorWithParams(ErrNginxReloadFailed, output)
}