first commit

This commit is contained in:
admin 2026-01-27 09:55:57 +07:00
commit b312c9e35e
7 changed files with 851 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Бинарные файлы
registry-view
*.exe
*.exe~
*.dll
*.so
*.dylib
# Конфигурация с паролями (не коммитить!)
config.yaml
# Тестовые файлы
*.test
# Временные файлы
*.tmp
*.log
# IDE
.vscode/
.idea/
*.swp
*.swo
*~

103
README.md Normal file
View File

@ -0,0 +1,103 @@
# Docker Registry Viewer
Программа на Go для просмотра списка образов из Docker Registry с поддержкой авторизации.
## Возможности
- Подключение к Docker Registry с авторизацией (Basic Auth и Bearer Token)
- Получение списка всех репозиториев
- Получение списка тегов для каждого репозитория
- Поддержка пагинации для больших реестров
- Конфигурация через YAML файл
- Поддержка учетных данных из `~/.docker/config.json` (как в Docker CLI)
## Установка
```bash
go mod download
```
## Конфигурация
Все настройки хранятся в файле `config.yaml`. Скопируйте пример конфигурации:
```bash
cp config.yaml.example config.yaml
```
### Способ 1: Указать учетные данные в config.yaml
```yaml
registry:
url: "https://registry.example.com"
username: "admin"
password: "password123"
debug: false
```
### Способ 2: Использовать учетные данные из Docker config
Вы можете использовать учетные данные из `~/.docker/config.json` (те же, что использует Docker CLI):
```yaml
registry:
url: "https://registry.example.com"
use_docker_config: true
debug: false
```
Программа автоматически найдет учетные данные для указанного registry в файле `~/.docker/config.json`.
**Примечание:** Если `use_docker_config: false` и не указаны `username`/`password`, программа попытается автоматически загрузить их из Docker config как fallback.
## Использование
### Базовое использование
```bash
go run main.go
```
Программа автоматически загрузит конфигурацию из `config.yaml`.
### Указание другого файла конфигурации
```bash
go run main.go -config /path/to/config.yaml
# или краткая форма
go run main.go -c /path/to/config.yaml
```
### Сборка
```bash
go build -o registry-view
./registry-view
```
### Режим отладки
Для диагностики проблем с авторизацией включите режим отладки в `config.yaml`:
```yaml
debug: true
```
Режим отладки покажет:
- URL запросов
- HTTP статусы ответов
- Заголовки ответов
- Тела ответов
- Процесс получения токенов
## Формат вывода
Программа выводит:
1. Список всех репозиториев с нумерацией
2. Детальную информацию: для каждого репозитория список всех тегов
## Примечания
- Программа поддерживает как Basic Authentication, так и Bearer Token (OAuth2)
- Автоматически обрабатывает пагинацию при большом количестве репозиториев
- Для работы с Registry API v2 требуется соответствующий доступ

185
config.go Normal file
View File

