Skip to content
This repository was archived by the owner on May 9, 2025. It is now read-only.

Commit 1310643

Browse files
committed
feat: add pull request creation
1 parent 631261d commit 1310643

File tree

10 files changed

+298
-16
lines changed

10 files changed

+298
-16
lines changed

apis/delivery/v1alpha1/condition_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ const (
1919

2020
// GitRepositoryPushFailedReason is used when the needed pushing to a git repository failed.
2121
GitRepositoryPushFailedReason = "GitRepositoryPushFailed"
22+
23+
// CreatePullRequestFailedReason is used when creating a pull request failed.
24+
CreatePullRequestFailedReason = "CreatePullRequestFailed"
2225
)

apis/delivery/v1alpha1/sync_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ type CommitTemplate struct {
1818
Message string `json:"message"`
1919
}
2020

21+
// PullRequestTemplate provides information for the created pull request.
22+
type PullRequestTemplate struct {
23+
Title string `json:"title,omitempty"`
24+
Description string `json:"description,omitempty"`
25+
Base string `json:"base,omitempty"`
26+
}
27+
2128
// SyncSpec defines the desired state of Sync
2229
type SyncSpec struct {
2330
SnapshotRef v1.LocalObjectReference `json:"snapshotRef"`
@@ -31,6 +38,8 @@ type SyncSpec struct {
3138
Branch string `json:"branch,omitempty"`
3239
//+optional
3340
AutomaticPullRequestCreation bool `json:"automaticPullRequestCreation,omitempty"`
41+
//+optional
42+
PullRequestTemplate PullRequestTemplate `json:"pullRequestTemplate,omitempty"`
3443
}
3544

3645
// SyncStatus defines the observed state of Sync

apis/delivery/v1alpha1/zz_generated.deepcopy.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/delivery.ocm.software_syncs.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ spec:
5858
type: string
5959
prune:
6060
type: boolean
61+
pullRequestTemplate:
62+
description: PullRequestTemplate provides information for the created
63+
pull request.
64+
properties:
65+
base:
66+
type: string
67+
description:
68+
type: string
69+
title:
70+
type: string
71+
type: object
6172
repositoryRef:
6273
description: LocalObjectReference contains enough information to let
6374
you locate the referenced object inside the same namespace.

controllers/delivery/sync_controller.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11+
"time"
1112

1213
"github.com/fluxcd/pkg/apis/meta"
1314
"github.com/fluxcd/pkg/runtime/conditions"
1415
"github.com/fluxcd/pkg/runtime/patch"
16+
"github.com/open-component-model/git-controller/pkg/providers"
1517
corev1 "k8s.io/api/core/v1"
1618
apierrors "k8s.io/apimachinery/pkg/api/errors"
1719
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -25,15 +27,16 @@ import (
2527

2628
"github.com/open-component-model/git-controller/apis/delivery/v1alpha1"
2729
mpasv1alpha1 "github.com/open-component-model/git-controller/apis/mpas/v1alpha1"
28-
providers "github.com/open-component-model/git-controller/pkg"
30+
"github.com/open-component-model/git-controller/pkg"
2931
)
3032

3133
// SyncReconciler reconciles a Sync object
3234
type SyncReconciler struct {
3335
client.Client
3436
Scheme *runtime.Scheme
3537

36-
Git providers.Git
38+
Git pkg.Git
39+
Provider providers.Provider
3740
}
3841

3942
//+kubebuilder:rbac:groups=delivery.ocm.software,resources=syncs,verbs=get;list;watch;create;update;patch;delete
@@ -161,15 +164,25 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
161164
return ctrl.Result{}, retErr
162165
}
163166

167+
branch := obj.Spec.Branch
168+
if branch == "" && obj.Spec.AutomaticPullRequestCreation {
169+
branch = fmt.Sprintf("branch-%d", time.Now().Unix())
170+
} else if branch == "" && !obj.Spec.AutomaticPullRequestCreation {
171+
retErr = fmt.Errorf("branch cannot be empty if automatic pull request creation is not enabled")
172+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.GitRepositoryPushFailedReason, retErr.Error())
173+
174+
return ctrl.Result{}, retErr
175+
}
176+
164177
// trim any trailing `/` and then just add.
165178
log.V(4).Info("crafting artifact URL to download from", "url", snapshot.Status.RepositoryURL)
166-
opts := &providers.PushOptions{
179+
opts := &pkg.PushOptions{
167180
URL: repository.GetRepositoryURL(),
168181
Message: obj.Spec.CommitTemplate.Message,
169182
Name: obj.Spec.CommitTemplate.Name,
170183
Email: obj.Spec.CommitTemplate.Email,
171184
Snapshot: snapshot,
172-
Branch: obj.Spec.Branch,
185+
Branch: branch,
173186
SubPath: obj.Spec.SubPath,
174187
}
175188

@@ -185,6 +198,15 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
185198

186199
obj.Status.Digest = digest
187200

201+
if obj.Spec.AutomaticPullRequestCreation {
202+
if err := r.Provider.CreatePullRequest(ctx, branch, *obj, *repository); err != nil {
203+
retErr = fmt.Errorf("failed to create pull request: %w", err)
204+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.CreatePullRequestFailedReason, retErr.Error())
205+
206+
return ctrl.Result{}, retErr
207+
}
208+
}
209+
188210
// Remove any stale Ready condition, most likely False, set above. Its value
189211
// is derived from the overall result of the reconciliation in the deferred
190212
// block at the very end.
@@ -200,10 +222,10 @@ func (r *SyncReconciler) SetupWithManager(mgr ctrl.Manager) error {
200222
Complete(r)
201223
}
202224

