354 lines
8.7 KiB
Go
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
|
|
}
|