mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
Git-sourced deploys never materialized a checkout: sourcePath was build_path resolved against the server cwd, so builds consumed the backend's own directory. Remote placement could only run registry-pulled images. - internal/source: per-deployment checkout (init/fetch/checkout) at branch or commit; GIT_ASKPASS keeps tokens out of argv, .git/config, and the build context; dumb-transport and SHA-refusal fallbacks; build_path validated inside the checkout. - BuildManager.PackageContext: produces the same tar context Build ships to the daemon — generated Dockerfile at tar root, .git excluded. Fixes nixpacks passing the Dockerfile path as content. - Agent artifact endpoint: GET /api/agents/artifacts/:id streams build contexts and docker-save tarballs behind agent-token auth; hex-validated ids, per-deployment cleanup, boot-time purge. - Agent commands: build_image (docker build -, --build-arg values via process env, not argv) and load_image (docker load for containr-* rollback tags no registry serves). - Deployment flow: remote git builds package context → artifact → node docker-builds the deployment tag; remote rollbacks of local tags export via SaveImage → artifact → node docker-loads. Verified end-to-end: agent-pinned git service built on the node (build_image → create_container → deployed) and rollback shipped a docker-save tarball (load_image → deployed). Artifact auth returns 401/404 correctly; workspaces and blobs are removed after deploy. Authored By: TDvorak <[email protected]>
137 lines
3.5 KiB
Go
137 lines
3.5 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// containrWorkDir is the build workspace root — same dir the BuildManager
|
|
// was constructed with; checkouts and agent artifacts live beneath it.
|
|
const containrWorkDir = "/tmp/containr-builds"
|
|
|
|
// artifacts is a one-shot blob store for node agents: build contexts and
|
|
// docker-save tarballs the server produced on behalf of a deployment. Files
|
|
// live under workdir/artifacts/<id> and are removed when the deployment
|
|
// finishes or after maxArtifactAge.
|
|
var artifacts = &artifactStore{files: map[string]string{}}
|
|
|
|
type artifactStore struct {
|
|
mu sync.Mutex
|
|
dir string
|
|
files map[string]string
|
|
}
|
|
|
|
const maxArtifactAge = 2 // hours — a deployment can never outlive this
|
|
|
|
// initArtifacts points the store at dir (created on demand).
|
|
func initArtifacts(dir string) {
|
|
artifacts.mu.Lock()
|
|
defer artifacts.mu.Unlock()
|
|
artifacts.dir = dir
|
|
_ = os.MkdirAll(dir, 0o755)
|
|
}
|
|
|
|
// artifactDir returns the store root, defaulting under the build workdir.
|
|
func artifactDir() string {
|
|
artifacts.mu.Lock()
|
|
defer artifacts.mu.Unlock()
|
|
if artifacts.dir != "" {
|
|
return artifacts.dir
|
|
}
|
|
return filepath.Join(containrWorkDir, "artifacts")
|
|
}
|
|
|
|
// putArtifact streams rc into the store and returns the artifact id.
|
|
func putArtifact(rc io.Reader) (string, error) {
|
|
buf := make([]byte, 16)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
id := hex.EncodeToString(buf)
|
|
dir := artifactDir()
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
path := filepath.Join(dir, id+".tar")
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := io.Copy(f, rc); err != nil {
|
|
f.Close()
|
|
os.Remove(path)
|
|
return "", err
|
|
}
|
|
f.Close()
|
|
|
|
artifacts.mu.Lock()
|
|
artifacts.files[id] = path
|
|
artifacts.mu.Unlock()
|
|
return id, nil
|
|
}
|
|
|
|
// removeArtifact deletes the file and forgets the id. Safe on missing ids.
|
|
func removeArtifact(id string) {
|
|
artifacts.mu.Lock()
|
|
path, ok := artifacts.files[id]
|
|
delete(artifacts.files, id)
|
|
artifacts.mu.Unlock()
|
|
if ok {
|
|
_ = os.Remove(path)
|
|
return
|
|
}
|
|
// Registered before a restart — remove best-effort by name.
|
|
if id != "" {
|
|
_ = os.Remove(filepath.Join(artifactDir(), id+".tar"))
|
|
}
|
|
}
|
|
|
|
// ServeArtifact streams a stored blob to a token-authenticated agent. Files
|
|
// persist across downloads — spread builds share one context — and are
|
|
// deleted by the deployment that created them.
|
|
func (h *NodeAgentHandler) ServeArtifact(c *gin.Context) {
|
|
if !h.isValidAgentAuthToken(c.Request.Context(), agentAuthTokenFromRequest(c)) {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid auth token"})
|
|
return
|
|
}
|
|
id := c.Param("id")
|
|
raw, err := hex.DecodeString(id)
|
|
if len(id) != 32 || err != nil || len(raw) != 16 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Artifact not found"})
|
|
return
|
|
}
|
|
path := filepath.Join(artifactDir(), id+".tar")
|
|
info, err := os.Stat(path)
|
|
if err != nil || info.IsDir() {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Artifact not found"})
|
|
return
|
|
}
|
|
c.Header("Content-Type", "application/x-tar")
|
|
c.File(path)
|
|
}
|
|
|
|
// purgeStaleArtifacts drops files older than maxArtifactAge. Called on boot.
|
|
func purgeStaleArtifacts() {
|
|
entries, err := os.ReadDir(artifactDir())
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, e := range entries {
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if info.ModTime().Before(time.Now().Add(-maxArtifactAge * time.Hour)) {
|
|
_ = os.Remove(filepath.Join(artifactDir(), e.Name()))
|
|
}
|
|
}
|
|
}
|