203-
func (r *SyncReconciler) parseAuthSecret(secret *corev1.Secret, opts *providers.PushOptions) {
225+
func (r *SyncReconciler) parseAuthSecret(secret *corev1.Secret, opts *pkg.PushOptions) {
204226
if _, ok := secret.Data["identity"]; ok {
205-
opts.Auth = &providers.Auth{
206-
SSH: &providers.SSH{
227+
opts.Auth = &pkg.Auth{
228+
SSH: &pkg.SSH{
207229
PemBytes: secret.Data["identity"],
208230
User: string(secret.Data["username"]),
209231
Password: string(secret.Data["password"]),
@@ -212,8 +234,8 @@ func (r *SyncReconciler) parseAuthSecret(secret *corev1.Secret, opts *providers.
212234
return
213235
}
214236
// default to basic auth.
215-
opts.Auth = &providers.Auth{
216-
BasicAuth: &providers.BasicAuth{
237+
opts.Auth = &pkg.Auth{
238+
BasicAuth: &pkg.BasicAuth{
217239
Username: string(secret.Data["username"]),
218240
Password: string(secret.Data["password"]),
219241
},

pkg/providers/gitea/gitea.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ package gitea
77
import (
88
"context"
99
"fmt"
10+
"time"
1011

1112
"code.gitea.io/sdk/gitea"
13+
deliveryv1alpha1 "github.com/open-component-model/git-controller/apis/delivery/v1alpha1"
1214
v1 "k8s.io/api/core/v1"
1315
"k8s.io/apimachinery/pkg/types"
1416
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -90,6 +92,66 @@ func (c *Client) CreateRepository(ctx context.Context, obj mpasv1alpha1.Reposito
9092
return nil
9193
}
9294

93-
func (c *Client) CreatePullRequest(ctx context.Context, owner, repo, title, branch, description string) error {
95+
func (c *Client) CreatePullRequest(ctx context.Context, sync deliveryv1alpha1.Sync, repository mpasv1alpha1.Repository) error {
96+
if repository.Spec.Provider != providerType {
97+
if c.next == nil {
98+
return fmt.Errorf("can't handle provider type '%s' and no next provider is configured", repository.Spec.Provider)
99+
}
100+
101+
return c.next.CreatePullRequest(ctx, sync, repository)
102+
}
103+
104+
secret := &v1.Secret{}
105+
if err := c.client.Get(ctx, types.NamespacedName{
106+
Name: repository.Spec.Credentials.SecretRef.Name,
107+
Namespace: repository.Namespace,
108+
}, secret); err != nil {
109+
return fmt.Errorf("failed to get secret: %w", err)
110+
}
111+
112+
token, ok := secret.Data[tokenKey]
113+
if !ok {
114+
return fmt.Errorf("token '%s' not found in secret", tokenKey)
115+
}
116+
117+
domain := defaultDomain
118+
if repository.Spec.Domain != "" {
119+
domain = repository.Spec.Domain
120+
}
121+
122+
client, err := gitea.NewClient(domain, gitea.SetToken(string(token)))
123+
if err != nil {
124+
return fmt.Errorf("failed to create gitea client: %w", err)
125+
}
126+
127+
var (
128+
title = providers.DefaultTitle
129+
base = providers.DefaultBaseBranch
130+
description = providers.DefaultDescription
131+
)
132+
133+
if sync.Spec.PullRequestTemplate.Title != "" {
134+
title = sync.Spec.PullRequestTemplate.Title
135+
}
136+
137+
if sync.Spec.PullRequestTemplate.Base != "" {
138+
base = sync.Spec.PullRequestTemplate.Base
139+
}
140+
141+
if sync.Spec.PullRequestTemplate.Description != "" {
142+
description = sync.Spec.PullRequestTemplate.Description
143+
}
144+
145+
branch := fmt.Sprintf("branch-%d", time.Now().Unix())
146+
147+
if _, _, err := client.CreatePullRequest(repository.Spec.Owner, repository.Spec.RepositoryName, gitea.CreatePullRequestOption{
148+
Head: branch,
149+
Base: base,
150+
Title: title,
151+
Body: description,
152+
}); err != nil {
153+
return fmt.Errorf("failed to create pull request: %w", err)
154+
}
155+
94156
return nil
95157
}

pkg/providers/github/github.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/fluxcd/go-git-providers/github"
1212
"github.com/fluxcd/go-git-providers/gitprovider"
13+
deliveryv1alpha1 "github.com/open-component-model/git-controller/apis/delivery/v1alpha1"
1314
v1 "k8s.io/api/core/v1"
1415
"k8s.io/apimachinery/pkg/types"
1516
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -91,6 +92,33 @@ func (c *Client) constructAuthenticationOption(ctx context.Context, obj mpasv1al
9192
return gitprovider.WithOAuth2Token(string(token)), nil
9293
}
9394

94-
func (c *Client) CreatePullRequest(ctx context.Context, owner, repo, title, branch, description string) error {
95-
return nil
95+
func (c *Client) CreatePullRequest(ctx context.Context, sync deliveryv1alpha1.Sync, repository mpasv1alpha1.Repository) error {
96+
if repository.Spec.Provider != providerType {
97+
if c.next == nil {
98+
return fmt.Errorf("can't handle provider type '%s' and no next provider is configured", repository.Spec.Provider)
99+
}
100+
101+
return c.next.CreatePullRequest(ctx, sync, repository)
102+
}
103+
104+
authenticationOption, err := c.constructAuthenticationOption(ctx, repository)
105+
if err != nil {
106+
return err
107+
}
108+
109+
domain := defaultDomain
110+
if repository.Spec.Domain != "" {
111+
domain = repository.Spec.Domain
112+
}
113+
114+
gc, err := github.NewClient(authenticationOption, gitprovider.WithDomain(domain))
115+
if err != nil {
116+
return fmt.Errorf("failed to create github client: %w", err)
117+
}
118+
119+
if repository.Spec.IsOrganization {
120+
return gogit.CreateOrganizationPullRequest(ctx, gc, domain, sync.Spec.PullRequestTemplate, repository.Spec)
121+
}
122+
123+
return gogit.CreateUserPullRequest(ctx, gc, domain, sync.Spec.PullRequestTemplate, repository.Spec)
96124
}

pkg/providers/gitlab/gitlab.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/fluxcd/go-git-providers/gitlab"
1212
"github.com/fluxcd/go-git-providers/gitprovider"
13+
deliveryv1alpha1 "github.com/open-component-model/git-controller/apis/delivery/v1alpha1"
1314
v1 "k8s.io/api/core/v1"
1415
"k8s.io/apimachinery/pkg/types"
1516
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -75,7 +76,7 @@ func (c *Client) CreateRepository(ctx context.Context, obj mpasv1alpha1.Reposito
7576

7677
gc, err := gitlab.NewClient(string(token), tokenType, gitprovider.WithDomain(domain))
7778
if err != nil {
78-
return fmt.Errorf("failed to create github client: %w", err)
79+
return fmt.Errorf("failed to create gitlab client: %w", err)
7980
}
8081

8182
if obj.Spec.IsOrganization {
@@ -85,6 +86,41 @@ func (c *Client) CreateRepository(ctx context.Context, obj mpasv1alpha1.Reposito
8586
return gogit.CreateUserRepository(ctx, gc, domain, obj.Spec)
8687
}
8788

88-
func (c *Client) CreatePullRequest(ctx context.Context, owner, repo, title, branch, description string) error {
89-
return nil
89+
func (c *Client) CreatePullRequest(ctx context.Context, branch string, sync deliveryv1alpha1.Sync, repository mpasv1alpha1.Repository) error {
90+
if repository.Spec.Provider != providerType {
91+
if c.next == nil {
92+
return fmt.Errorf("can't handle provider type '%s' and no next provider is configured", repository.Spec.Provider)
93+
}
94+
95+
return c.next.CreatePullRequest(ctx, branch, sync, repository)
96+
}
97+
98+
secret := &v1.Secret{}
99+
if err := c.client.Get(ctx, types.NamespacedName{
100+
Name: repository.Spec.Credentials.SecretRef.Name,
101+
Namespace: sync.Namespace,
102+
}, secret); err != nil {
103+
return fmt.Errorf("failed to get secret: %w", err)
104+
}
105+
106+
token, ok := secret.Data[tokenKey]
107+
if !ok {
108+
return fmt.Errorf("token '%s' not found in secret", tokenKey)
109+
}
110+
111+
domain := defaultDomain
112+
if repository.Spec.Domain != "" {
113+
domain = repository.Spec.Domain
114+
}
115+
116+
gc, err := gitlab.NewClient(string(token), tokenType, gitprovider.WithDomain(domain))
117+
if err != nil {
118+
return fmt.Errorf("failed to create gitlab client: %w", err)
119+
}
120+
121+
if repository.Spec.IsOrganization {
122+
return gogit.CreateOrganizationPullRequest(ctx, gc, domain, branch, sync.Spec.PullRequestTemplate, repository.Spec)
123+
}
124+
125+
return gogit.CreateUserPullRequest(ctx, gc, domain, branch, sync.Spec.PullRequestTemplate, repository.Spec)
90126
}

0 commit comments

Comments
 (0)