Skip to content

Commit 171862a

Browse files
feat: Backend to provide REST APIs can to pull sync history (#1890) (#26)
Signed-off-by: Keith Chong <[email protected]> Co-authored-by: Chetan Banavikalmutt <[email protected]>
1 parent e77a9b0 commit 171862a

File tree

4 files changed

+374
-2
lines changed

4 files changed

+374
-2
lines changed

pkg/httpapi/api.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func NewRouter(c git.ClientFactory, s secrets.SecretGetter, kc ctrlclient.Client
7070
api.HandlerFunc(http.MethodGet, "/applications", api.ListApplications)
7171
api.HandlerFunc(http.MethodGet, "/environments/:env/application/:app", api.GetApplication)
7272
api.HandlerFunc(http.MethodGet, "/environment/:env/application/:app", api.GetApplicationDetails)
73+
api.HandlerFunc(http.MethodGet, "/history/environment/:env/application/:app", api.GetApplicationHistory)
7374
return api
7475
}
7576

@@ -226,6 +227,80 @@ func (a *APIRouter) ListApplications(w http.ResponseWriter, r *http.Request) {
226227
marshalResponse(w, applicationsToAppsResponse(apps, parsedRepoURL.String()))
227228
}
228229

230+
func (a *APIRouter) GetApplicationHistory(w http.ResponseWriter, r *http.Request) {
231+
params := httprouter.ParamsFromContext(r.Context())
232+
envName, appName := params.ByName("env"), params.ByName("app")
233+
app := &argoV1aplha1.Application{}
234+
235+
repoURL := strings.TrimSpace(r.URL.Query().Get("url"))
236+
if repoURL == "" {
237+
log.Println("ERROR: please provide a valid GitOps repo URL")
238+
http.Error(w, "please provide a valid GitOps repo URL", http.StatusBadRequest)
239+
return
240+
}
241+
242+
parsedRepoURL, err := url.Parse(repoURL)
243+
if err != nil {
244+
log.Printf("ERROR: failed to parse URL, error: %v", err)
245+
http.Error(w, fmt.Sprintf("failed to parse URL, error: %v", err), http.StatusBadRequest)
246+
return
247+
}
248+
249+
parsedRepoURL.RawQuery = ""
250+
251+
appList := &argoV1aplha1.ApplicationList{}
252+
var listOptions []ctrlclient.ListOption
253+
254+
listOptions = append(listOptions, ctrlclient.InNamespace(""), ctrlclient.MatchingFields{
255+
"metadata.name": fmt.Sprintf("%s-%s", envName, appName),
256+
})
257+
258+
err = a.k8sClient.List(r.Context(), appList, listOptions...)
259+
if err != nil {
260+
log.Printf("ERROR: failed to get application list: %v", err)
261+
http.Error(w, fmt.Sprintf("failed to get list of application, err: %v", err), http.StatusBadRequest)
262+
return
263+
}
264+
265+
for _, a := range appList.Items {
266+
if a.Spec.Source.RepoURL == parsedRepoURL.String() {
267+
app = &a
268+
}
269+
}
270+
271+
if app == nil {
272+
log.Printf("ERROR: failed to get application %s: %v", appName, err)
273+
http.Error(w, fmt.Sprintf("failed to get the application %s, err: %v", appName, err), http.StatusBadRequest)
274+
return
275+
}
276+
277+
var deployedTime, revision string
278+
hist := app.Status.History
279+
var historyList = make([]envHistory, 0)
280+
for _, h := range hist {
281+
revision = h.Revision
282+
t := h.DeployedAt
283+
if !t.IsZero() {
284+
deployedTime = t.String()
285+
}
286+
commitInfo, err := a.getCommitInfo(app.Name, revision)
287+
if err != nil {
288+
log.Printf("ERROR: failed to retrieve revision metadata for app %s: %v", appName, err)
289+
http.Error(w, fmt.Sprintf("failed to retrieve revision metadata for app %s, err: %v", appName, err), http.StatusBadRequest)
290+
}
291+
hist := envHistory{
292+
Author: commitInfo["author"],
293+
Message: commitInfo["message"],
294+
Revision: revision,
295+
RepoUrl: h.Source.RepoURL,
296+
Environment: envName,
297+
DeployedAt: deployedTime,
298+
}
299+
historyList = append([]envHistory{hist}, historyList...)
300+
}
301+
marshalResponse(w, historyList)
302+
}
303+
229304
func (a *APIRouter) GetApplicationDetails(w http.ResponseWriter, r *http.Request) {
230305
params := httprouter.ParamsFromContext(r.Context())
231306
envName, appName := params.ByName("env"), params.ByName("app")

pkg/httpapi/api_test.go

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,129 @@ func TestGetApplicationDetails(t *testing.T) {
509509
baseURL = tmp
510510
}
511511

512+
func TestGetApplicationHistory(t *testing.T) {
513+
err := argoV1aplha1.AddToScheme(scheme.Scheme)
514+
if err != nil {
515+
t.Fatal(err)
516+
}
517+
518+
builder := fake.NewClientBuilder()
519+
kc := builder.Build()
520+
521+
ts, _ := makeServer(t, func(router *APIRouter) {
522+
router.k8sClient = kc
523+
})
524+
525+
// create test ArgoCD Server to handle http requests
526+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
527+
u := r.URL.String()
528+
if strings.Contains(u, "session") {
529+
m := map[string]string{
530+
"token": "testing",
531+
}
532+
marshalResponse(w, m)
533+
} else if strings.Contains(u, "metadata") {
534+
m := map[string]string{
535+
"author": "test",
536+
"message": "testMessage",
537+
}
538+
marshalResponse(w, m)
539+
}
540+
}))
541+
defer server.Close()
542+
tmp := baseURL
543+
baseURL = server.URL
544+
545+
var createOptions []ctrlclient.CreateOption
546+
app, _ := testArgoApplication("testdata/application3.yaml")
547+
// create argocd test-app
548+
err = kc.Create(context.TODO(), app, createOptions...)
549+
if err != nil {
550+
t.Fatal(err)
551+
}
552+
553+
// create argocd instance creds secret
554+
secret := &corev1.Secret{
555+
ObjectMeta: metav1.ObjectMeta{
556+
Name: defaultArgoCDInstance + "-cluster",
557+
Namespace: defaultArgocdNamespace,
558+
},
559+
Data: map[string][]byte{
560+
"admin.password": []byte("abc"),
561+
},
562+
}
563+
err = kc.Create(context.TODO(), secret, createOptions...)
564+
if err != nil {
565+
t.Fatal(err)
566+
}
567+
568+
options := url.Values{
569+
"url": []string{"https://github.com/test-repo/gitops.git"},
570+
}
571+
req := makeClientRequest(t, "Bearer testing",
572+
fmt.Sprintf("%s/history/environment/%s/application/%s?%s", ts.URL, "dev", "app-taxi", options.Encode()))
573+
res, err := ts.Client().Do(req)
574+
if err != nil {
575+
t.Fatal(err)
576+
}
577+
fmt.Println("history res is ", res)
578+
579+
want := []envHistory{
580+
{
581+
Author: "test",
582+
Message: "testMessage",
583+
Revision: "a0c7298faead28f7f60a5106afbb18882ad220a7",
584+
Environment: "dev",
585+
RepoUrl: "https://github.com/test-repo/gitops.git",
586+
DeployedAt: time.Date(2022, time.Month(4), 22, 17, 11, 29, 0, time.UTC).Local().String(),
587+
},
588+
{
589+
Author: "test",
590+
Message: "testMessage",
591+
Revision: "3f6965bd65d9294b8fec5d6e2dc3dad08e33a8fe",
592+
Environment: "dev",
593+
RepoUrl: "https://github.com/test-repo/gitops.git",
594+
DeployedAt: time.Date(2022, time.Month(4), 21, 14, 17, 49, 0, time.UTC).Local().String(),
595+
},
596+
{
597+
Author: "test",
598+
Message: "testMessage",
599+
Revision: "e5585fcf22366e2d066e0936cbd8a0508756d02d",
600+
Environment: "dev",
601+
RepoUrl: "https://github.com/test-repo/gitops.git",
602+
DeployedAt: time.Date(2022, time.Month(4), 21, 14, 16, 51, 0, time.UTC).Local().String(),
603+
},
604+
{
605+
Author: "test",
606+
Message: "testMessage",
607+
Revision: "3f6965bd65d9294b8fec5d6e2dc3dad08e33a8fe",
608+
Environment: "dev",
609+
RepoUrl: "https://github.com/test-repo/gitops.git",
610+
DeployedAt: time.Date(2022, time.Month(4), 21, 14, 16, 50, 0, time.UTC).Local().String(),
611+
},
612+
{
613+
Author: "test",
614+
Message: "testMessage",
615+
Revision: "e5585fcf22366e2d066e0936cbd8a0508756d02d",
616+
Environment: "dev",
617+
RepoUrl: "https://github.com/test-repo/gitops.git",
618+
DeployedAt: time.Date(2022, time.Month(4), 21, 14, 14, 27, 0, time.UTC).Local().String(),
619+
},
620+
{
621+
Author: "test",
622+
Message: "testMessage",
623+
Revision: "e5585fcf22366e2d066e0936cbd8a0508756d02d",
624+
Environment: "dev",
625+
RepoUrl: "https://github.com/test-repo/gitops.git",
626+
DeployedAt: time.Date(2022, time.Month(4), 19, 18, 19, 52, 0, time.UTC).Local().String(),
627+
},
628+
}
629+
assertJSONResponseHistory(t, res, want)
630+
631+
//reset BaseURL
632+
baseURL = tmp
633+
}
634+
512635
func testArgoApplication(appCr string) (*argoV1aplha1.Application, error) {
513636
applicationYaml, _ := ioutil.ReadFile(appCr)
514637
app := &argoV1aplha1.Application{}
@@ -588,7 +711,7 @@ func makeServer(t *testing.T, opts ...routerOptionFunc) (*httptest.Server, *stub
588711
return ts, sf.client
589712
}
590713

591-
func assertJSONResponse(t *testing.T, res *http.Response, want map[string]interface{}) {
714+
func readBody(t *testing.T, res *http.Response) []byte {
592715
t.Helper()
593716
if res.StatusCode != http.StatusOK {
594717
defer res.Body.Close()
@@ -609,9 +732,26 @@ func assertJSONResponse(t *testing.T, res *http.Response, want map[string]interf
609732
if h := res.Header.Get("Access-Control-Allow-Origin"); h != "*" {
610733
t.Fatalf("wanted '*' got %s", h)
611734
}
735+
return b
736+
}
737+
738+
func assertJSONResponse(t *testing.T, res *http.Response, want map[string]interface{}) {
739+
b := readBody(t, res)
612740
got := map[string]interface{}{}
613741

614-
err = json.Unmarshal(b, &got)
742+
err := json.Unmarshal(b, &got)
743+
if err != nil {
744+
t.Fatalf("failed to parse %s: %s", b, err)
745+
}
746+
if diff := cmp.Diff(want, got); diff != "" {
747+
t.Fatalf("JSON response failed:\n%s", diff)
748+
}
749+
}
750+
751+
func assertJSONResponseHistory(t *testing.T, res *http.Response, want []envHistory) {
752+
b := readBody(t, res)
753+
got := make([]envHistory, 0)
754+
err := json.Unmarshal(b, &got)
615755
if err != nil {
616756
t.Fatalf("failed to parse %s: %s", b, err)
617757
}

pkg/httpapi/manifest.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ type envHealthResource struct {
3434
Status string `json:"status,omitempty"`
3535
}
3636

37+
type envHistory struct {
38+
Author string `json:"author,omitempty"`
39+
Message string `json:"message,omitempty"`
40+
Revision string `json:"revision,omitempty"`
41+
Environment string `json:"environment,omitempty"`
42+
RepoUrl string `json:"repo_url,omitempty"`
43+
DeployedAt string `json:"deployed_at,omitempty"`
44+
}
45+
3746
func (e environment) findService(n string) *service {
3847
for _, a := range e.Apps {
3948
for _, s := range a.Services {

0 commit comments

Comments
 (0)