1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-22 14:59:41 +02:00
portainer/api/http/proxy/factory/gitlab/client.go

130 lines
3.7 KiB
Go

package gitlab
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/segmentio/encoding/json"
"oras.land/oras-go/v2/registry/remote/retry"
)
// Repository represents a GitLab registry repository
type Repository struct {
ID int `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
ProjectID int `json:"project_id"`
Location string `json:"location"`
CreatedAt string `json:"created_at"`
Status string `json:"status"`
}
// Client represents a GitLab API client
type Client struct {
httpClient *http.Client
baseURL string
}
// NewClient creates a new GitLab API client
// it currently is an http client because only GetRegistryRepositoryNames is needed (oras supports other commands).
// if we need to support other commands, consider using the gitlab client library.
func NewClient(baseURL, token string) *Client {
return &Client{
httpClient: NewHTTPClient(token),
baseURL: baseURL,
}
}
// GetRegistryRepositoryNames fetches registry repository names for a given project.
// It's a small http client wrapper instead of using the gitlab client library because listing repositories is the only known operation that isn't directly supported by oras
func (c *Client) GetRegistryRepositoryNames(ctx context.Context, projectID int) ([]string, error) {
url := fmt.Sprintf("%s/api/v4/projects/%d/registry/repositories", c.baseURL, projectID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitLab API returned status %d: %s", resp.StatusCode, resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var repositories []Repository
if err := json.Unmarshal(body, &repositories); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// Extract repository names
names := make([]string, len(repositories))
for i, repo := range repositories {
// the full path is required for further repo operations
names[i] = repo.Path
}
return names, nil
}
type Transport struct {
httpTransport *http.Transport
}
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
// interface for proxying requests to the Gitlab API.
func NewTransport() *Transport {
return &Transport{
httpTransport: &http.Transport{},
}
}
// RoundTrip is the implementation of the http.RoundTripper interface
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
token := request.Header.Get("Private-Token")
if token == "" {
return nil, errors.New("no gitlab token provided")
}
r, err := http.NewRequest(request.Method, request.URL.String(), request.Body)
if err != nil {
return nil, err
}
r.Header.Set("Private-Token", token)
return transport.httpTransport.RoundTrip(r)
}
// NewHTTPClient creates a new HTTP client configured for GitLab API requests
func NewHTTPClient(token string) *http.Client {
return &http.Client{
Transport: &tokenTransport{
token: token,
transport: retry.NewTransport(&http.Transport{}), // Use ORAS retry transport for consistent rate limiting and error handling
},
Timeout: 1 * time.Minute,
}
}
// tokenTransport automatically adds the Private-Token header to requests
type tokenTransport struct {
token string
transport http.RoundTripper
}
func (t *tokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Private-Token", t.token)
return t.transport.RoundTrip(req)
}