Skip to content

Commit

Permalink
Merge pull request #152 from controlplaneio-fluxcd/resourceset-input-…
Browse files Browse the repository at this point in the history
…provider-branch

Implement `GitHubBranch` and `GitLabBranch` input providers
  • Loading branch information
stefanprodan authored Jan 22, 2025
2 parents 4d7f93e + 198bb31 commit 52d1f53
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 5 deletions.
4 changes: 3 additions & 1 deletion api/v1/resourcesetinputprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ import (

const (
ResourceSetInputProviderKind = "ResourceSetInputProvider"
InputProviderGitHubBranch = "GitHubBranch"
InputProviderGitHubPullRequest = "GitHubPullRequest"
InputProviderGitLabBranch = "GitLabBranch"
InputProviderGitLabMergeRequest = "GitLabMergeRequest"
)

// ResourceSetInputProviderSpec defines the desired state of ResourceSetInputProvider
type ResourceSetInputProviderSpec struct {
// Type specifies the type of the input provider.
// +kubebuilder:validation:Enum=GitHubPullRequest;GitLabMergeRequest
// +kubebuilder:validation:Enum=GitHubBranch;GitHubPullRequest;GitLabBranch;GitLabMergeRequest
// +required
Type string `json:"type"`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ spec:
type:
description: Type specifies the type of the input provider.
enum:
- GitHubBranch
- GitHubPullRequest
- GitLabBranch
- GitLabMergeRequest
type: string
url:
Expand Down
16 changes: 12 additions & 4 deletions docs/api/v1/resourcesetinputprovider.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,28 @@ The `.spec.type` field is required and specifies the type of the provider.
The following types are supported:

- `GitHubPullRequest`: fetches input values from opened GitHub Pull Requests.
- `GitHubBranch`: fetches input values from GitHub repository branches.
- `GitLabMergeRequest`: fetches input values from opened GitLab Merge Requests.
- `GitLabBranch`: fetches input values from GitLab project branches.

For both `GitHubPullRequest` and `GitLabMergeRequest` types, flux-operator will
export in `.status.exportedInputs` a set of input values for each
Pull Request or Merge Request that matches the [filter](#filter) criteria.
For all types, the flux-operator will export in `.status.exportedInputs` a
set of input values for each Pull/Merge Request or Branch
that matches the [filter](#filter) criteria.

The [exported inputs](#exported-inputs-status) structure is:
For Pull/Merge Requests the [exported inputs](#exported-inputs-status) structure is:

- `id`: the ID number of the PR/MR (type string).
- `sha`: the commit SHA of the PR/MR (type string).
- `branch`: the branch name of the PR/MR (type string).
- `author`: the author username of the PR/MR (type string).
- `title`: the title of the PR/MR (type string).

For Git Branches the [exported inputs](#exported-inputs-status) structure is:

- `id`: the Adler-32 checksum of the branch name (type string).
- `branch`: the branch name (type string).
- `sha`: the commit SHA corresponding to the branch HEAD (type string).

### URL

The `.spec.url` field is required and specifies the HTTP/S URL of the provider.
Expand Down
5 changes: 5 additions & 0 deletions internal/controller/resourcesetinputprovider_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ func (r *ResourceSetInputProviderReconciler) callProvider(ctx context.Context,

var results []gitprovider.Result
switch {
case strings.HasSuffix(obj.Spec.Type, "Branch"):
results, err = provider.ListBranches(ctx, opts)
if err != nil {
return nil, fmt.Errorf("failed to list branches: %w", err)
}
case strings.HasSuffix(obj.Spec.Type, "Request"):
results, err = provider.ListRequests(ctx, opts)
if err != nil {
Expand Down
176 changes: 176 additions & 0 deletions internal/controller/resourcesetinputprovider_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,182 @@ import (
fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1"
)

func TestResourceSetInputProviderReconciler_GitLabBranch_LifeCycle(t *testing.T) {
g := NewWithT(t)
reconciler := getResourceSetInputProviderReconciler()
rsetReconciler := getResourceSetReconciler()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

ns, err := testEnv.CreateNamespace(ctx, "test")
g.Expect(err).ToNot(HaveOccurred())

objDef := fmt.Sprintf(`
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSetInputProvider
metadata:
name: test
namespace: "%[1]s"
spec:
type: GitLabBranch
url: "https://gitlab.com/stefanprodan/podinfo"
filter:
includeBranch: "^patch-[1|2]$"
`, ns.Name)

setDef := fmt.Sprintf(`
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
name: test
namespace: "%[1]s"
spec:
inputsFrom:
- kind: ResourceSetInputProvider
name: test
resources:
- apiVersion: v1
kind: ConfigMap
metadata:
name: test-<< inputs.id >>
namespace: "%[1]s"
data:
branch: << inputs.branch | quote >>
sha: << inputs.sha | quote >>
`, ns.Name)

obj := &fluxcdv1.ResourceSetInputProvider{}
err = yaml.Unmarshal([]byte(objDef), obj)
g.Expect(err).ToNot(HaveOccurred())

// Create the ResourceSetInputProvider.
err = testEnv.Create(ctx, obj)
g.Expect(err).ToNot(HaveOccurred())

// Initialize the ResourceSetInputProvider.
r, err := reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(obj),
})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(r.Requeue).To(BeTrue())

// Retrieve the inputs.
r, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(obj),
})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(r.Requeue).To(BeFalse())

// Check if the ResourceSetInputProvider was marked as ready.
result := &fluxcdv1.ResourceSetInputProvider{}
err = testClient.Get(ctx, client.ObjectKeyFromObject(obj), result)
g.Expect(err).ToNot(HaveOccurred())

logObjectStatus(t, result)
g.Expect(conditions.GetReason(result, meta.ReadyCondition)).To(BeIdenticalTo(meta.ReconciliationSucceededReason))
g.Expect(result.Status.LastExportedRevision).To(BeIdenticalTo("sha256:be31afc5e49da21b12fdca6a2cad6916cad26f4bbde8c16e5822359f75c1d46a"))

// Create a ResourceSet referencing the ResourceSetInputProvider.
rset := &fluxcdv1.ResourceSet{}
err = yaml.Unmarshal([]byte(setDef), rset)
g.Expect(err).ToNot(HaveOccurred())
err = testEnv.Create(ctx, rset)
g.Expect(err).ToNot(HaveOccurred())

// Initialize the ResourceSet instance.
_, err = rsetReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(rset),
})
g.Expect(err).ToNot(HaveOccurred())

// Reconcile the ResourceSet instance.
_, err = rsetReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(rset),
})
g.Expect(err).ToNot(HaveOccurred())

// Check if the ResourceSet generated the resources.
result1CM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-183501423",
Namespace: ns.Name,
},
}
err = testClient.Get(ctx, client.ObjectKeyFromObject(result1CM), result1CM)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(result1CM.Data).To(HaveKeyWithValue("branch", "patch-1"))
g.Expect(result1CM.Data).To(HaveKeyWithValue("sha", "cebef2d870bc83b37f43c470bae205fca094bacc"))

result2CM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-183566960",
Namespace: ns.Name,
},
}
err = testClient.Get(ctx, client.ObjectKeyFromObject(result2CM), result2CM)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(result2CM.Data).To(HaveKeyWithValue("branch", "patch-2"))
g.Expect(result2CM.Data).To(HaveKeyWithValue("sha", "a275fb0322466eaa1a74485a4f79f88d7c8858e8"))

