Skip to content

Commit 3f074ec

Browse files
feat: added api to get all applications using the Application CRs (#20)
* feat: get all applications using the Application CR * added changes to verify gitops url * fixed issues * added unit tests
1 parent cb23572 commit 3f074ec

File tree

9 files changed

+1043
-162
lines changed

9 files changed

+1043
-162
lines changed

go.mod

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,62 @@ module github.com/redhat-developer/gitops-backend
33
go 1.15
44

55
require (
6+
github.com/argoproj/argo-cd v0.8.1-0.20210326223336-719d6a9c252e
7+
github.com/argoproj/pkg v0.9.0 // indirect
68
github.com/emicklei/go-restful v2.12.0+incompatible // indirect
7-
github.com/evanphx/json-patch v4.5.0+incompatible // indirect
89
github.com/go-git/go-git/v5 v5.1.0
9-
github.com/go-openapi/jsonreference v0.19.3 // indirect
1010
github.com/go-openapi/spec v0.19.8 // indirect
1111
github.com/go-openapi/swag v0.19.9 // indirect
12-
github.com/gogo/protobuf v1.3.1 // indirect
13-
github.com/google/go-cmp v0.4.1
14-
github.com/google/gofuzz v1.1.0 // indirect
15-
github.com/googleapis/gnostic v0.4.0 // indirect
12+
github.com/google/go-cmp v0.5.2
1613
github.com/jenkins-x/go-scm v1.5.151
17-
github.com/json-iterator/go v1.1.9 // indirect
1814
github.com/julienschmidt/httprouter v1.2.0
19-
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
2015
github.com/mailru/easyjson v0.7.1 // indirect
2116
github.com/mitchellh/mapstructure v1.3.2 // indirect
2217
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
23-
github.com/onsi/ginkgo v1.12.3 // indirect
2418
github.com/openshift/api v3.9.0+incompatible
2519
github.com/pelletier/go-toml v1.6.0 // indirect
26-
github.com/pkg/errors v0.9.1 // indirect
27-
github.com/prometheus/client_golang v1.1.0
28-
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
29-
github.com/prometheus/common v0.7.0 // indirect
30-
github.com/prometheus/procfs v0.0.5 // indirect
20+
github.com/prometheus/client_golang v1.7.1
21+
github.com/robfig/cron v1.2.0 // indirect
3122
github.com/shurcooL/githubv4 v0.0.0-20191102174205-af46314aec7b // indirect
23+
github.com/sirupsen/logrus v1.7.0
3224
github.com/spf13/cast v1.3.1 // indirect
33-
github.com/spf13/cobra v1.0.0
34-
github.com/spf13/jwalterweatherman v1.1.0 // indirect
25+
github.com/spf13/cobra v1.1.1
3526
github.com/spf13/viper v1.7.0
36-
github.com/stretchr/testify v1.6.1 // indirect
37-
go.uber.org/zap v1.10.0
38-
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect
39-
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
40-
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
41-
google.golang.org/appengine v1.6.5 // indirect
42-
gopkg.in/ini.v1 v1.52.0 // indirect
43-
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
44-
k8s.io/api v0.17.6
45-
k8s.io/apimachinery v0.17.6
46-
k8s.io/client-go v0.17.6
47-
k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19 // indirect
27+
go.uber.org/zap v1.15.0
28+
k8s.io/api v0.21.0
29+
k8s.io/apimachinery v0.21.0
30+
k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible
31+
k8s.io/kubernetes v1.21.0 // indirect
32+
sigs.k8s.io/controller-runtime v0.8.3
4833
sigs.k8s.io/kustomize v2.0.3+incompatible
4934
sigs.k8s.io/yaml v1.2.0
5035
)
36+
37+
replace (
38+
k8s.io/api => k8s.io/api v0.21.0
39+
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.0
40+
k8s.io/apimachinery => k8s.io/apimachinery v0.21.1-rc.0
41+
k8s.io/apiserver => k8s.io/apiserver v0.21.0
42+
k8s.io/cli-runtime => k8s.io/cli-runtime v0.21.0
43+
k8s.io/client-go => k8s.io/client-go v0.21.0
44+
k8s.io/cloud-provider => k8s.io/cloud-provider v0.21.0
45+
k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.21.0
46+
k8s.io/code-generator => k8s.io/code-generator v0.21.1-rc.0
47+
k8s.io/component-base => k8s.io/component-base v0.21.0
48+
k8s.io/component-helpers => k8s.io/component-helpers v0.21.0
49+
k8s.io/controller-manager => k8s.io/controller-manager v0.21.0
50+
k8s.io/cri-api => k8s.io/cri-api v0.21.1-rc.0
51+
k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.21.0
52+
k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.21.0
53+
k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.21.0
54+
k8s.io/kube-proxy => k8s.io/kube-proxy v0.21.0
55+
k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.21.0
56+
k8s.io/kubectl => k8s.io/kubectl v0.21.0
57+
k8s.io/kubelet => k8s.io/kubelet v0.21.0
58+
k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.21.0
59+
k8s.io/metrics => k8s.io/metrics v0.21.0
60+
k8s.io/mount-utils => k8s.io/mount-utils v0.21.1-rc.0
61+
k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.21.0
62+
k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.21.0
63+
k8s.io/sample-controller => k8s.io/sample-controller v0.21.0
64+
)

go.sum

Lines changed: 787 additions & 128 deletions
Large diffs are not rendered by default.

pkg/cmd/root.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"log"
66
"net/http"
77

8+
argoV1aplha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
9+
"github.com/prometheus/client_golang/prometheus/promhttp"
810
"github.com/spf13/cobra"
911
"github.com/spf13/viper"
12+
"k8s.io/client-go/kubernetes/scheme"
1013
"k8s.io/client-go/rest"
14+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
1115

12-
"github.com/prometheus/client_golang/prometheus/promhttp"
1316
"github.com/redhat-developer/gitops-backend/pkg/git"
1417
"github.com/redhat-developer/gitops-backend/pkg/health"
1518
"github.com/redhat-developer/gitops-backend/pkg/httpapi"
@@ -27,6 +30,9 @@ const (
2730

2831
func init() {
2932
cobra.OnInitialize(initConfig)
33+
if err := argoV1aplha1.AddToScheme(scheme.Scheme); err != nil {
34+
log.Fatalf("failed to initialize ArgoCD scheme, err: %v", err)
35+
}
3036
}
3137

3238
func logIfError(e error) {
@@ -127,6 +133,10 @@ func makeAPIRouter(m metrics.Interface) (*httpapi.APIRouter, error) {
127133
secretGetter := secrets.NewFromConfig(
128134
&rest.Config{Host: config.Host},
129135
viper.GetBool(insecureFlag))
130-
router := httpapi.NewRouter(cf, secretGetter)
136+
k8sClient, err := ctrlclient.New(config, ctrlclient.Options{})
137+
if err != nil {
138+
return nil, err
139+
}
140+
router := httpapi.NewRouter(cf, secretGetter, k8sClient)
131141
return router, nil
132142
}

pkg/httpapi/api.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
"net/url"
1010
"strings"
1111

12+
argoV1aplha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
1213
"github.com/julienschmidt/httprouter"
1314
"k8s.io/apimachinery/pkg/types"
15+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
1416
"sigs.k8s.io/yaml"
1517

1618
"github.com/redhat-developer/gitops-backend/pkg/git"
@@ -33,18 +35,21 @@ type APIRouter struct {
3335
secretGetter secrets.SecretGetter
3436
secretRef types.NamespacedName
3537
resourceParser parser.ResourceParser
38+
k8sClient ctrlclient.Client
3639
}
3740

3841
// NewRouter creates and returns a new APIRouter.
39-
func NewRouter(c git.ClientFactory, s secrets.SecretGetter) *APIRouter {
42+
func NewRouter(c git.ClientFactory, s secrets.SecretGetter, kc ctrlclient.Client) *APIRouter {
4043
api := &APIRouter{
4144
Router: httprouter.New(),
4245
gitClientFactory: c,
4346
secretGetter: s,
4447
secretRef: DefaultSecretRef,
4548
resourceParser: parser.ParseFromGit,
49+
k8sClient: kc,
4650
}
4751
api.HandlerFunc(http.MethodGet, "/pipelines", api.GetPipelines)
52+
api.HandlerFunc(http.MethodGet, "/applications", api.ListApplications)
4853
api.HandlerFunc(http.MethodGet, "/environments/:env/application/:app", api.GetApplication)
4954
return api
5055
}
@@ -161,6 +166,41 @@ func (a *APIRouter) GetApplication(w http.ResponseWriter, r *http.Request) {
161166
marshalResponse(w, appEnvironments)
162167
}
163168

169+
func (a *APIRouter) ListApplications(w http.ResponseWriter, r *http.Request) {
170+
repoURL := strings.TrimSpace(r.URL.Query().Get("url"))
171+
if repoURL == "" {
172+
http.Error(w, "please provide a valid GitOps repo URL", http.StatusBadRequest)
173+
return
174+
}
175+
176+
parsedRepoURL, err := url.Parse(repoURL)
177+
if err != nil {
178+
http.Error(w, fmt.Sprintf("failed to parse URL, error: %v", err), http.StatusBadRequest)
179+
return
180+
}
181+
182+
parsedRepoURL.RawQuery = ""
183+
184+
appList := &argoV1aplha1.ApplicationList{}
185+
var listOptions []ctrlclient.ListOption
186+
187+
listOptions = append(listOptions, ctrlclient.InNamespace(""))
188+
189+
err = a.k8sClient.List(r.Context(), appList, listOptions...)
190+
if err != nil {
191+
log.Printf("ERROR: failed to get application list: %v", err)
192+
http.Error(w, fmt.Sprintf("failed to get list of application, err: %v", err), http.StatusBadRequest)
193+
return
194+
}
195+
196+
apps := make([]*argoV1aplha1.Application, 0)
197+
for _, app := range appList.Items {
198+
apps = append(apps, app.DeepCopy())
199+
}
200+
201+
marshalResponse(w, applicationsToAppsResponse(apps, parsedRepoURL.String()))
202+
}
203+
164204
func (a *APIRouter) getAuthToken(ctx context.Context, req *http.Request) (string, error) {
165205
token := AuthToken(ctx)
166206
secret, ok := secretRefFromQuery(req.URL.Query())

pkg/httpapi/api_test.go

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ import (
1212
"strings"
1313
"testing"
1414

15+
argoV1aplha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
1516
gogit "github.com/go-git/go-git/v5"
1617
"github.com/google/go-cmp/cmp"
1718
"github.com/redhat-developer/gitops-backend/pkg/git"
1819
"github.com/redhat-developer/gitops-backend/pkg/parser"
1920
"github.com/redhat-developer/gitops-backend/test"
2021
"k8s.io/apimachinery/pkg/types"
22+
"k8s.io/client-go/kubernetes/scheme"
23+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
24+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2125
"sigs.k8s.io/yaml"
2226
)
2327

@@ -290,6 +294,72 @@ func TestParseURL(t *testing.T) {
290294
}
291295
}
292296

297+
func TestListApplications(t *testing.T) {
298+
err := argoV1aplha1.AddToScheme(scheme.Scheme)
299+
if err != nil {
300+
t.Fatal(err)
301+
}
302+
303+
builder := fake.NewClientBuilder()
304+
kc := builder.Build()
305+
306+
ts, _ := makeServer(t, func(router *APIRouter) {
307+
router.k8sClient = kc
308+
})
309+
310+
var createOptions []ctrlclient.CreateOption
311+
app, _ := testArgoApplication()
312+
err = kc.Create(context.TODO(), app, createOptions...)
313+
if err != nil {
314+
t.Fatal(err)
315+
}
316+
317+
url := "https://github.com/test-repo/gitops.git?ref=HEAD"
318+
req := makeClientRequest(t, "Bearer testing", fmt.Sprintf("%s/applications?url=%s", ts.URL, url))
319+
res, err := ts.Client().Do(req)
320+
if err != nil {
321+
t.Fatal(err)
322+
}
323+
324+
assertJSONResponse(t, res, map[string]interface{}{
325+
"applications": []interface{}{
326+
map[string]interface{}{
327+
"name": "test-app",
328+
"repo_url": "https://github.com/test-repo/gitops.git",
329+
"environments": []interface{}{"dev"},
330+
},
331+
},
332+
})
333+
}
334+
335+
func TestListApplications_badURL(t *testing.T) {
336+
builder := fake.NewClientBuilder()
337+
kc := builder.Build()
338+
339+
ts, _ := makeServer(t, func(router *APIRouter) {
340+
router.k8sClient = kc
341+
})
342+
343+
req := makeClientRequest(t, "Bearer testing", fmt.Sprintf("%s/applications", ts.URL))
344+
resp, err := ts.Client().Do(req)
345+
if err != nil {
346+
t.Fatal(err)
347+
}
348+
349+
assertHTTPError(t, resp, http.StatusBadRequest, "please provide a valid GitOps repo URL")
350+
}
351+
352+
func testArgoApplication() (*argoV1aplha1.Application, error) {
353+
applicationYaml, _ := ioutil.ReadFile("testdata/application.yaml")
354+
app := &argoV1aplha1.Application{}
355+
err := yaml.Unmarshal(applicationYaml, app)
356+
if err != nil {
357+
return nil, err
358+
}
359+
360+
return app, err
361+
}
362+
293363
func newClient() *stubClient {
294364
return &stubClient{files: make(map[string]string)}
295365
}
@@ -347,7 +417,8 @@ func makeServer(t *testing.T, opts ...routerOptionFunc) (*httptest.Server, *stub
347417
testKey: "token",
348418
}
349419
sf := &stubClientFactory{client: newClient()}
350-
router := NewRouter(sf, sg)
420+
var kc ctrlclient.Client
421+
router := NewRouter(sf, sg, kc)
351422
for _, o := range opts {
352423
o(router)
353424
}

pkg/httpapi/conversion.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package httpapi
22

3-
import "sort"
3+
import (
4+
log "github.com/sirupsen/logrus"
5+
"sort"
6+
"strings"
7+
8+
argoV1aplha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
9+
)
410

511
// TODO: this should really import the config from the upstream and use it to
612
// unmarshal.
@@ -24,3 +30,42 @@ func pipelinesToAppsResponse(cfg *config) *appsResponse {
2430
}
2531
return &appsResponse{Apps: apps}
2632
}
33+
34+
func applicationsToAppsResponse(appSet []*argoV1aplha1.Application, repoURL string) *appsResponse {
35+
appsMap := make(map[string]appResponse)
36+
var appName string
37+
repoURL = strings.TrimSuffix(repoURL, ".git")
38+
39+
for _, app := range appSet {
40+
if repoURL != strings.TrimSuffix(app.Spec.Source.RepoURL, ".git") {
41+
log.Printf("repoURL[%v], doesn not match with Source Repo URL[%v]", repoURL, strings.TrimSuffix(app.Spec.Source.RepoURL, ".git"))
42+
continue
43+
}
44+
if app.ObjectMeta.Labels != nil {
45+
appName = app.ObjectMeta.Labels["app.kubernetes.io/name"]
46+
}
47+
48+
if appName == "" {
49+
log.Println("AppName is empty")
50+
continue
51+
}
52+
53+
if appResp, ok := appsMap[appName]; !ok {
54+
appsMap[appName] = appResponse{
55+
Name: appName,
56+
RepoURL: app.Spec.Source.RepoURL,
57+
Environments: []string{app.Spec.Destination.Namespace},
58+
}
59+
} else {
60+
appResp.Environments = append(appResp.Environments, app.Spec.Destination.Namespace)
61+
appsMap[appName] = appResp
62+
}
63+
}
64+
65+
var apps []appResponse
66+
for _, app := range appsMap {
67+
apps = append(apps, app)
68+
}
69+
70+
return &appsResponse{Apps: apps}
71+
}

pkg/httpapi/conversion_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package httpapi
22

33
import (
4+
"fmt"
5+
argoV1aplha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
46
"testing"
57

68
"github.com/google/go-cmp/cmp"
@@ -23,3 +25,27 @@ func TestPipelinesToAppsResponse(t *testing.T) {
2325
t.Fatalf("failed to parse:\n%s", diff)
2426
}
2527
}
28+
29+
func TestApplicationsToAppsResponse(t *testing.T) {
30+
var apps []*argoV1aplha1.Application
31+
app, _ := testArgoApplication()
32+
apps = append(apps, app)
33+
34+
want := &appsResponse{
35+
Apps: []appResponse{
36+
{Name: "test-app", RepoURL: "https://github.com/test-repo/gitops.git", Environments: []string{"dev"}},
37+
},
38+
}
39+
40+
resp := applicationsToAppsResponse(apps, "https://github.com/test-repo/gitops")
41+
42+
if diff := cmp.Diff(want, resp); diff != "" {
43+
t.Fatal(fmt.Errorf("WANT[%v] != RECEIVED[%v], diff=%s", want, resp, diff))
44+
}
45+
46+
resp = applicationsToAppsResponse(apps, "https://github.com/test-repo/gitops.git")
47+
48+
if diff := cmp.Diff(want, resp); diff != "" {
49+
t.Fatal(fmt.Errorf("WANT[%v] != RECEIVED[%v], diff=%s", want, resp, diff))
50+
}
51+
}

0 commit comments

Comments
 (0)