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 == ":" { 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 }