// Изменено: 2026-03-11 // Harbor API клиент — управление проектами (create/ensure) перед push образов. // Почему здесь: kaniko не создаёт Harbor-проект сам, нужен явный вызов API. // Все операции идемпотентны - повторный вызов EnsureProject безопасен. package harbor import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" ) // Client — минимальный HTTP-клиент для Harbor API v2. // Используется только для создания проектов; пуш образов делает kaniko напрямую. type Client struct { baseURL string // https://{registryHost} user string password string http *http.Client } // New создаёт Harbor клиент. // registryHost — хост реестра без схемы (e.g. "pearlharbor.registryk8s.services.ngcloud.ru"). func New(registryHost, user, password string) *Client { return &Client{ baseURL: "https://" + registryHost, user: user, password: password, http: &http.Client{Timeout: 15 * time.Second}, } } // EnsureProject идемпотентно создаёт Harbor проект с именем name (private). // GET /api/v2.0/projects?name={name} → если не найден → POST /api/v2.0/projects. // Возвращает nil если проект уже существовал или был успешно создан. func (c *Client) EnsureProject(ctx context.Context, name string) error { // Harbor GET projects возвращает список проектов по фильтру name= req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v2.0/projects?name=%s", c.baseURL, name), nil) if err != nil { return fmt.Errorf("harbor: build GET request: %w", err) } req.SetBasicAuth(c.user, c.password) resp, err := c.http.Do(req) if err != nil { return fmt.Errorf("harbor: GET project %q: %w", name, err) } defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { return fmt.Errorf("harbor: unauthorized — проверьте HARBOR_USER и HARBOR_PASS") } var projects []struct { Name string `json:"name"` } if err := json.NewDecoder(resp.Body).Decode(&projects); err != nil { return fmt.Errorf("harbor: decode GET response: %w", err) } for _, p := range projects { if p.Name == name { return nil // проект уже существует } } // Проект не найден — создаём body, _ := json.Marshal(map[string]interface{}{ "project_name": name, "public": false, }) req, err = http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v2.0/projects", bytes.NewReader(body)) if err != nil { return fmt.Errorf("harbor: build POST request: %w", err) } req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(c.user, c.password) resp2, err := c.http.Do(req) if err != nil { return fmt.Errorf("harbor: POST project %q: %w", name, err) } defer resp2.Body.Close() // 201 Created — успешно; 409 Conflict — гонка (кто-то создал параллельно), оба ок if resp2.StatusCode != http.StatusCreated && resp2.StatusCode != http.StatusConflict { return fmt.Errorf("harbor: POST project %q: unexpected status %d", name, resp2.StatusCode) } return nil }