Skip to content

Commit 96d3ebe

Browse files
committed
Add envtest integration tests.
Add GitHub example to the README.md
1 parent 83dbd19 commit 96d3ebe

File tree

3 files changed

+278
-2
lines changed

3 files changed

+278
-2
lines changed

README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,115 @@ $ IMG=my-org/my-image:tag make deploy
152152

153153
There's an additional `release` target that will generate a file `release-<version>.yaml` which contains all the necessary files to deploy your controller.
154154

155+
## Examples of the Cloud Event Body
156+
157+
## GitHub
158+
159+
```json
160+
{
161+
"author": {
162+
"avatar_url": "https://avatars.githubusercontent.com/u/867746?v=4",
163+
"events_url": "https://api.github.com/users/example/events{/privacy}",
164+
"followers_url": "https://api.github.com/users/example/followers",
165+
"following_url": "https://api.github.com/users/example/following{/other_user}",
166+
"gists_url": "https://api.github.com/users/example/gists{/gist_id}",
167+
"gravatar_id": "",
168+
"html_url": "https://github.com/example",
169+
"id": 867746,
170+
"login": "example",
171+
"node_id": "MDQ6VXNlcjg2Nzc0Ng==",
172+
"organizations_url": "https://api.github.com/users/example/orgs",
173+
"received_events_url": "https://api.github.com/users/example/received_events",
174+
"repos_url": "https://api.github.com/users/example/repos",
175+
"site_admin": false,
176+
"starred_url": "https://api.github.com/users/example/starred{/owner}{/repo}",
177+
"subscriptions_url": "https://api.github.com/users/example/subscriptions",
178+
"type": "User",
179+
"url": "https://api.github.com/users/example",
180+
"user_view_type": "public"
181+
},
182+
"comments_url": "https://api.github.com/repos/example/repo/commits/0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384/comments",
183+
"commit": {
184+
"author": {
185+
"date": "2023-12-15T09:35:55Z",
186+
"email": "[email protected]",
187+
"name": "Kevin McDermott"
188+
},
189+
"comment_count": 0,
190+
"committer": {
191+
"date": "2023-12-15T09:35:55Z",
192+
"email": "[email protected]",
193+
"name": "GitHub"
194+
},
195+
"message": "Update kustomization.yaml",
196+
"tree": {
197+
"sha": "bb194c136b49ab0cd44a06e51d3ed5c8c32b7d39",
198+
"url": "https://api.github.com/repos/example/repo/git/trees/bb194c136b49ab0cd44a06e51d3ed5c8c32b7d39"
199+
},
200+
"url": "https://api.github.com/repos/example/repo/git/commits/0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384",
201+
"verification": {
202+
"payload": "tree bb194c136b49ab0cd44a06e51d3ed5c8c32b7d39\nparent 72c6f14b1be29dd6cc80a722018165a0e10ff378\nauthor Example User <[email protected]> 1702632955 +0000\ncommitter GitHub <[email protected]> 1702632955 +0000\n\nUpdate kustomization.yaml",
203+
"reason": "valid",
204+
"signature": "-----BEGIN PGP SIGNATURE-----\n\nwsBcBAABCAAQBQJlfB37CRBK7hj4Ov3rIwAAVSsIAJoFQvKj76XKhSt90JGl2D+L\nlNtr1+t4u7AMUwtFQRyAdjgPXZ+Z6r/4echXWHTKtBuNAhpbyXjWnY1BIaqN1xm/\n4BSILbA4VTmoQ9ICATdlzoxNOmO5xineSCFth/bMguZpfoNkoJIoIMBzU1wDZP7L\nruC4I4lc4JaD1SqNvdBSLt3cq3aT3iTqdFjP6CTNN+g0C3WlL+8BfYZoWOvVywDD\nxDkBm/ApMuzEE0YGGyyJcZ8k9r+1pNq2g2qblab1zZKrdcKls48OvyWIQoWwcX5Z\nskTrg/wsXI1lI9EBH3ooIyrWvutWLUxaVoar3kAl9EghobTJPhEHlRCtkqmRcaI=\n=WJHF\n-----END PGP SIGNATURE-----\n",
205+
"verified": true,
206+
"verified_at": "2024-01-16T19:59:59Z"
207+
}
208+
},
209+
"committer": {
210+
"avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4",
211+
"events_url": "https://api.github.com/users/web-flow/events{/privacy}",
212+
"followers_url": "https://api.github.com/users/web-flow/followers",
213+
"following_url": "https://api.github.com/users/web-flow/following{/other_user}",
214+
"gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}",
215+
"gravatar_id": "",
216+
"html_url": "https://github.com/web-flow",
217+
"id": 19864447,
218+
"login": "web-flow",
219+
"node_id": "MDQ6VXNlcjE5ODY0NDQ3",
220+
"organizations_url": "https://api.github.com/users/web-flow/orgs",
221+
"received_events_url": "https://api.github.com/users/web-flow/received_events",
222+
"repos_url": "https://api.github.com/users/web-flow/repos",
223+
"site_admin": false,
224+
"starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}",
225+
"subscriptions_url": "https://api.github.com/users/web-flow/subscriptions",
226+
"type": "User",
227+
"url": "https://api.github.com/users/web-flow",
228+
"user_view_type": "public"
229+
},
230+
"files": [
231+
{
232+
"additions": 1,
233+
"blob_url": "https://github.com/example/repo/blob/0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384/examples%2Fkustomize%2Fenvironments%2Fstaging%2Fkustomization.yaml",
234+
"changes": 8,
235+
"contents_url": "https://api.github.com/repos/example/repo/contents/examples%2Fkustomize%2Fenvironments%2Fstaging%2Fkustomization.yaml?ref=0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384",
236+
"deletions": 7,
237+
"filename": "examples/kustomize/environments/staging/kustomization.yaml",
238+
"patch": "@@ -1,13 +1,7 @@\n namespace: staging\n images:\n - name: example/repo\n- newTag: demo\n-apiVersion: kustomize.config.k8s.io/v1beta1\n-kind: Kustomization\n+ newTag: v1.2.3\n resources:\n - ../../base\n - namespace.yaml\n-labels:\n-- includeSelectors: true\n- pairs:\n- gitops.pro/pipeline-environment: staging",
239+
"raw_url": "https://github.com/example/repo/raw/0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384/examples%2Fkustomize%2Fenvironments%2Fstaging%2Fkustomization.yaml",
240+
"sha": "5f136971df5821e8c39dd3515cae156ffc8887ad",
241+
"status": "modified"
242+
}
243+
],
244+
"html_url": "https://github.com/example/repo/commit/0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384",
245+
"node_id": "C_kwDOEAWX1doAKDA0NjljOWI0YTlmZGJlYzVmZTdhMDYwMDBiYTBjNWRhZDk5YjAzODQ",
246+
"parents": [
247+
{
248+
"html_url": "https://github.com/example/repo/commit/72c6f14b1be29dd6cc80a722018165a0e10ff378",
249+
"sha": "72c6f14b1be29dd6cc80a722018165a0e10ff378",
250+
"url": "https://api.github.com/repos/example/repo/commits/72c6f14b1be29dd6cc80a722018165a0e10ff378"
251+
}
252+
],
253+
"sha": "0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384",
254+
"stats": {
255+
"additions": 1,
256+
"deletions": 7,
257+
"total": 8
258+
},
259+
"url": "https://api.github.com/repos/example/repo/commits/0469c9b4a9fdbec5fe7a06000ba0c5dad99b0384"
260+
}
261+
```
262+
263+
155264
## Development
156265

