pkg/gitprovider/gitlab.go (302 lines of code) (raw):
package gitprovider
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/go-resty/resty/v2"
)
type gitlabWebHook struct {
ID int `json:"id"`
URL string `json:"url"`
}
type gitlabNamespace struct {
ID int `json:"id"`
}
type GitLabClient struct {
restyClient *resty.Client
}
const (
retryCount = 3
gitLabTokenHeaderName = "PRIVATE-TOKEN"
projectIDPathParam = "project-id"
)
// NewGitLabClient creates a new GitLab client.
func NewGitLabClient(restyClient *resty.Client) *GitLabClient {
restyClient.SetRetryCount(retryCount)
restyClient.AddRetryCondition(
func(response *resty.Response, err error) bool {
return response.IsError()
},
)
return &GitLabClient{restyClient: restyClient}
}
// CreateWebHook creates a new webhook for the given project.
func (c *GitLabClient) CreateWebHook(
ctx context.Context,
gitlabURL,
token,
projectID,
webHookSecret,
webHookURL string,
skipTLS bool,
) (*WebHook, error) {
c.restyClient.HostURL = gitlabURL
webHook := &gitlabWebHook{}
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetBody(map[string]interface{}{
"url": webHookURL,
"merge_requests_events": true,
"note_events": true,
"push_events": false,
"token": webHookSecret,
"enable_ssl_verification": !skipTLS,
}).
SetPathParams(map[string]string{
projectIDPathParam: projectID,
}).
SetResult(webHook).
Post("/api/v4/projects/{project-id}/hooks")
if err != nil {
return nil, fmt.Errorf("failed to create GitLab web hook: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("failed to create GitLab web hook: %s", resp.String())
}
return convertGitlabWebhook(webHook), nil
}
// CreateWebHookIfNotExists checks if a webhook with a given URL exists in the project.
// If a webhook exists function returns it. If not, creates a new one.
func (c *GitLabClient) CreateWebHookIfNotExists(
ctx context.Context,
gitlabURL,
token,
projectID,
webHookSecret,
webHookURL string,
skipTLS bool,
) (*WebHook, error) {
webHooks, err := c.GetWebHooks(ctx, gitlabURL, token, projectID)
if err != nil {
return nil, err
}
for _, webHook := range webHooks {
if webHook.URL == webHookURL {
return webHook, nil
}
}
return c.CreateWebHook(ctx, gitlabURL, token, projectID, webHookSecret, webHookURL, skipTLS)
}
// GetWebHook gets a webhook by ID for the given project.
func (c *GitLabClient) GetWebHook(
ctx context.Context,
gitlabURL,
token,
projectID string,
webHookRef string,
) (*WebHook, error) {
c.restyClient.HostURL = gitlabURL
webHook := &gitlabWebHook{}
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetPathParams(map[string]string{
projectIDPathParam: projectID,
"hook-id": webHookRef,
}).
SetResult(webHook).
Get("/api/v4/projects/{project-id}/hooks/{hook-id}")
if err != nil {
return nil, fmt.Errorf("failed to get GitLab web hook: %w", err)
}
if resp.StatusCode() == http.StatusNotFound {
return nil, fmt.Errorf("failed to get GitLab web hook: %w", ErrWebHookNotFound)
}
if resp.IsError() {
return nil, fmt.Errorf("failed to get GitLab web hook: %s", resp.String())
}
return convertGitlabWebhook(webHook), nil
}
// GetWebHooks gets a webhook by the given project.
func (c *GitLabClient) GetWebHooks(
ctx context.Context,
gitlabURL,
token,
projectID string,
) ([]*WebHook, error) {
c.restyClient.HostURL = gitlabURL
var webHooks []*gitlabWebHook
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetPathParams(map[string]string{
projectIDPathParam: projectID,
}).
SetResult(&webHooks).
Get("/api/v4/projects/{project-id}/hooks")
if err != nil {
return nil, fmt.Errorf("failed to get GitLab web hooks: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("failed to get GitLab web hooks: %s", resp.String())
}
hooks := make([]*WebHook, len(webHooks))
for i, hook := range webHooks {
hooks[i] = convertGitlabWebhook(hook)
}
return hooks, nil
}
// DeleteWebHook deletes webhook by ID for the given project.
func (c *GitLabClient) DeleteWebHook(
ctx context.Context,
gitlabURL,
token,
projectID string,
webHookRef string,
) error {
c.restyClient.HostURL = gitlabURL
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetPathParams(map[string]string{
projectIDPathParam: projectID,
"hook-id": webHookRef,
}).
Delete("/api/v4/projects/{project-id}/hooks/{hook-id}")
if err != nil {
return fmt.Errorf("failed to delete GitLab web hook: %w", err)
}
if resp.StatusCode() == http.StatusNotFound {
return fmt.Errorf("failed to delete GitLab web hook: %w", ErrWebHookNotFound)
}
if resp.IsError() {
return fmt.Errorf("failed to delete GitLab web hook: %s", resp.String())
}
return nil
}
// CreateProject creates a new project in GitLab.
func (c *GitLabClient) CreateProject(
ctx context.Context,
gitlabURL,
token,
projectID string,
) error {
namespace, path, err := decodeProjectID(projectID)
if err != nil {
return err
}
gitLabNs, err := c.getNamespace(ctx, gitlabURL, token, namespace)
if err != nil {
return err
}
c.restyClient.HostURL = gitlabURL
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetBody(map[string]any{
"path": path,
"namespace_id": gitLabNs.ID,
}).
Post("/api/v4/projects")
if err != nil {
return fmt.Errorf("failed to create GitLab project: %w", err)
}
if resp.IsError() {
return fmt.Errorf("failed to create GitLab project: %s", resp.String())
}
return nil
}
// ProjectExists checks if a project exists.
func (c *GitLabClient) ProjectExists(
ctx context.Context,
gitlabURL,
token,
projectID string,
) (bool, error) {
c.restyClient.HostURL = gitlabURL
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetPathParams(map[string]string{
"projectID": projectID,
}).
Get("/api/v4/projects/{projectID}")
if err != nil {
return false, fmt.Errorf("failed to check if GitLab project exists: %w", err)
}
if resp.IsError() {
if resp.StatusCode() == http.StatusNotFound {
return false, nil
}
return false, fmt.Errorf("failed to get GitLab project: %s", resp.String())
}
return true, nil
}
func (c *GitLabClient) SetDefaultBranch(
ctx context.Context,
gitlabURL,
token,
projectID,
branch string,
) error {
c.restyClient.HostURL = gitlabURL
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetPathParams(map[string]string{
"projectID": projectID,
}).
SetBody(map[string]string{
"default_branch": branch,
}).
Put("/api/v4/projects/{projectID}")
if err != nil {
return fmt.Errorf("failed to set GitLab default branch: %w", err)
}
if resp.IsError() {
return fmt.Errorf("failed to set GitLab default branch: %s", resp.String())
}
return nil
}
func (c *GitLabClient) getNamespace(
ctx context.Context,
gitlabURL,
token,
namespaceFullPath string,
) (*gitlabNamespace, error) {
c.restyClient.HostURL = gitlabURL
ns := &gitlabNamespace{}
resp, err := c.restyClient.
R().
SetContext(ctx).
SetHeader(gitLabTokenHeaderName, token).
SetPathParams(map[string]string{
"gitlabNamespace": namespaceFullPath,
}).
SetResult(ns).
Get("/api/v4/namespaces/{gitlabNamespace}")
if err != nil {
return nil, fmt.Errorf("failed to get GitLab namespace: %w", err)
}
if resp.IsError() {
return nil, fmt.Errorf("failed to get GitLab namespace: %s", resp.String())
}
return ns, nil
}
func decodeProjectID(projectID string) (namespace, path string, err error) {
lastSlashIndex := strings.LastIndex(projectID, "/")
if lastSlashIndex == -1 || lastSlashIndex == len(projectID)-1 || lastSlashIndex == 0 {
return "", "", fmt.Errorf("invalid project ID: %s", projectID)
}
return projectID[:lastSlashIndex], projectID[lastSlashIndex+1:], nil
}
func convertGitlabWebhook(hook *gitlabWebHook) *WebHook {
if hook == nil {
return nil
}
return &WebHook{
ID: strconv.Itoa(hook.ID),
URL: hook.URL,
}
}