feat(docker-watcher): phase 5 - registry client & poller
Gitea registry client with tag listing and pattern matching, cron-based polling scheduler with first-poll safety, poll state persistence. DeployTriggerer interface for decoupled deploy triggering.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alexei/docker-watcher/internal/store"
|
||||
)
|
||||
|
||||
// AutoCreateProject creates a new project and a default "dev" stage from an
|
||||
// unknown image. It inspects the Docker image to extract defaults (EXPOSE port,
|
||||
// healthcheck, labels).
|
||||
//
|
||||
// The auto-created project uses:
|
||||
// - Name: derived from image name (e.g. "web-app-launcher")
|
||||
// - Image: full owner/name path
|
||||
// - Port: first EXPOSE port from the image, or 0 if none
|
||||
// - Healthcheck: from image HEALTHCHECK instruction, if present
|
||||
// - A single "dev" stage with auto_deploy=true and tag_pattern="*"
|
||||
func AutoCreateProject(
|
||||
ctx context.Context,
|
||||
st *store.Store,
|
||||
inspector ImageInspector,
|
||||
parsed ParsedImage,
|
||||
) (store.Project, store.Stage, error) {
|
||||
// Build the full image ref for inspection (registry/owner/name:tag).
|
||||
imageRef := buildImageRef(parsed)
|
||||
|
||||
var port int
|
||||
var healthcheck string
|
||||
|
||||
// Attempt to inspect the image for metadata. If inspection fails (image
|
||||
// not pulled locally), proceed with zero defaults.
|
||||
if inspector != nil {
|
||||
info, err := inspector.InspectImage(ctx, imageRef)
|
||||
if err != nil {
|
||||
log.Printf("[webhook] image inspection failed for %s (using defaults): %v", imageRef, err)
|
||||
} else {
|
||||
port = extractPort(info.ExposedPorts)
|
||||
healthcheck = info.Healthcheck
|
||||
}
|
||||
}
|
||||
|
||||
project, err := st.CreateProject(store.Project{
|
||||
Name: parsed.Name,
|
||||
Registry: parsed.Registry,
|
||||
Image: parsed.FullName(),
|
||||
Port: port,
|
||||
Healthcheck: healthcheck,
|
||||
Env: "{}",
|
||||
Volumes: "{}",
|
||||
})
|
||||
if err != nil {
|
||||
return store.Project{}, store.Stage{}, fmt.Errorf("create project: %w", err)
|
||||
}
|
||||
|
||||
stage, err := st.CreateStage(store.Stage{
|
||||
ProjectID: project.ID,
|
||||
Name: "dev",
|
||||
TagPattern: "*",
|
||||
AutoDeploy: true,
|
||||
MaxInstances: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return store.Project{}, store.Stage{}, fmt.Errorf("create default stage: %w", err)
|
||||
}
|
||||
|
||||
return project, stage, nil
|
||||
}
|
||||
|
||||
// buildImageRef reconstructs a pullable image reference from parsed components.
|
||||
func buildImageRef(parsed ParsedImage) string {
|
||||
var parts []string
|
||||
if parsed.Registry != "" {
|
||||
parts = append(parts, parsed.Registry)
|
||||
}
|
||||
if parsed.Owner != "" {
|
||||
parts = append(parts, parsed.Owner)
|
||||
}
|
||||
parts = append(parts, parsed.Name)
|
||||
|
||||
ref := strings.Join(parts, "/")
|
||||
if parsed.Tag != "" {
|
||||
ref += ":" + parsed.Tag
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
// extractPort parses the first exposed port from Docker EXPOSE entries.
|
||||
// Entries are in the form "8080/tcp" or "8080". Returns 0 if none found.
|
||||
func extractPort(exposedPorts []string) int {
|
||||
if len(exposedPorts) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Take the first port entry.
|
||||
raw := exposedPorts[0]
|
||||
// Strip protocol suffix (e.g. "/tcp", "/udp").
|
||||
if idx := strings.Index(raw, "/"); idx != -1 {
|
||||
raw = raw[:idx]
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return port
|
||||
}
|
||||
Reference in New Issue
Block a user