mirror of
https://github.com/chenasraf/sofmani.git
synced 2026-05-17 17:28:04 +00:00
217 lines
6.7 KiB
Go
Executable File
217 lines
6.7 KiB
Go
Executable File
package installer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/chenasraf/sofmani/appconfig"
|
|
"github.com/chenasraf/sofmani/logger"
|
|
"github.com/chenasraf/sofmani/platform"
|
|
)
|
|
|
|
// DockerInstaller is an installer for Docker images.
|
|
type DockerInstaller struct {
|
|
InstallerBase
|
|
// Config is the application configuration.
|
|
Config *appconfig.AppConfig
|
|
// Info is the installer data.
|
|
Info *appconfig.InstallerData
|
|
}
|
|
|
|
// DockerOpts represents options for the DockerInstaller.
|
|
type DockerOpts struct {
|
|
// Flags is a string of flags to pass to the `docker run` command.
|
|
Flags *string
|
|
// Platform is a platform-specific map of Docker platform strings (e.g., "linux/amd64").
|
|
Platform *platform.PlatformMap[string]
|
|
// SkipIfUnavailable indicates whether to skip installation if Docker is unavailable.
|
|
SkipIfUnavailable *bool
|
|
}
|
|
|
|
// NewDockerInstaller creates a new DockerInstaller.
|
|
func NewDockerInstaller(cfg *appconfig.AppConfig, installer *appconfig.InstallerData) *DockerInstaller {
|
|
return &DockerInstaller{
|
|
InstallerBase: InstallerBase{Data: installer},
|
|
Config: cfg,
|
|
Info: installer,
|
|
}
|
|
}
|
|
|
|
// Validate validates the installer configuration.
|
|
func (i *DockerInstaller) Validate() []ValidationError {
|
|
errors := i.BaseValidate()
|
|
return errors
|
|
}
|
|
|
|
// Install implements IInstaller.
|
|
func (i *DockerInstaller) Install() error {
|
|
if !isDockerAvailable() {
|
|
if i.GetOpts().SkipIfUnavailable != nil && *i.GetOpts().SkipIfUnavailable {
|
|
logger.Debug("Docker not available, skipping install")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("docker is not available")
|
|
}
|
|
return i.runOrStartContainer(false)
|
|
}
|
|
|
|
// Update implements IInstaller.
|
|
func (i *DockerInstaller) Update() error {
|
|
if !isDockerAvailable() {
|
|
if i.GetOpts().SkipIfUnavailable != nil && *i.GetOpts().SkipIfUnavailable {
|
|
logger.Debug("Docker not available, skipping update")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("docker is not available")
|
|
}
|
|
|
|
image := *i.Info.Name
|
|
containerName := i.GetContainerName()
|
|
|
|
logger.Debug("Pulling updated image: %s", image)
|
|
if err := i.RunCmdPassThrough("docker", "pull", image); err != nil {
|
|
return fmt.Errorf("failed to pull image: %w", err)
|
|
}
|
|
|
|
logger.Debug("Removing existing container: %s", containerName)
|
|
err := i.RunCmdPassThrough("docker", "rm", "-f", containerName)
|
|
if err != nil {
|
|
logger.Debug("Failed to remove existing container: %s, error: %v", containerName, err)
|
|
return fmt.Errorf("failed to remove existing container: %w", err)
|
|
}
|
|
|
|
logger.Debug("Running updated container: %s", containerName)
|
|
return i.runOrStartContainer(true)
|
|
}
|
|
|
|
// CheckNeedsUpdate implements IInstaller.
|
|
func (i *DockerInstaller) CheckNeedsUpdate() (bool, error) {
|
|
// Always assume an update is available
|
|
return true, nil
|
|
}
|
|
|
|
// CheckIsInstalled implements IInstaller.
|
|
func (i *DockerInstaller) CheckIsInstalled() (bool, error) {
|
|
if i.HasCustomInstallCheck() {
|
|
return i.RunCustomInstallCheck()
|
|
}
|
|
|
|
containerName := i.GetContainerName()
|
|
cmd := exec.Command("docker", "inspect", containerName)
|
|
err := cmd.Run()
|
|
return err == nil, nil
|
|
}
|
|
|
|
// GetData implements IInstaller.
|
|
func (i *DockerInstaller) GetData() *appconfig.InstallerData {
|
|
return i.Info
|
|
}
|
|
|
|
// GetOpts returns the parsed options for the DockerInstaller.
|
|
func (i *DockerInstaller) GetOpts() *DockerOpts {
|
|
opts := &DockerOpts{}
|
|
if i.Info.Opts != nil {
|
|
if flags, ok := (*i.Info.Opts)["flags"].(string); ok {
|
|
opts.Flags = &flags
|
|
}
|
|
if raw, ok := (*i.Info.Opts)["platform"]; ok && raw != nil {
|
|
opts.Platform = platform.NewPlatformMap[string](raw)
|
|
}
|
|
}
|
|
if skip, ok := (*i.Info.Opts)["skip_if_unavailable"].(bool); ok {
|
|
opts.SkipIfUnavailable = &skip
|
|
}
|
|
return opts
|
|
}
|
|
|
|
// GetContainerName returns the name of the Docker container.
|
|
// It uses the BinName from the installer data if provided, otherwise it uses the installer name.
|
|
func (i *DockerInstaller) GetContainerName() string {
|
|
if i.Info.BinName != nil && len(*i.Info.BinName) > 0 {
|
|
return *i.Info.BinName
|
|
}
|
|
return *i.Info.Name
|
|
}
|
|
|
|
// Helpers
|
|
|
|
// runOrStartContainer runs or starts a Docker container.
|
|
// If forceRun is true, it will always run a new container. Otherwise, it will start an existing container if found.
|
|
func (i *DockerInstaller) runOrStartContainer(forceRun bool) error {
|
|
containerName := i.GetContainerName()
|
|
image := *i.Info.Name
|
|
opts := i.GetOpts()
|
|
|
|
flags := "-d --restart always"
|
|
if opts.Flags != nil {
|
|
flat := strings.Join(strings.Fields(*opts.Flags), " ")
|
|
flags += " " + flat
|
|
}
|
|
|
|
if !forceRun {
|
|
exists := exec.Command("docker", "inspect", containerName).Run() == nil
|
|
if exists {
|
|
return i.RunCmdAsFile(fmt.Sprintf(`docker start "%s"`, containerName))
|
|
}
|
|
}
|
|
|
|
return i.RunCmdAsFile(fmt.Sprintf(`docker run %s --name "%s" "%s"`, flags, containerName, image))
|
|
}
|
|
|
|
// DockerManifestList represents the structure of a Docker manifest list.
|
|
type DockerManifestList struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
MediaType string `json:"mediaType"`
|
|
Manifests []struct {
|
|
Digest string `json:"digest"`
|
|
Platform struct {
|
|
Architecture string `json:"architecture"`
|
|
OS string `json:"os"`
|
|
} `json:"platform"`
|
|
} `json:"manifests"`
|
|
}
|
|
|
|
// extractDigestFromManifest extracts the digest for a specific OS and architecture from a Docker manifest list.
|
|
func extractDigestFromManifest(jsonData []byte, osTarget, archTarget string) (string, error) {
|
|
var manifest DockerManifestList
|
|
logger.Debug("Parsing manifest JSON data for OS: %s, Arch: %s", osTarget, archTarget)
|
|
if err := json.Unmarshal(jsonData, &manifest); err != nil {
|
|
logger.Debug("Failed to parse manifest JSON: %v", err)
|
|
return "", fmt.Errorf("failed to parse manifest JSON: %w", err)
|
|
}
|
|
|
|
for _, m := range manifest.Manifests {
|
|
if m.Platform.OS == osTarget && m.Platform.Architecture == archTarget {
|
|
return strings.TrimPrefix(m.Digest, "sha256:"), nil
|
|
}
|
|
}
|
|
logger.Debug("No matching digest found for OS: %s, Arch: %s", osTarget, archTarget)
|
|
logger.Debug("Available manifests: %v", manifest.Manifests)
|
|
return "", fmt.Errorf("no digest found for %s/%s", osTarget, archTarget)
|
|
}
|
|
|
|
// GetPlatformArchWithFallback attempts to determine the best architecture for a Docker image,
|
|
// considering a preferred architecture and a list of fallbacks.
|
|
func GetPlatformArchWithFallback(preferred string, fallbacks ...string) string {
|
|
image := "ghcr.io/open-webui/open-webui:main"
|
|
cmd := exec.Command("docker", "manifest", "inspect", image)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return preferred
|
|
}
|
|
for _, arch := range append([]string{preferred}, fallbacks...) {
|
|
if strings.Contains(string(out), fmt.Sprintf(`"architecture": "%s"`, arch)) {
|
|
return arch
|
|
}
|
|
}
|
|
return preferred
|
|
}
|
|
|
|
// isDockerAvailable checks if Docker is available on the system.
|
|
func isDockerAvailable() bool {
|
|
err := exec.Command("docker", "info").Run()
|
|
return err == nil
|
|
}
|