157266
This is a kubebuilder derived controller.
@@ -162,3 +271,4 @@ This is a kubebuilder derived controller.
162271
- [ ] Support generic Git repository polling
163272
- [ ] Allow more generic HTTP event sending (cloud-events don't appear to allow signing)
164273
- [ ] Support for custom TLS CAs in the client
274+
- [ ] Support for [CDEvents](https://cdevents.dev/) if possible

internal/controller/polledrepository_controller.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import (
3030
ctrl "sigs.k8s.io/controller-runtime"
3131
"sigs.k8s.io/controller-runtime/pkg/client"
3232
"sigs.k8s.io/controller-runtime/pkg/predicate"
33-
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3433

3534
pollingv1 "github.com/gitops-tools/gitpoller-controller/api/v1alpha1"
3635
"github.com/gitops-tools/gitpoller-controller/pkg/git"
@@ -64,6 +63,7 @@ type PolledRepositoryReconciler struct {
6463
// Reconcile is part of the main Kubernetes reconciliation loop which aims to
6564
// move the current state of the cluster closer to the desired state.
6665
func (r *PolledRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
66+
6767
reqLogger := logr.FromContextOrDiscard(ctx)
6868
reqLogger.Info("reconciling PolledRepository")
6969

@@ -85,13 +85,20 @@ func (r *PolledRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Req
8585

8686
authToken, err := r.authTokenForRepo(ctx, reqLogger, req.Namespace, repo)
8787
if err != nil {
88+
// TODO: Patch this!
89+
repo.Status.LastError = err.Error()
8890
reqLogger.Error(err, "Getting the auth token failed")
89-
return reconcile.Result{}, fmt.Errorf("failed to get auth token: %w", err)
91+
if err := r.Client.Status().Update(ctx, &repo); err != nil {
92+
reqLogger.Error(err, "unable to update Repository status")
93+
}
94+
// TODO: improve the error
95+
return ctrl.Result{}, err
9096
}
9197

9298
// TODO: handle pollerFactory returning nil/error
9399
newStatus, commit, err := r.PollerFactory(r.HTTPClient, &repo, endpoint, authToken).Poll(ctx, repoName, repo.Status.PollStatus)
94100
if err != nil {
101+
// TODO: Patch this!
95102
repo.Status.LastError = err.Error()
96103
reqLogger.Error(err, "repository poll failed")
97104
if err := r.Client.Status().Update(ctx, &repo); err != nil {
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
Copyright 2024.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"net/http"
23+
"net/http/httptest"
24+
"os"
25+
"path/filepath"
26+
"testing"
27+
"time"
28+
29+
pollingv1 "github.com/gitops-tools/gitpoller-controller/api/v1alpha1"
30+
"github.com/gitops-tools/gitpoller-controller/pkg/cloudevents"
31+
"github.com/gitops-tools/gitpoller-controller/pkg/git"
32+
"github.com/gitops-tools/gitpoller-controller/test/utils"
33+
"github.com/google/go-cmp/cmp"
34+
"github.com/onsi/gomega"
35+
corev1 "k8s.io/api/core/v1"
36+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
"k8s.io/apimachinery/pkg/runtime"
38+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
39+
ctrl "sigs.k8s.io/controller-runtime"
40+
"sigs.k8s.io/controller-runtime/pkg/client"
41+
"sigs.k8s.io/controller-runtime/pkg/envtest"
42+
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
43+
)
44+
45+
const (
46+
timeout = 5 * time.Second
47+
)
48+
49+
func TestPolledRepositoryController(t *testing.T) {
50+
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
51+
t.Skip("Not setup for envtest correctly please set KUBEBUILDER_ASSETS")
52+
}
53+
54+
scheme := runtime.NewScheme()
55+
utils.AssertNoError(t, clientgoscheme.AddToScheme(scheme))
56+
utils.AssertNoError(t, pollingv1.AddToScheme(scheme))
57+
58+
testEnv := &envtest.Environment{
59+
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
60+
ErrorIfCRDPathMissing: true,
61+
}
62+
63+
cfg, err := testEnv.Start()
64+
if err != nil {
65+
t.Fatalf("failed to start test environment: %s", err)
66+
}
67+
68+
t.Cleanup(func() {
69+
if err := testEnv.Stop(); err != nil {
70+
t.Fatalf("failed to stop test environment: %s", err)
71+
}
72+
})
73+
74+
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
75+
Scheme: scheme,
76+
Metrics: metricsserver.Options{
77+
BindAddress: "0", // Do not start the metrics server.
78+
},
79+
})
80+
utils.AssertNoError(t, err)
81+
82+
k8sClient := k8sManager.GetClient()
83+
84+
if err := (&PolledRepositoryReconciler{
85+
Client: k8sClient,
86+
Scheme: scheme,
87+
HTTPClient: http.DefaultClient,
88+
PollerFactory: func(cl *http.Client, repo *pollingv1.PolledRepository, endpoint, token string) git.CommitPoller {
89+
return MakeCommitPoller(cl, repo, endpoint, token)
90+
},
91+
EventDispatcher: cloudevents.CloudEventDispatcher{},
92+
}).SetupWithManager(k8sManager); err != nil {
93+
t.Fatalf("Failed to start PolledRepositoryReconciler: %v", err)
94+
}
95+
96+
ctx, cancel := context.WithCancel(context.Background())
97+
defer cancel()
98+
99+
go func() {
100+
utils.AssertNoError(t, k8sManager.Start(ctx))
101+
}()
102+
103+
<-k8sManager.Elected()
104+
105+
utils.AssertNoError(t, k8sClient.Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "testing"}}))
106+
107+
t.Run("polling a github repository", func(t *testing.T) {
108+
dispatches := make(chan map[string]any, 1)
109+
ts := httptest.NewServer(http.HandlerFunc(notificationHandler(dispatches)))
110+
t.Cleanup(ts.Close)
111+
112+
repo := newPolledRepository(withEndpoint(ts.URL))
113+
repoKey := client.ObjectKeyFromObject(repo)
114+
115+
utils.AssertNoError(t, k8sClient.Create(context.Background(), repo))
116+
t.Cleanup(func() {
117+
deleteObject(t, k8sClient, repo)
118+
})
119+
120+
gomega.NewWithT(t).Eventually(func() string {
121+
utils.AssertNoError(t, client.IgnoreNotFound(k8sClient.Get(context.Background(), repoKey, repo)))
122+
return repo.Status.PollStatus.SHA
123+
}, timeout).Should(gomega.Not(gomega.Equal("")))
124+
125+
received := <-dispatches
126+
127+
if diff := cmp.Diff(repo.Status.PollStatus.SHA, received["sha"]); diff != "" {
128+
t.Errorf("incorrect notification: diff -want +got\n%s", diff)
129+
}
130+
})
131+
}
132+
133+
func deleteObject(t *testing.T, cl client.Client, obj client.Object) {
134+
t.Helper()
135+
if err := cl.Delete(context.TODO(), obj); err != nil {
136+
t.Fatal(err)
137+
}
138+
}
139+
140+
func notificationHandler(dispatches chan<- map[string]any) func(http.ResponseWriter, *http.Request) {
141+
return func(w http.ResponseWriter, req *http.Request) {
142+
decoder := json.NewDecoder(req.Body)
143+
// TODO: This could do so much more, but we have tests for the
144+
// CloudEvent dispatcher so maybe not?
145+
result := map[string]any{}
146+
if err := decoder.Decode(&result); err != nil {
147+
http.Error(w, "failed to decode", http.StatusBadRequest)
148+
return
149+
}
150+
151+
dispatches <- result
152+
}
153+
}
154+
155+
func withEndpoint(s string) func(*pollingv1.PolledRepository) {
156+
return func(r *pollingv1.PolledRepository) {
157+
r.Spec.Endpoint = s
158+
}
159+
}

0 commit comments

Comments
 (0)