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 }