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

Commit 12e52c6

Browse files
authored
feat: add pull request creation (#18)
* feat: add pull request creation * fix naming, branch checkout logic and uncompression * import shuffling * add extra test to verify pull and CreatePullRequest are not called if digest is already defined
1 parent 631261d commit 12e52c6

File tree

16 files changed

+554
-75
lines changed

16 files changed

+554
-75
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: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,34 @@ type CommitTemplate struct {
1616
Name string `json:"name"`
1717
Email string `json:"email"`
1818
Message string `json:"message"`
19+
20+
//+optional
21+
TargetBranch string `json:"targetBranch,omitempty"`
22+
//+optional
23+
//+kubebuilder:default:=main
24+
BaseBranch string `json:"baseBranch,omitempty"`
25+
}
26+
27+
// PullRequestTemplate provides information for the created pull request.
28+
type PullRequestTemplate struct {
29+
Title string `json:"title,omitempty"`
30+
Description string `json:"description,omitempty"`
31+
Base string `json:"base,omitempty"`
1932
}
2033

2134
// SyncSpec defines the desired state of Sync
2235
type SyncSpec struct {
2336
SnapshotRef v1.LocalObjectReference `json:"snapshotRef"`
2437
RepositoryRef v1.LocalObjectReference `json:"repositoryRef"`
2538
Interval metav1.Duration `json:"interval"`
26-
CommitTemplate *CommitTemplate `json:"commitTemplate"`
39+
CommitTemplate CommitTemplate `json:"commitTemplate"`
2740
SubPath string `json:"subPath"`
2841
Prune bool `json:"prune,omitempty"`
2942

30-
//+optional
31-
Branch string `json:"branch,omitempty"`
3243
//+optional
3344
AutomaticPullRequestCreation bool `json:"automaticPullRequestCreation,omitempty"`
45+
//+optional
46+
PullRequestTemplate PullRequestTemplate `json:"pullRequestTemplate,omitempty"`
3447
}
3548

3649
// SyncStatus defines the observed state of Sync

apis/delivery/v1alpha1/zz_generated.deepcopy.go

Lines changed: 18 additions & 6 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: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,21 @@ spec:
3737
properties:
3838
automaticPullRequestCreation:
3939
type: boolean
40-
branch:
41-
type: string
4240
commitTemplate:
4341
description: CommitTemplate defines the details of the commit to the
4442
external repository.
4543
properties:
44+
baseBranch:
45+
default: main
46+
type: string
4647
email:
4748
type: string
4849
message:
4950
type: string
5051
name:
5152
type: string
53+
targetBranch:
54+
type: string
5255
required:
5356
- email
5457
- message
@@ -58,6 +61,17 @@ spec:
5861
type: string
5962
prune:
6063
type: boolean
64+
pullRequestTemplate:
65+
description: PullRequestTemplate provides information for the created
66+
pull request.
67+
properties:
68+
base:
69+
type: string
70+
description:
71+
type: string
72+
title:
73+
type: string
74+
type: object
6175
repositoryRef:
6276
description: LocalObjectReference contains enough information to let
6377
you locate the referenced object inside the same namespace.

controllers/delivery/sync_controller.go

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11+
"time"
1112

1213
"github.com/fluxcd/pkg/apis/meta"
1314
"github.com/fluxcd/pkg/runtime/conditions"
@@ -18,22 +19,26 @@ import (
1819
"k8s.io/apimachinery/pkg/runtime"
1920
"k8s.io/apimachinery/pkg/types"
2021
ctrl "sigs.k8s.io/controller-runtime"
22+
"sigs.k8s.io/controller-runtime/pkg/builder"
2123
"sigs.k8s.io/controller-runtime/pkg/client"
2224
"sigs.k8s.io/controller-runtime/pkg/log"
25+
"sigs.k8s.io/controller-runtime/pkg/predicate"
2326

2427
ocmv1 "github.com/open-component-model/ocm-controller/api/v1alpha1"
2528

2629
"github.com/open-component-model/git-controller/apis/delivery/v1alpha1"
2730
mpasv1alpha1 "github.com/open-component-model/git-controller/apis/mpas/v1alpha1"
28-
providers "github.com/open-component-model/git-controller/pkg"
31+
"github.com/open-component-model/git-controller/pkg"
32+
"github.com/open-component-model/git-controller/pkg/providers"
2933
)
3034

3135
// SyncReconciler reconciles a Sync object
3236
type SyncReconciler struct {
3337
client.Client
3438
Scheme *runtime.Scheme
3539

36-
Git providers.Git
40+
Git pkg.Git
41+
Provider providers.Provider
3742
}
3843

3944
//+kubebuilder:rbac:groups=delivery.ocm.software,resources=syncs,verbs=get;list;watch;create;update;patch;delete
@@ -53,7 +58,7 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
5358
)
5459

5560
log := log.FromContext(ctx)
56-
log.V(4).Info("starting reconcile loop for snapshot")
61+
5762
obj := &v1alpha1.Sync{}
5863
if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
5964
if apierrors.IsNotFound(err) {
@@ -139,6 +144,8 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
139144
return ctrl.Result{}, retErr
140145
}
141146

