Alerting: Provisioning Status Differentiation for ALL resources (#115773)
* Show different badge for converted prometheus provisioned resource * Update contact points and templates to populate provenance * Update notification policies * Handle non k8s contact point in ContactPointHeader * Fix provenance check in enhanceContactPointsWithMetadata * Update translations * Fix unused import * Refactor provenance enum * Derive provisioned status from provenance in Route type * Remove unused imports * Treat PROVENANCE_NONE as no provenance in isRouteProvisione * Rename KnownProvenance.None to .Empty to avoid confusion * Change copy text for resources with converted_prometheus provenance * Derive provisioned status from provenance in GrafanaManagedContactPoint * Fix linter errors * Extract helper method to check if contact point is provisioned * Replace string literal with constant * Refactor KnownProvenance enum values Refactored the KnownProvenance enum to better reflect the known provenances defined by the backend. Also refactored the methods where we assert if a resource is provisioned to better reflect the cases for which a provenance value reflects no provisioning. A resource is considered not provisioned when the provenance is equal to '', 'none' or undefined. * Use provenance to infer provenance status for Templates Refactored useNotificationTemplateMetadata to use only provenance value, and extracted method used to assert if resource is provisioned or not to k8s/utils in order to be more resource agnostic. * Replace empty string with 'none' for KnownProvenance enum The empty string valye for provenance gets mapped to the string literal 'none' before being passed down in the api response, therefore we can use only 'none' * Replace PROVENANCE_NONE with KnownProvenance.None Replaced the constant PROVENANCE_NONE with the KnownProvenance.None enum value since the values where duplicated * Fix JSDoc * Change copy text for ProvisioningBadge * Add missing tooltip in notification policy badge * Add missing tooltip in TemplatesTable badge * fix conflicts --------- Co-authored-by: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
This commit is contained in:
co-authored by
Sonia Aguilar
Sonia Aguilar
parent
9f44f868aa
commit
d0df6b8de4
@@ -0,0 +1,67 @@
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { KnownProvenance } from '../types/knownProvenance';
|
||||
|
||||
import { ProvisioningBadge } from './Provisioning';
|
||||
|
||||
describe('ProvisioningBadge', () => {
|
||||
describe('when the provenance is file', () => {
|
||||
it('should render the badge with the correct text', () => {
|
||||
render(<ProvisioningBadge provenance={KnownProvenance.File} />);
|
||||
|
||||
expect(screen.getByText('Provisioned')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Imported')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct tooltip text', async () => {
|
||||
const { user } = render(<ProvisioningBadge tooltip provenance={KnownProvenance.File} />);
|
||||
|
||||
const badge = screen.getByText('Provisioned');
|
||||
await user.hover(badge);
|
||||
|
||||
expect(
|
||||
screen.getByText('This resource has been provisioned via file and cannot be edited through the UI')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the provenance is ConvertedPrometheus', () => {
|
||||
it('should render the badge with the correct text', () => {
|
||||
render(<ProvisioningBadge provenance={KnownProvenance.ConvertedPrometheus} />);
|
||||
|
||||
expect(screen.getByText('Imported')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Provisioned')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct tooltip text', async () => {
|
||||
const { user } = render(<ProvisioningBadge tooltip provenance={KnownProvenance.ConvertedPrometheus} />);
|
||||
|
||||
const badge = screen.getByText('Imported');
|
||||
await user.hover(badge);
|
||||
|
||||
expect(
|
||||
screen.getByText('This resource has been provisioned via Prometheus/Mimir and cannot be edited through the UI')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the provenance is API', () => {
|
||||
it('should render the badge with the correct text', () => {
|
||||
render(<ProvisioningBadge provenance={KnownProvenance.API} />);
|
||||
|
||||
expect(screen.getByText('Provisioned')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Imported')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correct tooltip text', async () => {
|
||||
const { user } = render(<ProvisioningBadge tooltip provenance={KnownProvenance.API} />);
|
||||
|
||||
const badge = screen.getByText('Provisioned');
|
||||
await user.hover(badge);
|
||||
|
||||
expect(
|
||||
screen.getByText('This resource has been provisioned via api and cannot be edited through the UI')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import { ComponentPropsWithoutRef } from 'react';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Badge, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { KnownProvenance } from '../types/knownProvenance';
|
||||
|
||||
export enum ProvisionedResource {
|
||||
ContactPoint = 'contact point',
|
||||
Template = 'template',
|
||||
@@ -64,11 +66,17 @@ export const ProvisioningBadge = ({
|
||||
*/
|
||||
provenance?: string;
|
||||
}) => {
|
||||
const badge = <Badge text={t('alerting.provisioning-badge.badge.text-provisioned', 'Provisioned')} color="purple" />;
|
||||
const isConvertedPrometheus = provenance === KnownProvenance.ConvertedPrometheus;
|
||||
const badgeText = isConvertedPrometheus
|
||||
? t('alerting.provisioning-badge.badge.text-converted-prometheus', 'Imported')
|
||||
: t('alerting.provisioning-badge.badge.text-provisioned', 'Provisioned');
|
||||
const badgeColor = isConvertedPrometheus ? 'blue' : 'purple';
|
||||
const badge = <Badge text={badgeText} color={badgeColor} />;
|
||||
|
||||
if (tooltip) {
|
||||
const provenanceText = isConvertedPrometheus ? 'Prometheus/Mimir' : provenance;
|
||||
const provenanceTooltip = (
|
||||
<Trans i18nKey="alerting.provisioning.badge-tooltip-provenance" values={{ provenance }}>
|
||||
<Trans i18nKey="alerting.provisioning.badge-tooltip-provenance" values={{ provenance: provenanceText }}>
|
||||
This resource has been provisioned via {{ provenance }} and cannot be edited through the UI
|
||||
</Trans>
|
||||
);
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
|
||||
import { ContactPointHeader } from './ContactPointHeader';
|
||||
import { ContactPointWithMetadata } from './utils';
|
||||
|
||||
setupMswServer();
|
||||
|
||||
const renderWithProvider = (component: React.ReactElement, alertmanagerSourceName?: string) => {
|
||||
return render(
|
||||
<AlertmanagerProvider accessType="notification" alertmanagerSourceName={alertmanagerSourceName}>
|
||||
{component}
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('ContactPointHeader', () => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
]);
|
||||
});
|
||||
|
||||
const mockContactPoint: ContactPointWithMetadata = {
|
||||
id: 'test-contact-point',
|
||||
name: 'Test Contact Point',
|
||||
provenance: KnownProvenance.API,
|
||||
policies: [],
|
||||
grafana_managed_receiver_configs: [],
|
||||
};
|
||||
|
||||
it('shows Provisioned badge when contact point has file provenance via K8s annotations', () => {
|
||||
const contactPointWithFile = {
|
||||
...mockContactPoint,
|
||||
provenance: KnownProvenance.File,
|
||||
};
|
||||
|
||||
renderWithProvider(<ContactPointHeader contactPoint={contactPointWithFile} onDelete={jest.fn()} />);
|
||||
|
||||
expect(screen.getByText('Provisioned')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct badge when contact point has converted_prometheus provenance', () => {
|
||||
const contactPointWithConvertedPrometheus = {
|
||||
...mockContactPoint,
|
||||
provenance: KnownProvenance.ConvertedPrometheus,
|
||||
};
|
||||
|
||||
renderWithProvider(<ContactPointHeader contactPoint={contactPointWithConvertedPrometheus} onDelete={jest.fn()} />);
|
||||
|
||||
expect(screen.getByText('Imported')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+9
-8
@@ -13,6 +13,7 @@ import {
|
||||
canDeleteEntity,
|
||||
canEditEntity,
|
||||
getAnnotation,
|
||||
isProvisionedResource,
|
||||
shouldUseK8sApi,
|
||||
} from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
|
||||
@@ -31,13 +32,15 @@ interface ContactPointHeaderProps {
|
||||
}
|
||||
|
||||
export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeaderProps) => {
|
||||
const { name, id, provisioned, policies = [] } = contactPoint;
|
||||
const { name, id, provenance, policies = [] } = contactPoint;
|
||||
const styles = useStyles2(getStyles);
|
||||
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
|
||||
const usingK8sApi = shouldUseK8sApi(selectedAlertmanager!);
|
||||
|
||||
const isProvisioned = isProvisionedResource(provenance);
|
||||
|
||||
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportContactPoint);
|
||||
const [editSupported, editAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
|
||||
const [deleteSupported, deleteAllowed] = useAlertmanagerAbility(AlertmanagerAction.UpdateContactPoint);
|
||||
@@ -70,14 +73,14 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
/** Does the current user have permissions to edit the contact point? */
|
||||
const hasAbilityToEdit = usingK8sApi ? canEditEntity(contactPoint) : editAllowed;
|
||||
/** Can the contact point actually be edited via the UI? */
|
||||
const contactPointIsEditable = !provisioned;
|
||||
const contactPointIsEditable = !isProvisioned;
|
||||
/** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be edited? */
|
||||
const canEdit = editSupported && hasAbilityToEdit && contactPointIsEditable;
|
||||
|
||||
/** Does the current user have permissions to delete the contact point? */
|
||||
const hasAbilityToDelete = usingK8sApi ? canDeleteEntity(contactPoint) : deleteAllowed;
|
||||
/** Can the contact point actually be deleted, regardless of permissions? i.e. ensuring it isn't provisioned and isn't referenced elsewhere */
|
||||
const contactPointIsDeleteable = !provisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules;
|
||||
const contactPointIsDeleteable = !isProvisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules;
|
||||
/** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be deleted? */
|
||||
const canBeDeleted = deleteSupported && hasAbilityToDelete && contactPointIsDeleteable;
|
||||
|
||||
@@ -130,7 +133,7 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
|
||||
const reasonsDeleteIsDisabled = [
|
||||
!hasAbilityToDelete ? cannotDeleteNoPermissions : '',
|
||||
provisioned ? cannotDeleteProvisioned : '',
|
||||
isProvisioned ? cannotDeleteProvisioned : '',
|
||||
numberOfPoliciesPreventingDeletion > 0 ? cannotDeletePolicies : '',
|
||||
numberOfRules ? cannotDeleteRules : '',
|
||||
].filter(Boolean);
|
||||
@@ -209,15 +212,13 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
|
||||
{referencedByRulesText}
|
||||
</TextLink>
|
||||
)}
|
||||
{provisioned && (
|
||||
<ProvisioningBadge tooltip provenance={getAnnotation(contactPoint, K8sAnnotations.Provenance)} />
|
||||
)}
|
||||
{isProvisioned && <ProvisioningBadge tooltip provenance={provenance} />}
|
||||
{!isReferencedByAnything && <UnusedContactPointBadge />}
|
||||
<Spacer />
|
||||
<LinkButton
|
||||
tooltipPlacement="top"
|
||||
tooltip={
|
||||
provisioned
|
||||
isProvisioned
|
||||
? t(
|
||||
'alerting.contact-point-header.tooltip-provisioned-contact-points',
|
||||
'Provisioned contact points cannot be edited in the UI'
|
||||
|
||||
+4
-1
@@ -13,6 +13,7 @@ import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions, mockDataSource } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { ContactPoint } from './ContactPoint';
|
||||
@@ -305,7 +306,9 @@ describe('contact points', () => {
|
||||
});
|
||||
|
||||
it('should disable buttons when provisioned', async () => {
|
||||
const { user } = renderWithProvider(<ContactPoint contactPoint={{ ...basicContactPoint, provisioned: true }} />);
|
||||
const { user } = renderWithProvider(
|
||||
<ContactPoint contactPoint={{ ...basicContactPoint, provenance: KnownProvenance.File }} />
|
||||
);
|
||||
|
||||
expect(screen.getByText(/provisioned/i)).toBeInTheDocument();
|
||||
|
||||
|
||||
+10
-10
@@ -50,7 +50,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
},
|
||||
],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -93,7 +93,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
"name": "lotsa-emails",
|
||||
"policies": [],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -129,7 +129,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
"name": "OnCall Conctact point",
|
||||
"policies": [],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -178,7 +178,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
},
|
||||
],
|
||||
"provisioned": true,
|
||||
"provenance": "api",
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -243,7 +243,7 @@ exports[`useContactPoints should return contact points with status 1`] = `
|
||||
},
|
||||
"name": "Slack with multiple channels",
|
||||
"policies": [],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
@@ -301,7 +301,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
},
|
||||
],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -344,7 +344,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
"name": "lotsa-emails",
|
||||
"policies": [],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -383,7 +383,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
"name": "OnCall Conctact point",
|
||||
"policies": [],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -432,7 +432,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
},
|
||||
],
|
||||
"provisioned": true,
|
||||
"provenance": "api",
|
||||
},
|
||||
{
|
||||
"grafana_managed_receiver_configs": [
|
||||
@@ -497,7 +497,7 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag
|
||||
},
|
||||
"name": "Slack with multiple channels",
|
||||
"policies": [],
|
||||
"provisioned": false,
|
||||
"provenance": undefined,
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
|
||||
+234
@@ -6,10 +6,13 @@ import { disablePlugin } from 'app/features/alerting/unified/mocks/server/config
|
||||
import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { setAlertmanagerConfig } from '../../mocks/server/entities/alertmanagers';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
|
||||
import { useContactPointsWithStatus } from './useContactPoints';
|
||||
|
||||
@@ -69,4 +72,235 @@ describe('useContactPoints', () => {
|
||||
expect(snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Provenance handling', () => {
|
||||
it('should extract provenance when provenance is "api"', async () => {
|
||||
// Set up alertmanager config with a receiver that has API provenance
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'api-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-1',
|
||||
name: 'api-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
provenance: 'api', // This will be used by the K8s mock handler
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'api-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
expect(contactPoint?.provenance).toBe(KnownProvenance.API);
|
||||
});
|
||||
|
||||
it('should extract provenance when provenance is "file"', async () => {
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'file-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-2',
|
||||
name: 'file-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
provenance: 'file',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'file-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
expect(contactPoint?.provenance).toBe(KnownProvenance.File);
|
||||
});
|
||||
|
||||
it('should extract provenance when provenance is "converted_prometheus"', async () => {
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'mimir-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-3',
|
||||
name: 'mimir-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
provenance: 'converted_prometheus',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'mimir-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
expect(contactPoint?.provenance).toBe(KnownProvenance.ConvertedPrometheus);
|
||||
});
|
||||
|
||||
it('should map "none" provenance annotation to undefined', async () => {
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'none-provenance-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-4',
|
||||
name: 'none-provenance-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
// No provenance field - will default to PROVENANCE_NONE in mock handler
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'none-provenance-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
// The mock handler sets PROVENANCE_NONE ('none') when no provenance is found
|
||||
// parseK8sReceiver converts 'none' to undefined
|
||||
expect(contactPoint?.provenance).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing annotations gracefully', async () => {
|
||||
// This test verifies that when annotations are undefined, provenance is handled correctly
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
receivers: [
|
||||
{
|
||||
name: 'no-annotations-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-5',
|
||||
name: 'no-annotations-contact-point',
|
||||
type: 'email',
|
||||
disableResolveMessage: false,
|
||||
settings: {
|
||||
addresses: 'test@example.com',
|
||||
},
|
||||
secureFields: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, config);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useContactPointsWithStatus({
|
||||
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||
fetchPolicies: false,
|
||||
fetchStatuses: false,
|
||||
}),
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
const contactPoint = result.current.contactPoints?.find((cp) => cp.name === 'no-annotations-contact-point');
|
||||
expect(contactPoint).toBeDefined();
|
||||
// When annotations are missing, the mock handler should set provenance to undefined
|
||||
expect(contactPoint?.provenance).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } f
|
||||
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
|
||||
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import { GrafanaManagedContactPoint, Receiver } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { getAPINamespace } from '../../../../../api/utils';
|
||||
@@ -21,7 +21,9 @@ import { useAsync } from '../../hooks/useAsync';
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
|
||||
import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config';
|
||||
import { K8sAnnotations } from '../../utils/k8s/constants';
|
||||
|
||||
import { enhanceContactPointsWithMetadata } from './utils';
|
||||
|
||||
@@ -78,10 +80,13 @@ const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
|
||||
type K8sReceiver = ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;
|
||||
|
||||
const parseK8sReceiver = (item: K8sReceiver): GrafanaManagedContactPoint => {
|
||||
const metadataProvenance = item.metadata.annotations?.[K8sAnnotations.Provenance];
|
||||
const provenance = metadataProvenance === KnownProvenance.None ? undefined : metadataProvenance;
|
||||
|
||||
return {
|
||||
id: item.metadata.name || item.metadata.uid || item.spec.title,
|
||||
name: item.spec.title,
|
||||
provisioned: isK8sEntityProvisioned(item),
|
||||
provenance: provenance,
|
||||
grafana_managed_receiver_configs: item.spec.integrations,
|
||||
metadata: item.metadata,
|
||||
};
|
||||
|
||||
+8
-7
@@ -16,7 +16,8 @@ import {
|
||||
deleteNotificationTemplateAction,
|
||||
updateNotificationTemplateAction,
|
||||
} from '../../reducers/alertmanager/notificationTemplates';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from '../../utils/k8s/constants';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { K8sAnnotations } from '../../utils/k8s/constants';
|
||||
import { getAnnotation, shouldUseK8sApi } from '../../utils/k8s/utils';
|
||||
import { ensureDefine } from '../../utils/templates';
|
||||
import { TemplateFormValues } from '../receivers/TemplateForm';
|
||||
@@ -79,7 +80,7 @@ function templateGroupsToTemplates(
|
||||
function templateGroupToTemplate(
|
||||
templateGroup: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup
|
||||
): NotificationTemplate {
|
||||
const provenance = getAnnotation(templateGroup, K8sAnnotations.Provenance) ?? PROVENANCE_NONE;
|
||||
const provenance = getAnnotation(templateGroup, K8sAnnotations.Provenance) ?? KnownProvenance.None;
|
||||
return {
|
||||
// K8s entities should always have a metadata.name property. The type is marked as optional because it's also used in other places
|
||||
uid: templateGroup.metadata.name ?? templateGroup.spec.title,
|
||||
@@ -96,8 +97,8 @@ function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemp
|
||||
uid: title,
|
||||
title,
|
||||
content,
|
||||
// Undefined, null or empty string should be converted to PROVENANCE_NONE
|
||||
provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE,
|
||||
// Undefined, null or empty string should be converted to KnownProvenance.None
|
||||
provenance: (config.template_file_provenances ?? {})[title] || KnownProvenance.None,
|
||||
missing: !templates.includes(title),
|
||||
}));
|
||||
}
|
||||
@@ -272,7 +273,7 @@ export function useValidateNotificationTemplate({
|
||||
}
|
||||
|
||||
interface NotificationTemplateMetadata {
|
||||
isProvisioned: boolean;
|
||||
provenance?: string;
|
||||
}
|
||||
|
||||
export function useNotificationTemplateMetadata(
|
||||
@@ -280,11 +281,11 @@ export function useNotificationTemplateMetadata(
|
||||
): NotificationTemplateMetadata {
|
||||
if (!template) {
|
||||
return {
|
||||
isProvisioned: false,
|
||||
provenance: KnownProvenance.None,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isProvisioned: Boolean(template.provenance) && template.provenance !== PROVENANCE_NONE,
|
||||
provenance: template.provenance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { GrafanaManagedContactPoint } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall';
|
||||
|
||||
import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY } from './constants';
|
||||
import {
|
||||
ReceiverConfigWithMetadata,
|
||||
enhanceContactPointsWithMetadata,
|
||||
getReceiverDescription,
|
||||
isAutoGeneratedPolicy,
|
||||
summarizeEmailAddresses,
|
||||
@@ -128,3 +132,110 @@ describe('summarizeEmailAddresses', () => {
|
||||
expect(summarizeEmailAddresses('foo@foo.com\n bar@bar.com ')).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enhanceContactPointsWithMetadata', () => {
|
||||
it('should extract provenance from receiver configs when contact point has no provenance', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
provenance: KnownProvenance.API,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBe(KnownProvenance.API);
|
||||
});
|
||||
|
||||
it('should prefer contact point provenance over receiver config provenance', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
provenance: KnownProvenance.File, // Provenance on contact point (from K8s)
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
provenance: KnownProvenance.API, // Different provenance on receiver config
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBe(KnownProvenance.File);
|
||||
});
|
||||
|
||||
it('should extract provenance from first receiver config that has it', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid-1',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
// No provenance on first receiver
|
||||
},
|
||||
{
|
||||
uid: 'test-uid-2',
|
||||
name: 'test-contact-point',
|
||||
type: 'slack',
|
||||
settings: { recipient: '#channel' },
|
||||
secureFields: {},
|
||||
provenance: KnownProvenance.ConvertedPrometheus, // Provenance on second receiver
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBe(KnownProvenance.ConvertedPrometheus);
|
||||
});
|
||||
|
||||
it('should have undefined provenance when neither contact point nor receiver configs have provenance', () => {
|
||||
const contactPoint: GrafanaManagedContactPoint = {
|
||||
name: 'test-contact-point',
|
||||
grafana_managed_receiver_configs: [
|
||||
{
|
||||
uid: 'test-uid',
|
||||
name: 'test-contact-point',
|
||||
type: 'email',
|
||||
settings: { addresses: 'test@example.com' },
|
||||
secureFields: {},
|
||||
// No provenance
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const enhanced = enhanceContactPointsWithMetadata({
|
||||
contactPoints: [contactPoint],
|
||||
notifiers: [],
|
||||
status: [],
|
||||
});
|
||||
|
||||
expect(enhanced[0].provenance).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,9 +146,16 @@ export function enhanceContactPointsWithMetadata({
|
||||
|
||||
const id = getContactPointIdentifier(contactPoint);
|
||||
|
||||
// Extract provenance from contactPoint first; else, search in its receivers
|
||||
const contactPointProvenance =
|
||||
'provenance' in contactPoint && contactPoint.provenance !== undefined
|
||||
? contactPoint.provenance
|
||||
: receivers.find((receiver) => Boolean(receiver.provenance))?.provenance;
|
||||
|
||||
return {
|
||||
...contactPoint,
|
||||
id,
|
||||
provenance: contactPointProvenance,
|
||||
policies:
|
||||
alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? []),
|
||||
grafana_managed_receiver_configs: receivers.map((receiver, index) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
IoK8SApimachineryPkgApisMetaV1ObjectMeta,
|
||||
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
|
||||
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
|
||||
import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import {
|
||||
isK8sEntityProvisioned,
|
||||
shouldUseK8sApi,
|
||||
@@ -62,7 +62,7 @@ const parseAmTimeInterval: (interval: MuteTimeInterval, provenance: string) => M
|
||||
return {
|
||||
...interval,
|
||||
id: interval.name,
|
||||
provisioned: Boolean(provenance && provenance !== PROVENANCE_NONE),
|
||||
provisioned: Boolean(provenance && provenance !== KnownProvenance.None),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+6
-2
@@ -11,7 +11,7 @@ import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alertin
|
||||
import { FormAmRoute } from 'app/features/alerting/unified/types/amroutes';
|
||||
import { addUniqueIdentifierToRoute } from 'app/features/alerting/unified/utils/amroutes';
|
||||
import { getErrorCode, stringifyErrorLike } from 'app/features/alerting/unified/utils/misc';
|
||||
import { ObjectMatcher, ROUTES_META_SYMBOL, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ObjectMatcher, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { anyOfRequestState, isError } from '../../hooks/useAsync';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
@@ -27,6 +27,7 @@ import { useAddPolicyModal, useAlertGroupsModal, useDeletePolicyModal, useEditPo
|
||||
import { Policy } from './Policy';
|
||||
import { TIMING_OPTIONS_DEFAULTS } from './timingOptions';
|
||||
import {
|
||||
isRouteProvisioned,
|
||||
useAddNotificationPolicy,
|
||||
useDeleteNotificationPolicy,
|
||||
useNotificationPolicyRoute,
|
||||
@@ -99,6 +100,8 @@ export const NotificationPoliciesList = () => {
|
||||
}
|
||||
return;
|
||||
}, [defaultPolicy]);
|
||||
const routeProvenance = defaultPolicy?.provenance;
|
||||
const isRootRouteProvisioned = rootRoute ? isRouteProvisioned(rootRoute) : false;
|
||||
|
||||
// useAsync could also work but it's hard to wait until it's done in the tests
|
||||
// Combining with useEffect gives more predictable results because the condition is in useEffect
|
||||
@@ -244,7 +247,8 @@ export const NotificationPoliciesList = () => {
|
||||
currentRoute={defaults(rootRoute, TIMING_OPTIONS_DEFAULTS)}
|
||||
contactPointsState={contactPointsState.receivers}
|
||||
readOnly={!hasConfigurationAPI}
|
||||
provisioned={rootRoute[ROUTES_META_SYMBOL]?.provisioned}
|
||||
provisioned={isRootRouteProvisioned}
|
||||
provenance={routeProvenance}
|
||||
alertManagerSourceName={selectedAlertmanager}
|
||||
onAddPolicy={openAddModal}
|
||||
onEditPolicy={openEditModal}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||
import { mockReceiversState } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import {
|
||||
@@ -331,6 +332,60 @@ describe('Policy', () => {
|
||||
const customPolicy = screen.getByTestId('am-route-container');
|
||||
expect(within(customPolicy).getByTestId('matches-all')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct badge when policy has file provenance', () => {
|
||||
const mockRoute: RouteWithID = {
|
||||
id: 'test-route',
|
||||
receiver: 'test-receiver',
|
||||
routes: [],
|
||||
};
|
||||
|
||||
renderPolicy(
|
||||
<Policy
|
||||
readOnly
|
||||
isDefaultPolicy
|
||||
currentRoute={mockRoute}
|
||||
contactPointsState={mockReceiversState()}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onEditPolicy={noop}
|
||||
onAddPolicy={noop}
|
||||
onDeletePolicy={noop}
|
||||
onShowAlertInstances={noop}
|
||||
provisioned
|
||||
provenance={KnownProvenance.File}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('Provisioned');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows correct badge when policy has converted_prometheus provenance', () => {
|
||||
const mockRoute: RouteWithID = {
|
||||
id: 'test-route',
|
||||
receiver: 'test-receiver',
|
||||
routes: [],
|
||||
};
|
||||
|
||||
renderPolicy(
|
||||
<Policy
|
||||
readOnly
|
||||
isDefaultPolicy
|
||||
currentRoute={mockRoute}
|
||||
contactPointsState={mockReceiversState()}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
onEditPolicy={noop}
|
||||
onAddPolicy={noop}
|
||||
onDeletePolicy={noop}
|
||||
onShowAlertInstances={noop}
|
||||
provisioned
|
||||
provenance={KnownProvenance.ConvertedPrometheus}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('Imported');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Doesn't matter which path the routes use, it just needs to match the initialEntries history entry to render the element
|
||||
|
||||
@@ -61,6 +61,7 @@ interface PolicyComponentProps {
|
||||
contactPointsState?: ReceiversState;
|
||||
readOnly?: boolean;
|
||||
provisioned?: boolean;
|
||||
provenance?: string;
|
||||
inheritedProperties?: InheritableProperties;
|
||||
routesMatchingFilters?: RoutesMatchingFilters;
|
||||
|
||||
@@ -89,6 +90,7 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
contactPointsState,
|
||||
readOnly = false,
|
||||
provisioned = false,
|
||||
provenance,
|
||||
alertManagerSourceName,
|
||||
currentRoute,
|
||||
inheritedProperties,
|
||||
@@ -255,7 +257,7 @@ const Policy = (props: PolicyComponentProps) => {
|
||||
<Spacer />
|
||||
{/* TODO maybe we should move errors to the gutter instead? */}
|
||||
{errors.length > 0 && <Errors errors={errors} />}
|
||||
{provisioned && <ProvisioningBadge />}
|
||||
{provisioned && <ProvisioningBadge tooltip provenance={provenance} />}
|
||||
<Stack direction="row" gap={0.5}>
|
||||
{!isAutoGenerated && !readOnly && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateNotificationPolicy]}>
|
||||
|
||||
+90
-1
@@ -1,9 +1,15 @@
|
||||
import { MatcherOperator, ROUTES_META_SYMBOL, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route } from '../../openapi/routesApi.gen';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
|
||||
|
||||
import { createKubernetesRoutingTreeSpec, k8sSubRouteToRoute, routeToK8sSubRoute } from './useNotificationPolicyRoute';
|
||||
import {
|
||||
createKubernetesRoutingTreeSpec,
|
||||
isRouteProvisioned,
|
||||
k8sSubRouteToRoute,
|
||||
routeToK8sSubRoute,
|
||||
} from './useNotificationPolicyRoute';
|
||||
|
||||
test('k8sSubRouteToRoute', () => {
|
||||
const input: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route = {
|
||||
@@ -115,3 +121,86 @@ test('createKubernetesRoutingTreeSpec', () => {
|
||||
expect(tree.metadata.name).toBe(ROOT_ROUTE_NAME);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('isRouteProvisioned', () => {
|
||||
it('returns false when route has no provenance', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false when route has KnownProvenance.None in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.None,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false when route has KnownProvenance.None at top level', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
provenance: KnownProvenance.None,
|
||||
};
|
||||
expect(isRouteProvisioned(route)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true when route has file provenance in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.File,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when route has api provenance in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.API,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when route has converted_prometheus provenance in metadata', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: KnownProvenance.ConvertedPrometheus,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when route has file provenance at top level', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
provenance: KnownProvenance.File,
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('falls back to top-level provenance when metadata provenance is missing', () => {
|
||||
const route: Route = {
|
||||
receiver: 'test-receiver',
|
||||
provenance: KnownProvenance.File,
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provenance: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isRouteProvisioned(route)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
+10
-4
@@ -22,8 +22,8 @@ import {
|
||||
} from '../../reducers/alertmanager/notificationPolicyRoutes';
|
||||
import { FormAmRoute } from '../../types/amroutes';
|
||||
import { addUniqueIdentifierToRoute } from '../../utils/amroutes';
|
||||
import { PROVENANCE_NONE, ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
|
||||
import { isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils';
|
||||
import { K8sAnnotations, ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
|
||||
import { getAnnotation, isProvisionedResource, shouldUseK8sApi } from '../../utils/k8s/utils';
|
||||
import { routeAdapter } from '../../utils/routeAdapter';
|
||||
import {
|
||||
InsertPosition,
|
||||
@@ -33,6 +33,11 @@ import {
|
||||
omitRouteFromRouteTree,
|
||||
} from '../../utils/routeTree';
|
||||
|
||||
export function isRouteProvisioned(route: Route): boolean {
|
||||
const provenance = route[ROUTES_META_SYMBOL]?.provenance ?? route.provenance;
|
||||
return isProvisionedResource(provenance);
|
||||
}
|
||||
|
||||
const k8sRoutesToRoutesMemoized = memoize(k8sRoutesToRoutes, { maxSize: 1 });
|
||||
|
||||
const {
|
||||
@@ -82,7 +87,7 @@ const parseAmConfigRoute = memoize((route: Route): Route => {
|
||||
return {
|
||||
...route,
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provisioned: Boolean(route.provenance && route.provenance !== PROVENANCE_NONE),
|
||||
provenance: route.provenance,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -232,10 +237,11 @@ function k8sRoutesToRoutes(routes: ComGithubGrafanaGrafanaPkgApisAlertingNotific
|
||||
...route.spec.defaults,
|
||||
routes: route.spec.routes?.map(k8sSubRouteToRoute),
|
||||
[ROUTES_META_SYMBOL]: {
|
||||
provisioned: isK8sEntityProvisioned(route),
|
||||
provenance: getAnnotation(route, K8sAnnotations.Provenance),
|
||||
resourceVersion: route.metadata.resourceVersion,
|
||||
name: route.metadata.name,
|
||||
},
|
||||
provenance: getAnnotation(route, K8sAnnotations.Provenance),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { AITemplateButtonComponent } from '../../enterprise-components/AI/AIGenTemplateButton/addAITemplateButton';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { isProvisionedResource } from '../../utils/k8s/utils';
|
||||
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
|
||||
import { EditorColumnHeader } from '../EditorColumnHeader';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
@@ -122,7 +123,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
// AI feedback state
|
||||
const [aiGeneratedTemplate, setAiGeneratedTemplate] = useState(false);
|
||||
|
||||
const { isProvisioned } = useNotificationTemplateMetadata(originalTemplate);
|
||||
const { provenance } = useNotificationTemplateMetadata(originalTemplate);
|
||||
const isProvisioned = isProvisionedResource(provenance);
|
||||
const originalTemplatePrefill: TemplateFormValues | undefined = originalTemplate
|
||||
? { title: originalTemplate.title, content: originalTemplate.content }
|
||||
: undefined;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { render, screen, within } from 'test/test-utils';
|
||||
|
||||
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
import { setupMswServer } from '../../mockApi';
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { NotificationTemplate } from '../contact-points/useNotificationTemplates';
|
||||
|
||||
import { TemplatesTable } from './TemplatesTable';
|
||||
|
||||
const mockTemplates: Array<Partial<NotificationTemplate>> = [
|
||||
{
|
||||
uid: 'mimir-template',
|
||||
title: 'mimir-template',
|
||||
content: '{{ define "mimir-template" }}Template from Mimir{{ end }}',
|
||||
provenance: KnownProvenance.ConvertedPrometheus,
|
||||
},
|
||||
{
|
||||
uid: 'file-template',
|
||||
title: 'file-template',
|
||||
content: '{{ define "file-template" }}File provisioned template{{ end }}',
|
||||
provenance: KnownProvenance.File,
|
||||
},
|
||||
{
|
||||
uid: 'api-template',
|
||||
title: 'api-template',
|
||||
content: '{{ define "api-template" }}API provisioned template{{ end }}',
|
||||
provenance: KnownProvenance.API,
|
||||
},
|
||||
{
|
||||
uid: 'no-provenance-template',
|
||||
title: 'no-provenance-template',
|
||||
content: '{{ define "no-provenance-template" }}No provenance template{{ end }}',
|
||||
provenance: KnownProvenance.None,
|
||||
},
|
||||
{
|
||||
uid: 'undefined-provenance-template',
|
||||
title: 'undefined-provenance-template',
|
||||
content: '{{ define "undefined-provenance-template" }}Undefined provenance template{{ end }}',
|
||||
provenance: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const renderWithProvider = (templates: Array<Partial<NotificationTemplate>>) => {
|
||||
return render(
|
||||
<AlertmanagerProvider accessType={'notification'}>
|
||||
<TemplatesTable alertManagerName={GRAFANA_RULES_SOURCE_NAME} templates={templates as NotificationTemplate[]} />
|
||||
<AppNotificationList />
|
||||
</AlertmanagerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
setupMswServer();
|
||||
|
||||
describe('TemplatesTable', () => {
|
||||
beforeEach(() => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows "Imported" badge for templates with converted_prometheus provenance', () => {
|
||||
const templates = [mockTemplates[0]]; // mimir-template
|
||||
renderWithProvider(templates);
|
||||
|
||||
const templateRow = screen.getByRole('row', { name: /mimir-template/i });
|
||||
const badge = within(templateRow).getByText('Imported');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Provisioned" badge for templates with other provenance', () => {
|
||||
// api and file templates
|
||||
[mockTemplates[1], mockTemplates[2]].forEach((template) => {
|
||||
renderWithProvider([template]);
|
||||
|
||||
const templateRow = screen.getByRole('row', { name: new RegExp(template.title ?? '', 'i') });
|
||||
const badge = within(templateRow).getByText('Provisioned');
|
||||
expect(badge).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show badge for templates with KnownProvenance.None or empty string provenance', () => {
|
||||
// no-provenance-template and undefined-provenance-template
|
||||
[mockTemplates[3], mockTemplates[4]].forEach((template) => {
|
||||
renderWithProvider([template]);
|
||||
|
||||
const templateRow = screen.getByRole('row', { name: new RegExp(template.title ?? '', 'i') });
|
||||
expect(within(templateRow).queryByText('Provisioned')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { AlertmanagerAction } from '../../hooks/useAbilities';
|
||||
import { getAlertTableStyles } from '../../styles/table';
|
||||
import { isProvisionedResource } from '../../utils/k8s/utils';
|
||||
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
@@ -128,7 +129,8 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
const isGrafanaAlertmanager = alertManagerName === GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { isProvisioned } = useNotificationTemplateMetadata(notificationTemplate);
|
||||
const { provenance } = useNotificationTemplateMetadata(notificationTemplate);
|
||||
const isProvisioned = isProvisionedResource(provenance);
|
||||
|
||||
const { uid, title: name, content: template, missing } = notificationTemplate;
|
||||
const misconfiguredBadgeText = t('alerting.templates.misconfigured-badge-text', 'Misconfigured');
|
||||
@@ -139,7 +141,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
<CollapseToggle isCollapsed={!isExpanded} onToggle={() => setIsExpanded(!isExpanded)} />
|
||||
</td>
|
||||
<td>
|
||||
{name} {isProvisioned && <ProvisioningBadge />}{' '}
|
||||
{name} {isProvisioned && <ProvisioningBadge tooltip provenance={provenance} />}{' '}
|
||||
{missing && !isGrafanaAlertmanager && (
|
||||
<Tooltip
|
||||
content={
|
||||
|
||||
+9
-6
@@ -9,7 +9,11 @@ import {
|
||||
} from 'app/features/alerting/unified/components/contact-points/useContactPoints';
|
||||
import { showManageContactPointPermissions } from 'app/features/alerting/unified/components/contact-points/utils';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { canEditEntity, canModifyProtectedEntity } from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import {
|
||||
canEditEntity,
|
||||
canModifyProtectedEntity,
|
||||
isProvisionedResource,
|
||||
} from 'app/features/alerting/unified/utils/k8s/utils';
|
||||
import {
|
||||
GrafanaManagedContactPoint,
|
||||
GrafanaManagedReceiverConfig,
|
||||
@@ -127,7 +131,8 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
// If there is no contact point it means we're creating a new one, so scoped permissions doesn't exist yet
|
||||
const hasScopedEditPermissions = contactPoint ? canEditEntity(contactPoint) : true;
|
||||
const hasScopedEditProtectedPermissions = contactPoint ? canModifyProtectedEntity(contactPoint) : true;
|
||||
const isEditable = !readOnly && hasScopedEditPermissions && !contactPoint?.provisioned;
|
||||
const isProvisioned = isProvisionedResource(contactPoint?.provenance);
|
||||
const isEditable = !readOnly && hasScopedEditPermissions && !isProvisioned;
|
||||
const isTestable = !readOnly;
|
||||
const canEditProtectedFields = editMode ? hasScopedEditProtectedPermissions : true;
|
||||
|
||||
@@ -170,10 +175,8 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{contactPoint?.provisioned && hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
<ImportedContactPointAlert />
|
||||
)}
|
||||
{contactPoint?.provisioned && !hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
{isProvisioned && hasLegacyIntegrations(contactPoint, grafanaNotifiers) && <ImportedContactPointAlert />}
|
||||
{isProvisioned && !hasLegacyIntegrations(contactPoint, grafanaNotifiers) && (
|
||||
<ProvisioningAlert resource={ProvisionedResource.ContactPoint} />
|
||||
)}
|
||||
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
|
||||
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
|
||||
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { NotificationChannelOption } from 'app/features/alerting/unified/types/alerting';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { DEFAULT_TEMPLATES } from 'app/features/alerting/unified/utils/template-constants';
|
||||
import { AccessControlAction } from 'app/types/accessControl';
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('getTemplateOptions function', () => {
|
||||
uid: title,
|
||||
title,
|
||||
content,
|
||||
provenance: PROVENANCE_NONE,
|
||||
provenance: KnownProvenance.None,
|
||||
};
|
||||
});
|
||||
const defaultTemplates = parseTemplates(DEFAULT_TEMPLATES);
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route,
|
||||
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree,
|
||||
} from 'app/features/alerting/unified/openapi/routesApi.gen';
|
||||
import { K8sAnnotations, PROVENANCE_NONE, ROOT_ROUTE_NAME } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { K8sAnnotations, ROOT_ROUTE_NAME } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { AlertManagerCortexConfig, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
/**
|
||||
@@ -66,7 +67,7 @@ export const getUserDefinedRoutingTree: (
|
||||
name: ROOT_ROUTE_NAME,
|
||||
namespace: 'default',
|
||||
annotations: {
|
||||
[K8sAnnotations.Provenance]: PROVENANCE_NONE,
|
||||
[K8sAnnotations.Provenance]: KnownProvenance.None,
|
||||
},
|
||||
// Resource versions are much shorter than this in reality, but this is an easy way
|
||||
// for us to mock the concurrency logic and check if the policies have updated since the last fetch
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
|
||||
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
const usedByPolicies = ['grafana-default-email'];
|
||||
const usedByRules = ['grafana-default-email'];
|
||||
@@ -23,7 +24,7 @@ const getReceiversList = () => {
|
||||
const provenance =
|
||||
contactPoint.grafana_managed_receiver_configs?.find((integration) => {
|
||||
return integration.provenance;
|
||||
})?.provenance || PROVENANCE_NONE;
|
||||
})?.provenance || KnownProvenance.None;
|
||||
return {
|
||||
metadata: {
|
||||
// This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses
|
||||
|
||||
@@ -3,8 +3,9 @@ import { HttpResponse, http } from 'msw';
|
||||
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
|
||||
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup } from 'app/features/alerting/unified/openapi/templatesApi.gen';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { PROVENANCE_ANNOTATION } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
@@ -14,7 +15,7 @@ const mappedTemplates = Object.entries(
|
||||
).map<ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup>(([title, template]) => ({
|
||||
metadata: {
|
||||
name: titleToK8sResourceName(title), // K8s uses unique identifiers for resources
|
||||
annotations: { [PROVENANCE_ANNOTATION]: config.template_file_provenances?.[title] || PROVENANCE_NONE },
|
||||
annotations: { [PROVENANCE_ANNOTATION]: config.template_file_provenances?.[title] || KnownProvenance.None },
|
||||
},
|
||||
spec: {
|
||||
title: title,
|
||||
|
||||
@@ -4,7 +4,8 @@ import { base64UrlEncode } from '@grafana/alerting';
|
||||
import { filterBySelector } from 'app/features/alerting/unified/mocks/server/handlers/k8s/utils';
|
||||
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
|
||||
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { KnownProvenance } from 'app/features/alerting/unified/types/knownProvenance';
|
||||
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
/** UID of a time interval that we expect to follow all happy paths within tests/mocks */
|
||||
export const TIME_INTERVAL_UID_HAPPY_PATH = 'f4eae7a4895fa786';
|
||||
@@ -21,7 +22,7 @@ const allTimeIntervals = getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNo
|
||||
{
|
||||
metadata: {
|
||||
annotations: {
|
||||
[K8sAnnotations.Provenance]: PROVENANCE_NONE,
|
||||
[K8sAnnotations.Provenance]: KnownProvenance.None,
|
||||
},
|
||||
name: base64UrlEncode(TIME_INTERVAL_NAME_HAPPY_PATH),
|
||||
uid: TIME_INTERVAL_UID_HAPPY_PATH,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum KnownProvenance {
|
||||
None = 'none' /** Provenance value given for entities that were not provisioned */,
|
||||
API = 'api',
|
||||
File = 'file',
|
||||
ConvertedPrometheus = 'converted_prometheus',
|
||||
}
|
||||
@@ -4,9 +4,6 @@
|
||||
* */
|
||||
export const PROVENANCE_ANNOTATION = 'grafana.com/provenance';
|
||||
|
||||
/** Value of {@link PROVENANCE_ANNOTATION} given for entities that were not provisioned */
|
||||
export const PROVENANCE_NONE = 'none';
|
||||
|
||||
export enum K8sAnnotations {
|
||||
Provenance = 'grafana.com/provenance',
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { encodeFieldSelector } from './utils';
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
|
||||
import { encodeFieldSelector, isProvisionedResource } from './utils';
|
||||
|
||||
describe('encodeFieldSelector', () => {
|
||||
it('should escape backslashes', () => {
|
||||
@@ -25,3 +27,29 @@ describe('encodeFieldSelector', () => {
|
||||
expect(encodeFieldSelector('foo=bar,bar=baz,qux\\foo')).toBe('foo\\=bar\\,bar\\=baz\\,qux\\\\foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isProvisionedResource', () => {
|
||||
it('should return true when provenance is API', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.API)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when provenance is File', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.File)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when provenance is ConvertedPrometheus', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.ConvertedPrometheus)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when provenance is none', () => {
|
||||
expect(isProvisionedResource(KnownProvenance.None)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when provenance is undefined', () => {
|
||||
expect(isProvisionedResource(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for any other non-empty string', () => {
|
||||
expect(isProvisionedResource('custom-provenance')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||
|
||||
import { KnownProvenance } from '../../types/knownProvenance';
|
||||
|
||||
/**
|
||||
* Should we call the kubernetes-style API for managing alertmanager entities?
|
||||
@@ -22,7 +24,7 @@ type EntityToCheck = {
|
||||
*/
|
||||
export const isK8sEntityProvisioned = (k8sEntity: EntityToCheck) => {
|
||||
const provenance = getAnnotation(k8sEntity, K8sAnnotations.Provenance);
|
||||
return Boolean(provenance && provenance !== PROVENANCE_NONE);
|
||||
return isProvisionedResource(provenance);
|
||||
};
|
||||
|
||||
export const ANNOTATION_PREFIX_ACCESS = 'grafana.com/access/';
|
||||
@@ -59,3 +61,7 @@ export const stringifyFieldSelector = (fieldSelectors: FieldSelector[]): string
|
||||
.map(([key, value, operator = '=']) => `${key}${operator}${encodeFieldSelector(value)}`)
|
||||
.join(',');
|
||||
};
|
||||
|
||||
export function isProvisionedResource(provenance?: string): boolean {
|
||||
return Boolean(provenance && provenance !== KnownProvenance.None);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ export interface GrafanaManagedContactPoint {
|
||||
/** If parsed from k8s API, we'll have an ID property */
|
||||
id?: string;
|
||||
metadata?: IoK8SApimachineryPkgApisMetaV1ObjectMeta;
|
||||
provisioned?: boolean;
|
||||
provenance?: string;
|
||||
grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[];
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export type Route = {
|
||||
provenance?: string;
|
||||
/** this is used to add additional metadata to the routes without interfering with original route definition (symbols aren't iterable) */
|
||||
[ROUTES_META_SYMBOL]?: {
|
||||
provisioned?: boolean;
|
||||
provenance?: string;
|
||||
resourceVersion?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
@@ -2184,6 +2184,7 @@
|
||||
},
|
||||
"provisioning-badge": {
|
||||
"badge": {
|
||||
"text-converted-prometheus": "Imported",
|
||||
"text-provisioned": "Provisioned"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user