diff --git a/docs/operator-manual/argocd-cm.yaml b/docs/operator-manual/argocd-cm.yaml index 49458d40be9292..88daa86c643345 100644 --- a/docs/operator-manual/argocd-cm.yaml +++ b/docs/operator-manual/argocd-cm.yaml @@ -320,6 +320,10 @@ data: # cluster.inClusterEnabled indicates whether to allow in-cluster server address. This is enabled by default. cluster.inClusterEnabled: "true" + # The maximum number of pod logs to render in UI. If the application has more than this number of pods, the logs will not be rendered. + # This is to prevent the UI from becoming unresponsive when rendering a large number of logs. Default is 10. + server.maxPodLogsToRender: 10 + # Application pod logs RBAC enforcement enables control over who can and who can't view application pod logs. # When you enable the switch, pod logs will be visible only to admin role by default. Other roles/users will not be able to view them via cli and UI. # When you enable the switch, viewing pod logs for other roles/users will require explicit RBAC allow policies (allow get on logs subresource). diff --git a/server/application/application.go b/server/application/application.go index ec0db45a11f22c..a794cfd44e4eae 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -65,7 +65,6 @@ import ( type AppResourceTreeFn func(ctx context.Context, app *appv1.Application) (*appv1.ApplicationTree, error) const ( - maxPodLogsToRender = 10 backgroundPropagationPolicy string = "background" foregroundPropagationPolicy string = "foreground" ) @@ -1579,8 +1578,13 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application. return nil } - if len(pods) > maxPodLogsToRender { - return errors.New("Max pods to view logs are reached. Please provide more granular query.") + maxPodLogsToRender, err := s.settingsMgr.GetMaxPodLogsToRender() + if err != nil { + return fmt.Errorf("error getting MaxPodLogsToRender config: %w", err) + } + + if int64(len(pods)) > maxPodLogsToRender { + return status.Error(codes.InvalidArgument, "max pods to view logs are reached. Please provide more granular query") } var streams []chan logEntry diff --git a/server/application/application_test.go b/server/application/application_test.go index 51c912ff05109c..58e51d4075b46a 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -132,10 +132,10 @@ func newTestAppServer(t *testing.T, objects ...runtime.Object) *Server { _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) enf.SetDefaultRole("role:admin") } - return newTestAppServerWithEnforcerConfigure(f, t, objects...) + return newTestAppServerWithEnforcerConfigure(f, t, map[string]string{}, objects...) } -func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), t *testing.T, objects ...runtime.Object) *Server { +func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), t *testing.T, additionalConfig map[string]string, objects ...runtime.Object) *Server { kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNamespace, @@ -144,6 +144,7 @@ func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), t *testing.T, "app.kubernetes.io/part-of": "argocd", }, }, + Data: additionalConfig, }, &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "argocd-secret", @@ -752,7 +753,7 @@ func TestNoAppEnumeration(t *testing.T) { } }) testDeployment := kube.MustToUnstructured(&deployment) - appServer := newTestAppServerWithEnforcerConfigure(f, t, testApp, testHelmApp, testDeployment) + appServer := newTestAppServerWithEnforcerConfigure(f, t, map[string]string{}, testApp, testHelmApp, testDeployment) noRoleCtx := context.Background() // nolint:staticcheck @@ -1272,7 +1273,7 @@ g, group-49, role:test3 ` _ = enf.SetUserPolicy(policy) } - appServer := newTestAppServerWithEnforcerConfigure(f, t, objects...) + appServer := newTestAppServerWithEnforcerConfigure(f, t, map[string]string{}, objects...) res, err := appServer.List(ctx, &application.ApplicationQuery{}) @@ -1987,6 +1988,108 @@ func TestLogsGetSelectedPod(t *testing.T) { }) } +func TestMaxPodLogsRender(t *testing.T) { + + defaultMaxPodLogsToRender, _ := newTestAppServer(t).settingsMgr.GetMaxPodLogsToRender() + + // Case: number of pods to view logs is less than defaultMaxPodLogsToRender + podNumber := int(defaultMaxPodLogsToRender - 1) + appServer, adminCtx := createAppServerWithMaxLodLogs(t, podNumber) + + t.Run("PodLogs", func(t *testing.T) { + err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: adminCtx}) + statusCode, _ := status.FromError(err) + assert.Equal(t, codes.OK, statusCode.Code()) + }) + + // Case: number of pods higher than defaultMaxPodLogsToRender + podNumber = int(defaultMaxPodLogsToRender + 1) + appServer, adminCtx = createAppServerWithMaxLodLogs(t, podNumber) + + t.Run("PodLogs", func(t *testing.T) { + err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: adminCtx}) + assert.NotNil(t, err) + statusCode, _ := status.FromError(err) + assert.Equal(t, codes.InvalidArgument, statusCode.Code()) + assert.Equal(t, "rpc error: code = InvalidArgument desc = max pods to view logs are reached. Please provide more granular query", err.Error()) + }) + + // Case: number of pods to view logs is less than customMaxPodLogsToRender + customMaxPodLogsToRender := int64(15) + podNumber = int(customMaxPodLogsToRender - 1) + appServer, adminCtx = createAppServerWithMaxLodLogs(t, podNumber, customMaxPodLogsToRender) + + t.Run("PodLogs", func(t *testing.T) { + err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: adminCtx}) + statusCode, _ := status.FromError(err) + assert.Equal(t, codes.OK, statusCode.Code()) + }) + + // Case: number of pods higher than customMaxPodLogsToRender + customMaxPodLogsToRender = int64(15) + podNumber = int(customMaxPodLogsToRender + 1) + appServer, adminCtx = createAppServerWithMaxLodLogs(t, podNumber, customMaxPodLogsToRender) + + t.Run("PodLogs", func(t *testing.T) { + err := appServer.PodLogs(&application.ApplicationPodLogsQuery{Name: pointer.String("test")}, &TestPodLogsServer{ctx: adminCtx}) + assert.NotNil(t, err) + statusCode, _ := status.FromError(err) + assert.Equal(t, codes.InvalidArgument, statusCode.Code()) + assert.Equal(t, "rpc error: code = InvalidArgument desc = max pods to view logs are reached. Please provide more granular query", err.Error()) + }) +} + +// createAppServerWithMaxLodLogs creates a new app server with given number of pods and resources +func createAppServerWithMaxLodLogs(t *testing.T, podNumber int, maxPodLogsToRender ...int64) (*Server, context.Context) { + runtimeObjects := make([]runtime.Object, podNumber+1) + resources := make([]appsv1.ResourceStatus, podNumber) + + for i := 0; i < podNumber; i++ { + pod := v1.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("pod-%d", i), + Namespace: "test", + }, + } + resources[i] = appsv1.ResourceStatus{ + Group: pod.GroupVersionKind().Group, + Kind: pod.GroupVersionKind().Kind, + Version: pod.GroupVersionKind().Version, + Name: pod.Name, + Namespace: pod.Namespace, + Status: "Synced", + } + runtimeObjects[i] = kube.MustToUnstructured(&pod) + } + + testApp := newTestApp(func(app *appsv1.Application) { + app.Name = "test" + app.Status.Resources = resources + }) + runtimeObjects[podNumber] = testApp + + noRoleCtx := context.Background() + // nolint:staticcheck + adminCtx := context.WithValue(noRoleCtx, "claims", &jwt.MapClaims{"groups": []string{"admin"}}) + + if len(maxPodLogsToRender) > 0 { + f := func(enf *rbac.Enforcer) { + _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) + enf.SetDefaultRole("role:admin") + } + formatInt := strconv.FormatInt(maxPodLogsToRender[0], 10) + appServer := newTestAppServerWithEnforcerConfigure(f, t, map[string]string{"server.maxPodLogsToRender": formatInt}, runtimeObjects...) + return appServer, adminCtx + } else { + appServer := newTestAppServer(t, runtimeObjects...) + return appServer, adminCtx + } +} + // refreshAnnotationRemover runs an infinite loop until it detects and removes refresh annotation or given context is done func refreshAnnotationRemover(t *testing.T, ctx context.Context, patched *int32, appServer *Server, appName string, ch chan string) { for ctx.Err() == nil { diff --git a/util/settings/settings.go b/util/settings/settings.go index 82b4d72dc23c81..45da68945a59f8 100644 --- a/util/settings/settings.go +++ b/util/settings/settings.go @@ -103,6 +103,8 @@ type ArgoCDSettings struct { InClusterEnabled bool `json:"inClusterEnabled"` // ServerRBACLogEnforceEnable temporary var indicates whether rbac will be enforced on logs ServerRBACLogEnforceEnable bool `json:"serverRBACLogEnforceEnable"` + // MaxPodLogsToRender the maximum number of pod logs to render + MaxPodLogsToRender int64 `json:"maxPodLogsToRender"` // ExecEnabled indicates whether the UI exec feature is enabled ExecEnabled bool `json:"execEnabled"` // ExecShells restricts which shells are allowed for `exec` and in which order they are tried @@ -485,6 +487,8 @@ const ( inClusterEnabledKey = "cluster.inClusterEnabled" // settingsServerRBACLogEnforceEnable is the key to configure whether logs RBAC enforcement is enabled settingsServerRBACLogEnforceEnableKey = "server.rbac.log.enforce.enable" + // MaxPodLogsToRender the maximum number of pod logs to render + settingsMaxPodLogsToRender = "server.maxPodLogsToRender" // helmValuesFileSchemesKey is the key to configure the list of supported helm values file schemas helmValuesFileSchemesKey = "helm.valuesFileSchemes" // execEnabledKey is the key to configure whether the UI exec feature is enabled @@ -788,6 +792,19 @@ func (mgr *SettingsManager) GetServerRBACLogEnforceEnable() (bool, error) { return strconv.ParseBool(argoCDCM.Data[settingsServerRBACLogEnforceEnableKey]) } +func (mgr *SettingsManager) GetMaxPodLogsToRender() (int64, error) { + argoCDCM, err := mgr.getConfigMap() + if err != nil { + return 10, err + } + + if argoCDCM.Data[settingsMaxPodLogsToRender] == "" { + return 10, nil + } + + return strconv.ParseInt(argoCDCM.Data[settingsMaxPodLogsToRender], 10, 64) +} + func (mgr *SettingsManager) GetDeepLinks(deeplinkType string) ([]DeepLink, error) { argoCDCM, err := mgr.getConfigMap() if err != nil { @@ -1457,6 +1474,13 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi if settings.PasswordPattern == "" { settings.PasswordPattern = common.PasswordPatten } + if maxPodLogsToRenderStr, ok := argoCDCM.Data[settingsMaxPodLogsToRender]; ok { + if val, err := strconv.ParseInt(maxPodLogsToRenderStr, 10, 64); err != nil { + log.Warnf("Failed to parse '%s' key: %v", settingsMaxPodLogsToRender, err) + } else { + settings.MaxPodLogsToRender = val + } + } settings.InClusterEnabled = argoCDCM.Data[inClusterEnabledKey] != "false" settings.ExecEnabled = argoCDCM.Data[execEnabledKey] == "true" execShells := argoCDCM.Data[execShellsKey]