diff --git a/hack/docker-compose/loki/promtail-config.yml b/hack/docker-compose/loki/promtail-config.yml index 585d71f81..60e47d3ec 100644 --- a/hack/docker-compose/loki/promtail-config.yml +++ b/hack/docker-compose/loki/promtail-config.yml @@ -46,5 +46,5 @@ scrape_configs: k8s_namespace_name: kube-public log_type: application __path__: /var/log/*log - kubernetes_pod_name: alertmanager-main-0 - kubernetes_container_name: test-container + kubernetes_pod_name: kube-public-0 + kubernetes_container_name: test-container-kube diff --git a/web/cypress/e2e/integration/logs-dev-page.cy.ts b/web/cypress/e2e/integration/logs-dev-page.cy.ts index 6255f5169..851d377e3 100644 --- a/web/cypress/e2e/integration/logs-dev-page.cy.ts +++ b/web/cypress/e2e/integration/logs-dev-page.cy.ts @@ -291,6 +291,10 @@ describe('Logs Dev Page', () => { statusCode: 403, body: 'You are not authorized to list pods in this namespace', }).as('resourceQuery'); + cy.intercept('/api/kubernetes/api/v1/namespaces/my-namespace/pods', { + statusCode: 403, + body: 'You are not authorized to list pods in this namespace', + }).as('k8sPodsQuery'); cy.visit(LOGS_DEV_PAGE_URL); diff --git a/web/locales/en/plugin__logging-view-plugin.json b/web/locales/en/plugin__logging-view-plugin.json index 5e174ac01..e6a4683a3 100644 --- a/web/locales/en/plugin__logging-view-plugin.json +++ b/web/locales/en/plugin__logging-view-plugin.json @@ -1,6 +1,11 @@ { - "Label {{tenantKey}} is required to display the alert metrics": "label {{tenantKey}} is required to display the alert metrics", - "You do not have permission to view logs in the selected namespace.": "You do not have permissions to view logs in the selected namespace.", + "No namespaces found": "No namespaces found", + "No pods found in the selected namespace(s)": "No pods found in the selected namespace(s)", + "No pods found": "No pods found", + "No containers found in the selected namespace(s)": "No containers found in the selected namespace(s)", + "No containers found": "No containers found", + "Label {{tenantKey}} is required to display the alert metrics": "Label {{tenantKey}} is required to display the alert metrics", + "You do not have permission to view logs in the selected namespace.": "You do not have permission to view logs in the selected namespace.", "Try selecting a different namespace that you have access to, or ask your administrator to grant you the required role": "Try selecting a different namespace that you have access to, or ask your administrator to grant you the required role", "Try selecting a specific namespace": "Try selecting a specific namespace", "you may have access to view logs in specific namespaces but not cluster-wide.": "you may have access to view logs in specific namespaces but not cluster-wide.", @@ -23,6 +28,7 @@ "Explain Log Volume": "Explain Log Volume", "Search by {{attributeName}}": "Search by {{attributeName}}", "Attribute": "Attribute", + "No results found": "No results found", "No results found for": "No results found for", "No options available": "No options available", "Filter by {{attributeName}}": "Filter by {{attributeName}}", @@ -133,11 +139,11 @@ "Last 2 days": "Last 2 days", "Last 1 week": "Last 1 week", "Last 2 weeks": "Last 2 weeks", - "Save": "Save", - "Cancel": "Cancel", "From": "From", "To": "To", "Invalid date range": "Invalid date range", + "Save": "Save", + "Cancel": "Cancel", "Select timezone": "Select timezone", "Clear input value": "Clear input value", "All Timezones": "All Timezones", diff --git a/web/src/attribute-filters.tsx b/web/src/attribute-filters.tsx index a22985f38..3f7a7142b 100644 --- a/web/src/attribute-filters.tsx +++ b/web/src/attribute-filters.tsx @@ -107,7 +107,11 @@ const lokiSeriesDataSource = mapper: (data: SeriesResponse) => Array<{ option: string; value: string }>; }) => async (): Promise> => { - const { abort, request } = executeSeries({ match, tenant, config }); + const { abort, request } = executeSeries({ + match: match.filter(notEmptyString), + tenant, + config, + }); if (resourceAbort.lokiSeries) { resourceAbort.lokiSeries(); @@ -142,12 +146,14 @@ const resourceDataSource = const { request, abort } = cancellableFetch(endpoint); - const abortFunction = resourceAbort[resource]; + const abortKey = namespace ? `${resource}-${namespace}` : resource; + + const abortFunction = resourceAbort[abortKey]; if (abortFunction) { abortFunction(); } - resourceAbort[resource] = abort; + resourceAbort[abortKey] = abort; const response = await request(); @@ -177,6 +183,35 @@ const getAttributeLabels = (schema: Schema) => { return { namespaceLabel, podLabel, containerLabel }; }; +const getTenantNamespaceQuery = (tenant: string, namespaceLabel: string): string | undefined => { + const infraPattern = 'openshift-.*|openshift|default|kube-.*'; + switch (tenant) { + case 'infrastructure': + return `${namespaceLabel}=~"${infraPattern}"`; + case 'application': + return `${namespaceLabel}!~"${infraPattern}"`; + default: + return undefined; + } +}; + +const mergeSettledResults = (results: Array>): Option[] => { + const allRejected = results.every((r) => r.status === 'rejected'); + if (allRejected && results.length > 0) { + throw (results[0] as PromiseRejectedResult).reason; + } + + const uniqueValues = new Set(); + results.forEach((result) => { + if (result.status === 'fulfilled') { + result.value.forEach((option) => uniqueValues.add(option.value)); + } + }); + return Array.from(uniqueValues) + .sort() + .map((v) => ({ option: v, value: v })); +}; + const getNamespaceAttributeOptions = ( tenant: string, config: Config, @@ -213,19 +248,7 @@ const getNamespaceAttributeOptions = ( })().then((options) => options.filter((opt) => lokiTenantFilter(opt.value))); return Promise.allSettled([filteredProjectList, filteredLokiNamespaceList]).then( - (results) => { - const namespaceOptions: Set = new Set(); - results.forEach((result) => { - if (result.status === 'fulfilled') { - result.value.forEach((option) => { - namespaceOptions.add(option.value); - }); - } - }); - return Array.from(namespaceOptions) - .sort() - .map((ns) => ({ option: ns, value: ns })); - }, + mergeSettledResults, ); }; }; @@ -264,10 +287,12 @@ export const availableAttributes = ({ tenant, config, schema, + t, }: { tenant: string; config: Config; schema: Schema; + t: (key: string) => string; }): AttributeList => { const { namespaceLabel, podLabel, containerLabel } = getAttributeLabels(schema); @@ -290,19 +315,35 @@ export const availableAttributes = ({ id: 'namespace', options: getNamespaceAttributeOptions(tenant, config, schema), valueType: 'checkbox-select', + emptyStateMessage: t('No namespaces found'), }, { name: 'Pods', label: podLabel, id: 'pod', - options: getPodAttributeOptions(tenant, config, schema), + options: (filters) => { + const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined; + + return getPodAttributeOptions(tenant, config, schema, selectedNamespaces)(); + }, valueType: 'checkbox-select', + emptyStateMessage: (filters) => { + const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined; + if (selectedNamespaces && selectedNamespaces.length > 0) { + return t('No pods found in the selected namespace(s)'); + } + return t('No pods found'); + }, }, { name: 'Containers', label: containerLabel, id: 'container', - options: getContainerAttributeOptions(tenant, config, schema), + options: (filters) => { + const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined; + + return getContainerAttributeOptions(tenant, config, schema, selectedNamespaces)(); + }, expandSelection: (selections) => { const podSelections = new Set(); const containerSelections = new Set(); @@ -354,6 +395,13 @@ export const availableAttributes = ({ return filters.pod.has(pod) && filters.container.has(container); }, valueType: 'checkbox-select', + emptyStateMessage: (filters) => { + const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined; + if (selectedNamespaces && selectedNamespaces.length > 0) { + return t('No containers found in the selected namespace(s)'); + } + return t('No containers found'); + }, }, ]; }; @@ -362,6 +410,7 @@ export const availableDevConsoleAttributes = ( tenant: string, config: Config, schema: Schema, + namespace?: string, ): AttributeList => { const { namespaceLabel, podLabel, containerLabel } = getAttributeLabels(schema); @@ -376,6 +425,8 @@ export const availableDevConsoleAttributes = ( return [contentAttribute]; } + const lokiNamespaceQuery = namespace ? `{ ${namespaceLabel}="${namespace}" }` : undefined; + return [ contentAttribute, { @@ -398,22 +449,50 @@ export const availableDevConsoleAttributes = ( name: 'Pods', label: podLabel, id: 'pod', - options: lokiLabelValuesDataSource({ - config, - tenant, - labelName: podLabel, - }), + options: () => { + const sources: Array> = [ + lokiLabelValuesDataSource({ + config, + tenant, + labelName: podLabel, + query: lokiNamespaceQuery, + })(), + ]; + if (namespace) { + sources.push(resourceDataSource({ resource: 'pods', namespace })()); + } + return Promise.allSettled(sources).then(mergeSettledResults); + }, valueType: 'checkbox-select', }, { name: 'Containers', label: containerLabel, id: 'container', - options: lokiLabelValuesDataSource({ - config, - tenant, - labelName: containerLabel, - }), + options: () => { + const sources: Array> = [ + lokiLabelValuesDataSource({ + config, + tenant, + labelName: containerLabel, + query: lokiNamespaceQuery, + })(), + ]; + if (namespace) { + sources.push( + resourceDataSource({ + resource: 'pods', + namespace, + mapper: (resource) => + resource?.spec?.containers.map((c) => ({ + option: c.name, + value: c.name, + })) ?? [], + })(), + ); + } + return Promise.allSettled(sources).then(mergeSettledResults); + }, valueType: 'checkbox-select', }, ]; @@ -775,38 +854,116 @@ const getPodAttributeOptions = ( tenant: string, config: Config, schema: Schema, + namespaces?: Array, ): (() => Promise) => { const { podLabel } = getAttributeLabels(schema); + const namespacedPodsResources: Array> = []; + + // get pods in selected namespaces for users that have restricted access + for (const ns of namespaces || []) { + namespacedPodsResources.push(resourceDataSource({ resource: 'pods', namespace: ns })()); + } + + const namespaceLabel = getStreamLabelsFromSchema(schema).Namespace; + const namespacesQuery = + namespaces && namespaces.length > 0 + ? `${namespaceLabel}=~"${namespaces.join('|')}"` + : getTenantNamespaceQuery(tenant, namespaceLabel) ?? ''; + + const podResource = + namespaces && namespaces.length > 0 + ? namespacedPodsResources + : [ + resourceDataSource({ + resource: 'pods', + filter: (resource) => { + switch (tenant) { + case 'infrastructure': + return namespaceBelongsToInfrastructureTenant(resource.metadata?.namespace || ''); + case 'application': + return !namespaceBelongsToInfrastructureTenant( + resource.metadata?.namespace || '', + ); + } + + return true; + }, + })(), + ]; + return () => Promise.allSettled>([ lokiLabelValuesDataSource({ config, tenant, labelName: podLabel, + query: namespacesQuery ? `{ ${namespacesQuery} }` : undefined, })(), - resourceDataSource({ resource: 'pods' })(), - ]).then((results) => { - const podOptions: Set = new Set(); - results.forEach((result) => { - if (result.status === 'fulfilled') { - result.value.forEach((option) => { - podOptions.add(option.value); - }); - } - }); - return Array.from(podOptions).map((pod) => ({ option: pod, value: pod })); - }); + ...podResource, + ]).then(mergeSettledResults); }; const getContainerAttributeOptions = ( tenant: string, config: Config, schema: Schema, + namespaces?: Array, ): (() => Promise) => { const { containerLabel, podLabel } = getAttributeLabels(schema); - const seriesQuery = `{ ${containerLabel}!="", ${podLabel}!="" }`; + const namespacedPodsResources: Array> = []; + + // get containers in selected namespaces for users that have restricted access + for (const ns of namespaces || []) { + namespacedPodsResources.push( + resourceDataSource({ + resource: 'pods', + namespace: ns, + mapper: (resource) => + resource?.spec?.containers.map((container) => ({ + option: `${resource?.metadata?.name} / ${container.name}`, + value: `${resource?.metadata?.name} / ${container.name}`, + })) ?? [], + })(), + ); + } + + const namespaceLabel = getStreamLabelsFromSchema(schema).Namespace; + const namespacesQuery = + namespaces && namespaces.length > 0 + ? `${namespaceLabel}=~"${namespaces.join('|')}"` + : getTenantNamespaceQuery(tenant, namespaceLabel) ?? ''; + + const seriesQuery = namespacesQuery + ? `{ ${containerLabel}!="", ${podLabel}!="", ${namespacesQuery} }` + : `{ ${containerLabel}!="", ${podLabel}!="" }`; + + const podResource = + namespaces && namespaces.length > 0 + ? namespacedPodsResources + : [ + resourceDataSource({ + resource: 'pods', + filter: (resource) => { + switch (tenant) { + case 'infrastructure': + return namespaceBelongsToInfrastructureTenant(resource.metadata?.namespace || ''); + case 'application': + return !namespaceBelongsToInfrastructureTenant( + resource.metadata?.namespace || '', + ); + } + + return true; + }, + mapper: (resource) => + resource?.spec?.containers.map((container) => ({ + option: `${resource?.metadata?.name} / ${container.name}`, + value: `${resource?.metadata?.name} / ${container.name}`, + })) ?? [], + })(), + ]; return () => Promise.allSettled>([ @@ -829,26 +986,6 @@ const getContainerAttributeOptions = ( })); }, })(), - resourceDataSource({ - resource: 'pods', - mapper: (resource) => - resource?.spec?.containers.map((container) => ({ - option: `${resource?.metadata?.name} / ${container.name}`, - value: `${resource?.metadata?.name} / ${container.name}`, - })) ?? [], - })(), - ]).then((results) => { - const uniqueContainers = new Set(); - results.forEach((result) => { - if (result.status === 'fulfilled') { - result.value.forEach((option) => { - uniqueContainers.add(option.value); - }); - } - }); - return Array.from(uniqueContainers).map((container) => ({ - option: container, - value: container, - })); - }); + ...podResource, + ]).then(mergeSettledResults); }; diff --git a/web/src/components/filters/attribute-filter.tsx b/web/src/components/filters/attribute-filter.tsx index 4483da1a7..44c0abf1c 100644 --- a/web/src/components/filters/attribute-filter.tsx +++ b/web/src/components/filters/attribute-filter.tsx @@ -170,12 +170,15 @@ export const AttributeFilter: React.FC = ({ ); + const namespaces = filters['namespace'] ? Array.from(filters['namespace']) : []; + const namespacesKey = namespaces.sort().join('-'); + const renderAttributeValueComponent = (attribute: Attribute) => { switch (attribute.valueType) { case 'text': { return ( = ({ case 'select': return ( = ({ case 'checkbox-select': return ( void; + getAttributeOptions: (filters?: Filters) => void; attributeOptions: Array