Skip to content

Commit

Permalink
fix: Allow retrieving badges in other namespaces
Browse files Browse the repository at this point in the history
Signed-off-by: jannfis <jann@mistrust.net>
  • Loading branch information
jannfis committed Sep 12, 2023
1 parent b7ef32e commit 14ebb39
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 50 deletions.
62 changes: 47 additions & 15 deletions server/badge/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,28 @@ import (

healthutil "github.com/argoproj/gitops-engine/pkg/health"
"k8s.io/apimachinery/pkg/api/errors"
validation "k8s.io/apimachinery/pkg/api/validation"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"

appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/v2/util/argo"
"github.com/argoproj/argo-cd/v2/util/assets"
"github.com/argoproj/argo-cd/v2/util/security"
"github.com/argoproj/argo-cd/v2/util/settings"
)

// NewHandler creates handler serving to do api/badge endpoint
func NewHandler(appClientset versioned.Interface, settingsMrg *settings.SettingsManager, namespace string) http.Handler {
return &Handler{appClientset: appClientset, namespace: namespace, settingsMgr: settingsMrg}
func NewHandler(appClientset versioned.Interface, settingsMrg *settings.SettingsManager, namespace string, enabledNamespaces []string) http.Handler {
return &Handler{appClientset: appClientset, namespace: namespace, settingsMgr: settingsMrg, enabledNamespaces: enabledNamespaces}
}

// Handler used to get application in order to access health/sync
type Handler struct {
namespace string
appClientset versioned.Interface
settingsMgr *settings.SettingsManager
namespace string
appClientset versioned.Interface
settingsMgr *settings.SettingsManager
enabledNamespaces []string
}

var (
Expand Down Expand Up @@ -75,21 +78,50 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
enabled = sets.StatusBadgeEnabled
}

reqNs := ""
if ns, ok := r.URL.Query()["namespace"]; ok && enabled {
if errs := validation.NameIsDNSSubdomain(strings.ToLower(ns[0]), false); len(errs) == 0 {
if security.IsNamespaceEnabled(ns[0], h.namespace, h.enabledNamespaces) {
reqNs = ns[0]
} else {
notFound = true
}
} else {
w.WriteHeader(http.StatusBadRequest)
return
}
} else {
reqNs = h.namespace
}

//Sample url: http://localhost:8080/api/badge?name=123
if name, ok := r.URL.Query()["name"]; ok && enabled {
if app, err := h.appClientset.ArgoprojV1alpha1().Applications(h.namespace).Get(context.Background(), name[0], v1.GetOptions{}); err == nil {
health = app.Status.Health.Status
status = app.Status.Sync.Status
if app.Status.OperationState != nil && app.Status.OperationState.SyncResult != nil {
revision = app.Status.OperationState.SyncResult.Revision
if name, ok := r.URL.Query()["name"]; ok && enabled && !notFound {
if errs := validation.NameIsDNSLabel(strings.ToLower(name[0]), false); len(errs) == 0 {
if app, err := h.appClientset.ArgoprojV1alpha1().Applications(reqNs).Get(context.Background(), name[0], v1.GetOptions{}); err == nil {
health = app.Status.Health.Status
status = app.Status.Sync.Status
if app.Status.OperationState != nil && app.Status.OperationState.SyncResult != nil {
revision = app.Status.OperationState.SyncResult.Revision
}
} else {
if errors.IsNotFound(err) {
notFound = true
}
}
} else if errors.IsNotFound(err) {
notFound = true
} else {
w.WriteHeader(http.StatusBadRequest)
return
}
}
//Sample url: http://localhost:8080/api/badge?project=default
if projects, ok := r.URL.Query()["project"]; ok && enabled {
if apps, err := h.appClientset.ArgoprojV1alpha1().Applications(h.namespace).List(context.Background(), v1.ListOptions{}); err == nil {
if projects, ok := r.URL.Query()["project"]; ok && enabled && !notFound {
for _, p := range projects {
if errs := validation.NameIsDNSLabel(strings.ToLower(p), false); len(p) > 0 && len(errs) != 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
}
if apps, err := h.appClientset.ArgoprojV1alpha1().Applications(reqNs).List(context.Background(), v1.ListOptions{}); err == nil {
applicationSet := argo.FilterByProjects(apps.Items, projects)
for _, a := range applicationSet {
if a.Status.Sync.Status != appv1.SyncStatusCodeSynced {
Expand Down
145 changes: 111 additions & 34 deletions server/badge/badge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/argoproj/gitops-engine/pkg/health"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
Expand All @@ -41,7 +42,19 @@ var (
},
}
testApp = v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "testApp", Namespace: "default"},
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "default"},
Status: v1alpha1.ApplicationStatus{
Sync: v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeSynced},
Health: v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
OperationState: &v1alpha1.OperationState{
SyncResult: &v1alpha1.SyncOperationResult{
Revision: "aa29b85",
},
},
},
}
testApp2 = v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "argocd-test"},
Status: v1alpha1.ApplicationStatus{
Sync: v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeSynced},
Health: v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
Expand All @@ -53,15 +66,15 @@ var (
},
}
testProject = v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{Name: "testProject", Namespace: "default"},
ObjectMeta: v1.ObjectMeta{Name: "test-project", Namespace: "default"},
Spec: v1alpha1.AppProjectSpec{},
}
)

func TestHandlerFeatureIsEnabled(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default")
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=testApp", nil)
handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default", []string{})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
Expand All @@ -81,6 +94,7 @@ func TestHandlerFeatureIsEnabled(t *testing.T) {
func TestHandlerFeatureProjectIsEnabled(t *testing.T) {
projectTests := []struct {
testApp []*v1alpha1.Application
response int
apiEndPoint string
namespace string
health string
Expand All @@ -89,42 +103,105 @@ func TestHandlerFeatureProjectIsEnabled(t *testing.T) {
statusColor color.RGBA
}{
{createApplications([]string{"Healthy:Synced", "Healthy:Synced"}, []string{"default", "default"}, "test"),
"/api/badge?project=default", "test", "Healthy", "Synced", Green, Green},
{createApplications([]string{"Healthy:Synced", "Healthy:OutOfSync"}, []string{"testProject", "testProject"}, "default"),
"/api/badge?project=testProject", "default", "Healthy", "OutOfSync", Green, Orange},
http.StatusOK, "/api/badge?project=default", "test", "Healthy", "Synced", Green, Green},
{createApplications([]string{"Healthy:Synced", "Healthy:OutOfSync"}, []string{"test-project", "test-project"}, "default"),
http.StatusOK, "/api/badge?project=test-project", "default", "Healthy", "OutOfSync", Green, Orange},
{createApplications([]string{"Healthy:Synced", "Degraded:Synced"}, []string{"default", "default"}, "test"),
"/api/badge?project=default", "test", "Degraded", "Synced", Red, Green},
{createApplications([]string{"Healthy:Synced", "Degraded:OutOfSync"}, []string{"testProject", "testProject"}, "default"),
"/api/badge?project=testProject", "default", "Degraded", "OutOfSync", Red, Orange},
{createApplications([]string{"Healthy:Synced", "Healthy:Synced"}, []string{"testProject", "default"}, "test"),
"/api/badge?project=default&project=testProject", "test", "Healthy", "Synced", Green, Green},
{createApplications([]string{"Healthy:OutOfSync", "Healthy:Synced"}, []string{"testProject", "default"}, "default"),
"/api/badge?project=default&project=testProject", "default", "Healthy", "OutOfSync", Green, Orange},
{createApplications([]string{"Degraded:Synced", "Healthy:Synced"}, []string{"testProject", "default"}, "test"),
"/api/badge?project=default&project=testProject", "test", "Degraded", "Synced", Red, Green},
{createApplications([]string{"Degraded:OutOfSync", "Healthy:OutOfSync"}, []string{"testProject", "default"}, "default"),
"/api/badge?project=default&project=testProject", "default", "Degraded", "OutOfSync", Red, Orange},
{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"testProject", "default"}, "default"),
"/api/badge?project=", "default", "Unknown", "Unknown", Purple, Purple},
http.StatusOK, "/api/badge?project=default", "test", "Degraded", "Synced", Red, Green},
{createApplications([]string{"Healthy:Synced", "Degraded:OutOfSync"}, []string{"test-project", "test-project"}, "default"),
http.StatusOK, "/api/badge?project=test-project", "default", "Degraded", "OutOfSync", Red, Orange},
{createApplications([]string{"Healthy:Synced", "Healthy:Synced"}, []string{"test-project", "default"}, "test"),
http.StatusOK, "/api/badge?project=default&project=test-project", "test", "Healthy", "Synced", Green, Green},
{createApplications([]string{"Healthy:OutOfSync", "Healthy:Synced"}, []string{"test-project", "default"}, "default"),
http.StatusOK, "/api/badge?project=default&project=test-project", "default", "Healthy", "OutOfSync", Green, Orange},
{createApplications([]string{"Degraded:Synced", "Healthy:Synced"}, []string{"test-project", "default"}, "test"),
http.StatusOK, "/api/badge?project=default&project=test-project", "test", "Degraded", "Synced", Red, Green},
{createApplications([]string{"Degraded:OutOfSync", "Healthy:OutOfSync"}, []string{"test-project", "default"}, "default"),
http.StatusOK, "/api/badge?project=default&project=test-project", "default", "Degraded", "OutOfSync", Red, Orange},
{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
http.StatusOK, "/api/badge?project=", "default", "Unknown", "Unknown", Purple, Purple},
{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
http.StatusBadRequest, "/api/badge?project=test$project", "default", "Unknown", "Unknown", Purple, Purple},
{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
http.StatusOK, "/api/badge?project=unknown", "default", "Unknown", "Unknown", Purple, Purple},
{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
http.StatusBadRequest, "/api/badge?name=foo_bar", "default", "Unknown", "Unknown", Purple, Purple},
{createApplications([]string{"Unknown:Unknown", "Unknown:Unknown"}, []string{"test-project", "default"}, "default"),
http.StatusOK, "/api/badge?name=foobar", "default", "Not Found", "", Purple, Purple},
}
for _, tt := range projectTests {
argoCDCm.ObjectMeta.Namespace = tt.namespace
argoCDSecret.ObjectMeta.Namespace = tt.namespace
settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), tt.namespace)
handler := NewHandler(appclientset.NewSimpleClientset(&testProject, tt.testApp[0], tt.testApp[1]), settingsMgr, tt.namespace)
handler := NewHandler(appclientset.NewSimpleClientset(&testProject, tt.testApp[0], tt.testApp[1]), settingsMgr, tt.namespace, []string{})
rr := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, tt.apiEndPoint, nil)
assert.NoError(t, err)
handler.ServeHTTP(rr, req)
require.Equal(t, tt.response, rr.Result().StatusCode)
if rr.Result().StatusCode != 400 {
assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
response := rr.Body.String()
require.Greater(t, len(response), 2)
assert.Equal(t, toRGBString(tt.healthColor), leftRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, toRGBString(tt.statusColor), rightRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, tt.health, leftTextPattern.FindStringSubmatch(response)[1])
assert.Equal(t, tt.status, rightTextPattern.FindStringSubmatch(response)[1])
}
}
}