147+
log.V(4).Info("found target snapshot")
148+
142149
repository := &mpasv1alpha1.Repository{}
143150
if err := r.Get(ctx, types.NamespacedName{
144151
Namespace: obj.Namespace,
@@ -150,6 +157,8 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
150157
return ctrl.Result{}, retErr
151158
}
152159

160+
log.V(4).Info("found target repository")
161+
153162
authSecret := &corev1.Secret{}
154163
if err := r.Get(ctx, types.NamespacedName{
155164
Namespace: obj.Namespace,
@@ -161,16 +170,35 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
161170
return ctrl.Result{}, retErr
162171
}
163172

173+
log.V(4).Info("found authentication secret")
174+
175+
baseBranch := obj.Spec.CommitTemplate.BaseBranch
176+
if baseBranch == "" {
177+
baseBranch = "main"
178+
}
179+
targetBranch := obj.Spec.CommitTemplate.TargetBranch
180+
if targetBranch == "" && obj.Spec.AutomaticPullRequestCreation {
181+
targetBranch = fmt.Sprintf("branch-%d", time.Now().Unix())
182+
} else if targetBranch == "" && !obj.Spec.AutomaticPullRequestCreation {
183+
retErr = fmt.Errorf("branch cannot be empty if automatic pull request creation is not enabled")
184+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.GitRepositoryPushFailedReason, retErr.Error())
185+
186+
return ctrl.Result{}, retErr
187+
}
188+
189+
log.Info("preparing to push snapshot content", "base", baseBranch, "target", targetBranch)
190+
164191
// trim any trailing `/` and then just add.
165192
log.V(4).Info("crafting artifact URL to download from", "url", snapshot.Status.RepositoryURL)
166-
opts := &providers.PushOptions{
167-
URL: repository.GetRepositoryURL(),
168-
Message: obj.Spec.CommitTemplate.Message,
169-
Name: obj.Spec.CommitTemplate.Name,
170-
Email: obj.Spec.CommitTemplate.Email,
171-
Snapshot: snapshot,
172-
Branch: obj.Spec.Branch,
173-
SubPath: obj.Spec.SubPath,
193+
opts := &pkg.PushOptions{
194+
URL: repository.GetRepositoryURL(),
195+
Message: obj.Spec.CommitTemplate.Message,
196+
Name: obj.Spec.CommitTemplate.Name,
197+
Email: obj.Spec.CommitTemplate.Email,
198+
Snapshot: snapshot,
199+
BaseBranch: baseBranch,
200+
TargetBranch: targetBranch,
201+
SubPath: obj.Spec.SubPath,
174202
}
175203

176204
r.parseAuthSecret(authSecret, opts)
@@ -183,27 +211,42 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
183211
return ctrl.Result{}, retErr
184212
}
185213

214+
log.Info("target content pushed with digest", "base", baseBranch, "target", targetBranch, "digest", digest)
215+
186216
obj.Status.Digest = digest
187217

218+
if obj.Spec.AutomaticPullRequestCreation {
219+
log.Info("automatic pull-request creation is enabled, preparing to create a pull request")
220+
221+
if err := r.Provider.CreatePullRequest(ctx, targetBranch, *obj, *repository); err != nil {
222+
retErr = fmt.Errorf("failed to create pull request: %w", err)
223+
conditions.MarkFalse(obj, meta.ReadyCondition, v1alpha1.CreatePullRequestFailedReason, retErr.Error())
224+
225+
return ctrl.Result{}, retErr
226+
}
227+
}
228+
188229
// Remove any stale Ready condition, most likely False, set above. Its value
189230
// is derived from the overall result of the reconciliation in the deferred
190231
// block at the very end.
191232
conditions.Delete(obj, meta.ReadyCondition)
192233

234+
log.Info("successfully reconciled sync object")
235+
193236
return result, retErr
194237
}
195238

196239
// SetupWithManager sets up the controller with the Manager.
197240
func (r *SyncReconciler) SetupWithManager(mgr ctrl.Manager) error {
198241
return ctrl.NewControllerManagedBy(mgr).
199-
For(&v1alpha1.Sync{}).
242+
For(&v1alpha1.Sync{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
200243
Complete(r)
201244
}
202245

203-
func (r *SyncReconciler) parseAuthSecret(secret *corev1.Secret, opts *providers.PushOptions) {
246+
func (r *SyncReconciler) parseAuthSecret(secret *corev1.Secret, opts *pkg.PushOptions) {
204247
if _, ok := secret.Data["identity"]; ok {
205-
opts.Auth = &providers.Auth{
206-
SSH: &providers.SSH{
248+
opts.Auth = &pkg.Auth{
249+
SSH: &pkg.SSH{
207250
PemBytes: secret.Data["identity"],
208251
User: string(secret.Data["username"]),
209252
Password: string(secret.Data["password"]),
@@ -212,8 +255,8 @@ func (r *SyncReconciler) parseAuthSecret(secret *corev1.Secret, opts *providers.
212255
return
213256
}
214257
// default to basic auth.
215-
opts.Auth = &providers.Auth{
216-
BasicAuth: &providers.BasicAuth{
258+
opts.Auth = &pkg.Auth{
259+
BasicAuth: &pkg.BasicAuth{
217260
Username: string(secret.Data["username"]),
218261
Password: string(secret.Data["password"]),
219262
},

0 commit comments

Comments
 (0)