mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat: build-on-node — git checkouts, remote image builds, image transport (#60)
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]>
This commit is contained in:
1 parent
d57ac72aa5
commit
3cc84553f4
18 files changed
+1020
-51
No files matched your search
@@ -316,7 +316,7 @@ func processCommands(ctx context.Context, client *http.Client, cfg agentConfig,
|
||||
if item.Type == "self_upgrade" {
|
||||
result, reexecPath, runErr = selfUpgrade(cfg, item.Payload)
|
||||
} else {
|
||||
result, runErr = executeCommand(item)
|
||||
result, runErr = executeCommand(cfg, item)
|
||||
}
|
||||
status := "completed"
|
||||
errText := ""
|
||||
@@ -343,7 +343,7 @@ func processCommands(ctx context.Context, client *http.Client, cfg agentConfig,
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeCommand(item command) (string, error) {
|
||||
func executeCommand(cfg agentConfig, item command) (string, error) {
|
||||
switch item.Type {
|
||||
case "create_container":
|
||||
return createContainer(item.Payload)
|
||||
@@ -360,6 +360,10 @@ func executeCommand(item command) (string, error) {
|
||||
return out, nil
|
||||
}
|
||||
return out, err
|
||||
case "build_image":
|
||||
return buildImage(cfg, item.Payload)
|
||||
case "load_image":
|
||||
return loadImage(cfg, item.Payload)
|
||||
case "prune":
|
||||
return dockerPrune(item.Payload)
|
||||
case "system_df":
|
||||
@@ -568,6 +572,120 @@ func createContainer(payload map[string]interface{}) (string, error) {
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// fetchArtifact downloads a server-side deploy artifact (build context or
|
||||
// docker-save tarball) into a temp file the docker CLI consumes.
|
||||
func fetchArtifact(cfg agentConfig, artifactID string) (*os.File, error) {
|
||||
if len(artifactID) != 32 {
|
||||
return nil, fmt.Errorf("invalid artifact id")
|
||||
}
|
||||
url := fmt.Sprintf("%s/api/agents/artifacts/%s", cfg.APIURL, artifactID)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("X-Containr-Agent-Token", cfg.Token)
|
||||
resp, err := (&http.Client{Timeout: 10 * time.Minute}).Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download artifact: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("download artifact: server returned %d", resp.StatusCode)
|
||||
}
|
||||
f, err := os.CreateTemp("", "containr-artifact-*.tar")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
return nil, fmt.Errorf("write artifact: %w", err)
|
||||
}
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// buildImage docker-builds a packaged context tar on this node. The context
|
||||
// already carries its Dockerfile at the tar root, so `docker build -` can
|
||||
// stream it straight from stdin. Result reports the image digest so the
|
||||
// server records what each node actually produced.
|
||||
func buildImage(cfg agentConfig, payload map[string]interface{}) (string, error) {
|
||||
image := stringValue(payload["image"])
|
||||
artifactID := stringValue(payload["artifact_id"])
|
||||
if image == "" || artifactID == "" {
|
||||
return "", errors.New("build_image requires image and artifact_id")
|
||||
}
|
||||
f, err := fetchArtifact(cfg, artifactID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
args := []string{"build", "-t", image}
|
||||
if boolValue(payload["no_cache"]) {
|
||||
args = append(args, "--no-cache")
|
||||
}
|
||||
// Bare --build-arg keys read their values from the process env — keeps
|
||||
// token-bearing args out of the node's process list.
|
||||
env := os.Environ()
|
||||
if buildArgs, ok := payload["build_args"].(map[string]interface{}); ok {
|
||||
for k, v := range buildArgs {
|
||||
args = append(args, "--build-arg", k)
|
||||
env = append(env, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
}
|
||||
args = append(args, "-")
|
||||
cmd := exec.Command("docker", args...)
|
||||
cmd.Stdin = f
|
||||
cmd.Env = env
|
||||
outBytes, err := cmd.CombinedOutput()
|
||||
out := strings.TrimSpace(string(outBytes))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("docker build failed: %s", tailOutput(out, 4096))
|
||||
}
|
||||
digest, _ := runDocker("inspect", "-f", "{{.Id}}", image)
|
||||
size, _ := runDocker("inspect", "-f", "{{.Size}}", image)
|
||||
result, _ := json.Marshal(map[string]string{
|
||||
"image": image, "id": digest, "size": size,
|
||||
"log_tail": tailOutput(out, 4096),
|
||||
})
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// loadImage imports a docker-save tarball — rollbacks of locally-built tags
|
||||
// reach remote nodes this way since no registry serves containr-* images.
|
||||
func loadImage(cfg agentConfig, payload map[string]interface{}) (string, error) {
|
||||
artifactID := stringValue(payload["artifact_id"])
|
||||
if artifactID == "" {
|
||||
return "", errors.New("load_image requires artifact_id")
|
||||
}
|
||||
f, err := fetchArtifact(cfg, artifactID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
out, err := runDockerStdin(f, "load")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("docker load failed: %s", tailOutput(out, 4096))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// tailOutput bounds command results — full docker build logs would swamp
|
||||
// the agent_commands row.
|
||||
func tailOutput(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[len(s)-max:]
|
||||
}
|
||||
|
||||
// registryHostOf mirrors the server's registryHost: first path segment with
|
||||
// a "." or ":" is a registry host; bare names mean Docker Hub.
|
||||
func registryHostOf(imageRef string) string {
|
||||
@@ -779,6 +897,15 @@ func runDocker(args ...string) (string, error) {
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
|
||||
// runDockerStdin pipes stdin into docker — build contexts and saved image
|
||||
// tarballs stream in without a second copy on disk.
|
||||
func runDockerStdin(stdin io.Reader, args ...string) (string, error) {
|
||||
cmd := exec.Command("docker", args...)
|
||||
cmd.Stdin = stdin
|
||||
output, err := cmd.CombinedOutput()
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
|
||||
func dockerAvailable() bool {
|
||||
_, err := exec.LookPath("docker")
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user