@ -0,0 +1,185 @@
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
// Config представляет структуру конфигурации
type Config struct {
Registry struct {
URL string `yaml:"url"`
Username string `yaml:"username"`
Password string `yaml:"password"`
} `yaml:"registry"`
Debug bool `yaml:"debug"`
UseDockerConfig bool `yaml:"use_docker_config"` // Использовать ~/.docker/config.json
}
// DockerConfig представляет структуру Docker config.json
type DockerConfig struct {
Auths map[string]DockerAuth `json:"auths"`
}
// DockerAuth представляет учетные данные для одного registry
type DockerAuth struct {
Auth string `json:"auth"` // base64(username:password)
Username string `json:"username"` // альтернативный формат
Password string `json:"password"` // альтернативный формат
}
// LoadDockerConfig загружает учетные данные из ~/.docker/config.json
func LoadDockerConfig(registryURL string) (username, password string, err error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", "", fmt.Errorf("не удалось получить домашнюю директорию: %w", err)
}
dockerConfigPath := filepath.Join(homeDir, ".docker", "config.json")
data, err := os.ReadFile(dockerConfigPath)
if err != nil {
return "", "", fmt.Errorf("не удалось прочитать Docker config: %w", err)
}
var dockerConfig DockerConfig
if err := json.Unmarshal(data, &dockerConfig); err != nil {
return "", "", fmt.Errorf("ошибка парсинга Docker config: %w", err)
}
// Парсим URL registry для поиска подходящей записи
registryHost, err := extractRegistryHost(registryURL)
if err != nil {
return "", "", fmt.Errorf("ошибка парсинга URL registry: %w", err)
}
// Ищем подходящий registry в конфиге
// Проверяем точное совпадение и варианты с/без протокола
var auth DockerAuth
found := false
// Варианты для поиска
searchKeys := []string{
registryURL, // полный URL
registryHost, // только хост
strings.TrimPrefix(registryURL, "https://"), // без https://
strings.TrimPrefix(registryURL, "http://"), // без http://
}
for _, key := range searchKeys {
if a, ok := dockerConfig.Auths[key]; ok {
auth = a
found = true
fmt.Printf("[DEBUG] Найдены учетные данные в Docker config по ключу: %s\n", key)
break
}
}
// Если не нашли точное совпадение, пробуем частичное
if !found {
for dockerKey, a := range dockerConfig.Auths {
if strings.Contains(registryHost, dockerKey) || strings.Contains(dockerKey, registryHost) {
auth = a
found = true
break
}
}
}
if !found {
return "", "", fmt.Errorf("не найдены учетные данные для registry %s в Docker config", registryURL)
}
// Извлекаем username и password
if auth.Auth != "" {
// Формат: base64(username:password)
decoded, err := base64.StdEncoding.DecodeString(auth.Auth)
if err != nil {
return "", "", fmt.Errorf("ошибка декодирования auth: %w", err)
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("неверный формат auth в Docker config")
}
return parts[0], parts[1], nil
} else if auth.Username != "" && auth.Password != "" {
// Альтернативный формат: отдельные поля
return auth.Username, auth.Password, nil
}
return "", "", fmt.Errorf("не найдены учетные данные в Docker config для %s", registryURL)
}
// extractRegistryHost извлекает хост из URL registry
func extractRegistryHost(registryURL string) (string, error) {
u, err := url.Parse(registryURL)
if err != nil {
return "", err
}
return u.Host, nil
}
// LoadConfig загружает конфигурацию из файла
func LoadConfig(configPath string) (*Config, error) {
// Если путь не указан, пробуем стандартные места
if configPath == "" {
configPath = "config.yaml"
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("ошибка чтения файла конфигурации %s: %w", configPath, err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("ошибка парсинга конфигурации: %w", err)
}
// Валидация обязательных полей
if config.Registry.URL == "" {
return nil, fmt.Errorf("registry.url обязателен в конфигурации")
}
// Если включено использование Docker config, всегда используем его
if config.UseDockerConfig {
username, password, err := LoadDockerConfig(config.Registry.URL)
if err != nil {
return nil, fmt.Errorf("не удалось загрузить учетные данные из Docker config: %w", err)
}
config.Registry.Username = username
config.Registry.Password = password
if config.Debug {
fmt.Printf("[DEBUG] Загружены учетные данные из Docker config для %s\n", config.Registry.URL)
fmt.Printf("[DEBUG] Username: %s\n", username)
}
} else if config.Registry.Username == "" || config.Registry.Password == "" {
// Если не указаны учетные данные в YAML, пробуем загрузить из Docker config как fallback
username, password, err := LoadDockerConfig(config.Registry.URL)
if err != nil {
return nil, fmt.Errorf("не указаны учетные данные в config.yaml и не удалось загрузить из Docker config: %w", err)
}
config.Registry.Username = username
config.Registry.Password = password
if config.Debug {
fmt.Printf("[DEBUG] Автоматически загружены учетные данные из Docker config (fallback)\n")
fmt.Printf("[DEBUG] Username: %s\n", username)
}
} else if config.Debug {
fmt.Printf("[DEBUG] Используются учетные данные из config.yaml\n")
fmt.Printf("[DEBUG] Username: %s\n", config.Registry.Username)
}
// Финальная проверка
if config.Registry.Username == "" || config.Registry.Password == "" {
return nil, fmt.Errorf("не указаны учетные данные (username/password). Укажите в config.yaml или используйте use_docker_config: true")
}
return &config, nil
}

22
config.yaml.example Normal file
View File

@ -0,0 +1,22 @@
# Пример конфигурации для Docker Registry Viewer
# Скопируйте этот файл в config.yaml и заполните своими данными
# Настройки Docker Registry
registry:
# URL Docker Registry (например: https://registry.example.com или http://localhost:5000)
url: "https://registry.example.com"
# Имя пользователя для авторизации (опционально, если используется use_docker_config)
username: "admin"
# Пароль для авторизации (опционально, если используется use_docker_config)
password: "password123"
# Использовать учетные данные из ~/.docker/config.json (true/false)
# Если true, username и password из config.yaml будут проигнорированы
# Если false и не указаны username/password, будет попытка загрузить из Docker config
use_docker_config: false
# Режим отладки (true/false)
# При включении показывает детальную информацию о запросах и ответах
debug: false

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module registry-view
go 1.21
require gopkg.in/yaml.v3 v3.0.1

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

508
main.go Normal file
View File

@ -0,0 +1,508 @@
package main
import (
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
)
// RegistryClient представляет клиент для работы с Docker Registry
type RegistryClient struct {
BaseURL string
Username string
Password string
Client *http.Client
Debug bool
token string // кэшированный токен
}
// NewRegistryClient создает новый клиент для работы с Registry
func NewRegistryClient(baseURL, username, password string, debug bool) *RegistryClient {
return &RegistryClient{
BaseURL: strings.TrimSuffix(baseURL, "/"),
Username: username,
Password: password,
Client: &http.Client{},
Debug: debug,
}
}
// authenticate выполняет авторизацию и возвращает токен
// scope - опциональный scope для запроса токена (например, "registry:catalog:*")
func (r *RegistryClient) authenticate(scope string) (string, error) {
authURL := fmt.Sprintf("%s/v2/", r.BaseURL)
if r.Debug {
fmt.Printf("[DEBUG] Попытка авторизации на: %s\n", authURL)
}
req, err := http.NewRequest("GET", authURL, nil)
if err != nil {
return "", fmt.Errorf("ошибка создания запроса: %w", err)
}
// Добавляем Basic Auth заголовок
auth := base64.StdEncoding.EncodeToString([]byte(r.Username + ":" + r.Password))
req.Header.Set("Authorization", "Basic "+auth)
resp, err := r.Client.Do(req)
if err != nil {
return "", fmt.Errorf("ошибка выполнения запроса: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if r.Debug {
fmt.Printf("[DEBUG] Статус ответа: %d\n", resp.StatusCode)
fmt.Printf("[DEBUG] Заголовки: %v\n", resp.Header)
fmt.Printf("[DEBUG] Тело ответа: %s\n", string(body))
}
// Если получили 401, проверяем заголовок WWW-Authenticate для Bearer token
if resp.StatusCode == http.StatusUnauthorized {
wwwAuth := resp.Header.Get("WWW-Authenticate")
if r.Debug {
fmt.Printf("[DEBUG] WWW-Authenticate заголовок: %s\n", wwwAuth)
}
if wwwAuth != "" {
if strings.HasPrefix(wwwAuth, "Bearer") {
// Извлекаем realm, service и scope из заголовка
realm, service, scopeFromHeader := parseWwwAuthenticate(wwwAuth)
if r.Debug {
fmt.Printf("[DEBUG] Парсинг WWW-Authenticate: realm=%s, service=%s, scope=%s\n", realm, service, scopeFromHeader)
}
if realm != "" {
// Используем переданный scope, если он указан, иначе используем из заголовка
tokenScope := scope
if tokenScope == "" {
tokenScope = scopeFromHeader
}
token, err := r.getBearerToken(realm, service, tokenScope)
if err != nil {
return "", fmt.Errorf("не удалось получить Bearer token: %w", err)
}
return token, nil
}
} else if strings.HasPrefix(wwwAuth, "Basic") {
// Если требуется Basic Auth, но мы его уже отправили, значит неправильные учетные данные
return "", fmt.Errorf("неверные учетные данные (Basic Auth отклонен). Проверьте username и password")
}
}
// Если не удалось получить Bearer token и получили 401
return "", fmt.Errorf("ошибка авторизации: статус %d, WWW-Authenticate: %s, ответ: %s",
resp.StatusCode, wwwAuth, string(body))
}
// Если авторизация прошла успешно (200)
if resp.StatusCode == http.StatusOK {
if r.Debug {
fmt.Printf("[DEBUG] Авторизация успешна (Basic Auth)\n")
}
return "", nil // Basic Auth достаточно
}
return "", fmt.Errorf("неожиданный статус ответа: %d, тело: %s", resp.StatusCode, string(body))
}
// parseWwwAuthenticate парсит заголовок WWW-Authenticate
func parseWwwAuthenticate(header string) (realm, service, scope string) {
// Формат: Bearer realm="...",service="...",scope="..."
// Убираем префикс "Bearer " если есть
header = strings.TrimPrefix(header, "Bearer ")
header = strings.TrimSpace(header)
// Парсим параметры, учитывая что значения могут быть в кавычках
parts := strings.Split(header, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "realm=") {
realm = strings.Trim(strings.TrimPrefix(part, "realm="), "\"")
} else if strings.HasPrefix(part, "service=") {
service = strings.Trim(strings.TrimPrefix(part, "service="), "\"")
} else if strings.HasPrefix(part, "scope=") {
scope = strings.Trim(strings.TrimPrefix(part, "scope="), "\"")
}
}
return
}
// getBearerToken получает Bearer token из auth service
func (r *RegistryClient) getBearerToken(realm, service, scope string) (string, error) {
// Формируем URL для получения токена
tokenURL := realm
if !strings.Contains(tokenURL, "?") {
tokenURL += "?"
} else {
tokenURL += "&"
}
// Добавляем параметры
params := []string{}
if service != "" {
params = append(params, "service="+service)
}
// Добавляем scope только если он указан (не добавляем по умолчанию)
// Для GitLab Registry нужно запрашивать токен с конкретным scope
if scope != "" {
params = append(params, "scope="+scope)
}
tokenURL += strings.Join(params, "&")
if r.Debug {
fmt.Printf("[DEBUG] Запрос токена по URL: %s\n", tokenURL)
}
req, err := http.NewRequest("GET", tokenURL, nil)
if err != nil {
return "", fmt.Errorf("ошибка создания запроса токена: %w", err)
}
// Добавляем Basic Auth для получения токена
auth := base64.StdEncoding.EncodeToString([]byte(r.Username + ":" + r.Password))
req.Header.Set("Authorization", "Basic "+auth)
resp, err := r.Client.Do(req)
if err != nil {
return "", fmt.Errorf("ошибка получения токена: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if r.Debug {
fmt.Printf("[DEBUG] Статус ответа токена: %d\n", resp.StatusCode)
fmt.Printf("[DEBUG] Тело ответа токена: %s\n", string(body))
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("ошибка получения токена: статус %d, тело: %s", resp.StatusCode, string(body))
}
// Пробуем разные форматы ответа
var tokenResp struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
}
if err := json.Unmarshal(body, &tokenResp); err != nil {
return "", fmt.Errorf("ошибка парсинга токена: %w, тело: %s", err, string(body))
}
// Используем token или access_token
token := tokenResp.Token
if token == "" {
token = tokenResp.AccessToken
}
if token == "" {
return "", fmt.Errorf("токен не найден в ответе: %s", string(body))
}
if r.Debug {
fmt.Printf("[DEBUG] Токен успешно получен (длина: %d)\n", len(token))
// Декодируем JWT для отладки (только payload, без проверки подписи)
parts := strings.Split(token, ".")
if len(parts) >= 2 {
// Декодируем payload (вторая часть)
payload := parts[1]
// Добавляем padding если нужно
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
decoded, err := base64.RawURLEncoding.DecodeString(payload)
if err == nil {
var claims map[string]interface{}
if json.Unmarshal(decoded, &claims) == nil {
fmt.Printf("[DEBUG] JWT claims: %+v\n", claims)
}
}
}
}
return token, nil
}
// ListRepositories возвращает список всех репозиториев в Registry
func (r *RegistryClient) ListRepositories() ([]string, error) {
// Формируем запрос к API для получения списка репозиториев
reposURL := fmt.Sprintf("%s/v2/_catalog", r.BaseURL)
// Поддерживаем пагинацию
var allRepos []string
url := reposURL
for {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("ошибка создания запроса: %w", err)
}
// Добавляем авторизацию
if r.token != "" {
req.Header.Set("Authorization", "Bearer "+r.token)
} else {
auth := base64.StdEncoding.EncodeToString([]byte(r.Username + ":" + r.Password))
req.Header.Set("Authorization", "Basic "+auth)
}
resp, err := r.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("ошибка выполнения запроса: %w", err)
}
defer resp.Body.Close()
// Если получили 401, нужно получить токен с правильным scope
if resp.StatusCode == http.StatusUnauthorized {
body, _ := io.ReadAll(resp.Body)
wwwAuth := resp.Header.Get("WWW-Authenticate")
if r.Debug {
fmt.Printf("[DEBUG] Получен 401 при запросе каталога, WWW-Authenticate: %s\n", wwwAuth)
}
// Если есть WWW-Authenticate с scope, используем его для получения токена
if wwwAuth != "" && strings.HasPrefix(wwwAuth, "Bearer") {
realm, service, scopeFromHeader := parseWwwAuthenticate(wwwAuth)
if realm != "" {
// Используем scope из заголовка, если он есть
scope := scopeFromHeader
if scope == "" {
// Если scope не указан в заголовке, используем стандартный для каталога
scope = "registry:catalog:*"
}
if r.Debug {
fmt.Printf("[DEBUG] Запрос токена с scope: %s, service: %s\n", scope, service)
}
// Получаем токен напрямую через getBearerToken, так как authenticate делает лишний запрос
token, err := r.getBearerToken(realm, service, scope)
if err != nil {
return nil, fmt.Errorf("ошибка получения токена: %w, оригинальный ответ: %s", err, string(body))
}
r.token = token
// Повторяем запрос с новым токеном
req.Header.Set("Authorization", "Bearer "+r.token)
resp, err = r.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("ошибка выполнения запроса после получения токена: %w", err)
}
defer resp.Body.Close()
}
}
// Если все еще 401, возвращаем ошибку
if resp.StatusCode == http.StatusUnauthorized {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ошибка авторизации при запросе каталога: статус %d, тело: %s", resp.StatusCode, string(body))
}
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ошибка получения списка репозиториев: статус %d, тело: %s", resp.StatusCode, string(body))
}
var catalog struct {
Repositories []string `json:"repositories"`
}
if err := json.NewDecoder(resp.Body).Decode(&catalog); err != nil {
return nil, fmt.Errorf("ошибка парсинга ответа: %w", err)
}
allRepos = append(allRepos, catalog.Repositories...)
// Проверяем наличие ссылки на следующую страницу
link := resp.Header.Get("Link")
if link == "" {
break
}
// Парсим ссылку на следующую страницу
nextURL := extractNextURL(link)
if nextURL == "" {
break
}
url = r.BaseURL + nextURL
}
return allRepos, nil
}
// extractNextURL извлекает URL следующей страницы из заголовка Link
func extractNextURL(link string) string {
// Формат: <url>; rel="next"
parts := strings.Split(link, ";")
if len(parts) > 0 {
url := strings.TrimSpace(parts[0])
url = strings.Trim(url, "<>")
return url
}
return ""
}
// ListTags возвращает список тегов для конкретного репозитория
func (r *RegistryClient) ListTags(repository string) ([]string, error) {
tagsURL := fmt.Sprintf("%s/v2/%s/tags/list", r.BaseURL, repository)
req, err := http.NewRequest("GET", tagsURL, nil)
if err != nil {
return nil, fmt.Errorf("ошибка создания запроса: %w", err)
}
if r.token != "" {
req.Header.Set("Authorization", "Bearer "+r.token)
} else {
auth := base64.StdEncoding.EncodeToString([]byte(r.Username + ":" + r.Password))
req.Header.Set("Authorization", "Basic "+auth)
}
resp, err := r.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("ошибка выполнения запроса: %w", err)
}
defer resp.Body.Close()
// Если получили 401, получаем токен с правильным scope для этого репозитория
if resp.StatusCode == http.StatusUnauthorized {
body, _ := io.ReadAll(resp.Body)
wwwAuth := resp.Header.Get("WWW-Authenticate")
if wwwAuth != "" && strings.HasPrefix(wwwAuth, "Bearer") {
realm, service, scopeFromHeader := parseWwwAuthenticate(wwwAuth)
if realm != "" {
scope := scopeFromHeader
if scope == "" {
// Если scope не указан, используем scope для конкретного репозитория
scope = fmt.Sprintf("repository:%s:pull", repository)
}
if r.Debug {
fmt.Printf("[DEBUG] Запрос токена для репозитория с scope: %s, service: %s\n", scope, service)
}
// Получаем токен напрямую через getBearerToken
token, err := r.getBearerToken(realm, service, scope)
if err != nil {
return nil, fmt.Errorf("ошибка получения токена: %w, оригинальный ответ: %s", err, string(body))
}
r.token = token
// Повторяем запрос с новым токеном
req.Header.Set("Authorization", "Bearer "+r.token)
resp, err = r.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("ошибка выполнения запроса после получения токена: %w", err)
}
defer resp.Body.Close()
}
}
if resp.StatusCode == http.StatusUnauthorized {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ошибка авторизации при запросе тегов: статус %d, тело: %s", resp.StatusCode, string(body))
}
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("ошибка получения списка тегов: статус %d, тело: %s", resp.StatusCode, string(body))
}
var tagsResp struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
if err := json.NewDecoder(resp.Body).Decode(&tagsResp); err != nil {
return nil, fmt.Errorf("ошибка парсинга ответа: %w", err)
}
return tagsResp.Tags, nil
}
func main() {
// Парсим аргументы командной строки
var configPath string
flag.StringVar(&configPath, "config", "config.yaml", "Путь к файлу конфигурации")
flag.StringVar(&configPath, "c", "config.yaml", "Путь к файлу конфигурации (краткая форма)")
flag.Parse()
// Загружаем конфигурацию
config, err := LoadConfig(configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка загрузки конфигурации: %v\n", err)
fmt.Fprintf(os.Stderr, "Использование: %s [-config=config.yaml]\n", os.Args[0])
os.Exit(1)
}
// Создаем клиент
client := NewRegistryClient(
config.Registry.URL,
config.Registry.Username,
config.Registry.Password,
config.Debug,
)
fmt.Printf("Подключение к Docker Registry: %s\n", config.Registry.URL)
fmt.Printf("Пользователь: %s\n", config.Registry.Username)
if client.Debug {
fmt.Println("Режим отладки включен")
}
fmt.Println("Получение списка репозиториев...\n")
// Получаем список репозиториев
repos, err := client.ListRepositories()
if err != nil {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
os.Exit(1)
}
if len(repos) == 0 {
fmt.Println("Репозитории не найдены.")
return
}
fmt.Printf("Найдено репозиториев: %d\n\n", len(repos))
fmt.Println("Список репозиториев:")
fmt.Println(strings.Repeat("=", 80))
for i, repo := range repos {
fmt.Printf("%d. %s\n", i+1, repo)
}
// Опционально: показать теги для каждого репозитория
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("\nДетальная информация (репозиторий -> теги):")
fmt.Println(strings.Repeat("=", 80))
for _, repo := range repos {
tags, err := client.ListTags(repo)
if err != nil {
fmt.Printf(" %s: [ошибка получения тегов: %v]\n", repo, err)
continue
}
if len(tags) == 0 {
fmt.Printf(" %s: [нет тегов]\n", repo)
} else {
fmt.Printf(" %s:\n", repo)
for _, tag := range tags {
fmt.Printf(" - %s\n", tag)
}
}
}
}