Compare commits

..

1 Commits

Author SHA1 Message Date
nityanandagohain
ef748e9ce2 fix: add userauditable to ttl setting 2026-05-11 10:28:38 +05:30
19 changed files with 548 additions and 354 deletions

View File

@@ -4647,6 +4647,10 @@ components:
properties:
duration:
type: string
endTime:
format: date-time
nullable: true
type: string
repeatOn:
items:
$ref: '#/components/schemas/RuletypesRepeatOn'
@@ -4654,7 +4658,11 @@ components:
type: array
repeatType:
$ref: '#/components/schemas/RuletypesRepeatType'
startTime:
format: date-time
type: string
required:
- startTime
- duration
- repeatType
type: object
@@ -4818,7 +4826,6 @@ components:
type: string
required:
- timezone
- startTime
type: object
RuletypesScheduleType:
enum:

View File

@@ -6881,12 +6881,23 @@ export interface RuletypesRecurrenceDTO {
* @type string
*/
duration: string;
/**
* @type string
* @format date-time
* @nullable true
*/
endTime?: Date | null;
/**
* @type array
* @nullable true
*/
repeatOn?: RuletypesRepeatOnDTO[] | null;
repeatType: RuletypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: Date;
}
export interface RuletypesRenotifyDTO {
@@ -7068,7 +7079,7 @@ export interface RuletypesScheduleDTO {
* @type string
* @format date-time
*/
startTime: Date;
startTime?: Date;
/**
* @type string
*/

View File

@@ -18,7 +18,6 @@ import { Search } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { USER_ROLES } from 'types/roles';
import { DeepPartial } from 'utils/types';
import 'dayjs/locale/en';
@@ -26,7 +25,7 @@ import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
import { PlannedDowntimeForm } from './PlannedDowntimeForm';
import { PlannedDowntimeList } from './PlannedDowntimeList';
import {
defaultInitialValues,
defautlInitialValues,
deleteDowntimeHandler,
} from './PlannedDowntimeutils';
@@ -50,8 +49,8 @@ export function PlannedDowntime(): JSX.Element {
const urlQuery = useUrlQuery();
const [initialValues, setInitialValues] =
useState<DeepPartial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
defaultInitialValues,
useState<Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>>(
defautlInitialValues,
);
const downtimeSchedules = useListDowntimeSchedules();
@@ -151,7 +150,7 @@ export function PlannedDowntime(): JSX.Element {
icon={<PlusOutlined />}
type="primary"
onClick={(): void => {
setInitialValues({ ...defaultInitialValues, editMode: false });
setInitialValues({ ...defautlInitialValues, editMode: false });
setIsOpen(true);
setEditMode(false);
form.resetFields();

View File

@@ -1,10 +1,4 @@
import React, {
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { CheckOutlined } from '@ant-design/icons';
import {
Button,
@@ -44,8 +38,6 @@ import { defaultTo, isEmpty } from 'lodash-es';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ALL_TIME_ZONES } from 'utils/timeZoneUtil';
import { type PanelMode } from 'rc-picker/lib/interface';
import { DeepPartial } from 'utils/types';
import 'dayjs/locale/en';
@@ -77,13 +69,14 @@ interface PlannedDowntimeFormData {
endTime: dayjs.Dayjs | string;
recurrence?: RuletypesRecurrenceDTO | null;
alertRules: DefaultOptionType[];
recurrenceSelect?: RuletypesRecurrenceDTO;
timezone?: string;
}
const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
interface PlannedDowntimeFormProps {
initialValues: DeepPartial<
initialValues: Partial<
RuletypesPlannedMaintenanceDTO & {
editMode: boolean;
}
@@ -95,7 +88,7 @@ interface PlannedDowntimeFormProps {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
refetchAllSchedules: () => void;
isEditMode: boolean;
form: FormInstance;
form: FormInstance<any>;
}
export function PlannedDowntimeForm(
@@ -139,13 +132,14 @@ export function PlannedDowntimeForm(
const { notifications } = useNotifications();
const { showErrorModal } = useErrorModal();
const datePickerFooter = (mode: PanelMode): ReactNode =>
const datePickerFooter = (mode: any): any =>
mode === 'time' ? (
<span style={{ color: 'gray' }}>Please select the time</span>
) : null;
const saveHandler = useCallback(
const saveHanlder = useCallback(
async (values: PlannedDowntimeFormData) => {
const shouldKeepLocalTime = !isEditMode;
const data: RuletypesPostablePlannedMaintenanceDTO = {
alertIds: values.alertRules
.map((alert) => alert.value)
@@ -157,6 +151,7 @@ export function PlannedDowntimeForm(
values.startTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
),
timezone: values.timezone as string,
@@ -166,6 +161,7 @@ export function PlannedDowntimeForm(
values.endTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
)
: undefined,
@@ -206,24 +202,38 @@ export function PlannedDowntimeForm(
],
);
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
const { recurrence } = values;
const recurrenceData =
!recurrence ||
recurrence.repeatType === recurrenceOptions.doesNotRepeat.value
values?.recurrence?.repeatType === recurrenceOptions.doesNotRepeat.value
? undefined
: {
duration: recurrence.duration
? `${recurrence.duration}${durationUnit}`
duration: values.recurrence?.duration
? `${values.recurrence?.duration}${durationUnit}`
: undefined,
repeatOn: recurrence.repeatOn?.length ? recurrence.repeatOn : undefined,
repeatType: recurrence.repeatType,
endTime: !isEmpty(values.endTime)
? handleTimeConversion(
values.endTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
)
: undefined,
startTime: handleTimeConversion(
values.startTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
),
repeatOn: !values.recurrence?.repeatOn?.length
? undefined
: values.recurrence?.repeatOn,
repeatType: values.recurrence?.repeatType,
};
const payloadValues = {
...values,
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
};
await saveHandler(payloadValues);
await saveHanlder(payloadValues);
};
const formValidationRules = [
@@ -276,7 +286,7 @@ export function PlannedDowntimeForm(
: '',
recurrence: {
...initialValues.schedule?.recurrence,
repeatType: (!isScheduleRecurring(initialValues.schedule)
repeatType: (!isScheduleRecurring(initialValues?.schedule)
? recurrenceOptions.doesNotRepeat.value
: initialValues.schedule?.recurrence
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
@@ -306,6 +316,7 @@ export function PlannedDowntimeForm(
const getTimezoneFormattedTime = (
time: string | dayjs.Dayjs,
timeZone?: string,
isEditMode?: boolean,
format?: string,
): string => {
if (!time) {
@@ -314,11 +325,20 @@ export function PlannedDowntimeForm(
if (!timeZone) {
return dayjs(time).format(format);
}
return dayjs(time).tz(timeZone).format(format);
return dayjs(time).tz(timeZone, isEditMode).format(format);
};
const startTimeText = useMemo((): string => {
let startTime = formData.startTime;
let startTime = formData?.startTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
startTime =
(formData?.recurrence?.startTime
? dayjs(formData.recurrence.startTime).toISOString()
: '') ||
formData?.startTime ||
'';
}
if (!startTime) {
return '';
}
@@ -328,6 +348,7 @@ export function PlannedDowntimeForm(
startTime,
timezoneInitialValue,
formData?.timezone,
!isEditMode,
);
}
const daysOfWeek = formData?.recurrence?.repeatOn;
@@ -335,16 +356,21 @@ export function PlannedDowntimeForm(
const formattedStartTime = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
TIME_FORMAT,
);
const formattedStartDate = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
DATE_FORMAT,
);
const ordinalFormat = getTimezoneFormattedTime(
startTime,
formData.timezone,
!isEditMode,
ORDINAL_FORMAT,
);
@@ -361,10 +387,21 @@ export function PlannedDowntimeForm(
default:
return `Scheduled for ${formattedStartDate} starting at ${formattedStartTime}.`;
}
}, [formData, recurrenceType, timezoneInitialValue]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
const endTimeText = useMemo((): string => {
let endTime = formData?.endTime;
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
endTime =
(formData?.recurrence?.endTime
? dayjs(formData.recurrence.endTime).toISOString()
: '') || '';
if (!isEditMode && !endTime) {
endTime = formData?.endTime || '';
}
}
if (!endTime) {
return '';
}
@@ -374,21 +411,25 @@ export function PlannedDowntimeForm(
endTime,
timezoneInitialValue,
formData?.timezone,
!isEditMode,
);
}
const formattedEndTime = getTimezoneFormattedTime(
endTime,
formData.timezone,
!isEditMode,
TIME_FORMAT,
);
const formattedEndDate = getTimezoneFormattedTime(
endTime,
formData.timezone,
!isEditMode,
DATE_FORMAT,
);
return `Scheduled to end maintenance on ${formattedEndDate} at ${formattedEndTime}.`;
}, [formData, timezoneInitialValue]);
}, [formData, recurrenceType, isEditMode, timezoneInitialValue]);
return (
<Modal
@@ -423,7 +464,7 @@ export function PlannedDowntimeForm(
name="startTime"
rules={formValidationRules}
className={!isEmpty(startTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value) => ({
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>
@@ -504,7 +545,7 @@ export function PlannedDowntimeForm(
},
]}
className={!isEmpty(endTimeText) ? 'formItemWithBullet' : ''}
getValueProps={(value) => ({
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
>

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useEffect } from 'react';
import { ReactNode, useEffect } from 'react';
import { UseQueryResult } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Collapse, Flex, Space, Table, TableProps, Tag, Tooltip } from 'antd';
@@ -18,9 +18,8 @@ import { defaultTo } from 'lodash-es';
import { CalendarClock, PenLine, Trash2 } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { showErrorNotification } from 'utils/error';
import { DeepPartial } from 'utils/types';
import { showErrorNotification } from '../../utils/error';
import {
formatDateTime,
getAlertOptionsFromIds,
@@ -28,6 +27,7 @@ import {
getEndTime,
recurrenceInfo,
} from './PlannedDowntimeutils';
import './PlannedDowntime.styles.scss';
const { Panel } = Collapse;
@@ -136,7 +136,7 @@ export function CollapseListContent({
created_at?: string;
created_by_name?: string;
created_by_email?: string;
timeframe: [string | undefined, string | undefined];
timeframe: [string | undefined | null, string | undefined | null];
repeats?: RuletypesRecurrenceDTO | null;
updated_at?: string;
updated_by_name?: string;
@@ -192,12 +192,7 @@ export function CollapseListContent({
),
)}
{renderItems('Timezone', <Typography>{timezone || '-'}</Typography>)}
{renderItems(
'Repeats',
<Typography>
{recurrenceInfo(timeframe[0], timeframe[1], repeats)}
</Typography>,
)}
{renderItems('Repeats', <Typography>{recurrenceInfo(repeats)}</Typography>)}
{renderItems(
'Alerts silenced',
alertOptions?.length ? (
@@ -217,7 +212,7 @@ export function CollapseListContent({
export function CustomCollapseList(
props: DowntimeSchedulesTableData & {
setInitialValues: React.Dispatch<
React.SetStateAction<DeepPartial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;
@@ -288,7 +283,9 @@ export function CustomCollapseList(
schedule?.startTime?.toString(),
typeof endTime === 'string' ? endTime : endTime?.toString(),
]}
repeats={schedule?.recurrence}
repeats={
schedule?.recurrence as RuletypesRecurrenceDTO | null | undefined
}
updated_at={updatedAt ? dayjs(updatedAt).toISOString() : ''}
updated_by_name={defaultTo(updatedBy, '')}
alertOptions={alertOptions}
@@ -323,7 +320,7 @@ export function PlannedDowntimeList({
>;
alertOptions: DefaultOptionType[];
setInitialValues: React.Dispatch<
React.SetStateAction<DeepPartial<RuletypesPlannedMaintenanceDTO>>
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: string, name: string) => void;

View File

@@ -2,7 +2,7 @@ import { UseMutateAsyncFunction } from 'react-query';
import type { NotificationInstance } from 'antd/es/notification/interface';
import type { DefaultOptionType } from 'antd/es/select';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import {
import type {
DeleteDowntimeScheduleByIDPathParameters,
RenderErrorResponseDTO,
RuletypesPlannedMaintenanceDTO,
@@ -14,7 +14,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import dayjs from 'dayjs';
import { isEmpty, isEqual } from 'lodash-es';
import APIError from 'types/api/error';
import { DeepPartial } from 'utils/types';
type DateTimeString = string | null | undefined;
@@ -61,15 +60,13 @@ export const getAlertOptionsFromIds = (
);
export const recurrenceInfo = (
startTime?: string,
endTime?: string,
recurrence?: RuletypesRecurrenceDTO | null,
): string => {
if (!recurrence) {
return 'No';
}
const { duration, repeatOn, repeatType } = recurrence;
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
const formattedStartTime = startTime
? formatDateTime(dayjs(startTime).toISOString())
@@ -83,7 +80,7 @@ export const recurrenceInfo = (
return `Repeats - ${repeatType} ${weeklyRepeatString} from ${formattedStartTime} ${formattedEndTime} ${durationString}`;
};
export const defaultInitialValues: DeepPartial<
export const defautlInitialValues: Partial<
RuletypesPlannedMaintenanceDTO & { editMode: boolean }
> = {
name: '',
@@ -213,17 +210,39 @@ export const recurrenceOptionWithSubmenu: Option[] = [
recurrenceOptions.monthly,
];
export const getRecurrenceOptionFromValue = (
value?: string | Option | null,
): Option | null | undefined => {
if (!value) {
return null;
}
if (typeof value === 'string') {
return Object.values(recurrenceOptions).find(
(option) => option.value === value,
);
}
return value;
};
export const getEndTime = ({
kind,
schedule,
}: DeepPartial<
}: Partial<
RuletypesPlannedMaintenanceDTO & {
editMode: boolean;
}
>): string | dayjs.Dayjs =>
schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '';
>): string | dayjs.Dayjs => {
if (kind === 'fixed') {
return schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '';
}
return schedule?.recurrence?.endTime
? dayjs(schedule.recurrence.endTime).toISOString()
: '';
};
export const isScheduleRecurring = (
schedule?: DeepPartial<RuletypesPlannedMaintenanceDTO['schedule']> | null,
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {
@@ -253,6 +272,7 @@ export function handleTimeConversion(
dateValue: string | dayjs.Dayjs,
timezoneInit?: string,
timezone?: string,
shouldKeepLocalTime?: boolean,
): string {
const timezoneChanged = !isEqual(timezoneInit, timezone);
const initialTime = dayjs(dateValue).tz(timezoneInit);
@@ -260,5 +280,5 @@ export function handleTimeConversion(
const formattedTime = formatWithTimezone(initialTime, timezone);
return timezoneChanged
? formattedTime
: dayjs(dateValue).tz(timezone).format();
: dayjs(dateValue).tz(timezone, shouldKeepLocalTime).format();
}

View File

@@ -8,7 +8,7 @@ import {
} from 'api/generated/services/sigNoz.schemas';
export const buildSchedule = (
schedule: RuletypesScheduleDTO,
schedule: Partial<RuletypesScheduleDTO>,
): RuletypesScheduleDTO => ({
timezone: schedule?.timezone ?? '',
startTime: schedule?.startTime,
@@ -17,13 +17,16 @@ export const buildSchedule = (
});
export const createMockDowntime = (
overrides: Partial<RuletypesPlannedMaintenanceDTO> &
Pick<RuletypesPlannedMaintenanceDTO, 'schedule'>,
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
): RuletypesPlannedMaintenanceDTO => ({
id: overrides.id ?? '0',
name: overrides.name ?? '',
description: overrides.description ?? '',
schedule: overrides.schedule,
schedule: buildSchedule({
timezone: 'UTC',
startTime: new Date('2024-01-01'),
...overrides.schedule,
}),
alertIds: overrides.alertIds ?? [],
createdAt: overrides.createdAt,
createdBy: overrides.createdBy ?? '',

View File

@@ -19,6 +19,7 @@ export type ServerError = 500;
export type SuccessStatusCode = Created | Success | SuccessNoContent;
export type ErrorStatusCode =
| Forbidden
| Forbidden
| Unauthorized
| NotFound

View File

@@ -1,22 +0,0 @@
type Builtin =
| string
| number
| boolean
| bigint
| symbol
| null
| undefined
| Function // eslint-disable-line
| Date
| RegExp
| Error;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;

View File

@@ -1343,7 +1343,7 @@ func getLocalTableName(tableName string) string {
}
func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
instrumentationtypes.CodeNamespace: "clickhouse-reader",
@@ -1434,6 +1434,10 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),
@@ -1511,7 +1515,7 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
return &model.SetTTLResponseItem{Message: "move ttl has been successfully set up"}, nil
}
func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalTraces.StringValue(),
instrumentationtypes.CodeNamespace: "clickhouse-reader",
@@ -1572,6 +1576,10 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),
@@ -1687,7 +1695,7 @@ func (r *ClickHouseReader) hasCustomRetentionColumn(ctx context.Context) (bool,
return true, nil
}
func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error) {
func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, userID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalLogs.StringValue(),
@@ -1718,7 +1726,7 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m
ttlParams.ToColdStorageDuration = 0
}
ttlResult, apiErr := r.SetTTL(ctx, orgID, ttlParams)
ttlResult, apiErr := r.SetTTL(ctx, orgID, userID, ttlParams)
if apiErr != nil {
return nil, errorsV2.Wrapf(apiErr.Err, errorsV2.TypeInternal, errorsV2.CodeInternal, "failed to set standard TTL")
}
@@ -1847,6 +1855,10 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: params.DefaultTTLDays,
@@ -2185,24 +2197,24 @@ func (r *ClickHouseReader) validateTTLConditions(ctx context.Context, ttlConditi
// SetTTL sets the TTL for traces or metrics or logs tables.
// This is an async API which creates goroutines to set TTL.
// Status of TTL update is tracked with ttl_status table in sqlite db.
func (r *ClickHouseReader) SetTTL(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) SetTTL(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
// Keep only latest 100 transactions/requests
r.deleteTtlTransactions(ctx, orgID, 100)
switch params.Type {
case constants.TraceTTL:
return r.setTTLTraces(ctx, orgID, params)
return r.setTTLTraces(ctx, orgID, userID, params)
case constants.MetricsTTL:
return r.setTTLMetrics(ctx, orgID, params)
return r.setTTLMetrics(ctx, orgID, userID, params)
case constants.LogsTTL:
return r.setTTLLogs(ctx, orgID, params)
return r.setTTLLogs(ctx, orgID, userID, params)
default:
return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error while setting ttl. ttl type should be <metrics|traces>, got %v", params.Type)}
}
}
func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, userID string, params *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.TelemetrySignal: telemetrytypes.SignalMetrics.StringValue(),
instrumentationtypes.CodeNamespace: "clickhouse-reader",
@@ -2244,6 +2256,10 @@ func (r *ClickHouseReader) setTTLMetrics(ctx context.Context, orgID string, para
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
UserAuditable: types.UserAuditable{
CreatedBy: userID,
UpdatedBy: userID,
},
TransactionID: uuid,
TableName: tableName,
TTL: int(params.DelDuration),

View File

@@ -1655,7 +1655,7 @@ func (aH *APIHandler) setTTL(w http.ResponseWriter, r *http.Request) {
}
// Context is not used here as TTL is long duration DB operation
result, apiErr := aH.reader.SetTTL(context.Background(), claims.OrgID, ttlParams)
result, apiErr := aH.reader.SetTTL(context.Background(), claims.OrgID, claims.UserID, ttlParams)
if apiErr != nil {
if apiErr.Typ == model.ErrorConflict {
aH.HandleError(w, apiErr.Err, http.StatusConflict)
@@ -1684,7 +1684,7 @@ func (aH *APIHandler) setCustomRetentionTTL(w http.ResponseWriter, r *http.Reque
}
// Context is not used here as TTL is long duration DB operation
result, apiErr := aH.reader.SetTTLV2(context.Background(), claims.OrgID, &params)
result, apiErr := aH.reader.SetTTLV2(context.Background(), claims.OrgID, claims.UserID, &params)
if apiErr != nil {
render.Error(w, errorsV2.New(errorsV2.TypeInvalidInput, errorsV2.CodeInternal, apiErr.Error()))
return

View File

@@ -46,8 +46,8 @@ type Reader interface {
GetFlamegraphSpansForTrace(ctx context.Context, orgID valuer.UUID, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, error)
// Setter Interfaces
SetTTL(ctx context.Context, orgID string, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError)
SetTTLV2(ctx context.Context, orgID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error)
SetTTL(ctx context.Context, orgID string, userID string, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError)
SetTTLV2(ctx context.Context, orgID string, userID string, params *model.CustomRetentionTTLParams) (*model.CustomRetentionTTLResponse, error)
FetchTemporality(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string]map[v3.Temporality]bool, error)
GetMetricAggregateAttributes(ctx context.Context, orgID valuer.UUID, req *v3.AggregateAttributeRequest, skipSignozMetrics bool) (*v3.AggregateAttributeResponse, error)

View File

@@ -196,6 +196,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewDropUserDeletedAtFactory(sqlstore, sqlschema),
sqlmigration.NewMigrateAWSAllRegionsFactory(sqlstore),
sqlmigration.NewAddServiceAccountManagedRoleTransactionsFactory(sqlstore),
sqlmigration.NewUpdateTTLSettingUserAuditFactory(sqlstore, sqlschema),
)
}

View File

@@ -0,0 +1,84 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type updateTTLSettingUserAudit struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewUpdateTTLSettingUserAuditFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("update_ttl_setting_user_audit"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newUpdateTTLSettingUserAudit(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newUpdateTTLSettingUserAudit(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &updateTTLSettingUserAudit{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
}
func (migration *updateTTLSettingUserAudit) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *updateTTLSettingUserAudit) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("ttl_setting"))
if err != nil {
return err
}
columns := []*sqlschema.Column{
{
Name: sqlschema.ColumnName("created_by"),
DataType: sqlschema.DataTypeText,
Nullable: true,
},
{
Name: sqlschema.ColumnName("updated_by"),
DataType: sqlschema.DataTypeText,
Nullable: true,
},
}
for _, column := range columns {
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, nil)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *updateTTLSettingUserAudit) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -9,6 +9,7 @@ type TTLSetting struct {
bun.BaseModel `bun:"table:ttl_setting"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
TransactionID string `bun:"transaction_id,type:text,notnull"`
TableName string `bun:"table_name,type:text,notnull"`
TTL int `bun:"ttl,notnull,default:0"`

View File

@@ -11,7 +11,9 @@ 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
@@ -98,11 +100,11 @@ func (p *PostablePlannedMaintenance) Validate() error {
if _, err := time.LoadLocation(p.Schedule.Timezone); err != nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "invalid timezone in the payload")
}
if p.Schedule.StartTime.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing start time in the payload")
}
if !p.Schedule.EndTime.IsZero() && p.Schedule.StartTime.After(p.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
if !p.Schedule.StartTime.IsZero() && !p.Schedule.EndTime.IsZero() {
if p.Schedule.StartTime.After(p.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
}
}
if p.Schedule.Recurrence != nil {
@@ -112,6 +114,9 @@ func (p *PostablePlannedMaintenance) Validate() error {
if p.Schedule.Recurrence.Duration.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing duration in the payload")
}
if p.Schedule.Recurrence.EndTime != nil && p.Schedule.Recurrence.EndTime.Before(p.Schedule.Recurrence.StartTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "end time cannot be before start time")
}
}
return nil
}
@@ -154,34 +159,43 @@ func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
return false
}
// TODO(jatinderjit): `In(loc)` conversions seem redundant
currentTime := now.In(loc)
startTime := m.Schedule.StartTime.In(loc)
endTime := m.Schedule.EndTime.In(loc)
// Maintenance window hasn't yet started
if currentTime.Before(startTime) {
return false
}
// Maintenance window has ended
if !endTime.IsZero() && currentTime.After(endTime) {
return false
}
// Fixed schedule (startTime <= currentTime <= endTime)
if m.Schedule.Recurrence == nil {
return true
// 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)) {
return true
}
}
// recurring schedule
switch m.Schedule.Recurrence.RepeatType {
case RepeatTypeDaily:
return m.checkDaily(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeWeekly:
return m.checkWeekly(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeMonthly:
return m.checkMonthly(currentTime, m.Schedule.Recurrence, loc)
if m.Schedule.Recurrence != nil {
start := m.Schedule.Recurrence.StartTime
// Make sure the recurrence has started
if currentTime.Before(start.In(loc)) {
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)) {
return false
}
}
switch m.Schedule.Recurrence.RepeatType {
case RepeatTypeDaily:
return m.checkDaily(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeWeekly:
return m.checkWeekly(currentTime, m.Schedule.Recurrence, loc)
case RepeatTypeMonthly:
return m.checkMonthly(currentTime, m.Schedule.Recurrence, loc)
}
}
return false
@@ -192,7 +206,7 @@ func (m *PlannedMaintenance) ShouldSkip(ruleID string, now time.Time) bool {
func (m *PlannedMaintenance) checkDaily(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
candidate := time.Date(
currentTime.Year(), currentTime.Month(), currentTime.Day(),
m.Schedule.StartTime.Hour(), m.Schedule.StartTime.Minute(), 0, 0,
rec.StartTime.Hour(), rec.StartTime.Minute(), 0, 0,
loc,
)
if candidate.After(currentTime) {
@@ -220,7 +234,7 @@ func (m *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence,
// Build a candidate occurrence by rebasing today's date to the allowed weekday.
candidate := time.Date(
currentTime.Year(), currentTime.Month(), currentTime.Day(),
m.Schedule.StartTime.Hour(), m.Schedule.StartTime.Minute(), 0, 0,
rec.StartTime.Hour(), rec.StartTime.Minute(), 0, 0,
loc,
).AddDate(0, 0, delta)
// If the candidate is in the future, subtract 7 days.
@@ -237,8 +251,7 @@ func (m *PlannedMaintenance) checkWeekly(currentTime time.Time, rec *Recurrence,
// checkMonthly rebases the candidate occurrence using the recurrence's day-of-month.
// If the candidate for the current month is in the future, it uses the previous month.
func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence, loc *time.Location) bool {
startTime := m.Schedule.StartTime
refDay := startTime.Day()
refDay := rec.StartTime.Day()
year, month, _ := currentTime.Date()
lastDay := time.Date(year, month+1, 0, 0, 0, 0, 0, loc).Day()
day := refDay
@@ -246,7 +259,7 @@ func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence
day = lastDay
}
candidate := time.Date(year, month, day,
startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(),
rec.StartTime.Hour(), rec.StartTime.Minute(), rec.StartTime.Second(), rec.StartTime.Nanosecond(),
loc,
)
if candidate.After(currentTime) {
@@ -256,12 +269,12 @@ func (m *PlannedMaintenance) checkMonthly(currentTime time.Time, rec *Recurrence
lastDayPrev := time.Date(y, m+1, 0, 0, 0, 0, 0, loc).Day()
if refDay > lastDayPrev {
candidate = time.Date(y, m, lastDayPrev,
startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(),
rec.StartTime.Hour(), rec.StartTime.Minute(), rec.StartTime.Second(), rec.StartTime.Nanosecond(),
loc,
)
} else {
candidate = time.Date(y, m, refDay,
startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(),
rec.StartTime.Hour(), rec.StartTime.Minute(), rec.StartTime.Second(), rec.StartTime.Nanosecond(),
loc,
)
}
@@ -283,7 +296,14 @@ func (m *PlannedMaintenance) IsUpcoming() bool {
return false
}
now := time.Now().In(loc)
return now.Before(m.Schedule.StartTime)
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
return now.Before(m.Schedule.StartTime)
}
if m.Schedule.Recurrence != nil {
return now.Before(m.Schedule.Recurrence.StartTime)
}
return false
}
func (m *PlannedMaintenance) IsRecurring() bool {
@@ -300,16 +320,16 @@ func (m *PlannedMaintenance) Validate() error {
if m.Schedule.Timezone == "" {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing timezone in the payload")
}
_, err := time.LoadLocation(m.Schedule.Timezone)
if err != nil {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "invalid timezone in the payload")
}
if m.Schedule.StartTime.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing start time in the payload")
}
if !m.Schedule.EndTime.IsZero() && m.Schedule.StartTime.After(m.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
if m.Schedule.StartTime.After(m.Schedule.EndTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "start time cannot be after end time")
}
}
if m.Schedule.Recurrence != nil {
@@ -319,6 +339,9 @@ func (m *PlannedMaintenance) Validate() error {
if m.Schedule.Recurrence.Duration.IsZero() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "missing duration in the payload")
}
if m.Schedule.Recurrence.EndTime != nil && m.Schedule.Recurrence.EndTime.Before(m.Schedule.Recurrence.StartTime) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidPlannedMaintenancePayload, "end time cannot be before start time")
}
}
return nil
}

View File

@@ -7,7 +7,13 @@ import (
"github.com/SigNoz/signoz/pkg/valuer"
)
// Helper function to create a time pointer.
func timePtr(t time.Time) *time.Time {
return &t
}
func TestShouldSkipMaintenance(t *testing.T) {
cases := []struct {
name string
maintenance *PlannedMaintenance
@@ -18,9 +24,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "only-on-saturday",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "Europe/London",
StartTime: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
Timezone: "Europe/London",
Recurrence: &Recurrence{
StartTime: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("24h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday, RepeatOnTuesday, RepeatOnWednesday, RepeatOnThursday, RepeatOnFriday, RepeatOnSunday},
@@ -35,10 +41,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -52,10 +58,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Duration: valuer.MustParseTextDuration("4h"), // Until Tuesday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -69,10 +75,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("52h"), // Until Thursday 02:00
StartTime: time.Date(2024, 4, 1, 22, 0, 0, 0, time.UTC), // Monday 22:00
Duration: valuer.MustParseTextDuration("52h"), // Until Thursday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -86,10 +92,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-across-midnight-previous-day-not-in-repeaton",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 2, 22, 0, 0, 0, time.UTC), // Tuesday 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("4h"), // Until Wednesday 02:00
StartTime: time.Date(2024, 4, 2, 22, 0, 0, 0, time.UTC), // Tuesday 22:00
Duration: valuer.MustParseTextDuration("4h"), // Until Wednesday 02:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnTuesday}, // Only Tuesday
},
@@ -103,10 +109,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-across-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 23, 0, 0, 0, time.UTC), // 23:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"), // Until 01:00 next day
StartTime: time.Date(2024, 1, 1, 23, 0, 0, 0, time.UTC), // 23:00
Duration: valuer.MustParseTextDuration("2h"), // Until 01:00 next day
RepeatType: RepeatTypeDaily,
},
},
@@ -119,9 +125,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "at-start-time-boundary",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -135,9 +141,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "at-end-time-boundary",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -151,9 +157,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-multi-day-duration",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("72h"), // 3 days
RepeatType: RepeatTypeMonthly,
},
@@ -167,9 +173,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-multi-day-duration",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 28, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("72h"), // 3 days
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnSunday},
@@ -184,9 +190,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-crosses-to-next-month",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("48h"), // 2 days, crosses to Feb 1
RepeatType: RepeatTypeMonthly,
},
@@ -200,9 +206,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "timezone-offset-test",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "America/New_York", // UTC-5 or UTC-4 depending on DST
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.FixedZone("America/New_York", -5*3600)),
Timezone: "America/New_York", // UTC-5 or UTC-4 depending on DST
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.FixedZone("America/New_York", -5*3600)),
Duration: valuer.MustParseTextDuration("4h"),
RepeatType: RepeatTypeDaily,
},
@@ -216,9 +222,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-time-outside-window",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -232,10 +238,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring-maintenance-with-past-end-date",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
EndTime: time.Date(2024, 1, 10, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
EndTime: timePtr(time.Date(2024, 1, 10, 12, 0, 0, 0, time.UTC)),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -249,10 +255,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-spans-month-end",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 3, 31, 22, 0, 0, 0, time.UTC), // March 31, 22:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("6h"), // Until April 1, 04:00
StartTime: time.Date(2024, 3, 31, 22, 0, 0, 0, time.UTC), // March 31, 22:00
Duration: valuer.MustParseTextDuration("6h"), // Until April 1, 04:00
RepeatType: RepeatTypeMonthly,
},
},
@@ -265,9 +271,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-empty-repeaton",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{}, // Empty - should apply to all days
@@ -282,9 +288,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-february-fewer-days",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -297,9 +303,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-crosses-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 23, 30, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 23, 30, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("1h"), // Crosses to 00:30 next day
RepeatType: RepeatTypeDaily,
},
@@ -312,9 +318,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -327,9 +333,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 30, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("48h"), // 2 days duration
RepeatType: RepeatTypeMonthly,
},
@@ -342,10 +348,10 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "weekly-maintenance-crosses-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 23, 0, 0, 0, time.UTC), // Monday 23:00
Timezone: "UTC",
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"), // Until Tuesday 01:00
StartTime: time.Date(2024, 4, 1, 23, 0, 0, 0, time.UTC), // Monday 23:00
Duration: valuer.MustParseTextDuration("2h"), // Until Tuesday 01:00
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday}, // Only Monday
},
@@ -358,9 +364,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-days",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC), // January 31st
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -373,9 +379,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "daily-maintenance-crosses-midnight",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 22, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("4h"), // Until 02:00 next day
RepeatType: RepeatTypeDaily,
},
@@ -388,9 +394,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "monthly-maintenance-crosses-month-end-and-duration-is-2-hours",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 31, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
@@ -439,9 +445,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat sunday, saturday, weekly for 24 hours, in Us/Eastern timezone",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "US/Eastern",
StartTime: time.Date(2025, 3, 29, 20, 0, 0, 0, time.FixedZone("US/Eastern", -4*3600)),
Timezone: "US/Eastern",
Recurrence: &Recurrence{
StartTime: time.Date(2025, 3, 29, 20, 0, 0, 0, time.FixedZone("US/Eastern", -4*3600)),
Duration: valuer.MustParseTextDuration("24h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnSunday, RepeatOnSaturday},
@@ -455,9 +461,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -470,9 +476,9 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
@@ -485,176 +491,140 @@ func TestShouldSkipMaintenance(t *testing.T) {
name: "recurring maintenance, repeat daily from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
},
},
},
ts: time.Date(2024, 4, 1, 12, 10, 0, 0, time.UTC),
ts: time.Date(2024, 04, 1, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 4, 15, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 4, 14, 12, 10, 0, 0, time.UTC), // 14th 04 is sunday
skip: false,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 4, 16, 12, 10, 0, 0, time.UTC), // 16th 04 is tuesday
skip: false,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 5, 6, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 1, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
ts: time.Date(2024, 5, 6, 14, 0, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 4, 4, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 4, 4, 14, 10, 0, 0, time.UTC),
skip: false,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2024, 4, 4, 12, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
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, 14, 0, 0, 0, time.UTC), // daily at 14:00
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
Recurrence: &Recurrence{
Duration: valuer.MustParseTextDuration("2h"), // until 16:00
RepeatType: RepeatTypeDaily,
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
// 2026-04-15 11:00 is inside the fixed range but outside the daily 14:00-16:00 window.
ts: time.Date(2026, 4, 15, 11, 0, 0, 0, time.UTC),
ts: time.Date(2024, 04, 15, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 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
skip: false,
},
{
name: "recurring-daily-with-fixed-times-inside-daily-window",
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
StartTime: time.Date(2026, 4, 1, 14, 0, 0, 0, time.UTC),
EndTime: time.Date(2026, 4, 30, 18, 0, 0, 0, time.UTC),
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeDaily,
RepeatType: RepeatTypeWeekly,
RepeatOn: []RepeatOn{RepeatOnMonday},
},
},
},
// 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),
ts: time.Date(2024, 04, 16, 12, 10, 0, 0, time.UTC), // 16th 04 is tuesday
skip: false,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 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),
skip: true,
},
{
name: "recurring maintenance, repeat weekly on monday from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 01, 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),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 04, 04, 12, 10, 0, 0, time.UTC),
skip: true,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 04, 04, 14, 10, 0, 0, time.UTC),
skip: false,
},
{
name: "recurring maintenance, repeat monthly on 4th from 12:00 to 14:00",
maintenance: &PlannedMaintenance{
Schedule: &Schedule{
Timezone: "UTC",
Recurrence: &Recurrence{
StartTime: time.Date(2024, 04, 04, 12, 0, 0, 0, time.UTC),
Duration: valuer.MustParseTextDuration("2h"),
RepeatType: RepeatTypeMonthly,
},
},
},
ts: time.Date(2024, 05, 04, 12, 10, 0, 0, time.UTC),
skip: true,
},
}

View File

@@ -67,6 +67,8 @@ var RepeatOnAllMap = map[RepeatOn]time.Weekday{
}
type Recurrence struct {
StartTime time.Time `json:"startTime" required:"true"`
EndTime *time.Time `json:"endTime,omitempty"`
Duration valuer.TextDuration `json:"duration" required:"true"`
RepeatType RepeatType `json:"repeatType" required:"true"`
RepeatOn []RepeatOn `json:"repeatOn"`
@@ -103,16 +105,33 @@ func (s Schedule) MarshalJSON() ([]byte, error) {
endTime = time.Date(s.EndTime.Year(), s.EndTime.Month(), s.EndTime.Day(), s.EndTime.Hour(), s.EndTime.Minute(), s.EndTime.Second(), s.EndTime.Nanosecond(), loc)
}
var recurrence *Recurrence
if s.Recurrence != nil {
recStartTime := time.Date(s.Recurrence.StartTime.Year(), s.Recurrence.StartTime.Month(), s.Recurrence.StartTime.Day(), s.Recurrence.StartTime.Hour(), s.Recurrence.StartTime.Minute(), s.Recurrence.StartTime.Second(), s.Recurrence.StartTime.Nanosecond(), loc)
var recEndTime *time.Time
if s.Recurrence.EndTime != nil {
end := time.Date(s.Recurrence.EndTime.Year(), s.Recurrence.EndTime.Month(), s.Recurrence.EndTime.Day(), s.Recurrence.EndTime.Hour(), s.Recurrence.EndTime.Minute(), s.Recurrence.EndTime.Second(), s.Recurrence.EndTime.Nanosecond(), loc)
recEndTime = &end
}
recurrence = &Recurrence{
StartTime: recStartTime,
EndTime: recEndTime,
Duration: s.Recurrence.Duration,
RepeatType: s.Recurrence.RepeatType,
RepeatOn: s.Recurrence.RepeatOn,
}
}
return json.Marshal(&struct {
Timezone string `json:"timezone"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime,omitzero"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Recurrence *Recurrence `json:"recurrence,omitempty"`
}{
Timezone: s.Timezone,
StartTime: startTime,
EndTime: endTime,
Recurrence: s.Recurrence,
StartTime: startTime.Format(time.RFC3339),
EndTime: endTime.Format(time.RFC3339),
Recurrence: recurrence,
})
}
@@ -147,11 +166,34 @@ func (s *Schedule) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
// TODO(jatinderjit): if endTime.IsZero() then we should not set the endTime
s.EndTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), endTime.Hour(), endTime.Minute(), endTime.Second(), endTime.Nanosecond(), loc)
}
s.Timezone = aux.Timezone
s.Recurrence = aux.Recurrence
if aux.Recurrence != nil {
recStartTime, err := time.Parse(time.RFC3339, aux.Recurrence.StartTime.Format(time.RFC3339))
if err != nil {
return err
}
var recEndTime *time.Time
if aux.Recurrence.EndTime != nil {
end, err := time.Parse(time.RFC3339, aux.Recurrence.EndTime.Format(time.RFC3339))
if err != nil {
return err
}
endConverted := time.Date(end.Year(), end.Month(), end.Day(), end.Hour(), end.Minute(), end.Second(), end.Nanosecond(), loc)
recEndTime = &endConverted
}
s.Recurrence = &Recurrence{
StartTime: time.Date(recStartTime.Year(), recStartTime.Month(), recStartTime.Day(), recStartTime.Hour(), recStartTime.Minute(), recStartTime.Second(), recStartTime.Nanosecond(), loc),
EndTime: recEndTime,
Duration: aux.Recurrence.Duration,
RepeatType: aux.Recurrence.RepeatType,
RepeatOn: aux.Recurrence.RepeatOn,
}
}
return nil
}

View File

@@ -11,8 +11,8 @@ import (
type Schedule struct {
Timezone string `json:"timezone" required:"true"`
StartTime time.Time `json:"startTime" required:"true"`
EndTime time.Time `json:"endTime,omitzero"`
StartTime time.Time `json:"startTime,omitempty"`
EndTime time.Time `json:"endTime,omitempty"`
Recurrence *Recurrence `json:"recurrence"`
}