first commit
This commit is contained in:
commit
b312c9e35e
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
103
README.md
Normal 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
185
config.go
Normal 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
22
config.yaml.example
Normal 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
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module registry-view
|
||||
|
||||
go 1.21
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
4
go.sum
Normal file
4
go.sum
Normal 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
508
main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user