func TestHandlerNamespacesIsEnabled(t *testing.T) {
t.Run("Application in allowed namespace", func(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(&testApp2), settingsMgr, "default", []string{"argocd-test"})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&namespace=argocd-test", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)

assert.Equal(t, "private, no-store", rr.Header().Get("Cache-Control"))
assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))

response := rr.Body.String()
assert.Equal(t, toRGBString(tt.healthColor), leftRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, toRGBString(tt.statusColor), rightRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, tt.health, leftTextPattern.FindStringSubmatch(response)[1])
assert.Equal(t, tt.status, rightTextPattern.FindStringSubmatch(response)[1])
assert.Equal(t, toRGBString(Green), leftRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, toRGBString(Green), rightRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, "Healthy", leftTextPattern.FindStringSubmatch(response)[1])
assert.Equal(t, "Synced", rightTextPattern.FindStringSubmatch(response)[1])
assert.NotContains(t, response, "(aa29b85)")
})

}
t.Run("Application in disallowed namespace", func(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(&testApp2), settingsMgr, "default", []string{"argocd-test"})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&namespace=kube-system", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)

assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
response := rr.Body.String()
assert.Equal(t, toRGBString(Purple), leftRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, toRGBString(Purple), rightRectColorPattern.FindStringSubmatch(response)[1])
assert.Equal(t, "Not Found", leftTextPattern.FindStringSubmatch(response)[1])
assert.Equal(t, "", rightTextPattern.FindStringSubmatch(response)[1])

})

