Version final
This commit is contained in:
1 parent
2d4b09b9ea
commit
74b7a0c448
13 files changed
+1103
-7
No files matched your search
@@ -0,0 +1,353 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user