Files
registry-tui/client/registry.go
T
2026-10-31 18:32:28 +01:00

354 lines
8.7 KiB
Go

package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"os/exec"
)
type RegistryClient struct {
BaseURL string
HTTPClient *http.Client
}
func NewRegistryClient(baseURL string) *RegistryClient {
return &RegistryClient{
BaseURL: baseURL,
HTTPClient: &http.Client{},
}
}
type CatalogResponse struct {
Repositories []string `json:"repositories"`
}
type TagsResponse struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
type ManifestV2 struct {
Config struct {
Digest string `json:"digest"`
} `json:"config"`
Layers []struct {
Digest string `json:"digest"`
} `json:"layers"`
}
type LocalImageAPI struct {
RepoTags []string `json:"RepoTags"`
}
type ImageRowData struct {
Project string
RepoName string
ImageName string
Tag string
}
func (c *RegistryClient) GetCatalog() ([]string, error) {
url := fmt.Sprintf("%s/v2/_catalog", c.BaseURL)
resp, err := c.HTTPClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("el registro devolvió el código de estado %d", resp.StatusCode)
}
var catalog CatalogResponse
if err := json.NewDecoder(resp.Body).Decode(&catalog); err != nil {
return nil, err
}
return catalog.Repositories, nil
}
func (c *RegistryClient) GetTags(repo string) ([]string, error) {
url := fmt.Sprintf("%s/v2/%s/tags/list", c.BaseURL, repo)
resp, err := c.HTTPClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("el registro devolvió el código de estado %d", resp.StatusCode)
}
var tagsResp TagsResponse
if err := json.NewDecoder(resp.Body).Decode(&tagsResp); err != nil {
return nil, err
}
return tagsResp.Tags, nil
}
func (c *RegistryClient) DeleteManifest(repo, tag string) error {
manifestURL := fmt.Sprintf("%s/v2/%s/manifests/%s", c.BaseURL, repo, tag)
req, err := http.NewRequest("HEAD", manifestURL, nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("no se pudo obtener el manifest para borrar, estado: %d", resp.StatusCode)
}
digest := resp.Header.Get("Docker-Content-Digest")
if digest == "" {
return fmt.Errorf("el registro no devolvió el hash (Digest) necesario para el borrado")
}
deleteURL := fmt.Sprintf("%s/v2/%s/manifests/%s", c.BaseURL, repo, digest)
delReq, err := http.NewRequest("DELETE", deleteURL, nil)
if err != nil {
return err
}
delResp, err := c.HTTPClient.Do(delReq)
if err != nil {
return err
}
defer delResp.Body.Close()
if delResp.StatusCode != http.StatusAccepted && delResp.StatusCode != http.StatusOK {
return fmt.Errorf("el servidor rechazó el borrado (Estado HTTP %d)", delResp.StatusCode)
}
return nil
}
func (c *RegistryClient) CopyManifest(sourceRepo, sourceTag, destRepo, destTag string) error {
manifestURL := fmt.Sprintf("%s/v2/%s/manifests/%s", c.BaseURL, sourceRepo, sourceTag)
req, err := http.NewRequest("GET", manifestURL, nil)
if err != nil {
return err
}
manifestType := "application/vnd.docker.distribution.manifest.v2+json"
req.Header.Set("Accept", manifestType)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("no se pudo obtener el manifest origen, estado: %d", resp.StatusCode)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var manifest ManifestV2
if err := json.Unmarshal(bodyBytes, &manifest); err != nil {
return err
}
var digests []string
if manifest.Config.Digest != "" {
digests = append(digests, manifest.Config.Digest)
}
for _, layer := range manifest.Layers {
if layer.Digest != "" {
digests = append(digests, layer.Digest)
}
}
for _, digest := range digests {
mountURL := fmt.Sprintf("%s/v2/%s/blobs/uploads/?mount=%s&from=%s", c.BaseURL, destRepo, digest, sourceRepo)
mountReq, err := http.NewRequest("POST", mountURL, nil)
if err != nil {
return err
}
mountResp, err := c.HTTPClient.Do(mountReq)
if err != nil {
return err
}
mountResp.Body.Close()
}
putURL := fmt.Sprintf("%s/v2/%s/manifests/%s", c.BaseURL, destRepo, destTag)
putReq, err := http.NewRequest("PUT", putURL, bytes.NewReader(bodyBytes))
if err != nil {
return err
}
putReq.Header.Set("Content-Type", manifestType)
putResp, err := c.HTTPClient.Do(putReq)
if err != nil {
return err
}
defer putResp.Body.Close()
if putResp.StatusCode != http.StatusCreated && putResp.StatusCode != http.StatusOK {
return fmt.Errorf("no se pudo reestructurar la imagen, estado: %d", putResp.StatusCode)
}
return nil
}
func (c *RegistryClient) GetLocalImages() ([]ImageRowData, error) {
dockerClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", "/var/run/docker.sock")
},
},
}
resp, err := dockerClient.Get("http://unix/images/json")
if err != nil {
return nil, fmt.Errorf("no se pudo conectar con el demonio Docker: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error al listar imágenes locales, estado: %d", resp.StatusCode)
}
var images []LocalImageAPI
if err := json.NewDecoder(resp.Body).Decode(&images); err != nil {
return nil, err
}
var rows []ImageRowData
for _, img := range images {
for _, tagFull := range img.RepoTags {
if tagFull == "<none>:<none>" {
continue
}
lastColon := strings.LastIndex(tagFull, ":")
if lastColon == -1 {
continue
}
repo := tagFull[:lastColon]
tag := tagFull[lastColon+1:]
var project, imageName string
parts := strings.Split(repo, "/")
if len(parts) > 1 {
project = parts[0]
imageName = strings.Join(parts[1:], "/")
} else {
project = ""
imageName = repo
}
rows = append(rows, ImageRowData{
Project: project,
RepoName: repo,
ImageName: imageName,
Tag: tag,
})
}
}
return rows, nil
}
func (c *RegistryClient) DeleteLocalImage(repo, tag string) error {
dockerClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", "/var/run/docker.sock")
},
},
}
imageRef := fmt.Sprintf("%s:%s", repo, tag)
reqURL := fmt.Sprintf("http://unix/images/%s", imageRef)
req, err := http.NewRequest("DELETE", reqURL, nil)
if err != nil {
return err
}
resp, err := dockerClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("no se pudo borrar la imagen local, estado: %d", resp.StatusCode)
}
return nil
}
func (c *RegistryClient) TagLocalImage(sourceRepo, sourceTag, destRepo, destTag string) error {
dockerClient := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", "/var/run/docker.sock")
},
},
}
sourceRef := fmt.Sprintf("%s:%s", sourceRepo, sourceTag)
reqURL := fmt.Sprintf("http://unix/images/%s/tag?repo=%s&tag=%s", sourceRef, destRepo, destTag)
req, err := http.NewRequest("POST", reqURL, nil)
if err != nil {
return err
}
resp, err := dockerClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
return fmt.Errorf("no se pudo etiquetar la imagen local, estado: %d", resp.StatusCode)
}
return nil
}
func (c *RegistryClient) PushLocalImage(repo, tag string) error {
registryHost := strings.TrimPrefix(c.BaseURL, "http://")
registryHost = strings.TrimPrefix(registryHost, "https://")
sourceRef := fmt.Sprintf("%s:%s", repo, tag)
targetRef := fmt.Sprintf("%s/%s:%s", registryHost, repo, tag)
// 1. Etiquetar la imagen local para el registro
cmdTag := exec.Command("docker", "tag", sourceRef, targetRef)
if err := cmdTag.Run(); err != nil {
return fmt.Errorf("error al etiquetar la imagen para el registro: %v", err)
}
// 2. Ejecutar el push nativo de la CLI de Docker
cmdPush := exec.Command("docker", "push", targetRef)
if err := cmdPush.Run(); err != nil {
return fmt.Errorf("error al ejecutar docker push: %v", err)
}
// 3. Limpiar/eliminar la etiqueta temporal del registro creada en local
cmdRmi := exec.Command("docker", "rmi", targetRef)
if err := cmdRmi.Run(); err != nil {
// No devolvemos error crítico aquí para evitar interrumpir si el push ya fue exitoso,
// pero la etiqueta temporal se limpia de forma automática.
}
return nil
}