gito/services/core/internal/provider/provider.go
toki 8e6d531718 feat(provider): ProviderAdapter 인터페이스를 abstraction하여 다중 provider 지원으로 확장한다
- ProviderAdapter 인터페이스를 정의하고 AWS/GitLab provider 구현체를 추가한다
- CRD model에 ProviderRef 및 status 필드를 추가하여 provider 관리를 표준화한다
- ControlPlane에 provider registry를 통합하여 runtime/periodic event routing을 provider별로 분기한다
- Router에 routeByProvider 메서드를 추가하여 provider별 event 라우팅을 지원한다
- Event 시스템에 outbound event 전송 기능을 추가한다
- 관련 테스트를 추가하여 provider별 동작을 검증한다
2026-06-20 14:52:39 +09:00

247 lines
8 KiB
Go

package provider
import (
"context"
"strings"
"time"
"git.toki-labs.com/toki/gito/services/core/internal/core"
)
type ProviderID string
const (
ProviderGitHub ProviderID = "github"
ProviderGitLab ProviderID = "gitlab"
ProviderGitea ProviderID = "gitea"
ProviderForgejo ProviderID = "forgejo"
ProviderPlane ProviderID = "plane"
ProviderJira ProviderID = "jira"
)
type Capability uint64
const (
CapabilityChangeRequest Capability = 1 << iota
CapabilityWebhook
CapabilityChecks
CapabilityComment
)
func (c Capability) Has(flag Capability) bool {
return flag != 0 && c&flag == flag
}
type Config struct {
ID ProviderID
Endpoint string
CredentialRef string
Capabilities Capability
CreatedAt time.Time
UpdatedAt time.Time
}
func (c Config) Supports(flag Capability) bool {
return c.Capabilities.Has(flag)
}
func (c Config) Normalized() Config {
c.ID = ProviderID(strings.ToLower(strings.TrimSpace(string(c.ID))))
c.Endpoint = strings.TrimSpace(c.Endpoint)
c.CredentialRef = strings.TrimSpace(c.CredentialRef)
return c
}
type ChangeRequestAdapter interface {
CreateChangeRequest(ctx context.Context, input core.ChangeRequestCreate) (core.ChangeRequest, error)
UpdateChangeRequest(ctx context.Context, id string, input core.ChangeRequestUpdate) (core.ChangeRequest, error)
CommentChangeRequest(ctx context.Context, id string, input core.ChangeRequestComment) error
RequestChangeRequestReview(ctx context.Context, id string, input core.ChangeRequestReviewRequest) error
MergeChangeRequest(ctx context.Context, id string, input core.ChangeRequestMerge) (core.ChangeRequest, error)
CloseChangeRequest(ctx context.Context, id string, input core.ChangeRequestClose) (core.ChangeRequest, error)
}
type ChangeRequestMappingCandidate struct {
Provider ProviderID
ProviderObject string
Fields []ChangeRequestFieldMapping
States []ChangeRequestStateMapping
Actions []core.ChangeRequestAction
ExtensionFields []ChangeRequestExtensionField
}
type ChangeRequestFieldMapping struct {
Target string
Source string
Notes string
}
type ChangeRequestStateMapping struct {
State core.ChangeRequestState
Source string
Notes string
}
// ChangeRequestExtensionField describes provider-owned metadata that adapters
// keep outside core.ChangeRequest until a later milestone promotes it.
type ChangeRequestExtensionField struct {
Name string
Source string
Notes string
}
func ChangeRequestMappingCandidates() []ChangeRequestMappingCandidate {
return []ChangeRequestMappingCandidate{
GitHubChangeRequestMapping(),
GitLabChangeRequestMapping(),
GiteaChangeRequestMapping(),
}
}
func GitHubChangeRequestMapping() ChangeRequestMappingCandidate {
return ChangeRequestMappingCandidate{
Provider: ProviderGitHub,
ProviderObject: "pull_request",
Fields: []ChangeRequestFieldMapping{
{Target: "provider", Source: string(ProviderGitHub)},
{Target: "external_id", Source: "number/pull_number"},
{Target: "repo_id", Source: "Gito repo binding"},
{Target: "source_branch", Source: "head"},
{Target: "target_branch", Source: "base"},
{Target: "title", Source: "title"},
{Target: "body", Source: "body"},
{Target: "draft", Source: "draft"},
{Target: "state", Source: "state + merged check"},
},
States: []ChangeRequestStateMapping{
{State: core.ChangeRequestOpen, Source: "state=open"},
{State: core.ChangeRequestMerged, Source: "merged=true or merge endpoint success"},
{State: core.ChangeRequestClosed, Source: "state=closed and merged=false"},
},
Actions: changeRequestActions(),
ExtensionFields: []ChangeRequestExtensionField{
{Name: "head_repo", Source: "head_repo", Notes: "cross-repository create metadata"},
{Name: "maintainer_can_modify", Source: "maintainer_can_modify"},
{Name: "merge_method", Source: "merge_method"},
{Name: "issue", Source: "issue", Notes: "issue conversion and issue-only fields stay adapter-owned"},
},
}
}
func GitLabChangeRequestMapping() ChangeRequestMappingCandidate {
return ChangeRequestMappingCandidate{
Provider: ProviderGitLab,
ProviderObject: "merge_request",
Fields: []ChangeRequestFieldMapping{
{Target: "provider", Source: string(ProviderGitLab)},
{Target: "external_id", Source: "iid"},
{Target: "repo_id", Source: "Gito repo binding"},
{Target: "source_branch", Source: "source_branch"},
{Target: "target_branch", Source: "target_branch"},
{Target: "title", Source: "title"},
{Target: "body", Source: "description"},
{Target: "draft", Source: "draft"},
{Target: "state", Source: "state"},
},
States: []ChangeRequestStateMapping{
{State: core.ChangeRequestOpen, Source: "state=opened"},
{State: core.ChangeRequestMerged, Source: "state=merged"},
{State: core.ChangeRequestClosed, Source: "state=closed"},
},
Actions: changeRequestActions(),
ExtensionFields: []ChangeRequestExtensionField{
{Name: "project_id", Source: "project_id/source_project_id/target_project_id"},
{Name: "detailed_merge_status", Source: "detailed_merge_status"},
{Name: "squash_on_merge", Source: "squash_on_merge"},
{Name: "merge_after", Source: "merge_after"},
{Name: "blocking_discussions_resolved", Source: "blocking_discussions_resolved"},
},
}
}
func GiteaChangeRequestMapping() ChangeRequestMappingCandidate {
return ChangeRequestMappingCandidate{
Provider: ProviderGitea,
ProviderObject: "pull_request",
Fields: []ChangeRequestFieldMapping{
{Target: "provider", Source: string(ProviderGitea)},
{Target: "external_id", Source: "number"},
{Target: "repo_id", Source: "Gito repo binding"},
{Target: "source_branch", Source: "head"},
{Target: "target_branch", Source: "base"},
{Target: "title", Source: "title"},
{Target: "body", Source: "body"},
{Target: "draft", Source: "draft"},
{Target: "state", Source: "state + merged"},
},
States: []ChangeRequestStateMapping{
{State: core.ChangeRequestOpen, Source: "state=open"},
{State: core.ChangeRequestMerged, Source: "merged=true"},
{State: core.ChangeRequestClosed, Source: "state=closed and merged=false"},
},
Actions: changeRequestActions(),
ExtensionFields: []ChangeRequestExtensionField{
{Name: "mergeable", Source: "mergeable"},
{Name: "allow_maintainer_edit", Source: "allow_maintainer_edit"},
{Name: "merge_style", Source: "merge option style"},
{Name: "deadline", Source: "due_date"},
{Name: "requested_reviewers", Source: "requested_reviewers/requested_reviewers_teams"},
},
}
}
func changeRequestActions() []core.ChangeRequestAction {
return []core.ChangeRequestAction{
core.ChangeRequestActionCreate,
core.ChangeRequestActionUpdate,
core.ChangeRequestActionComment,
core.ChangeRequestActionRequestReview,
core.ChangeRequestActionMerge,
core.ChangeRequestActionClose,
}
}
type WebhookEvent struct {
Provider string
EventType string
ExternalID string
Payload []byte
}
// WebhookRequest represents a provider webhook with all metadata needed
// for signature verification and normalization.
type WebhookRequest struct {
Provider ProviderID
EventType string
ExternalID string
Payload []byte
Headers map[string][]string
Query map[string][]string
ReceivedAt time.Time
}
// WebhookCandidate represents a provider-neutral normalization candidate
// that contains minimal metadata without requiring provider secrets.
type WebhookCandidate struct {
Type string
Provider ProviderID
ExternalID string
Revision *core.RevisionEvent
}
// WebhookVerifier defines the interface for verifying webhook signatures.
type WebhookVerifier interface {
VerifyWebhook(ctx context.Context, req WebhookRequest) error
}
// WebhookNormalizer defines the interface for normalizing webhook events
// into provider-neutral candidates.
type WebhookNormalizer interface {
NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error)
}
// WebhookAdapter combines verification and normalization capabilities.
type WebhookAdapter interface {
WebhookVerifier
WebhookNormalizer
}