mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-19 08:20:34 +01:00
Compare commits
199 Commits
chore/butt
...
infraM/v2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9edd8f9322 | ||
|
|
b48851e487 | ||
|
|
9f43c77d6d | ||
|
|
ca837b5e36 | ||
|
|
279a71c5b3 | ||
|
|
7e63e35113 | ||
|
|
d5a50fe456 | ||
|
|
885b41356a | ||
|
|
b82809d289 | ||
|
|
0a73c167db | ||
|
|
9caa7b0d6f | ||
|
|
954dd7904a | ||
|
|
3e56be78f9 | ||
|
|
8dc8181e0c | ||
|
|
558f40b3f3 | ||
|
|
c9e8b2c0d9 | ||
|
|
ac9c922315 | ||
|
|
59cdbd8d90 | ||
|
|
346a274b10 | ||
|
|
8beb30c106 | ||
|
|
9e1927b0ee | ||
|
|
e37ae152e7 | ||
|
|
30b76c40a9 | ||
|
|
db3cee6889 | ||
|
|
8c4320150a | ||
|
|
591c056933 | ||
|
|
1915a905bb | ||
|
|
dc1924092a | ||
|
|
d90f640996 | ||
|
|
13fe33becb | ||
|
|
c15e2ce049 | ||
|
|
e562667c84 | ||
|
|
f2157d9375 | ||
|
|
f8b0739abf | ||
|
|
93ccc263df | ||
|
|
0d38219e58 | ||
|
|
0691ffdfa1 | ||
|
|
49c0d2a854 | ||
|
|
b4a394439e | ||
|
|
2d2c9b4d2a | ||
|
|
4765bba05c | ||
|
|
db79e6492e | ||
|
|
78da4ee819 | ||
|
|
7e812277ec | ||
|
|
48a1e90402 | ||
|
|
e33c14a70b | ||
|
|
6957b8ccfb | ||
|
|
b36a61e30d | ||
|
|
49483a18c4 | ||
|
|
2097686ad2 | ||
|
|
83f0d1ee7a | ||
|
|
6df86c5fdd | ||
|
|
ee58f35c6d | ||
|
|
ba0d3ea484 | ||
|
|
18c81c3495 | ||
|
|
b4876d4da8 | ||
|
|
0374bd272b | ||
|
|
1331341678 | ||
|
|
051edae843 | ||
|
|
994c9d426c | ||
|
|
1317a6c460 | ||
|
|
b4cde563e2 | ||
|
|
a55e65ae43 | ||
|
|
7f14bc0e0a | ||
|
|
1c9e31ad00 | ||
|
|
78b9ce6d4f | ||
|
|
9203602abd | ||
|
|
c10d278ec4 | ||
|
|
8b0e8f666e | ||
|
|
1d89b03f10 | ||
|
|
7d702763cc | ||
|
|
3a61a78986 | ||
|
|
46e833faba | ||
|
|
4bd7492629 | ||
|
|
401701e036 | ||
|
|
3bec0df0ad | ||
|
|
24fe9a986d | ||
|
|
520e92049c | ||
|
|
92d297ac9d | ||
|
|
eff29aefba | ||
|
|
ca73453c9e | ||
|
|
1d836d674f | ||
|
|
83724b0cde | ||
|
|
3050e37ec7 | ||
|
|
bdbaa32485 | ||
|
|
55b2215025 | ||
|
|
e90378e618 | ||
|
|
4592b78f48 | ||
|
|
d81c99feae | ||
|
|
65a456ff9e | ||
|
|
264577b673 | ||
|
|
b35c6676f9 | ||
|
|
c78c9a42db | ||
|
|
1095caa123 | ||
|
|
9043b49762 | ||
|
|
d4084a7494 | ||
|
|
27c564b3bf | ||
|
|
f02c491828 | ||
|
|
3d53b8f77f | ||
|
|
dffe94fec4 | ||
|
|
b7d4f18aae | ||
|
|
9ad2ec428a | ||
|
|
c9360fcf13 | ||
|
|
b5ab45db20 | ||
|
|
08f76aca78 | ||
|
|
983d4fe4f2 | ||
|
|
833af794c3 | ||
|
|
21b51d1fcc | ||
|
|
56f22682c8 | ||
|
|
9c8359940c | ||
|
|
4050880275 | ||
|
|
5e775f64f2 | ||
|
|
0189f23f46 | ||
|
|
49a36d4e3d | ||
|
|
9407d658ab | ||
|
|
5035712485 | ||
|
|
bab17c3615 | ||
|
|
37b44f4db9 | ||
|
|
99dd6e5f1e | ||
|
|
9c7131fa6a | ||
|
|
ad889a2e1d | ||
|
|
a4f6d0cbf5 | ||
|
|
589bed7c16 | ||
|
|
93843a1f48 | ||
|
|
88c43108fc | ||
|
|
ed4cf540e8 | ||
|
|
9e2dfa9033 | ||
|
|
d98d5d68ee | ||
|
|
2cb1c3b73b | ||
|
|
ae7ca497ad | ||
|
|
a579916961 | ||
|
|
4a16d56abf | ||
|
|
642b5ac3f0 | ||
|
|
a12112619c | ||
|
|
014785f1bc | ||
|
|
58ee797b10 | ||
|
|
82d236742f | ||
|
|
397e1ad5be | ||
|
|
8d6b25ca9b | ||
|
|
5fa6bd8b8d | ||
|
|
bd9977483b | ||
|
|
50fbdfeeef | ||
|
|
e2b1b73e87 | ||
|
|
cb9f3fd3e5 | ||
|
|
232acc343d | ||
|
|
2025afdccc | ||
|
|
d2f4d4af93 | ||
|
|
47ff7bbb8e | ||
|
|
724071c5dc | ||
|
|
4d24979358 | ||
|
|
042943b10a | ||
|
|
48a9be7ec8 | ||
|
|
a9504b2120 | ||
|
|
8755887c4a | ||
|
|
4cb4662b3a | ||
|
|
e6900dabc8 | ||
|
|
c1ba389b63 | ||
|
|
3a1f40234f | ||
|
|
2e4891fa63 | ||
|
|
04ebc0bec7 | ||
|
|
271f9b81ed | ||
|
|
6fa815c294 | ||
|
|
63ec518efb | ||
|
|
c4ca20dd90 | ||
|
|
e56cc4222b | ||
|
|
07d2944d7c | ||
|
|
dea01ae36a | ||
|
|
62ea5b54e2 | ||
|
|
e549a7e42f | ||
|
|
90e2ebb11f | ||
|
|
61baa1be7a | ||
|
|
b946fa665f | ||
|
|
2e049556e4 | ||
|
|
492a5e70d7 | ||
|
|
ba1f2771e8 | ||
|
|
7458fb4855 | ||
|
|
5f55f3938b | ||
|
|
3e8102485c | ||
|
|
861c682ea5 | ||
|
|
c8e5895dff | ||
|
|
82d72e7edb | ||
|
|
a3f8ecaaf1 | ||
|
|
19aada656c | ||
|
|
b21bb4280f | ||
|
|
bc0a4fdb5c | ||
|
|
37fb0e9254 | ||
|
|
aecfa1a174 | ||
|
|
b869d23d94 | ||
|
|
6ee3d44f76 | ||
|
|
462e554107 | ||
|
|
66afa73e6f | ||
|
|
54c604bcf4 | ||
|
|
c1be02ba54 | ||
|
|
d3c7ba8f45 | ||
|
|
039c4a0496 | ||
|
|
51a94b6bbc | ||
|
|
bbfbb94f52 | ||
|
|
d1eb9ef16f | ||
|
|
3db00f8bc3 |
@@ -80,6 +80,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
fineGrainedAuthz := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseFineGrainedAuthz, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
|
||||
Active: fineGrainedAuthz,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -144,6 +144,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
options={options}
|
||||
optionFilterProp="label"
|
||||
optionRender={(option): JSX.Element => (
|
||||
<Checkbox
|
||||
checked={value.includes(option.value as string)}
|
||||
@@ -162,6 +163,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
|
||||
return (
|
||||
<Select
|
||||
id={id}
|
||||
showSearch
|
||||
value={value || undefined}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
@@ -170,6 +172,7 @@ function RolesSelect(props: RolesSelectProps): JSX.Element {
|
||||
loading={loading}
|
||||
notFoundContent={notFoundContent}
|
||||
options={options}
|
||||
optionFilterProp="label"
|
||||
getPopupContainer={getPopupContainer}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
@@ -10,4 +10,5 @@ export enum FeatureKeys {
|
||||
ONBOARDING_V3 = 'onboarding_v3',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { BellDot, CircleAlert, ExternalLink, Save } from '@signozhq/icons';
|
||||
import { Button, FormInstance, Modal, SelectProps } from 'antd';
|
||||
import { Button, FormInstance, SelectProps } from 'antd';
|
||||
import { ConfirmDialog } from '@signozhq/ui/dialog';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
@@ -162,6 +163,7 @@ function FormAlertRules({
|
||||
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||
|
||||
const [detectionMethod, setDetectionMethod] = useState<string | null>(null);
|
||||
const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
||||
@@ -577,19 +579,16 @@ function FormAlertRules({
|
||||
});
|
||||
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries([
|
||||
await ruleCache.invalidateQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
|
||||
`${ruleId}`,
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setTimeout(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, 2000);
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
} catch (e) {
|
||||
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
|
||||
logData = {
|
||||
@@ -625,24 +624,9 @@ function FormAlertRules({
|
||||
urlQuery,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
const content = (
|
||||
<Typography.Text>
|
||||
{' '}
|
||||
{t('confirm_save_content_part1')}{' '}
|
||||
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
|
||||
{t('confirm_save_content_part2')}
|
||||
</Typography.Text>
|
||||
);
|
||||
Modal.confirm({
|
||||
icon: <CircleAlert size="md" />,
|
||||
title: t('confirm_save_title'),
|
||||
centered: true,
|
||||
content,
|
||||
onOk: saveRule,
|
||||
className: 'create-alert-modal',
|
||||
});
|
||||
}, [t, saveRule, currentQuery]);
|
||||
const onSaveHandler = useCallback(() => {
|
||||
setIsConfirmSaveOpen(true);
|
||||
}, []);
|
||||
|
||||
const onTestRuleHandler = useCallback(async () => {
|
||||
if (!isFormValid()) {
|
||||
@@ -988,6 +972,27 @@ function FormAlertRules({
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={isConfirmSaveOpen}
|
||||
onOpenChange={setIsConfirmSaveOpen}
|
||||
title={t('confirm_save_title')}
|
||||
titleIcon={<CircleAlert size={14} />}
|
||||
confirmText="OK"
|
||||
confirmColor="primary"
|
||||
onConfirm={async (): Promise<boolean> => {
|
||||
await saveRule();
|
||||
return true;
|
||||
}}
|
||||
onCancel={() => setIsConfirmSaveOpen(false)}
|
||||
width="narrow"
|
||||
>
|
||||
<Typography.Text>
|
||||
{t('confirm_save_content_part1')}{' '}
|
||||
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
|
||||
{t('confirm_save_content_part2')}
|
||||
</Typography.Text>
|
||||
</ConfirmDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
.actionContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actionBtn {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
height: unset;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
|
||||
:global(.ant-icon-btn) {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
composes: actionBtn;
|
||||
color: var(--danger-background) !important;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.deleteBtn:hover {
|
||||
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
|
||||
}
|
||||
|
||||
.deleteModal :global(.ant-modal-confirm-body) {
|
||||
align-items: center;
|
||||
}
|
||||
@@ -745,52 +745,6 @@
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 0px;
|
||||
|
||||
.dashboard-action-content {
|
||||
.section-1 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
height: unset;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
|
||||
.ant-icon-btn {
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-2 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
|
||||
.ant-typography {
|
||||
display: flex;
|
||||
padding: 12px 8px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--bg-cherry-400) !important;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
filterDashboards,
|
||||
} from './utils';
|
||||
|
||||
import styles from './DashboardActions.module.scss';
|
||||
import './DashboardList.styles.scss';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
@@ -436,57 +437,53 @@ function DashboardsList(): JSX.Element {
|
||||
{action && (
|
||||
<Popover
|
||||
content={
|
||||
<div className="dashboard-action-content">
|
||||
<section className="section-1">
|
||||
<Button
|
||||
type="text"
|
||||
className="action-btn"
|
||||
icon={<Expand size={12} />}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className="action-btn"
|
||||
icon={<SquareArrowOutUpRight size={12} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(getLink());
|
||||
}}
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className="action-btn"
|
||||
icon={<Link2 size={12} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(getLink()));
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className="action-btn"
|
||||
icon={<FileJson size={12} />}
|
||||
onClick={handleJsonExport}
|
||||
>
|
||||
Export JSON
|
||||
</Button>
|
||||
</section>
|
||||
<section className="section-2">
|
||||
<DeleteButton
|
||||
name={dashboard.name}
|
||||
id={dashboard.id}
|
||||
isLocked={dashboard.isLocked}
|
||||
createdBy={dashboard.createdBy}
|
||||
/>
|
||||
</section>
|
||||
<div className={styles.actionContent}>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.actionBtn}
|
||||
icon={<Expand size={12} />}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.actionBtn}
|
||||
icon={<SquareArrowOutUpRight size={12} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(getLink());
|
||||
}}
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.actionBtn}
|
||||
icon={<Link2 size={12} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(getLink()));
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.actionBtn}
|
||||
icon={<FileJson size={12} />}
|
||||
onClick={handleJsonExport}
|
||||
>
|
||||
Export JSON
|
||||
</Button>
|
||||
<DeleteButton
|
||||
name={dashboard.name}
|
||||
id={dashboard.id}
|
||||
isLocked={dashboard.isLocked}
|
||||
createdBy={dashboard.createdBy}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
placement="bottomRight"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.delete-modal {
|
||||
.ant-modal-confirm-body {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { CircleAlert, Trash2 } from '@signozhq/icons';
|
||||
import { Flex, Modal, Tooltip } from 'antd';
|
||||
import { Button, Modal, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -12,10 +12,8 @@ import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import styles from '../DashboardActions.module.scss';
|
||||
import { Data } from '../DashboardsList';
|
||||
import { TableLinkText } from './styles';
|
||||
|
||||
import './DeleteButton.styles.scss';
|
||||
|
||||
interface DeleteButtonProps {
|
||||
createdBy: string;
|
||||
@@ -85,7 +83,7 @@ export function DeleteButton({
|
||||
},
|
||||
},
|
||||
centered: true,
|
||||
className: 'delete-modal',
|
||||
className: styles.deleteModal,
|
||||
});
|
||||
}, [
|
||||
modal,
|
||||
@@ -109,10 +107,16 @@ export function DeleteButton({
|
||||
return '';
|
||||
};
|
||||
|
||||
const isDisabled = isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip placement="left" title={getDeleteTooltipContent()}>
|
||||
<TableLinkText
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.deleteBtn}
|
||||
icon={<Trash2 size={12} />}
|
||||
disabled={isDisabled}
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -120,13 +124,9 @@ export function DeleteButton({
|
||||
openConfirmationDialog();
|
||||
}
|
||||
}}
|
||||
className="delete-btn"
|
||||
disabled={isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor)}
|
||||
>
|
||||
<Flex align="center" justify="center" gap={4}>
|
||||
<Trash2 size={14} /> Delete dashboard
|
||||
</Flex>
|
||||
</TableLinkText>
|
||||
Delete Dashboard
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{contextHolder}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TableLinkText = styled.span<{ disabled: boolean }>`
|
||||
color: var(--destructive);
|
||||
cursor: ${({ disabled }): string => (disabled ? 'not-allowed' : 'pointer')};
|
||||
${({ disabled }): string => (disabled ? 'opacity: 0.5;' : '')}
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
`;
|
||||
@@ -21,14 +21,13 @@ import {
|
||||
buildRoleUpdatePermission,
|
||||
} from 'hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
|
||||
import type { AuthzResources } from '../utils';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { RoleType } from 'types/roles';
|
||||
import { handleApiError, toAPIError } from 'utils/errorUtils';
|
||||
|
||||
@@ -54,7 +53,8 @@ function RoleDetailsPage(): JSX.Element {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { activeLicense, isFetchingActiveLicense } = useAppContext();
|
||||
const { isRolesEnabled, isLoading: isRolesGateLoading } =
|
||||
useRolesFeatureGate();
|
||||
|
||||
const authzResources: AuthzResources = permissionsType.data;
|
||||
|
||||
@@ -161,7 +161,7 @@ function RoleDetailsPage(): JSX.Element {
|
||||
},
|
||||
});
|
||||
|
||||
if (isFetchingActiveLicense) {
|
||||
if (isRolesGateLoading) {
|
||||
return (
|
||||
<div className="role-details-page">
|
||||
<Skeleton
|
||||
@@ -173,7 +173,7 @@ function RoleDetailsPage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
if (activeLicense?.status !== LicenseStatus.VALID) {
|
||||
if (!isRolesEnabled) {
|
||||
return <Redirect to={ROUTES.ROLES_SETTINGS} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import {
|
||||
defaultFeatureFlags,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
waitFor,
|
||||
within,
|
||||
} from 'tests/test-utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import {
|
||||
invalidLicense,
|
||||
@@ -254,6 +256,34 @@ describe('RoleDetailsPage', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to the roles list when fine-grained authz flag is inactive', async () => {
|
||||
render(
|
||||
<Switch>
|
||||
<Route path="/settings/roles/:roleId">
|
||||
<RoleDetailsPage />
|
||||
</Route>
|
||||
<Route path="/settings/roles" exact>
|
||||
<div data-testid="roles-list-redirect-target" />
|
||||
</Route>
|
||||
</Switch>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: `/settings/roles/${CUSTOM_ROLE_ID}`,
|
||||
appContextOverrides: {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('roles-list-redirect-target'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('permission side panel', () => {
|
||||
beforeEach(() => {
|
||||
// Both hooks mocked so data renders synchronously — no React Query scheduler or MSW round-trip.
|
||||
|
||||
@@ -9,11 +9,10 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RoleListPermission } from 'hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { RoleType } from 'types/roles';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
@@ -32,8 +31,7 @@ interface RolesListingTableProps {
|
||||
function RolesListingTable({
|
||||
searchQuery,
|
||||
}: RolesListingTableProps): JSX.Element {
|
||||
const { activeLicense } = useAppContext();
|
||||
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
|
||||
const { isRolesEnabled } = useRolesFeatureGate();
|
||||
|
||||
const { permissions: listPerms, isLoading: isAuthZLoading } = useAuthZ([
|
||||
RoleListPermission,
|
||||
@@ -208,11 +206,11 @@ function RolesListingTable({
|
||||
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
|
||||
<div
|
||||
key={role.id}
|
||||
className={`roles-table-row${isValidLicense ? ' roles-table-row--clickable' : ''}`}
|
||||
role={isValidLicense ? 'button' : undefined}
|
||||
tabIndex={isValidLicense ? 0 : undefined}
|
||||
className={`roles-table-row${isRolesEnabled ? ' roles-table-row--clickable' : ''}`}
|
||||
role={isRolesEnabled ? 'button' : undefined}
|
||||
tabIndex={isRolesEnabled ? 0 : undefined}
|
||||
onClick={
|
||||
isValidLicense
|
||||
isRolesEnabled
|
||||
? (): void => {
|
||||
if (role.id) {
|
||||
navigateToRole(role.id, role.name);
|
||||
@@ -221,7 +219,7 @@ function RolesListingTable({
|
||||
: undefined
|
||||
}
|
||||
onKeyDown={
|
||||
isValidLicense
|
||||
isRolesEnabled
|
||||
? (e): void => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && role.id) {
|
||||
navigateToRole(role.id, role.name);
|
||||
|
||||
@@ -4,8 +4,7 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
import { RoleCreatePermission } from 'hooks/useAuthZ/permissions/role.permissions';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
|
||||
|
||||
import CreateRoleModal from './RolesComponents/CreateRoleModal';
|
||||
import RolesListingTable from './RolesComponents/RolesListingTable';
|
||||
@@ -15,8 +14,7 @@ import './RolesSettings.styles.scss';
|
||||
function RolesSettings(): JSX.Element {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const { activeLicense } = useAppContext();
|
||||
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
|
||||
const { isRolesEnabled } = useRolesFeatureGate();
|
||||
|
||||
return (
|
||||
<div className="roles-settings" data-testid="roles-settings">
|
||||
@@ -42,7 +40,7 @@ function RolesSettings(): JSX.Element {
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{isValidLicense && (
|
||||
{isRolesEnabled && (
|
||||
<AuthZTooltip checks={[RoleCreatePermission]}>
|
||||
<Button
|
||||
variant="solid"
|
||||
|
||||
@@ -4,7 +4,13 @@ import {
|
||||
} from 'mocks-server/__mockdata__/roles';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import {
|
||||
defaultFeatureFlags,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from 'tests/test-utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { invalidLicense, mockUseAuthZGrantAll } from 'tests/authz-test-utils';
|
||||
|
||||
@@ -176,6 +182,30 @@ describe('RolesSettings', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('hides the create button and disables row clicks when fine-grained authz flag is inactive', async () => {
|
||||
render(<RolesSettings />, undefined, {
|
||||
appContextOverrides: {
|
||||
featureFlags: defaultFeatureFlags.map((f) =>
|
||||
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
|
||||
? { ...f, active: false }
|
||||
: f,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
await expect(screen.findByText('signoz-admin')).resolves.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /custom role/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const rows = document.querySelectorAll('.roles-table-row');
|
||||
rows.forEach((row) => {
|
||||
expect(row).not.toHaveClass('roles-table-row--clickable');
|
||||
expect(row.getAttribute('role')).not.toBe('button');
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the create button and disables row clicks when license is not valid', async () => {
|
||||
render(<RolesSettings />, undefined, {
|
||||
appContextOverrides: { activeLicense: invalidLicense },
|
||||
|
||||
27
frontend/src/hooks/useRolesFeatureGate.ts
Normal file
27
frontend/src/hooks/useRolesFeatureGate.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
|
||||
export const useRolesFeatureGate = (): {
|
||||
isRolesEnabled: boolean;
|
||||
isLoading: boolean;
|
||||
} => {
|
||||
const {
|
||||
activeLicense,
|
||||
featureFlags,
|
||||
isFetchingActiveLicense,
|
||||
isFetchingFeatureFlags,
|
||||
} = useAppContext();
|
||||
|
||||
const isValidLicense = activeLicense?.status === LicenseStatus.VALID;
|
||||
const isFineGrainedAuthzEnabled =
|
||||
featureFlags?.find((f) => f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ)
|
||||
?.active ?? false;
|
||||
|
||||
return {
|
||||
isRolesEnabled: isValidLicense && isFineGrainedAuthzEnabled,
|
||||
isLoading:
|
||||
(isFetchingActiveLicense && !activeLicense) ||
|
||||
(isFetchingFeatureFlags && !featureFlags),
|
||||
};
|
||||
};
|
||||
@@ -105,6 +105,59 @@ jest.mock('react-i18next', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
export const defaultFeatureFlags = [
|
||||
{ name: FeatureKeys.SSO, active: true, usage: 0, usage_limit: -1, route: '' },
|
||||
{
|
||||
name: FeatureKeys.USE_SPAN_METRICS,
|
||||
active: false,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.GATEWAY,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.PREMIUM_SUPPORT,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.ONBOARDING,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.CHAT_SUPPORT,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.USE_FINE_GRAINED_AUTHZ,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
];
|
||||
|
||||
export function getAppContextMock(
|
||||
role: string,
|
||||
appContextOverrides?: Partial<IAppContext>,
|
||||
@@ -168,57 +221,7 @@ export function getAppContextMock(
|
||||
hasEditPermission: role === USER_ROLES.ADMIN || role === USER_ROLES.EDITOR,
|
||||
isFetchingUser: false,
|
||||
userFetchError: null,
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.SSO,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.USE_SPAN_METRICS,
|
||||
active: false,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.GATEWAY,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.PREMIUM_SUPPORT,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.ONBOARDING,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
{
|
||||
name: FeatureKeys.CHAT_SUPPORT,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
featureFlags: defaultFeatureFlags,
|
||||
isFetchingFeatureFlags: false,
|
||||
featureFlagsFetchError: null,
|
||||
hostsData: null,
|
||||
|
||||
@@ -9,7 +9,8 @@ var (
|
||||
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
@@ -70,6 +71,14 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureUseFineGrainedAuthz,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether fine-grained authorization is enabled",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -26,7 +26,7 @@ func buildClusterRecords(
|
||||
records := make([]inframonitoringtypes.ClusterRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
clusterName := labels[clusterNameAttrKey]
|
||||
clusterName := labels[inframonitoringtypes.ClusterNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.ClusterRecord{ // initialize with default values
|
||||
ClusterName: clusterName,
|
||||
@@ -87,6 +87,9 @@ func (m *module) getTopClusterGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,9 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// TODO(nikhilmantri0902): change to k8s.cluster.uid after showing the missing
|
||||
// data banner. Carried forward from v1 (see k8sClusterUIDAttrKey in
|
||||
// pkg/query-service/app/inframetrics/clusters.go).
|
||||
const clusterNameAttrKey = "k8s.cluster.name"
|
||||
|
||||
var clusterNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: clusterNameAttrKey,
|
||||
Name: inframonitoringtypes.ClusterNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildDaemonSetRecords(
|
||||
records := make([]inframonitoringtypes.DaemonSetRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
daemonSetName := labels[daemonSetNameAttrKey]
|
||||
daemonSetName := labels[inframonitoringtypes.DaemonSetNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.DaemonSetRecord{ // initialize with default values
|
||||
DaemonSetName: daemonSetName,
|
||||
@@ -95,6 +95,9 @@ func (m *module) getTopDaemonSetGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
daemonSetNameAttrKey = "k8s.daemonset.name"
|
||||
daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
|
||||
)
|
||||
const daemonSetsBaseFilterExpr = "k8s.daemonset.name != ''"
|
||||
|
||||
var daemonSetNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: daemonSetNameAttrKey,
|
||||
Name: inframonitoringtypes.DaemonSetNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildDeploymentRecords(
|
||||
records := make([]inframonitoringtypes.DeploymentRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
deploymentName := labels[deploymentNameAttrKey]
|
||||
deploymentName := labels[inframonitoringtypes.DeploymentNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.DeploymentRecord{ // initialize with default values
|
||||
DeploymentName: deploymentName,
|
||||
@@ -95,6 +95,9 @@ func (m *module) getTopDeploymentGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentNameAttrKey = "k8s.deployment.name"
|
||||
deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
|
||||
)
|
||||
const deploymentsBaseFilterExpr = "k8s.deployment.name != ''"
|
||||
|
||||
var deploymentNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: deploymentNameAttrKey,
|
||||
Name: inframonitoringtypes.DeploymentNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -190,6 +190,53 @@ func paginateWithBackfill(
|
||||
return pageGroups
|
||||
}
|
||||
|
||||
// paginateMetadataByName returns metadataMap groups sorted by name
|
||||
// (lexicographic, asc or desc), paginated by offset/limit, and rebuilt into
|
||||
// label maps using groupBy. When sortByMetaKey is non-empty, groups are sorted
|
||||
// by metadataMap[k][sortByMetaKey]; otherwise sorted by composite key directly.
|
||||
func paginateMetadataByName(
|
||||
metadataMap map[string]map[string]string,
|
||||
groupBy []qbtypes.GroupByKey,
|
||||
direction qbtypes.OrderDirection,
|
||||
offset, limit int,
|
||||
sortByMetaKey string,
|
||||
) []map[string]string {
|
||||
type entry struct{ compositeKey, sortVal string }
|
||||
entries := make([]entry, 0, len(metadataMap))
|
||||
for ck, meta := range metadataMap {
|
||||
sv := ck
|
||||
if sortByMetaKey != "" {
|
||||
sv = meta[sortByMetaKey]
|
||||
}
|
||||
entries = append(entries, entry{compositeKey: ck, sortVal: sv})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
if entries[i].sortVal != entries[j].sortVal {
|
||||
if direction == qbtypes.OrderDirectionAsc {
|
||||
return entries[i].sortVal < entries[j].sortVal
|
||||
}
|
||||
return entries[i].sortVal > entries[j].sortVal
|
||||
}
|
||||
return entries[i].compositeKey < entries[j].compositeKey
|
||||
})
|
||||
|
||||
if offset >= len(entries) {
|
||||
return nil
|
||||
}
|
||||
end := min(offset+limit, len(entries))
|
||||
|
||||
pageGroups := make([]map[string]string, 0, end-offset)
|
||||
for _, e := range entries[offset:end] {
|
||||
attrs := metadataMap[e.compositeKey]
|
||||
labels := make(map[string]string, len(groupBy))
|
||||
for _, gb := range groupBy {
|
||||
labels[gb.Name] = attrs[gb.Name]
|
||||
}
|
||||
pageGroups = append(pageGroups, labels)
|
||||
}
|
||||
return pageGroups
|
||||
}
|
||||
|
||||
// buildPageGroupsFilterExpr builds a filter expression that restricts results
|
||||
// to the given page of groups via IN clauses.
|
||||
// Returns e.g. "host.name IN ('h1','h2') AND os.type IN ('linux','windows')".
|
||||
|
||||
@@ -38,7 +38,7 @@ func (m *module) getPerGroupHostStatusCounts(
|
||||
uint64(req.Start), uint64(req.End), nil,
|
||||
)
|
||||
|
||||
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", hostNameAttrKey)
|
||||
hostNameExpr := fmt.Sprintf("JSONExtractString(labels, '%s')", inframonitoringtypes.HostNameAttrKey)
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
selectCols := make([]string, 0, len(req.GroupBy)+2)
|
||||
@@ -48,7 +48,7 @@ func (m *module) getPerGroupHostStatusCounts(
|
||||
)
|
||||
}
|
||||
|
||||
activeHostsSQ := m.getActiveHostsQuery(metricNames, hostNameAttrKey, sinceUnixMilli)
|
||||
activeHostsSQ := m.getActiveHostsQuery(metricNames, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
|
||||
selectCols = append(selectCols,
|
||||
fmt.Sprintf("uniqExactIf(%s, %s GLOBAL IN (%s)) AS active_host_count", hostNameExpr, hostNameExpr, sb.Var(activeHostsSQ)),
|
||||
fmt.Sprintf("uniqExactIf(%s, %s != '') AS total_host_count", hostNameExpr, hostNameExpr),
|
||||
@@ -142,7 +142,7 @@ func buildHostRecords(
|
||||
records := make([]inframonitoringtypes.HostRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
hostName := labels[hostNameAttrKey]
|
||||
hostName := labels[inframonitoringtypes.HostNameAttrKey]
|
||||
|
||||
activeStatus := inframonitoringtypes.HostStatusNone
|
||||
activeHostCount := 0
|
||||
@@ -216,6 +216,9 @@ func (m *module) getTopHostGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.HostNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.HostNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToHostsQueryNames[orderByKey]
|
||||
// The last entry is the formula/query whose value we sort by.
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
@@ -281,7 +284,7 @@ func (m *module) applyHostsActiveStatusFilter(req *inframonitoringtypes.Postable
|
||||
if req.Filter.FilterByStatus == inframonitoringtypes.HostStatusInactive {
|
||||
op = "NOT IN"
|
||||
}
|
||||
statusClause := fmt.Sprintf("%s %s (%s)", hostNameAttrKey, op, strings.Join(activeHosts, ", "))
|
||||
statusClause := fmt.Sprintf("%s %s (%s)", inframonitoringtypes.HostNameAttrKey, op, strings.Join(activeHosts, ", "))
|
||||
req.Filter.Expression = mergeFilterExpressions(req.Filter.Expression, statusClause)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,14 +7,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
hostNameAttrKey = "host.name"
|
||||
)
|
||||
|
||||
// Helper group-by key used across all queries.
|
||||
var hostNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: hostNameAttrKey,
|
||||
Name: inframonitoringtypes.HostNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildJobRecords(
|
||||
records := make([]inframonitoringtypes.JobRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
jobName := labels[jobNameAttrKey]
|
||||
jobName := labels[inframonitoringtypes.JobNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.JobRecord{ // initialize with default values
|
||||
JobName: jobName,
|
||||
@@ -103,6 +103,9 @@ func (m *module) getTopJobGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.JobNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
jobNameAttrKey = "k8s.job.name"
|
||||
jobsBaseFilterExpr = "k8s.job.name != ''"
|
||||
)
|
||||
const jobsBaseFilterExpr = "k8s.job.name != ''"
|
||||
|
||||
var jobNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: jobNameAttrKey,
|
||||
Name: inframonitoringtypes.JobNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -100,7 +100,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
// Determine active hosts: those with metrics reported in the last 10 minutes.
|
||||
// Compute the cutoff once so every downstream query/subquery agrees on what "active" means.
|
||||
sinceUnixMilli := time.Now().Add(-10 * time.Minute).UTC().UnixMilli()
|
||||
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, hostNameAttrKey, sinceUnixMilli)
|
||||
activeHostsMap, err := m.getActiveHosts(ctx, hostsTableMetricNamesList, inframonitoringtypes.HostNameAttrKey, sinceUnixMilli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (m *module) ListHosts(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
// When host.name is not in groupBy, we need to run an additional query to get the counts per group for the current page,
|
||||
// using the same filter expression as the main query (including user filters + page groups IN clause).
|
||||
hostCounts := make(map[string]groupHostStatusCounts)
|
||||
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, hostNameAttrKey)
|
||||
isHostNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.HostNameAttrKey)
|
||||
if !isHostNameInGroupBy {
|
||||
hostCounts, err = m.getPerGroupHostStatusCounts(ctx, req, hostsTableMetricNamesList, pageGroups, sinceUnixMilli)
|
||||
if err != nil {
|
||||
@@ -331,7 +331,7 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, nodeNameAttrKey)
|
||||
isNodeNameInGroupBy := isKeyInGroupByAttrs(req.GroupBy, inframonitoringtypes.NodeNameAttrKey)
|
||||
resp.Records = buildNodeRecords(isNodeNameInGroupBy, queryResp, pageGroups, req.GroupBy, metadataMap, nodeConditionCounts, podPhaseCounts)
|
||||
resp.Warning = queryResp.Warning
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func buildNamespaceRecords(
|
||||
records := make([]inframonitoringtypes.NamespaceRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
namespaceName := labels[namespaceNameAttrKey]
|
||||
namespaceName := labels[inframonitoringtypes.NamespaceNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.NamespaceRecord{ // initialize with default values
|
||||
NamespaceName: namespaceName,
|
||||
@@ -70,6 +70,9 @@ func (m *module) getTopNamespaceGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,13 +7,9 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
namespaceNameAttrKey = "k8s.namespace.name"
|
||||
)
|
||||
|
||||
var namespaceNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: namespaceNameAttrKey,
|
||||
Name: inframonitoringtypes.NamespaceNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@ func buildNodeRecords(
|
||||
records := make([]inframonitoringtypes.NodeRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
nodeName := labels[nodeNameAttrKey]
|
||||
nodeName := labels[inframonitoringtypes.NodeNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.NodeRecord{ // initialize with default values
|
||||
NodeName: nodeName,
|
||||
@@ -105,6 +105,9 @@ func (m *module) getTopNodeGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
@@ -199,7 +202,7 @@ func (m *module) getPerGroupNodeConditionCounts(
|
||||
timeSeriesFPs := sqlbuilder.NewSelectBuilder()
|
||||
timeSeriesFPsSelectCols := []string{
|
||||
"fingerprint",
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(nodeNameAttrKey)),
|
||||
fmt.Sprintf("JSONExtractString(labels, %s) AS node_name", timeSeriesFPs.Var(inframonitoringtypes.NodeNameAttrKey)),
|
||||
}
|
||||
for _, key := range req.GroupBy {
|
||||
timeSeriesFPsSelectCols = append(timeSeriesFPsSelectCols,
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
nodeNameAttrKey = "k8s.node.name"
|
||||
nodeConditionMetricName = "k8s.node.condition_ready"
|
||||
)
|
||||
const nodeConditionMetricName = "k8s.node.condition_ready"
|
||||
|
||||
var nodeNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: nodeNameAttrKey,
|
||||
Name: inframonitoringtypes.NodeNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -124,6 +124,9 @@ func (m *module) getTopPodGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.PodNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func buildStatefulSetRecords(
|
||||
records := make([]inframonitoringtypes.StatefulSetRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
statefulSetName := labels[statefulSetNameAttrKey]
|
||||
statefulSetName := labels[inframonitoringtypes.StatefulSetNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.StatefulSetRecord{ // initialize with default values
|
||||
StatefulSetName: statefulSetName,
|
||||
@@ -95,6 +95,9 @@ func (m *module) getTopStatefulSetGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
statefulSetNameAttrKey = "k8s.statefulset.name"
|
||||
statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
|
||||
)
|
||||
const statefulSetsBaseFilterExpr = "k8s.statefulset.name != ''"
|
||||
|
||||
var statefulSetNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: statefulSetNameAttrKey,
|
||||
Name: inframonitoringtypes.StatefulSetNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ func buildVolumeRecords(
|
||||
records := make([]inframonitoringtypes.VolumeRecord, 0, len(pageGroups))
|
||||
for _, labels := range pageGroups {
|
||||
compositeKey := compositeKeyFromLabels(labels, groupBy)
|
||||
pvcName := labels[persistentVolumeClaimNameAttrKey]
|
||||
pvcName := labels[inframonitoringtypes.PersistentVolumeClaimNameAttrKey]
|
||||
|
||||
record := inframonitoringtypes.VolumeRecord{ // initialize with default values
|
||||
PersistentVolumeClaimName: pvcName,
|
||||
@@ -75,6 +75,9 @@ func (m *module) getTopVolumeGroups(
|
||||
metadataMap map[string]map[string]string,
|
||||
) ([]map[string]string, error) {
|
||||
orderByKey := req.OrderBy.Key.Name
|
||||
if orderByKey == inframonitoringtypes.PersistentVolumeClaimNameAttrKey {
|
||||
return paginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PersistentVolumeClaimNameAttrKey), nil
|
||||
}
|
||||
queryNamesForOrderBy := orderByToVolumesQueryNames[orderByKey]
|
||||
rankingQueryName := queryNamesForOrderBy[len(queryNamesForOrderBy)-1]
|
||||
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
persistentVolumeClaimNameAttrKey = "k8s.persistentvolumeclaim.name"
|
||||
volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
|
||||
)
|
||||
const volumesBaseFilterExpr = "k8s.persistentvolumeclaim.name != ''"
|
||||
|
||||
var pvcNameGroupByKey = qbtypes.GroupByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: persistentVolumeClaimNameAttrKey,
|
||||
Name: inframonitoringtypes.PersistentVolumeClaimNameAttrKey,
|
||||
FieldContext: telemetrytypes.FieldContextResource,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
},
|
||||
|
||||
@@ -1784,6 +1784,15 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
fineGrainedAuthz := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureUseFineGrainedAuthz, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseFineGrainedAuthz.String()),
|
||||
Active: fineGrainedAuthz,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -2,9 +2,11 @@ package sqlrulestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@@ -14,10 +16,14 @@ import (
|
||||
|
||||
type maintenance struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewMaintenanceStore(store sqlstore.SQLStore) ruletypes.MaintenanceStore {
|
||||
return &maintenance{sqlstore: store}
|
||||
func NewMaintenanceStore(store sqlstore.SQLStore, providerSettings factory.ProviderSettings) ruletypes.MaintenanceStore {
|
||||
return &maintenance{
|
||||
sqlstore: store,
|
||||
logger: providerSettings.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string) ([]*ruletypes.PlannedMaintenance, error) {
|
||||
@@ -35,7 +41,11 @@ func (r *maintenance) ListPlannedMaintenance(ctx context.Context, orgID string)
|
||||
|
||||
gettablePlannedMaintenance := make([]*ruletypes.PlannedMaintenance, 0)
|
||||
for _, gettableMaintenancesRule := range gettableMaintenancesRules {
|
||||
gettablePlannedMaintenance = append(gettablePlannedMaintenance, gettableMaintenancesRule.ToPlannedMaintenance())
|
||||
m := gettableMaintenancesRule.ToPlannedMaintenance()
|
||||
gettablePlannedMaintenance = append(gettablePlannedMaintenance, m)
|
||||
if m.HasScheduleRecurrenceBoundsMismatch() {
|
||||
r.logger.WarnContext(ctx, "planned_downtime_recurrence_schedule_mismatch", slog.String("maintenance_id", m.ID.StringValue()))
|
||||
}
|
||||
}
|
||||
|
||||
return gettablePlannedMaintenance, nil
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewFactory(
|
||||
) factory.ProviderFactory[ruler.Ruler, ruler.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config ruler.Config) (ruler.Ruler, error) {
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore, providerSettings)
|
||||
|
||||
managerOpts := &rules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
|
||||
@@ -19,8 +19,8 @@ type Clusters struct {
|
||||
|
||||
type ClusterRecord struct {
|
||||
// TODO(nikhilmantri0902): once the underlying attr key is migrated to
|
||||
// k8s.cluster.uid (see clusterNameAttrKey TODO in implinframonitoring),
|
||||
// surface ClusterUID alongside (or replace) ClusterName.
|
||||
// k8s.cluster.uid (see ClusterNameAttrKey), surface ClusterUID alongside
|
||||
// (or replace) ClusterName.
|
||||
ClusterName string `json:"clusterName" required:"true"`
|
||||
ClusterCPU float64 `json:"clusterCPU" required:"true"`
|
||||
ClusterCPUAllocatable float64 `json:"clusterCPUAllocatable" required:"true"`
|
||||
@@ -88,6 +88,9 @@ func (req *PostableClusters) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == ClusterNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", ClusterNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const ClusterNameAttrKey = "k8s.cluster.name"
|
||||
|
||||
const (
|
||||
ClustersOrderByCPU = "cpu"
|
||||
ClustersOrderByCPUAllocatable = "cpu_allocatable"
|
||||
@@ -12,4 +14,5 @@ var ClustersValidOrderByKeys = []string{
|
||||
ClustersOrderByCPUAllocatable,
|
||||
ClustersOrderByMemory,
|
||||
ClustersOrderByMemoryAllocatable,
|
||||
ClusterNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -275,6 +275,57 @@ func TestPostableClusters_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableClusters{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: ClusterNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -88,6 +88,9 @@ func (req *PostableDaemonSets) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == DaemonSetNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", DaemonSetNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const DaemonSetNameAttrKey = "k8s.daemonset.name"
|
||||
|
||||
const (
|
||||
DaemonSetsOrderByCPU = "cpu"
|
||||
DaemonSetsOrderByCPURequest = "cpu_request"
|
||||
DaemonSetsOrderByCPULimit = "cpu_limit"
|
||||
DaemonSetsOrderByMemory = "memory"
|
||||
DaemonSetsOrderByMemoryRequest = "memory_request"
|
||||
DaemonSetsOrderByMemoryLimit = "memory_limit"
|
||||
DaemonSetsOrderByCPU = "cpu"
|
||||
DaemonSetsOrderByCPURequest = "cpu_request"
|
||||
DaemonSetsOrderByCPULimit = "cpu_limit"
|
||||
DaemonSetsOrderByMemory = "memory"
|
||||
DaemonSetsOrderByMemoryRequest = "memory_request"
|
||||
DaemonSetsOrderByMemoryLimit = "memory_limit"
|
||||
DaemonSetsOrderByDesiredNodes = "desired_nodes"
|
||||
DaemonSetsOrderByCurrentNodes = "current_nodes"
|
||||
)
|
||||
@@ -20,4 +22,5 @@ var DaemonSetsValidOrderByKeys = []string{
|
||||
DaemonSetsOrderByMemoryLimit,
|
||||
DaemonSetsOrderByDesiredNodes,
|
||||
DaemonSetsOrderByCurrentNodes,
|
||||
DaemonSetNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -257,6 +257,63 @@ func TestPostableDaemonSets_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableDaemonSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: DaemonSetNameAttrKey,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableDaemonSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: DaemonSetNameAttrKey,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableDaemonSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: DaemonSetNameAttrKey,
|
||||
},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -88,6 +88,9 @@ func (req *PostableDeployments) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == DeploymentNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", DeploymentNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const DeploymentNameAttrKey = "k8s.deployment.name"
|
||||
|
||||
const (
|
||||
DeploymentsOrderByCPU = "cpu"
|
||||
DeploymentsOrderByCPURequest = "cpu_request"
|
||||
@@ -20,4 +22,5 @@ var DeploymentsValidOrderByKeys = []string{
|
||||
DeploymentsOrderByMemoryLimit,
|
||||
DeploymentsOrderByDesiredPods,
|
||||
DeploymentsOrderByAvailablePods,
|
||||
DeploymentNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -257,6 +257,57 @@ func TestPostableDeployments_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableDeployments{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableDeployments{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableDeployments{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: DeploymentNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -100,6 +100,9 @@ func (req *PostableHosts) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == HostNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", HostNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -20,6 +20,8 @@ func (HostStatus) Enum() []any {
|
||||
}
|
||||
}
|
||||
|
||||
const HostNameAttrKey = "host.name"
|
||||
|
||||
const (
|
||||
HostsOrderByCPU = "cpu"
|
||||
HostsOrderByMemory = "memory"
|
||||
@@ -34,4 +36,5 @@ var HostsValidOrderByKeys = []string{
|
||||
HostsOrderByWait,
|
||||
HostsOrderByDiskUsage,
|
||||
HostsOrderByLoad15,
|
||||
HostNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -228,6 +228,57 @@ func TestHostsListRequest_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableHosts{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "os.type"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: HostNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -90,6 +90,9 @@ func (req *PostableJobs) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == JobNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", JobNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const JobNameAttrKey = "k8s.job.name"
|
||||
|
||||
const (
|
||||
JobsOrderByCPU = "cpu"
|
||||
JobsOrderByCPURequest = "cpu_request"
|
||||
@@ -24,4 +26,5 @@ var JobsValidOrderByKeys = []string{
|
||||
JobsOrderByActivePods,
|
||||
JobsOrderByFailedPods,
|
||||
JobsOrderBySuccessfulPods,
|
||||
JobNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -293,6 +293,57 @@ func TestPostableJobs_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableJobs{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableJobs{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableJobs{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: JobNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -82,6 +82,9 @@ func (req *PostableNamespaces) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == NamespaceNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", NamespaceNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const NamespaceNameAttrKey = "k8s.namespace.name"
|
||||
|
||||
const (
|
||||
NamespacesOrderByCPU = "cpu"
|
||||
NamespacesOrderByMemory = "memory"
|
||||
@@ -8,4 +10,5 @@ const (
|
||||
var NamespacesValidOrderByKeys = []string{
|
||||
NamespacesOrderByCPU,
|
||||
NamespacesOrderByMemory,
|
||||
NamespaceNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -221,6 +221,57 @@ func TestPostableNamespaces_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableNamespaces{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NamespaceNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -93,6 +93,9 @@ func (req *PostableNodes) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == NodeNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", NodeNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -27,6 +27,8 @@ const (
|
||||
NodeConditionNumNotReady = 0
|
||||
)
|
||||
|
||||
const NodeNameAttrKey = "k8s.node.name"
|
||||
|
||||
const (
|
||||
NodesOrderByCPU = "cpu"
|
||||
NodesOrderByCPUAllocatable = "cpu_allocatable"
|
||||
@@ -39,4 +41,5 @@ var NodesValidOrderByKeys = []string{
|
||||
NodesOrderByCPUAllocatable,
|
||||
NodesOrderByMemory,
|
||||
NodesOrderByMemoryAllocatable,
|
||||
NodeNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -239,6 +239,57 @@ func TestPostableNodes_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableNodes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableNodes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableNodes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.cluster.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: NodeNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -98,6 +98,9 @@ func (req *PostablePods) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == PodNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", PodNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -36,6 +36,8 @@ const (
|
||||
PodPhaseNumUnknown = 5
|
||||
)
|
||||
|
||||
const PodNameAttrKey = "k8s.pod.name"
|
||||
|
||||
const (
|
||||
PodsOrderByCPU = "cpu"
|
||||
PodsOrderByCPURequest = "cpu_request"
|
||||
@@ -52,4 +54,5 @@ var PodsValidOrderByKeys = []string{
|
||||
PodsOrderByMemory,
|
||||
PodsOrderByMemoryRequest,
|
||||
PodsOrderByMemoryLimit,
|
||||
PodNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -203,6 +203,57 @@ func TestPostablePods_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostablePods{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PodNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -88,6 +88,9 @@ func (req *PostableStatefulSets) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == StatefulSetNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", StatefulSetNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const StatefulSetNameAttrKey = "k8s.statefulset.name"
|
||||
|
||||
const (
|
||||
StatefulSetsOrderByCPU = "cpu"
|
||||
StatefulSetsOrderByCPURequest = "cpu_request"
|
||||
@@ -20,4 +22,5 @@ var StatefulSetsValidOrderByKeys = []string{
|
||||
StatefulSetsOrderByMemoryLimit,
|
||||
StatefulSetsOrderByDesiredPods,
|
||||
StatefulSetsOrderByCurrentPods,
|
||||
StatefulSetNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -257,6 +257,57 @@ func TestPostableStatefulSets_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableStatefulSets{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: StatefulSetNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -85,6 +85,9 @@ func (req *PostableVolumes) Validate() error {
|
||||
if req.OrderBy.Direction != qbtypes.OrderDirectionAsc && req.OrderBy.Direction != qbtypes.OrderDirectionDesc {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid order by direction: %s", req.OrderBy.Direction)
|
||||
}
|
||||
if req.OrderBy.Key.Name == PersistentVolumeClaimNameAttrKey && len(req.GroupBy) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "order by '%s' is only allowed when groupBy is empty", PersistentVolumeClaimNameAttrKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package inframonitoringtypes
|
||||
|
||||
const PersistentVolumeClaimNameAttrKey = "k8s.persistentvolumeclaim.name"
|
||||
|
||||
const (
|
||||
VolumesOrderByAvailable = "available"
|
||||
VolumesOrderByCapacity = "capacity"
|
||||
@@ -16,4 +18,5 @@ var VolumesValidOrderByKeys = []string{
|
||||
VolumesOrderByInodes,
|
||||
VolumesOrderByInodesFree,
|
||||
VolumesOrderByInodesUsed,
|
||||
PersistentVolumeClaimNameAttrKey,
|
||||
}
|
||||
|
||||
@@ -221,6 +221,57 @@ func TestPostableVolumes_Validate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orderBy name asc with empty groupBy is valid",
|
||||
req: &PostableVolumes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name desc with empty groupBy is valid",
|
||||
req: &PostableVolumes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionDesc,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "orderBy name with non-empty groupBy is rejected",
|
||||
req: &PostableVolumes{
|
||||
Start: 1000,
|
||||
End: 2000,
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "k8s.namespace.name"}},
|
||||
},
|
||||
OrderBy: &qbtypes.OrderBy{
|
||||
Key: qbtypes.OrderByKey{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: PersistentVolumeClaimNameAttrKey},
|
||||
},
|
||||
Direction: qbtypes.OrderDirectionAsc,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -11,9 +11,7 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeInvalidPlannedMaintenancePayload = errors.MustNewCode("invalid_planned_maintenance_payload")
|
||||
)
|
||||
var ErrCodeInvalidPlannedMaintenancePayload = errors.MustNewCode("invalid_planned_maintenance_payload")
|
||||
|
||||
type MaintenanceStatus struct {
|
||||
valuer.String
|
||||
@@ -133,6 +131,26 @@ type PlannedMaintenanceWithRules struct {
|
||||
Rules []*StorablePlannedMaintenanceRule `bun:"rel:has-many,join:id=planned_maintenance_id"`
|
||||
}
|
||||
|
||||
// HasScheduleRecurrenceBoundsMismatch reports whether a recurring maintenance
|
||||
// has different start/end bounds in Schedule and Schedule.Recurrence.
|
||||
//
|
||||
// This is used to detect if there are any entries with recurrence that don't
|
||||
// have the same timestamps stored at the schedule-level.
|
||||
// UI payloads duplicated those values in both places, but direct API users may
|
||||
// have stored bounds that are missing from, or different than, the schedule-level bounds.
|
||||
// We need to observe these before we can safely drop Recurrence.StartTime and
|
||||
// Recurrence.EndTime.
|
||||
func (m *PlannedMaintenance) HasScheduleRecurrenceBoundsMismatch() bool {
|
||||
recurrence := m.Schedule.Recurrence
|
||||
if recurrence == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return !recurrence.StartTime.Equal(m.Schedule.StartTime) ||
|
||||
(recurrence.EndTime == nil && !m.Schedule.EndTime.IsZero()) ||
|
||||
(recurrence.EndTime != nil && !recurrence.EndTime.Equal(m.Schedule.EndTime))
|
||||
}
|
||||
|
||||
func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
// Check if the alert ID is in the maintenance window
|
||||
found := false
|
||||
@@ -159,42 +177,43 @@ func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
currentTime := now.In(loc)
|
||||
startTime := m.Schedule.StartTime
|
||||
endTime := m.Schedule.EndTime
|
||||
recurrence := m.Schedule.Recurrence
|
||||
|
||||
// fixed schedule
|
||||
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
|
||||
startTime := m.Schedule.StartTime.In(loc)
|
||||
endTime := m.Schedule.EndTime.In(loc)
|
||||
if currentTime.Equal(startTime) || currentTime.Equal(endTime) ||
|
||||
(currentTime.After(startTime) && currentTime.Before(endTime)) {
|
||||
// fixed schedule — only when no recurrence is configured.
|
||||
// When recurrence is set, the recurring check below handles everything;
|
||||
// falling through here would cause the window to match the absolute
|
||||
// StartTime–EndTime range instead of the daily/weekly/monthly pattern.
|
||||
if recurrence == nil && !startTime.IsZero() && !endTime.IsZero() {
|
||||
if now.Equal(startTime) || now.Equal(endTime) ||
|
||||
(now.After(startTime) && now.Before(endTime)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// recurring schedule
|
||||
if m.Schedule.Recurrence != nil {
|
||||
start := m.Schedule.Recurrence.StartTime
|
||||
|
||||
if recurrence != nil {
|
||||
// Make sure the recurrence has started
|
||||
if currentTime.Before(start.In(loc)) {
|
||||
if now.Before(recurrence.StartTime) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if recurrence has expired
|
||||
if m.Schedule.Recurrence.EndTime != nil {
|
||||
endTime := *m.Schedule.Recurrence.EndTime
|
||||
if !endTime.IsZero() && currentTime.After(endTime.In(loc)) {
|
||||
if recurrence.EndTime != nil {
|
||||
if !recurrence.EndTime.IsZero() && now.After(*recurrence.EndTime) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
switch m.Schedule.Recurrence.RepeatType {
|
||||
currentTime := now.In(loc)
|
||||
switch recurrence.RepeatType {
|
||||
case RepeatTypeDaily:
|
||||
return m.checkDaily(currentTime, m.Schedule.Recurrence, loc)
|
||||
return m.checkDaily(currentTime, recurrence, loc)
|
||||
case RepeatTypeWeekly:
|
||||
return m.checkWeekly(currentTime, m.Schedule.Recurrence, loc)
|
||||
return m.checkWeekly(currentTime, recurrence, loc)
|
||||
case RepeatTypeMonthly:
|
||||
return m.checkMonthly(currentTime, m.Schedule.Recurrence, loc)
|
||||
return m.checkMonthly(currentTime, recurrence, loc)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ func timePtr(t time.Time) *time.Time {
|
||||
}
|
||||
|
||||
func TestShouldSkipMaintenance(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
maintenance *PlannedMaintenance
|
||||
@@ -499,7 +498,7 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 04, 1, 12, 10, 0, 0, time.UTC),
|
||||
ts: time.Date(2024, 4, 1, 12, 10, 0, 0, time.UTC),
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
@@ -508,14 +507,14 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeWeekly,
|
||||
RepeatOn: []RepeatOn{RepeatOnMonday},
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 04, 15, 12, 10, 0, 0, time.UTC),
|
||||
ts: time.Date(2024, 4, 15, 12, 10, 0, 0, time.UTC),
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
@@ -524,14 +523,14 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeWeekly,
|
||||
RepeatOn: []RepeatOn{RepeatOnMonday},
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 04, 14, 12, 10, 0, 0, time.UTC), // 14th 04 is sunday
|
||||
ts: time.Date(2024, 4, 14, 12, 10, 0, 0, time.UTC), // 14th 04 is sunday
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
@@ -540,14 +539,14 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeWeekly,
|
||||
RepeatOn: []RepeatOn{RepeatOnMonday},
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 04, 16, 12, 10, 0, 0, time.UTC), // 16th 04 is tuesday
|
||||
ts: time.Date(2024, 4, 16, 12, 10, 0, 0, time.UTC), // 16th 04 is tuesday
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
@@ -556,14 +555,14 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeWeekly,
|
||||
RepeatOn: []RepeatOn{RepeatOnMonday},
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 05, 06, 12, 10, 0, 0, time.UTC),
|
||||
ts: time.Date(2024, 5, 6, 12, 10, 0, 0, time.UTC),
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
@@ -572,14 +571,14 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeWeekly,
|
||||
RepeatOn: []RepeatOn{RepeatOnMonday},
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 05, 06, 14, 00, 0, 0, time.UTC),
|
||||
ts: time.Date(2024, 5, 6, 14, 0, 0, 0, time.UTC),
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
@@ -588,13 +587,13 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeMonthly,
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 04, 04, 12, 10, 0, 0, time.UTC),
|
||||
ts: time.Date(2024, 4, 4, 12, 10, 0, 0, time.UTC),
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
@@ -603,13 +602,13 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeMonthly,
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 04, 04, 14, 10, 0, 0, time.UTC),
|
||||
ts: time.Date(2024, 4, 4, 14, 10, 0, 0, time.UTC),
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
@@ -618,13 +617,52 @@ func TestShouldSkipMaintenance(t *testing.T) {
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
|
||||
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeMonthly,
|
||||
},
|
||||
},
|
||||
},
|
||||
ts: time.Date(2024, 05, 04, 12, 10, 0, 0, time.UTC),
|
||||
ts: time.Date(2024, 5, 4, 12, 10, 0, 0, time.UTC),
|
||||
skip: true,
|
||||
},
|
||||
// The recurrence should govern, when set. Not the fixed range.
|
||||
{
|
||||
name: "recurring-daily-with-fixed-times-outside-daily-window",
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
// These fixed fields should be ignored when Recurrence is set.
|
||||
StartTime: time.Date(2026, 4, 1, 10, 0, 0, 0, time.UTC),
|
||||
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC), // daily at 14:00
|
||||
Duration: valuer.MustParseTextDuration("2h"), // until 16:00
|
||||
RepeatType: RepeatTypeDaily,
|
||||
},
|
||||
},
|
||||
},
|
||||
// 11:00 is inside the fixed range but outside the daily 14:00-16:00 window.
|
||||
// Before the fix this returned true (bug); after fix it returns false.
|
||||
ts: time.Date(2026, 4, 15, 11, 0, 0, 0, time.UTC),
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "recurring-daily-with-fixed-times-inside-daily-window",
|
||||
maintenance: &PlannedMaintenance{
|
||||
Schedule: &Schedule{
|
||||
Timezone: "UTC",
|
||||
StartTime: time.Date(2026, 4, 1, 10, 0, 0, 0, time.UTC),
|
||||
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
|
||||
Recurrence: &Recurrence{
|
||||
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC),
|
||||
Duration: valuer.MustParseTextDuration("2h"),
|
||||
RepeatType: RepeatTypeDaily,
|
||||
},
|
||||
},
|
||||
},
|
||||
// 15:00 is inside the daily 14:00-16:00 window — should skip.
|
||||
ts: time.Date(2026, 4, 15, 15, 0, 0, 0, time.UTC),
|
||||
skip: true,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user