132 lines
4.3 KiB
Go
132 lines
4.3 KiB
Go
|
|
package service
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/url"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func (a *App) artifactRepo(node string) string {
|
||
|
|
return fmt.Sprintf("%s/%s/%s", strings.TrimRight(a.settings.HarborRegistry, "/"), strings.Trim(a.settings.HarborProject, "/"), node)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) ensureHarborProject() error {
|
||
|
|
if strings.TrimSpace(a.settings.HarborAPIBase) == "" || strings.TrimSpace(a.settings.HarborPassword) == "" {
|
||
|
|
return fmt.Errorf("harbor admin credentials are not configured")
|
||
|
|
}
|
||
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
||
|
|
project := strings.TrimSpace(a.settings.HarborProject)
|
||
|
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/projects?name=%s", strings.TrimRight(a.settings.HarborAPIBase, "/"), url.QueryEscape(project)), nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
req.SetBasicAuth(strings.TrimSpace(a.settings.HarborUsername), strings.TrimSpace(a.settings.HarborPassword))
|
||
|
|
resp, err := client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
if resp.StatusCode >= 300 {
|
||
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||
|
|
return fmt.Errorf("harbor project lookup failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||
|
|
}
|
||
|
|
var projects []struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
}
|
||
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&projects); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, item := range projects {
|
||
|
|
if strings.EqualFold(strings.TrimSpace(item.Name), project) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
payload := map[string]any{
|
||
|
|
"project_name": project,
|
||
|
|
"metadata": map[string]string{"public": "false"},
|
||
|
|
}
|
||
|
|
data, err := json.Marshal(payload)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%s/projects", strings.TrimRight(a.settings.HarborAPIBase, "/")), bytes.NewReader(data))
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
req.SetBasicAuth(strings.TrimSpace(a.settings.HarborUsername), strings.TrimSpace(a.settings.HarborPassword))
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
resp, err = client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusConflict {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||
|
|
return fmt.Errorf("harbor project create failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *App) pruneHarborArtifacts(node string, keep int) error {
|
||
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
||
|
|
repo := url.PathEscape(node)
|
||
|
|
apiBase := strings.TrimRight(a.settings.HarborAPIBase, "/")
|
||
|
|
project := url.PathEscape(strings.TrimSpace(a.settings.HarborProject))
|
||
|
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/projects/%s/repositories/%s/artifacts?page_size=100&with_tag=true", apiBase, project, repo), nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
req.SetBasicAuth(strings.TrimSpace(a.settings.HarborUsername), strings.TrimSpace(a.settings.HarborPassword))
|
||
|
|
resp, err := client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
if resp.StatusCode == http.StatusNotFound {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if resp.StatusCode >= 300 {
|
||
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||
|
|
return fmt.Errorf("harbor artifact list failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||
|
|
}
|
||
|
|
var artifacts []struct {
|
||
|
|
Digest string `json:"digest"`
|
||
|
|
PushTime string `json:"push_time"`
|
||
|
|
Tags []struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
} `json:"tags"`
|
||
|
|
}
|
||
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 2<<20)).Decode(&artifacts); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
sort.Slice(artifacts, func(i, j int) bool {
|
||
|
|
return artifacts[i].PushTime > artifacts[j].PushTime
|
||
|
|
})
|
||
|
|
for idx, artifact := range artifacts {
|
||
|
|
if idx < keep {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
ref := url.PathEscape(artifact.Digest)
|
||
|
|
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/projects/%s/repositories/%s/artifacts/%s", apiBase, project, repo, ref), nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
req.SetBasicAuth(strings.TrimSpace(a.settings.HarborUsername), strings.TrimSpace(a.settings.HarborPassword))
|
||
|
|
resp, err := client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
resp.Body.Close()
|
||
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusNotFound {
|
||
|
|
return fmt.Errorf("harbor artifact delete failed for %s: %s", artifact.Digest, resp.Status)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|