real init
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/containers"
|
||||
)
|
||||
|
||||
type ActionProxyPayload struct {
|
||||
Method string
|
||||
Action string
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type ActionProxyResponse struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
func ActionProxy(container_id, action string, payload ActionProxyPayload) (ActionProxyResponse, error) {
|
||||
// Forward the action to the container with the given ID, and return the result to the frontend
|
||||
|
||||
containers := containers.NewContainerCache().GetServices()
|
||||
for _, container := range containers {
|
||||
if container.ID == container_id {
|
||||
// Forward the action to this container
|
||||
|
||||
body := bytes.NewBuffer([]byte(payload.Data))
|
||||
|
||||
req, err := http.NewRequest(payload.Method, fmt.Sprintf("http://%s:%s/action/%s", container.ID[:12], container.Network.Ports[0].Host, payload.Action), body)
|
||||
if err != nil {
|
||||
return ActionProxyResponse{}, err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return ActionProxyResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(resp.Body)
|
||||
|
||||
return ActionProxyResponse{
|
||||
StatusCode: resp.StatusCode,
|
||||
Body: buf.String(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return ActionProxyResponse{}, fmt.Errorf("container with ID %s not found", container_id)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package consts
|
||||
|
||||
const GITEA_RO_TOKEN = ""
|
||||
const GITEA_RW_PACKAGE_TOKEN = ""
|
||||
const GITEA_ORG = ""
|
||||
const GITEA_USER = ""
|
||||
@@ -0,0 +1,66 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
|
||||
)
|
||||
|
||||
type ContainerCache struct {
|
||||
sync.RWMutex
|
||||
// Maps Container ID -> Container
|
||||
services map[string]types.Container
|
||||
UpdateCh chan struct{}
|
||||
}
|
||||
|
||||
func NewContainerCache() *ContainerCache {
|
||||
return &ContainerCache{
|
||||
services: make(map[string]types.Container),
|
||||
UpdateCh: make(chan struct{}, 10),
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *ContainerCache) AddService(c types.Container) {
|
||||
cache.Lock()
|
||||
if cache.services == nil {
|
||||
cache.services = make(map[string]types.Container)
|
||||
}
|
||||
cache.services[c.ID] = c
|
||||
cache.Unlock()
|
||||
|
||||
// Trigger update AFTER unlocking to prevent holding locks during channel sends
|
||||
cache.TriggerUpdate()
|
||||
}
|
||||
|
||||
func (cache *ContainerCache) RemoveService(c types.Container) {
|
||||
cache.Lock()
|
||||
_, exists := cache.services[c.ID]
|
||||
if exists {
|
||||
delete(cache.services, c.ID)
|
||||
}
|
||||
cache.Unlock()
|
||||
|
||||
// Only trigger an update if an item was actually removed
|
||||
if exists {
|
||||
cache.TriggerUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *ContainerCache) TriggerUpdate() {
|
||||
select {
|
||||
case cache.UpdateCh <- struct{}{}:
|
||||
default:
|
||||
// Channel buffer is full; subscriber already knows an update is pending
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *ContainerCache) GetServices() []types.Container {
|
||||
cache.RLock()
|
||||
defer cache.RUnlock()
|
||||
|
||||
services := make([]types.Container, 0, len(cache.services))
|
||||
for _, service := range cache.services {
|
||||
services = append(services, service)
|
||||
}
|
||||
return services
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package containers
|
||||
@@ -0,0 +1,117 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.DockerClient) {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("type", "container")
|
||||
filter.Add("event", "start")
|
||||
filter.Add("event", "die")
|
||||
filter.Add("event", "destroy")
|
||||
|
||||
messages, errs := dc.Client.Events(ctx, events.ListOptions{Filters: filter})
|
||||
|
||||
log.Println("Subscribed to Docker container events...")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-errs:
|
||||
log.Printf("Docker event stream error: %v", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
case event := <-messages:
|
||||
serviceLabel, ok := event.Actor.Attributes["org.dcntrlized.service"]
|
||||
if !ok {
|
||||
log.Default().Printf("Found unrelated service %s\n", serviceLabel)
|
||||
for attribute := range event.Actor.Attributes {
|
||||
log.Default().Printf("\tKey: %s\tValue: %s\n", attribute, event.Actor.Attributes[attribute])
|
||||
}
|
||||
continue
|
||||
}
|
||||
_ = serviceLabel
|
||||
containerID := event.Actor.ID
|
||||
containerName := event.Actor.Attributes["name"]
|
||||
|
||||
cache.Lock()
|
||||
|
||||
switch event.Action {
|
||||
case "start":
|
||||
// Inspect Container
|
||||
inspectData, inspectErr := dc.Client.ContainerInspect(ctx, containerID)
|
||||
|
||||
// Get Network information
|
||||
var containerNetwork types.Network
|
||||
|
||||
if inspectErr != nil {
|
||||
log.Printf("Failed to inspect container %s: %v", containerID, inspectErr)
|
||||
} else {
|
||||
netInfo, shouldCache, err := docker.ExtractNetworkInfo(ctx, dc, inspectData)
|
||||
if err != nil {
|
||||
log.Printf("Error extracting network details: %v", err)
|
||||
}
|
||||
if !shouldCache {
|
||||
fmt.Printf("Bypassing special network mode: %s\n", inspectData.HostConfig.NetworkMode)
|
||||
cache.Unlock()
|
||||
return
|
||||
}
|
||||
containerNetwork = netInfo
|
||||
}
|
||||
|
||||
// Get Versions
|
||||
ver, err := docker.GetContainerVersion(ctx, dc, containerID)
|
||||
|
||||
// Fetch Manifest
|
||||
host := containerID[:12]
|
||||
port := event.Actor.Attributes["org.dcntrlized.port"]
|
||||
manifest, err := docker.FetchManifestWithRetry(ctx, host, port, 20, 50*time.Millisecond)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching manifest for container %s: %v", containerID, err)
|
||||
cache.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// Save to Cache
|
||||
log.Printf("Service started. Adding container %s - %s to cache", host, containerName)
|
||||
cache.services[containerID] = types.Container{
|
||||
ID: containerID,
|
||||
Name: manifest.Name,
|
||||
DockerName: containerName,
|
||||
Image: event.Actor.Attributes["image"],
|
||||
CreatedAt: fmt.Sprint(event.TimeNano),
|
||||
Network: containerNetwork,
|
||||
Status: "running",
|
||||
Version: ver,
|
||||
}
|
||||
|
||||
case "die":
|
||||
if container, exists := cache.services[containerID]; exists {
|
||||
log.Printf("Service stopping: %s", containerName)
|
||||
container.Status = "stopping"
|
||||
cache.services[containerID] = container
|
||||
}
|
||||
|
||||
case "destroy":
|
||||
if container, exists := cache.services[containerID]; exists {
|
||||
log.Printf("Service destroyed: %s", containerName)
|
||||
cache.RemoveService(container)
|
||||
}
|
||||
}
|
||||
|
||||
cache.Unlock()
|
||||
cache.TriggerUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
|
||||
)
|
||||
|
||||
type DevService struct {
|
||||
ID string `json:"id"` // Use "localhost" or IP
|
||||
Port string `json:"port"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (cache *ContainerCache) LoadDevServices(filePath string) {
|
||||
log.Printf("Dev Mode: Loading services from %s", filePath)
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read dev services file: %v", err)
|
||||
}
|
||||
|
||||
var devServices []DevService
|
||||
if err := json.Unmarshal(data, &devServices); err != nil {
|
||||
log.Fatalf("Failed to parse dev services: %v", err)
|
||||
}
|
||||
|
||||
for _, ds := range devServices {
|
||||
cache.AddService(types.Container{
|
||||
ID: ds.ID,
|
||||
Name: ds.Name,
|
||||
Status: "unknown",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// DockerClient is a wrapper around the Docker client that provides additional functionality.
|
||||
type DockerClient struct {
|
||||
*client.Client
|
||||
}
|
||||
|
||||
// ContainerMetadata holds essential high-level metadata extracted from an inspection.
|
||||
type ContainerMetadata struct {
|
||||
ID string
|
||||
Name string
|
||||
Image string
|
||||
Version string
|
||||
IPAddress string
|
||||
Status string
|
||||
Created string
|
||||
Labels map[string]string
|
||||
Env map[string]string
|
||||
}
|
||||
|
||||
// NewDockerClient creates a new instance of DockerClient.
|
||||
func NewDockerClient() (*DockerClient, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DockerClient{cli}, nil
|
||||
}
|
||||
|
||||
func (dc *DockerClient) Close() error {
|
||||
return dc.Client.Close()
|
||||
}
|
||||
|
||||
func ListContainers(dc *DockerClient) ([]string, error) {
|
||||
// Lists containers with org.dcntrlized.service label
|
||||
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", "org.dcntrlized.service")
|
||||
options := container.ListOptions{
|
||||
All: true,
|
||||
Filters: filter,
|
||||
}
|
||||
|
||||
containers, err := dc.Client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containerNames []string
|
||||
for _, container := range containers {
|
||||
if _, ok := container.Labels["org.dcntrlized.service"]; ok {
|
||||
containerNames = append(containerNames, container.Names[0])
|
||||
}
|
||||
}
|
||||
return containerNames, nil
|
||||
}
|
||||
|
||||
// fetchManifestWithRetry polls the container's manifest endpoint until it succeeds or max retries are reached.
|
||||
// Could probably be simplified to (ctx, container) with maxRetries and retryDelay hardcoded, low priority TODO
|
||||
func FetchManifestWithRetry(ctx context.Context, host, port string, maxRetries int, retryDelay time.Duration) (*types.Manifest, error) {
|
||||
url := fmt.Sprintf("http://%s:%s/_manifest", host, port)
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
// Create a client that respects context deadlines/cancellation
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
|
||||
for range maxRetries {
|
||||
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if reqErr == nil {
|
||||
resp, err = client.Do(req)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(retryDelay):
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var manifest types.Manifest
|
||||
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode manifest: %w", err)
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
// GetContainerVersion retrieves the container or image version from inspection data.
|
||||
func GetContainerVersion(ctx context.Context, dc *DockerClient, containerID string) (string, error) {
|
||||
inspectData, err := dc.Client.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
logger.Error("Failed to inspect container: %v", err)
|
||||
return "", fmt.Errorf("failed to inspect container: %w", err)
|
||||
}
|
||||
|
||||
// 1. Check for standard image/version labels
|
||||
labels := inspectData.Config.Labels
|
||||
if version, ok := labels["org.opencontainers.image.version"]; ok && version != "" {
|
||||
logger.Debug("Found version from org.opencontainers.image.version label: %s", version)
|
||||
return version, nil
|
||||
}
|
||||
if version, ok := labels["version"]; ok && version != "" {
|
||||
logger.Debug("Found version from version label: %s", version)
|
||||
return version, nil
|
||||
}
|
||||
|
||||
logger.Debug("No version label found, falling back to image tag parsing for container %s", containerID)
|
||||
// 2. Fall back to parsing the image tag (e.g., "redis:7.0" -> "7.0")
|
||||
imageParts := strings.Split(inspectData.Config.Image, ":")
|
||||
if len(imageParts) > 1 {
|
||||
logger.Debug("Extracted version from image tag: %s", imageParts[len(imageParts)-1])
|
||||
return imageParts[len(imageParts)-1], nil
|
||||
}
|
||||
|
||||
logger.Debug("No version found in image tag, defaulting to 'latest' for container %s", containerID)
|
||||
// 3. If no version is found, default to "latest"
|
||||
return "latest", nil
|
||||
}
|
||||
|
||||
// ExtractNetworkInfo parses the network mode, active network name, and port mappings.
|
||||
func ExtractNetworkInfo(ctx context.Context, dc *DockerClient, inspectData container.InspectResponse) (types.Network, bool, error) {
|
||||
var netInfo types.Network
|
||||
|
||||
hostConfig := inspectData.HostConfig
|
||||
networkMode := string(hostConfig.NetworkMode)
|
||||
|
||||
// Host, none, and container-sharing modes bypass traditional network inspection
|
||||
if networkMode == "host" || networkMode == "none" || strings.HasPrefix(networkMode, "container:") {
|
||||
return netInfo, false, nil // Return false indicating non-standard/ignored network mode
|
||||
}
|
||||
|
||||
networkName := ""
|
||||
for nwName := range inspectData.NetworkSettings.Networks {
|
||||
if nwName == "dcntrlized_net" {
|
||||
netInfo.IPAddress.Address = inspectData.NetworkSettings.Networks[nwName].IPAddress
|
||||
netInfo.IPAddress.Version = types.IPv4 // Currently hardcoded (I'm assuming ^^ will always be an IPv4 IP but if not we should gather version correctly low priority TODO)
|
||||
}
|
||||
networkName = nwName
|
||||
break // First active network
|
||||
}
|
||||
if networkName == "" {
|
||||
networkName = "bridge"
|
||||
}
|
||||
|
||||
networkResource, err := dc.Client.NetworkInspect(ctx, networkName, network.InspectOptions{})
|
||||
if err != nil {
|
||||
netInfo.Mode = "bridge" // Fallback
|
||||
} else {
|
||||
netInfo.Mode = networkResource.Driver // e.g., "bridge", "overlay"
|
||||
}
|
||||
|
||||
for p := range hostConfig.PortBindings {
|
||||
hostPort, guestPort, err := p.Range()
|
||||
if err != nil {
|
||||
log.Printf("Error parsing port range for container: %v", err)
|
||||
continue
|
||||
}
|
||||
netInfo.Ports = append(netInfo.Ports, types.PortMapping{Host: hostPort, Guest: guestPort})
|
||||
}
|
||||
|
||||
netInfo.Name = networkName
|
||||
return netInfo, true, nil
|
||||
}
|
||||
|
||||
// GetContainerMetadata collects the primary useful metadata for a given container.
|
||||
func GetContainerMetadata(ctx context.Context, dc *DockerClient, containerID string) (*ContainerMetadata, error) {
|
||||
inspectData, err := dc.Client.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
|
||||
}
|
||||
|
||||
// Parse environment variables into a key-value map
|
||||
envMap := make(map[string]string)
|
||||
for _, env := range inspectData.Config.Env {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
envMap[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Extract primary IP address from the active network settings
|
||||
ipAddress := ""
|
||||
for _, netSetting := range inspectData.NetworkSettings.Networks {
|
||||
if netSetting.IPAddress != "" {
|
||||
ipAddress = netSetting.IPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
version, _ := GetContainerVersion(ctx, dc, containerID)
|
||||
|
||||
return &ContainerMetadata{
|
||||
ID: inspectData.ID,
|
||||
Name: strings.TrimPrefix(inspectData.Name, "/"), // Clean leading slash
|
||||
Image: inspectData.Config.Image,
|
||||
Version: version,
|
||||
IPAddress: ipAddress,
|
||||
Status: inspectData.State.Status,
|
||||
Created: inspectData.Created,
|
||||
Labels: inspectData.Config.Labels,
|
||||
Env: envMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetImageTagFromContainer(ctx context.Context, cli *client.Client, containerID string) (string, error) {
|
||||
// Step 1: Inspect the container
|
||||
containerJSON, err := cli.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to inspect container: %w", err)
|
||||
}
|
||||
|
||||
// containerJSON.Config.Image holds the image name or digest used to run it
|
||||
imageRef := containerJSON.Config.Image
|
||||
|
||||
// Step 2: Inspect the image to get its official RepoTags array
|
||||
var rawBody bytes.Buffer
|
||||
inspectResult, err := cli.ImageInspect(ctx, imageRef, client.ImageInspectWithRawResponse(&rawBody))
|
||||
if err != nil {
|
||||
|
||||
return "", fmt.Errorf("failed to inspect image %s: %w", imageRef, err)
|
||||
}
|
||||
|
||||
// If the image has repo tags, return the first match
|
||||
if len(inspectResult.RepoTags) > 0 {
|
||||
return inspectResult.RepoTags[0], nil
|
||||
}
|
||||
|
||||
return imageRef, nil // Fallback to raw reference if untagged
|
||||
}
|
||||
|
||||
/* func HasUpdateAvailable(ctx context.Context, cli *client.Client, containerID string) (bool, error) {
|
||||
dc, _ := NewDockerClient()
|
||||
ver, _ := GetContainerVersion(ctx, dc, containerID)
|
||||
|
||||
remote, _ := GetLatestRemoteVersion()
|
||||
} */
|
||||
|
||||
/* func GetLatestRemoteVersion(registryRepo string) (string, error) {
|
||||
// 1. Parse the repository reference (e.g., "git.local/dcntrlized/sampleservicedeploytest/sampleservicedeploytest")
|
||||
repo, err := name.NewRepository(registryRepo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse repository: %w", err)
|
||||
}
|
||||
|
||||
// 2. Fetch all tags from the remote registry (uses default keychain credentials like docker login)
|
||||
tags, err := remote.List(repo, remote.WithAuthFromKeychain(remote.DefaultKeychain))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to list remote tags: %w", err)
|
||||
}
|
||||
|
||||
var latestVersion string
|
||||
|
||||
// 3. Filter and find the highest semantic version
|
||||
for _, tag := range tags {
|
||||
// Ensure it follows semantic versioning format (e.g., v1.2.3)
|
||||
formattedTag := tag
|
||||
if !semver.IsValid(formattedTag) {
|
||||
// If it doesn't have a 'v' prefix, try prepending it to check if it's a valid semver core
|
||||
if semver.IsValid("v" + tag) {
|
||||
formattedTag = "v" + tag
|
||||
} else {
|
||||
continue // Skip non-semver tags like "latest"
|
||||
}
|
||||
}
|
||||
|
||||
if latestVersion == "" || semver.Compare(formattedTag, latestVersion) > 0 {
|
||||
latestVersion = formattedTag
|
||||
}
|
||||
}
|
||||
|
||||
if latestVersion == "" {
|
||||
return "", fmt.Errorf("no valid semantic version tags found in registry")
|
||||
}
|
||||
|
||||
return latestVersion, nil
|
||||
} */
|
||||
@@ -0,0 +1,88 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
infoLogger *log.Logger
|
||||
warnLogger *log.Logger
|
||||
errorLogger *log.Logger
|
||||
debugLogger *log.Logger
|
||||
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func initLogger() {
|
||||
if err := os.MkdirAll("logs", 0755); err != nil {
|
||||
log.Printf("Failed to create logs directory: %v. Falling back to standard output.", err)
|
||||
}
|
||||
|
||||
// 1. Setup Info Logger
|
||||
var infoOutput io.Writer = os.Stdout
|
||||
if infoFile, err := os.OpenFile("logs/info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
|
||||
infoOutput = io.MultiWriter(os.Stdout, infoFile)
|
||||
writeHeader(infoFile)
|
||||
}
|
||||
infoLogger = log.New(infoOutput, "INFO: ", log.Ldate|log.Ltime|log.LUTC)
|
||||
|
||||
// 2. Setup Warn Logger
|
||||
var warnOutput io.Writer = os.Stdout
|
||||
if warnFile, err := os.OpenFile("logs/warn.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
|
||||
warnOutput = io.MultiWriter(os.Stdout, warnFile)
|
||||
writeHeader(warnFile)
|
||||
}
|
||||
warnLogger = log.New(warnOutput, "WARN: ", log.Ldate|log.Ltime|log.LUTC)
|
||||
|
||||
// 3. Setup Error Logger
|
||||
var errorOutput io.Writer = os.Stderr
|
||||
if errorFile, err := os.OpenFile("logs/error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
|
||||
errorOutput = io.MultiWriter(os.Stderr, errorFile)
|
||||
writeHeader(errorFile)
|
||||
}
|
||||
// Note: increased call depth isn't an issue for Lshortfile here because
|
||||
// log.Logger.Printf correctly tracks the caller frame right above it.
|
||||
errorLogger = log.New(errorOutput, "ERROR: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile)
|
||||
|
||||
// 4. Setup Debug Logger
|
||||
var debugOutput io.Writer = os.Stdout
|
||||
if debugFile, err := os.OpenFile("logs/debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err == nil {
|
||||
debugOutput = io.MultiWriter(os.Stdout, debugFile)
|
||||
writeHeader(debugFile)
|
||||
}
|
||||
debugLogger = log.New(debugOutput, "DEBUG: ", log.Ldate|log.Ltime|log.LUTC)
|
||||
}
|
||||
|
||||
func writeHeader(file *os.File) {
|
||||
file.WriteString("========== Start Log ==========\n")
|
||||
file.WriteString("[INFO] Logger initialized\n")
|
||||
file.WriteString("[" + time.Now().Format(time.RFC3339) + "]\n")
|
||||
}
|
||||
|
||||
// Info accepts a format string and an optional, arbitrary number of arguments.
|
||||
func Info(format string, v ...any) {
|
||||
once.Do(initLogger)
|
||||
infoLogger.Printf(format, v...)
|
||||
}
|
||||
|
||||
// Warn accepts a format string and an optional, arbitrary number of arguments.
|
||||
func Warn(format string, v ...any) {
|
||||
once.Do(initLogger)
|
||||
warnLogger.Printf(format, v...)
|
||||
}
|
||||
|
||||
// Error accepts a format string and an optional, arbitrary number of arguments.
|
||||
func Error(format string, v ...any) {
|
||||
once.Do(initLogger)
|
||||
errorLogger.Printf(format, v...)
|
||||
}
|
||||
|
||||
// Debug accepts a format string and an optional, arbitrary number of arguments.
|
||||
func Debug(format string, v ...any) {
|
||||
once.Do(initLogger)
|
||||
debugLogger.Printf(format, v...)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package types
|
||||
|
||||
type PortMapping struct {
|
||||
Host int `json:"host"`
|
||||
Guest int `json:"guest"`
|
||||
}
|
||||
|
||||
type IPVersion string
|
||||
|
||||
const (
|
||||
IPv4 IPVersion = "v4"
|
||||
IPv6 IPVersion = "v6"
|
||||
)
|
||||
|
||||
type IPAddress struct {
|
||||
Version IPVersion `json:"version"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type Network struct {
|
||||
Name string `json:"name"`
|
||||
IPAddress IPAddress `json:"ipAddress"`
|
||||
Mode string `json:"mode"` // e.g., "bridge", "host", "none", "overlay", "ipvlan", "macvlan"
|
||||
Ports []PortMapping `json:"ports"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DockerName string `json:"docker_name"`
|
||||
Image string `json:"image"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Network Network `json:"network"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package types
|
||||
|
||||
type Manifest struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Author string `json:"author"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
HasAuth bool `json:"has_auth"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
Reference in New Issue
Block a user