t.Run("Request with illegal namespace", func(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(&testApp2), settingsMgr, "default", []string{"argocd-test"})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&namespace=kube()system", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)

assert.Equal(t, http.StatusBadRequest, rr.Result().StatusCode)
})
}

func createApplicationFeatureProjectIsEnabled(healthStatus health.HealthStatusCode, syncStatus v1alpha1.SyncStatusCode, appName, projectName, namespace string) *v1alpha1.Application {
Expand Down Expand Up @@ -176,8 +253,8 @@ func createApplications(appCombo, projectName []string, namespace string) []*v1a
}
func TestHandlerFeatureIsEnabledRevisionIsEnabled(t *testing.T) {
settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default")
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=testApp&revision=true", nil)
handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default", []string{})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&revision=true", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
Expand All @@ -199,8 +276,8 @@ func TestHandlerRevisionIsEnabledNoOperationState(t *testing.T) {
app.Status.OperationState = nil

settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(app), settingsMgr, "default")
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=testApp&revision=true", nil)
handler := NewHandler(appclientset.NewSimpleClientset(app), settingsMgr, "default", []string{})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&revision=true", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
Expand All @@ -222,8 +299,8 @@ func TestHandlerRevisionIsEnabledShortCommitSHA(t *testing.T) {
app.Status.OperationState.SyncResult.Revision = "abc"

settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(&argoCDCm, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(app), settingsMgr, "default")
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=testApp&revision=true", nil)
handler := NewHandler(appclientset.NewSimpleClientset(app), settingsMgr, "default", []string{})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app&revision=true", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
Expand All @@ -239,8 +316,8 @@ func TestHandlerFeatureIsDisabled(t *testing.T) {
delete(argoCDCmDisabled.Data, "statusbadge.enabled")

settingsMgr := settings.NewSettingsManager(context.Background(), fake.NewSimpleClientset(argoCDCmDisabled, &argoCDSecret), "default")
handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default")
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=testApp", nil)
handler := NewHandler(appclientset.NewSimpleClientset(&testApp), settingsMgr, "default", []string{})
req, err := http.NewRequest(http.MethodGet, "/api/badge?name=test-app", nil)
assert.NoError(t, err)

rr := httptest.NewRecorder()
Expand Down
2 changes: 1 addition & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
Handler: &handlerSwitcher{
handler: mux,
urlToHandler: map[string]http.Handler{
"/api/badge": badge.NewHandler(a.AppClientset, a.settingsMgr, a.Namespace),
"/api/badge": badge.NewHandler(a.AppClientset, a.settingsMgr, a.Namespace, a.ApplicationNamespaces),
common.LogoutEndpoint: logout.NewHandler(a.AppClientset, a.settingsMgr, a.sessionMgr, a.ArgoCDServerOpts.RootPath, a.ArgoCDServerOpts.BaseHRef, a.Namespace),
},
contentTypeToHandler: map[string]http.Handler{
Expand Down

0 comments on commit 14ebb39

Please sign in to comment.