mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-04 04:22:57 +00:00
785 lines
23 KiB
Go
785 lines
23 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"containr/internal/database"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// GitProvider represents a Git provider (GitHub, GitLab, Bitbucket)
|
|
type GitProvider struct {
|
|
ID string `json:"id" db:"id"`
|
|
Name string `json:"name" db:"name"` // github, gitlab, bitbucket
|
|
DisplayName string `json:"display_name" db:"display_name"`
|
|
APIUrl string `json:"api_url" db:"api_url"`
|
|
WebhookUrl string `json:"webhook_url" db:"webhook_url"`
|
|
UserID string `json:"user_id" db:"user_id"`
|
|
AccessToken string `json:"-" db:"access_token"` // Hidden in JSON responses
|
|
CreatedAt string `json:"created_at" db:"created_at"`
|
|
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// GitRepository represents a connected Git repository
|
|
type GitRepository struct {
|
|
ID string `json:"id" db:"id"`
|
|
ProviderID string `json:"provider_id" db:"provider_id"`
|
|
Name string `json:"name" db:"name"`
|
|
FullName string `json:"full_name" db:"full_name"`
|
|
Description string `json:"description" db:"description"`
|
|
CloneURL string `json:"clone_url" db:"clone_url"`
|
|
WebhookURL string `json:"webhook_url" db:"webhook_url"`
|
|
DefaultBranch string `json:"default_branch" db:"default_branch"`
|
|
IsPrivate bool `json:"is_private" db:"is_private"`
|
|
UserID string `json:"user_id" db:"user_id"`
|
|
CreatedAt string `json:"created_at" db:"created_at"`
|
|
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// GitWebhook represents a webhook configuration
|
|
type GitWebhook struct {
|
|
ID string `json:"id" db:"id"`
|
|
RepoID string `json:"repo_id" db:"repo_id"`
|
|
ProviderID string `json:"provider_id" db:"provider_id"`
|
|
Events string `json:"events" db:"events"` // JSON array of events
|
|
Secret string `json:"-" db:"webhook_secret"` // Hidden in JSON responses
|
|
RemoteWebhookID string `json:"remote_webhook_id" db:"remote_webhook_id"`
|
|
Active bool `json:"active" db:"active"`
|
|
BranchFilter string `json:"branch_filter,omitempty" db:"branch_filter"`
|
|
CreatedAt string `json:"created_at" db:"created_at"`
|
|
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// CreateGitProviderRequest represents a request to create a Git provider
|
|
type CreateGitProviderRequest struct {
|
|
Name string `json:"name" binding:"required,oneof=github gitlab bitbucket"`
|
|
DisplayName string `json:"display_name" binding:"required"`
|
|
AccessToken string `json:"access_token" binding:"required"`
|
|
}
|
|
|
|
// CreateGitRepoRequest represents a request to connect a Git repository
|
|
type CreateGitRepoRequest struct {
|
|
ProviderID string `json:"provider_id" binding:"required"`
|
|
RepoFullName string `json:"repo_full_name" binding:"required"`
|
|
}
|
|
|
|
// CreateWebhookRequest represents a request to create a webhook
|
|
type CreateWebhookRequest struct {
|
|
RepoID string `json:"repo_id" binding:"required"`
|
|
Events []string `json:"events" binding:"required"`
|
|
Branch string `json:"branch"`
|
|
}
|
|
|
|
type GitBranch struct {
|
|
Name string `json:"name"`
|
|
CommitHash string `json:"commit_hash"`
|
|
IsDefault bool `json:"is_default"`
|
|
}
|
|
|
|
type githubRepo struct {
|
|
Name string `json:"name"`
|
|
FullName string `json:"full_name"`
|
|
Description string `json:"description"`
|
|
CloneURL string `json:"clone_url"`
|
|
HTMLURL string `json:"html_url"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
Private bool `json:"private"`
|
|
}
|
|
|
|
type githubBranch struct {
|
|
Name string `json:"name"`
|
|
Commit struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"commit"`
|
|
}
|
|
|
|
type githubUser struct {
|
|
Login string `json:"login"`
|
|
}
|
|
|
|
func handleGetGitProviders(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
rows, err := db.Query(`
|
|
SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
|
|
FROM git_providers
|
|
WHERE user_id = $1
|
|
ORDER BY created_at DESC
|
|
`, userID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var providers []GitProvider
|
|
for rows.Next() {
|
|
var provider GitProvider
|
|
if err := rows.Scan(&provider.ID, &provider.Name, &provider.DisplayName, &provider.APIUrl,
|
|
&provider.WebhookUrl, &provider.UserID, &provider.CreatedAt, &provider.UpdatedAt); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
|
return
|
|
}
|
|
providers = append(providers, provider)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"providers": providers})
|
|
}
|
|
|
|
func handleCreateGitProvider(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
var req CreateGitProviderRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate the access token by making a test API call
|
|
if !validateGitToken(req.Name, req.AccessToken) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid access token for " + req.Name})
|
|
return
|
|
}
|
|
|
|
provider := GitProvider{
|
|
ID: uuid.New().String(),
|
|
Name: req.Name,
|
|
DisplayName: req.DisplayName,
|
|
AccessToken: req.AccessToken,
|
|
UserID: userID,
|
|
}
|
|
|
|
// Set provider-specific URLs
|
|
switch req.Name {
|
|
case "github":
|
|
provider.APIUrl = "https://api.github.com"
|
|
provider.WebhookUrl = "https://api.github.com"
|
|
case "gitlab":
|
|
provider.APIUrl = "https://gitlab.com/api/v4"
|
|
provider.WebhookUrl = "https://gitlab.com"
|
|
case "bitbucket":
|
|
provider.APIUrl = "https://api.bitbucket.org/2.0"
|
|
provider.WebhookUrl = "https://api.bitbucket.org/2.0"
|
|
}
|
|
|
|
_, err := db.Exec(`
|
|
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
|
`, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl,
|
|
provider.WebhookUrl, provider.AccessToken, provider.UserID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Git provider"})
|
|
return
|
|
}
|
|
|
|
// Return provider without access token
|
|
provider.AccessToken = ""
|
|
c.JSON(http.StatusCreated, provider)
|
|
}
|
|
|
|
func handleGetGitRepositories(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
providerID := c.Param("providerId")
|
|
|
|
// Validate UUID
|
|
if _, err := uuid.Parse(providerID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
|
|
return
|
|
}
|
|
|
|
// Get provider info
|
|
var provider GitProvider
|
|
err := db.QueryRow(`
|
|
SELECT id, name, access_token, api_url
|
|
FROM git_providers
|
|
WHERE id = $1 AND user_id = $2
|
|
`, providerID, userID).Scan(&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Git provider not found"})
|
|
return
|
|
}
|
|
|
|
// Fetch repositories from the Git provider
|
|
repos, err := fetchGitRepositories(provider.Name, provider.AccessToken, provider.APIUrl)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch repositories"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"repositories": repos})
|
|
}
|
|
|
|
func handleConnectGitRepository(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
var req CreateGitRepoRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate UUID
|
|
if _, err := uuid.Parse(req.ProviderID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
|
|
return
|
|
}
|
|
|
|
// Get provider info
|
|
var provider GitProvider
|
|
err := db.QueryRow(`
|
|
SELECT id, name, access_token, api_url
|
|
FROM git_providers
|
|
WHERE id = $1 AND user_id = $2
|
|
`, req.ProviderID, userID).Scan(&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Git provider not found"})
|
|
return
|
|
}
|
|
|
|
// Fetch repository details from Git provider
|
|
repoDetails, err := fetchGitRepositoryDetails(provider.Name, req.RepoFullName, provider.AccessToken, provider.APIUrl)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch repository details"})
|
|
return
|
|
}
|
|
|
|
// Check if repository is already connected
|
|
var existingID string
|
|
err = db.QueryRow(`
|
|
SELECT id FROM git_repositories
|
|
WHERE provider_id = $1 AND full_name = $2
|
|
`, req.ProviderID, req.RepoFullName).Scan(&existingID)
|
|
|
|
if err == nil {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Repository already connected", "repository_id": existingID})
|
|
return
|
|
}
|
|
|
|
// Create repository record
|
|
repo := GitRepository{
|
|
ID: uuid.New().String(),
|
|
ProviderID: req.ProviderID,
|
|
Name: repoDetails["name"].(string),
|
|
FullName: req.RepoFullName,
|
|
Description: repoDetails["description"].(string),
|
|
CloneURL: repoDetails["clone_url"].(string),
|
|
DefaultBranch: repoDetails["default_branch"].(string),
|
|
IsPrivate: repoDetails["private"].(bool),
|
|
UserID: userID,
|
|
}
|
|
|
|
_, err = db.Exec(`
|
|
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url,
|
|
default_branch, is_private, user_id, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
|
|
`, repo.ID, repo.ProviderID, repo.Name, repo.FullName, repo.Description,
|
|
repo.CloneURL, repo.DefaultBranch, repo.IsPrivate, repo.UserID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, repo)
|
|
}
|
|
|
|
func handleCreateWebhook(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
var req CreateWebhookRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate UUIDs
|
|
if _, err := uuid.Parse(req.RepoID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid repository ID"})
|
|
return
|
|
}
|
|
|
|
// Get repository and provider info
|
|
var repo GitRepository
|
|
var provider GitProvider
|
|
err := db.QueryRow(`
|
|
SELECT r.id, r.provider_id, r.full_name, r.user_id,
|
|
p.id, p.name, p.access_token, p.api_url, p.webhook_url
|
|
FROM git_repositories r
|
|
JOIN git_providers p ON r.provider_id = p.id
|
|
WHERE r.id = $1 AND r.user_id = $2
|
|
`, req.RepoID, userID).Scan(&repo.ID, &repo.ProviderID, &repo.FullName, &repo.UserID,
|
|
&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl, &provider.WebhookUrl)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
|
|
return
|
|
}
|
|
|
|
// Convert events to JSON
|
|
eventsJSON, _ := json.Marshal(req.Events)
|
|
webhookSecret := generateWebhookSecret()
|
|
|
|
// Create webhook on Git provider
|
|
webhookURL := fmt.Sprintf("%s/api/v1/webhooks/git/%s", publicBaseURL(c), req.RepoID)
|
|
remoteWebhookID, err := createGitWebhook(provider.Name, repo.FullName, provider.AccessToken,
|
|
provider.APIUrl, webhookURL, req.Events, webhookSecret)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create webhook on Git provider: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// Create webhook record
|
|
webhook := GitWebhook{
|
|
ID: uuid.New().String(),
|
|
RepoID: req.RepoID,
|
|
ProviderID: provider.ID,
|
|
Events: string(eventsJSON),
|
|
Secret: webhookSecret,
|
|
RemoteWebhookID: remoteWebhookID,
|
|
Active: true,
|
|
BranchFilter: req.Branch,
|
|
}
|
|
|
|
_, err = db.Exec(`
|
|
INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, remote_webhook_id, active, branch_filter, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
|
`, webhook.ID, webhook.RepoID, webhook.ProviderID, webhook.Events, webhook.Secret, webhook.RemoteWebhookID, webhook.Active, nullableString(webhook.BranchFilter))
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create webhook"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"webhook": webhook,
|
|
"remote_webhook_id": remoteWebhookID,
|
|
})
|
|
}
|
|
|
|
func handleGetConnectedRepositories(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
|
offset := (page - 1) * limit
|
|
|
|
rows, err := db.Query(`
|
|
SELECT r.id, r.provider_id, r.name, r.full_name, r.description, r.clone_url,
|
|
r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
|
|
p.name as provider_name, p.display_name
|
|
FROM git_repositories r
|
|
JOIN git_providers p ON r.provider_id = p.id
|
|
WHERE r.user_id = $1
|
|
ORDER BY r.updated_at DESC
|
|
LIMIT $2 OFFSET $3
|
|
`, userID, limit, offset)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var repositories []map[string]interface{}
|
|
for rows.Next() {
|
|
var repo GitRepository
|
|
var providerName, providerDisplayName string
|
|
if err := rows.Scan(&repo.ID, &repo.ProviderID, &repo.Name, &repo.FullName, &repo.Description,
|
|
&repo.CloneURL, &repo.DefaultBranch, &repo.IsPrivate, &repo.UserID, &repo.CreatedAt, &repo.UpdatedAt,
|
|
&providerName, &providerDisplayName); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
|
return
|
|
}
|
|
|
|
repositories = append(repositories, map[string]interface{}{
|
|
"id": repo.ID,
|
|
"provider_id": repo.ProviderID,
|
|
"name": repo.Name,
|
|
"full_name": repo.FullName,
|
|
"description": repo.Description,
|
|
"clone_url": repo.CloneURL,
|
|
"default_branch": repo.DefaultBranch,
|
|
"is_private": repo.IsPrivate,
|
|
"created_at": repo.CreatedAt,
|
|
"updated_at": repo.UpdatedAt,
|
|
"provider": map[string]string{
|
|
"name": providerName,
|
|
"display_name": providerDisplayName,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Get total count
|
|
var total int
|
|
err = db.QueryRow(`
|
|
SELECT COUNT(*) FROM git_repositories WHERE user_id = $1
|
|
`, userID).Scan(&total)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"repositories": repositories,
|
|
"pagination": gin.H{
|
|
"page": page,
|
|
"limit": limit,
|
|
"total": total,
|
|
},
|
|
})
|
|
}
|
|
|
|
func handleGetRepositoryBranches(c *gin.Context) {
|
|
userID := c.MustGet("user_id").(string)
|
|
db := c.MustGet("db").(*database.DB)
|
|
repoID := c.Param("repoId")
|
|
|
|
if _, err := uuid.Parse(repoID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid repository ID"})
|
|
return
|
|
}
|
|
|
|
var repo GitRepository
|
|
var provider GitProvider
|
|
err := db.QueryRow(`
|
|
SELECT r.id, r.full_name, r.default_branch,
|
|
p.id, p.name, p.access_token, p.api_url
|
|
FROM git_repositories r
|
|
JOIN git_providers p ON r.provider_id = p.id
|
|
WHERE r.id = $1 AND r.user_id = $2
|
|
`, repoID, userID).Scan(&repo.ID, &repo.FullName, &repo.DefaultBranch,
|
|
&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
|
|
return
|
|
}
|
|
|
|
branches, err := fetchGitBranches(provider.Name, provider.AccessToken, provider.APIUrl, repo.FullName, repo.DefaultBranch)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch branches: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"branches": branches})
|
|
}
|
|
|
|
// Helper functions (these would need to be implemented with actual Git provider API calls)
|
|
|
|
func validateGitToken(provider, token string) bool {
|
|
if strings.TrimSpace(token) == "" {
|
|
return false
|
|
}
|
|
|
|
if provider != "github" {
|
|
return true
|
|
}
|
|
|
|
body, status, err := gitProviderRequest("GET", "https://api.github.com", token, "/user", nil)
|
|
if err != nil || status < 200 || status >= 300 {
|
|
return false
|
|
}
|
|
|
|
var user githubUser
|
|
return json.Unmarshal(body, &user) == nil && user.Login != ""
|
|
}
|
|
|
|
func fetchGitRepositories(provider, token, apiUrl string) ([]map[string]interface{}, error) {
|
|
if provider != "github" {
|
|
return nil, fmt.Errorf("%s repository listing is not implemented yet", provider)
|
|
}
|
|
|
|
body, status, err := gitProviderRequest(
|
|
"GET",
|
|
apiUrl,
|
|
token,
|
|
"/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member",
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status < 200 || status >= 300 {
|
|
return nil, providerStatusError(status, body)
|
|
}
|
|
|
|
var repos []githubRepo
|
|
if err := json.Unmarshal(body, &repos); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]map[string]interface{}, 0, len(repos))
|
|
for _, repo := range repos {
|
|
result = append(result, map[string]interface{}{
|
|
"name": repo.Name,
|
|
"full_name": repo.FullName,
|
|
"description": repo.Description,
|
|
"clone_url": repo.CloneURL,
|
|
"default_branch": repo.DefaultBranch,
|
|
"private": repo.Private,
|
|
"html_url": repo.HTMLURL,
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func fetchGitRepositoryDetails(provider, repoFullName, token, apiUrl string) (map[string]interface{}, error) {
|
|
if provider != "github" {
|
|
return nil, fmt.Errorf("%s repository details are not implemented yet", provider)
|
|
}
|
|
|
|
path, err := githubRepoPath(repoFullName, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body, status, err := gitProviderRequest("GET", apiUrl, token, path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status < 200 || status >= 300 {
|
|
return nil, providerStatusError(status, body)
|
|
}
|
|
|
|
var repo githubRepo
|
|
if err := json.Unmarshal(body, &repo); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"name": repo.Name,
|
|
"description": repo.Description,
|
|
"clone_url": repo.CloneURL,
|
|
"default_branch": repo.DefaultBranch,
|
|
"private": repo.Private,
|
|
"html_url": repo.HTMLURL,
|
|
}, nil
|
|
}
|
|
|
|
func fetchGitBranches(provider, token, apiUrl, repoFullName, defaultBranch string) ([]GitBranch, error) {
|
|
if provider != "github" {
|
|
return nil, fmt.Errorf("%s branch listing is not implemented yet", provider)
|
|
}
|
|
|
|
path, err := githubRepoPath(repoFullName, "/branches?per_page=100")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body, status, err := gitProviderRequest("GET", apiUrl, token, path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status < 200 || status >= 300 {
|
|
return nil, providerStatusError(status, body)
|
|
}
|
|
|
|
var rawBranches []githubBranch
|
|
if err := json.Unmarshal(body, &rawBranches); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
branches := make([]GitBranch, 0, len(rawBranches))
|
|
for _, branch := range rawBranches {
|
|
branches = append(branches, GitBranch{
|
|
Name: branch.Name,
|
|
CommitHash: branch.Commit.SHA,
|
|
IsDefault: branch.Name == defaultBranch,
|
|
})
|
|
}
|
|
|
|
return branches, nil
|
|
}
|
|
|
|
func createGitWebhook(provider, repoFullName, token, apiUrl, targetURL string, events []string, secret string) (string, error) {
|
|
if provider != "github" {
|
|
return "", fmt.Errorf("%s webhooks are not implemented yet", provider)
|
|
}
|
|
|
|
path, err := githubRepoPath(repoFullName, "/hooks")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"name": "web",
|
|
"active": true,
|
|
"events": events,
|
|
"config": map[string]string{
|
|
"url": targetURL,
|
|
"content_type": "json",
|
|
"secret": secret,
|
|
"insecure_ssl": "0",
|
|
},
|
|
}
|
|
data, _ := json.Marshal(payload)
|
|
|
|
body, status, err := gitProviderRequest("POST", apiUrl, token, path, bytes.NewReader(data))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if status < 200 || status >= 300 {
|
|
return "", providerStatusError(status, body)
|
|
}
|
|
|
|
var response struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|
return "", err
|
|
}
|
|
if response.ID == 0 {
|
|
return "", fmt.Errorf("GitHub returned an empty webhook ID")
|
|
}
|
|
|
|
return strconv.FormatInt(response.ID, 10), nil
|
|
}
|
|
|
|
func generateWebhookSecret() string {
|
|
return "webhook-secret-" + uuid.New().String()
|
|
}
|
|
|
|
func gitProviderRequest(method, apiUrl, token, path string, body io.Reader) ([]byte, int, error) {
|
|
base := strings.TrimRight(apiUrl, "/")
|
|
if base == "" {
|
|
base = "https://api.github.com"
|
|
}
|
|
|
|
req, err := http.NewRequest(method, base+path, body)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
if strings.TrimSpace(token) != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, resp.StatusCode, err
|
|
}
|
|
|
|
return data, resp.StatusCode, nil
|
|
}
|
|
|
|
func githubRepoPath(repoFullName, suffix string) (string, error) {
|
|
parts := strings.Split(repoFullName, "/")
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return "", fmt.Errorf("repository must use owner/name format")
|
|
}
|
|
|
|
return "/repos/" + url.PathEscape(parts[0]) + "/" + url.PathEscape(parts[1]) + suffix, nil
|
|
}
|
|
|
|
func providerStatusError(status int, body []byte) error {
|
|
var response struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if err := json.Unmarshal(body, &response); err == nil && response.Message != "" {
|
|
return fmt.Errorf("provider returned %d: %s", status, response.Message)
|
|
}
|
|
|
|
return fmt.Errorf("provider returned %d", status)
|
|
}
|
|
|
|
func publicBaseURL(c *gin.Context) string {
|
|
for _, key := range []string{"CONTAINR_PUBLIC_URL", "PUBLIC_URL", "APP_URL"} {
|
|
if value := strings.TrimRight(os.Getenv(key), "/"); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
|
scheme = "https"
|
|
}
|
|
|
|
host := c.Request.Host
|
|
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
|
|
host = forwardedHost
|
|
}
|
|
|
|
return scheme + "://" + host
|
|
}
|
|
|
|
func nullableString(value string) any {
|
|
if strings.TrimSpace(value) == "" {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func fetchGitHubFile(provider, token, apiUrl, repoFullName, branch, filePath string) ([]byte, error) {
|
|
if provider != "github" {
|
|
return nil, fmt.Errorf("%s file fetch is not implemented yet", provider)
|
|
}
|
|
|
|
suffix := "/contents/" + strings.TrimLeft(filePath, "/")
|
|
if branch != "" {
|
|
suffix += "?ref=" + url.QueryEscape(branch)
|
|
}
|
|
|
|
path, err := githubRepoPath(repoFullName, suffix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body, status, err := gitProviderRequest("GET", apiUrl, token, path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status < 200 || status >= 300 {
|
|
return nil, providerStatusError(status, body)
|
|
}
|
|
|
|
var response struct {
|
|
Content string `json:"content"`
|
|
Encoding string `json:"encoding"`
|
|
}
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|
return nil, err
|
|
}
|
|
if response.Encoding != "base64" {
|
|
return nil, fmt.Errorf("unsupported GitHub content encoding: %s", response.Encoding)
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(response.Content, "\n", ""))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return decoded, nil
|
|
}
|