Skip to content

Commit f9c726e

Browse files
authored
Feat: added API for updated Application details page (#23)
* added new api for application details page using application CR * provide commit info - author, message and commit hash * fixed https client * added unit tests
1 parent b13129e commit f9c726e

File tree

4 files changed

+253
-2
lines changed

4 files changed

+253
-2
lines changed

pkg/httpapi/api.go

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package httpapi
22

33
import (
4+
"bytes"
45
"context"
6+
"crypto/tls"
57
"encoding/json"
68
"fmt"
9+
"io/ioutil"
710
"log"
811
"net/http"
912
"net/url"
1013
"strings"
1114

1215
argoV1aplha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
1316
"github.com/julienschmidt/httprouter"
17+
corev1 "k8s.io/api/core/v1"
1418
"k8s.io/apimachinery/pkg/types"
1519
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
1620
"sigs.k8s.io/yaml"
@@ -26,7 +30,13 @@ var DefaultSecretRef = types.NamespacedName{
2630
Namespace: "pipelines-app-delivery",
2731
}
2832

29-
const defaultRef = "HEAD"
33+
const (
34+
defaultRef = "HEAD"
35+
defaultArgoCDInstance = "openshift-gitops"
36+
defaultArgocdNamespace = "openshift-gitops"
37+
)
38+
39+
var baseURL = fmt.Sprintf("https://%s-server.%s.svc.cluster.local", defaultArgoCDInstance, defaultArgocdNamespace)
3040

3141
// APIRouter is an HTTP API for accessing app configurations.
3242
type APIRouter struct {
@@ -51,9 +61,16 @@ func NewRouter(c git.ClientFactory, s secrets.SecretGetter, kc ctrlclient.Client
5161
api.HandlerFunc(http.MethodGet, "/pipelines", api.GetPipelines)
5262
api.HandlerFunc(http.MethodGet, "/applications", api.ListApplications)
5363
api.HandlerFunc(http.MethodGet, "/environments/:env/application/:app", api.GetApplication)
64+
api.HandlerFunc(http.MethodGet, "/environment/:env/application/:app", api.GetApplicationDetails)
5465
return api
5566
}
5667

68+
type RevisionMeta struct {
69+
Author string `json:"author"`
70+
Message string `json:"message"`
71+
Revision string `json:"revision"`
72+
}
73+
5774
// GetPipelines fetches and returns the pipeline body.
5875
func (a *APIRouter) GetPipelines(w http.ResponseWriter, r *http.Request) {
5976
urlToFetch := r.URL.Query().Get("url")
@@ -201,6 +218,86 @@ func (a *APIRouter) ListApplications(w http.ResponseWriter, r *http.Request) {
201218
marshalResponse(w, applicationsToAppsResponse(apps, parsedRepoURL.String()))
202219
}
203220

221+
func (a *APIRouter) GetApplicationDetails(w http.ResponseWriter, r *http.Request) {
222+
params := httprouter.ParamsFromContext(r.Context())
223+
envName, appName := params.ByName("env"), params.ByName("app")
224+
app := &argoV1aplha1.Application{}
225+
var lastDeployed, revision string
226+
227+
repoURL := strings.TrimSpace(r.URL.Query().Get("url"))
228+
if repoURL == "" {
229+
log.Println("ERROR: please provide a valid GitOps repo URL")
230+
http.Error(w, "please provide a valid GitOps repo URL", http.StatusBadRequest)
231+
return
232+
}
233+
234+
parsedRepoURL, err := url.Parse(repoURL)
235+
if err != nil {
236+
log.Printf("ERROR: failed to parse URL, error: %v", err)
237+
http.Error(w, fmt.Sprintf("failed to parse URL, error: %v", err), http.StatusBadRequest)
238+
return
239+
}
240+
241+
parsedRepoURL.RawQuery = ""
242+
243+
appList := &argoV1aplha1.ApplicationList{}
244+
var listOptions []ctrlclient.ListOption
245+
246+
listOptions = append(listOptions, ctrlclient.InNamespace(""), ctrlclient.MatchingFields{
247+
"metadata.name": fmt.Sprintf("%s-%s", envName, appName),
248+
})
249+
250+
err = a.k8sClient.List(r.Context(), appList, listOptions...)
251+
if err != nil {
252+
log.Printf("ERROR: failed to get application list: %v", err)
253+
http.Error(w, fmt.Sprintf("failed to get list of application, err: %v", err), http.StatusBadRequest)
254+
return
255+
}
256+
257+
for _, a := range appList.Items {
258+
if a.Spec.Source.RepoURL == parsedRepoURL.String() {
259+
app = &a
260+
}
261+
}
262+
263+
if app == nil {
264+
log.Printf("ERROR: failed to get application %s: %v", appName, err)
265+
http.Error(w, fmt.Sprintf("failed to get the application %s, err: %v", appName, err), http.StatusBadRequest)
266+
return
267+
}
268+
269+
if len(app.Status.History) > 0 {
270+
revision = app.Status.History[len(app.Status.History)-1].Revision
271+
t := app.Status.History[len(app.Status.History)-1].DeployedAt
272+
if !t.IsZero() {
273+
lastDeployed = t.String()
274+
}
275+
}
276+
277+
commitInfo, err := a.getCommitInfo(app.Name, revision)
278+
if err != nil {
279+
log.Printf("ERROR: failed to retrieve revision metadata for app %s: %v", appName, err)
280+
http.Error(w, fmt.Sprintf("failed to retrieve revision metadata for app %s, err: %v", appName, err), http.StatusBadRequest)
281+
return
282+
}
283+
284+
revisionMeta := RevisionMeta{
285+
Author: strings.Split(commitInfo["author"], " ")[0],
286+
Message: commitInfo["message"],
287+
Revision: revision,
288+
}
289+
290+
appEnv := map[string]interface{}{
291+
"environment": app.Spec.Destination.Namespace,
292+
"cluster": app.Spec.Destination.Server,
293+
"lastDeployed": lastDeployed,
294+
"status": app.Status.Sync.Status,
295+
"revision": revisionMeta,
296+
}
297+
298+
marshalResponse(w, appEnv)
299+
}
300+
204301
func (a *APIRouter) getAuthToken(ctx context.Context, req *http.Request) (string, error) {
205302
token := AuthToken(ctx)
206303
secret, ok := secretRefFromQuery(req.URL.Query())
@@ -253,3 +350,72 @@ func marshalResponse(w http.ResponseWriter, v interface{}) {
253350
log.Printf("failed to encode response: %s", err)
254351
}
255352
}
353+
354+
func (a *APIRouter) getCommitInfo(app, revision string) (map[string]string, error) {
355+
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}
356+
argocdCreds := &corev1.Secret{}
357+
err := a.k8sClient.Get(context.TODO(),
358+
types.NamespacedName{
359+
Name: defaultArgoCDInstance + "-cluster",
360+
Namespace: defaultArgocdNamespace,
361+
}, argocdCreds)
362+
if err != nil {
363+
return nil, err
364+
}
365+
366+
payload := map[string]string{
367+
"username": "admin",
368+
"password": string(argocdCreds.Data["admin.password"]),
369+
}
370+
payloadBytes, err := json.Marshal(payload)
371+
if err != nil {
372+
return nil, err
373+
}
374+
375+
resp, err := client.Post(fmt.Sprintf("%s/api/v1/session", baseURL),
376+
"application/json", bytes.NewBuffer(payloadBytes))
377+
if err != nil {
378+
return nil, err
379+
}
380+
defer resp.Body.Close()
381+
bodyBytes, err := ioutil.ReadAll(resp.Body)
382+
if err != nil {
383+
return nil, err
384+
}
385+
386+
m := make(map[string]string)
387+
err = json.Unmarshal(bodyBytes, &m)
388+
if err != nil {
389+
return nil, err
390+
}
391+
392+
if token, ok := m["token"]; !ok || token == "" {
393+
return nil, fmt.Errorf("failed to retrieve JWT from the api-server")
394+
}
395+
396+
u := fmt.Sprintf("%s/api/v1/applications/%s/revisions/%s/metadata", baseURL, app, revision)
397+
req, err := http.NewRequest("GET", u, nil)
398+
if err != nil {
399+
return nil, err
400+
}
401+
402+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", m["token"]))
403+
req.Header.Add("content-type", "application/json")
404+
405+
resp, err = client.Do(req)
406+
if err != nil {
407+
return nil, err
408+
}
409+
410+
defer resp.Body.Close()
411+
bodyBytes, err = ioutil.ReadAll(resp.Body)
412+
if err != nil {
413+
return nil, err
414+
}
415+
416+
err = json.Unmarshal(bodyBytes, &m)
417+
if err != nil {
418+
return nil, err
419+
}
420+
return m, nil
421+
}

pkg/httpapi/api_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"github.com/redhat-developer/gitops-backend/pkg/git"
2020
"github.com/redhat-developer/gitops-backend/pkg/parser"
2121
"github.com/redhat-developer/gitops-backend/test"
22+
corev1 "k8s.io/api/core/v1"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2224
"k8s.io/apimachinery/pkg/types"
2325
"k8s.io/client-go/kubernetes/scheme"
2426
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -400,6 +402,88 @@ func TestListApplications_badURL(t *testing.T) {
400402
assertHTTPError(t, resp, http.StatusBadRequest, "please provide a valid GitOps repo URL")
401403
}
402404

405+
func TestGetApplicationDetails(t *testing.T) {
406+
err := argoV1aplha1.AddToScheme(scheme.Scheme)
407+
if err != nil {
408+
t.Fatal(err)
409+
}
410+
411+
builder := fake.NewClientBuilder()
412+
kc := builder.Build()
413+
414+
ts, _ := makeServer(t, func(router *APIRouter) {
415+
router.k8sClient = kc
416+
})
417+
418+
// create test ArgoCD Server to handle http requests
419+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
420+
u := r.URL.String()
421+
if strings.Contains(u, "session") {
422+
m := map[string]string{
423+
"token": "testing",
424+
}
425+
marshalResponse(w, m)
426+
} else if strings.Contains(u, "metadata") {
427+
m := map[string]string{
428+
"author": "test",
429+
"message": "testMessage",
430+
}
431+
marshalResponse(w, m)
432+
}
433+
}))
434+
defer server.Close()
435+
tmp := baseURL
436+
baseURL = server.URL
437+
438+
var createOptions []ctrlclient.CreateOption
439+
app, _ := testArgoApplication("testdata/application.yaml")
440+
// create argocd test-app
441+
err = kc.Create(context.TODO(), app, createOptions...)
442+
if err != nil {
443+
t.Fatal(err)
444+
}
445+
446+
// create argocd instance creds secret
447+
secret := &corev1.Secret{
448+
ObjectMeta: metav1.ObjectMeta{
449+
Name: defaultArgoCDInstance + "-cluster",
450+
Namespace: defaultArgocdNamespace,
451+
},
452+
Data: map[string][]byte{
453+
"admin.password": []byte("abc"),
454+
},
455+
}
456+
err = kc.Create(context.TODO(), secret, createOptions...)
457+
if err != nil {
458+
t.Fatal(err)
459+
}
460+
461+
options := url.Values{
462+
"url": []string{"https://github.com/test-repo/gitops.git"},
463+
}
464+
req := makeClientRequest(t, "Bearer testing",
465+
fmt.Sprintf("%s/environment/%s/application/%s?%s", ts.URL, "dev", "test-app", options.Encode()))
466+
res, err := ts.Client().Do(req)
467+
if err != nil {
468+
t.Fatal(err)
469+
}
470+
471+
assertJSONResponse(t, res, map[string]interface{}{
472+
"cluster": "https://kubernetes.default.svc",
473+
"environment": "dev",
474+
"status": "Synced",
475+
"lastDeployed": time.Date(2021, time.Month(5), 15, 2, 12, 13, 0, time.UTC).Local().String(),
476+
"revision": map[string]interface{}{
477+
"author": "test",
478+
"message": "testMessage",
479+
"revision": "123456789",
480+
},
481+
})
482+
483+
//reset BaseURL
484+
baseURL = tmp
485+
}
486+
403487
func testArgoApplication(appCr string) (*argoV1aplha1.Application, error) {
404488
applicationYaml, _ := ioutil.ReadFile(appCr)
405489
app := &argoV1aplha1.Application{}

pkg/httpapi/conversion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func applicationsToAppsResponse(appSet []*argoV1aplha1.Application, repoURL stri
3939

4040
for _, app := range appSet {
4141
if repoURL != strings.TrimSuffix(app.Spec.Source.RepoURL, ".git") {
42-
log.Printf("repoURL[%v], doesn not match with Source Repo URL[%v]", repoURL, strings.TrimSuffix(app.Spec.Source.RepoURL, ".git"))
42+
log.Printf("repoURL[%v], does not match with Source Repo URL[%v]", repoURL, strings.TrimSuffix(app.Spec.Source.RepoURL, ".git"))
4343
continue
4444
}
4545
if app.ObjectMeta.Labels != nil {

pkg/httpapi/testdata/application.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ spec:
1616
status:
1717
history:
1818
- deployedAt: "2021-05-15T02:12:13Z"
19+
revision: "123456789"
1920
sync:
2021
status: Synced

0 commit comments

Comments
 (0)