// Update the filter to exclude all results.
resultP := result.DeepCopy()
resultP.Spec.Filter.ExcludeBranch = "^patch-.*$"
err = testClient.Patch(ctx, resultP, client.MergeFrom(result))
g.Expect(err).ToNot(HaveOccurred())

r, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(obj),
})
g.Expect(err).ToNot(HaveOccurred())

// Check if the exported inputs were updated.
resultFinal := &fluxcdv1.ResourceSetInputProvider{}
err = testClient.Get(ctx, client.ObjectKeyFromObject(obj), resultFinal)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(resultFinal.Status.ExportedInputs).To(BeEmpty())
g.Expect(resultFinal.Status.LastExportedRevision).To(BeIdenticalTo("sha256:38e0b9de817f645c4bec37c0d4a3e58baecccb040f5718dc069a72c7385a0bed"))

// Reconcile the ResourceSet to remove the generated resources.
_, err = rsetReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(rset),
})
g.Expect(err).ToNot(HaveOccurred())

// Check if the generated resources were removed.
err = testClient.Get(ctx, client.ObjectKeyFromObject(result1CM), result1CM)
g.Expect(err).To(HaveOccurred())
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
err = testClient.Get(ctx, client.ObjectKeyFromObject(result2CM), result2CM)
g.Expect(err).To(HaveOccurred())
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())

// Delete the ResourceSetInputProvider.
err = testClient.Delete(ctx, obj)
g.Expect(err).ToNot(HaveOccurred())

r, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(obj),
})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(r.IsZero()).To(BeTrue())

// Check if the ResourceSetInputProvider was finalized.
result = &fluxcdv1.ResourceSetInputProvider{}
err = testClient.Get(ctx, client.ObjectKeyFromObject(obj), result)
g.Expect(err).To(HaveOccurred())
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())

// Delete the ResourceSet.
err = testClient.Delete(ctx, rset)
g.Expect(err).ToNot(HaveOccurred())

r, err = rsetReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(rset),
})
g.Expect(err).ToNot(HaveOccurred())
g.Expect(r.IsZero()).To(BeTrue())
}

func TestResourceSetInputProviderReconciler_GitHubPullRequest_LifeCycle(t *testing.T) {
g := NewWithT(t)
reconciler := getResourceSetInputProviderReconciler()
Expand Down

0 comments on commit 52d1f53

Please sign in to comment.