feat(edge/helm): add atomic and timeout options [BE-12481] (#1849)
This commit is contained in:
@@ -358,13 +358,15 @@ type (
|
||||
// HelmConfig represents the Helm configuration for an edge stack
|
||||
HelmConfig struct {
|
||||
// Path to a Helm chart folder for Helm git deployments
|
||||
ChartPath string `json:"HelmChartPath,omitempty" example:"charts/my-app"`
|
||||
ChartPath string `json:"ChartPath,omitempty" example:"charts/my-app"`
|
||||
// Array of paths to Helm values YAML files for Helm git deployments
|
||||
ValuesFiles []string `json:"HelmValuesFiles,omitempty" example:"['values/prod.yaml', 'values/secrets.yaml']"`
|
||||
ValuesFiles []string `json:"ValuesFiles,omitempty" example:"['values/prod.yaml', 'values/secrets.yaml']"`
|
||||
// Helm chart version from Chart.yaml (read-only, extracted during Git sync)
|
||||
Version string `json:"HelmVersion,omitempty" example:"1.2.3"`
|
||||
Version string `json:"Version,omitempty" example:"1.2.3"`
|
||||
// Enable automatic rollback on deployment failure (equivalent to helm --atomic flag)
|
||||
Atomic bool `json:"HelmAtomic,omitempty" example:"true"`
|
||||
Atomic bool `json:"Atomic" example:"true"`
|
||||
// Timeout for Helm operations (equivalent to helm --timeout flag)
|
||||
Timeout string `json:"Timeout,omitempty" example:"5m0s"`
|
||||
}
|
||||
|
||||
EdgeStackStatusForEnv struct {
|
||||
|
||||
@@ -3,10 +3,10 @@ import { describe, it, expect } from 'vitest';
|
||||
import { intervalValidation } from './IntervalField';
|
||||
|
||||
describe('intervalValidation', () => {
|
||||
it('rejects empty value with required message', async () => {
|
||||
it('rejects empty value with invalid duration format message', async () => {
|
||||
const schema = intervalValidation();
|
||||
await expect(schema.validate('')).rejects.toThrow(
|
||||
'This field is required.'
|
||||
'Invalid duration format. Use formats like 5m, 1h30m, 7d, or 300s'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,13 +26,13 @@ describe('intervalValidation', () => {
|
||||
await expect(schema.validate('90s')).resolves.toBe('90s');
|
||||
});
|
||||
|
||||
it('rejects sub-minute durations and invalid formats with minimum-interval message', async () => {
|
||||
it('rejects sub-minute durations and invalid formats', async () => {
|
||||
const schema = intervalValidation();
|
||||
await expect(schema.validate('30s')).rejects.toThrow(
|
||||
'Minimum interval is 1m'
|
||||
);
|
||||
await expect(schema.validate('abc')).rejects.toThrow(
|
||||
'Minimum interval is 1m'
|
||||
'Invalid duration format. Use formats like 5m, 1h30m, 7d, or 300s'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { string } from 'yup';
|
||||
import parse from 'parse-duration';
|
||||
|
||||
import { durationValidation } from '@/react/utils/validation';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { useCaretPosition } from '@@/form-components/useCaretPosition';
|
||||
@@ -42,22 +43,13 @@ export function IntervalField({
|
||||
}
|
||||
|
||||
export function intervalValidation() {
|
||||
return (
|
||||
string()
|
||||
.required('This field is required.')
|
||||
// TODO: find a regex that validates time.Duration
|
||||
// .matches(
|
||||
// // validate golang time.Duration format
|
||||
// // https://cs.opensource.google/go/go/+/master:src/time/format.go;l=1590
|
||||
// /[-+]?([0-9]*(\.[0-9]*)?[a-z]+)+/g,
|
||||
// 'Please enter a valid time interval.'
|
||||
// )
|
||||
.test('minimumInterval', 'Minimum interval is 1m', (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const minutes = parse(value, 'minute');
|
||||
return minutes !== null && minutes >= 1;
|
||||
})
|
||||
);
|
||||
return durationValidation(false) // Don't allow empty - field is required
|
||||
.required('This field is required.')
|
||||
.test('minimumInterval', 'Minimum interval is 1m', (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const minutes = parse(value, 'minute');
|
||||
return minutes !== null && minutes >= 1;
|
||||
});
|
||||
}
|
||||
|
||||
85
app/react/utils/validation.test.ts
Normal file
85
app/react/utils/validation.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { durationValidation } from './validation';
|
||||
|
||||
describe('durationValidation', () => {
|
||||
describe('empty value handling', () => {
|
||||
it('accepts empty when allowEmpty=true (default)', async () => {
|
||||
const schema = durationValidation();
|
||||
await expect(schema.validate('')).resolves.toBe('');
|
||||
await expect(schema.validate(undefined)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects empty when allowEmpty=false', async () => {
|
||||
const schema = durationValidation(false);
|
||||
await expect(schema.validate('')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid durations', () => {
|
||||
const schema = durationValidation();
|
||||
|
||||
it('accepts all time units', async () => {
|
||||
await expect(schema.validate('100ns')).resolves.toBe('100ns');
|
||||
await expect(schema.validate('500us')).resolves.toBe('500us');
|
||||
await expect(schema.validate('500µs')).resolves.toBe('500µs');
|
||||
await expect(schema.validate('100ms')).resolves.toBe('100ms');
|
||||
await expect(schema.validate('30s')).resolves.toBe('30s');
|
||||
await expect(schema.validate('5m')).resolves.toBe('5m');
|
||||
await expect(schema.validate('1h')).resolves.toBe('1h');
|
||||
await expect(schema.validate('7d')).resolves.toBe('7d');
|
||||
});
|
||||
|
||||
it('accepts decimal values', async () => {
|
||||
await expect(schema.validate('1.5h')).resolves.toBe('1.5h');
|
||||
await expect(schema.validate('0.5d')).resolves.toBe('0.5d');
|
||||
});
|
||||
|
||||
it('accepts chained units', async () => {
|
||||
await expect(schema.validate('1h30m')).resolves.toBe('1h30m');
|
||||
await expect(schema.validate('2d12h')).resolves.toBe('2d12h');
|
||||
await expect(schema.validate('2h45m30s')).resolves.toBe('2h45m30s');
|
||||
});
|
||||
|
||||
it('accepts zero and large values', async () => {
|
||||
await expect(schema.validate('0s')).resolves.toBe('0s');
|
||||
await expect(schema.validate('365d')).resolves.toBe('365d');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid durations', () => {
|
||||
const schema = durationValidation();
|
||||
|
||||
it('rejects numbers without units', async () => {
|
||||
await expect(schema.validate('300')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid units', async () => {
|
||||
await expect(schema.validate('5minutes')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
await expect(schema.validate('7days')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects spaces and invalid formats', async () => {
|
||||
await expect(schema.validate('5 m')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
await expect(schema.validate('m')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
await expect(schema.validate('-5m')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
await expect(schema.validate('abc')).rejects.toThrow(
|
||||
'Invalid duration format'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
51
app/react/utils/validation.ts
Normal file
51
app/react/utils/validation.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { string } from 'yup';
|
||||
|
||||
/**
|
||||
* Validates a duration string format (e.g., "5m0s", "10m", "1h30m")
|
||||
*
|
||||
* Valid units:
|
||||
* - ns (nanoseconds)
|
||||
* - us/µs (microseconds)
|
||||
* - ms (milliseconds)
|
||||
* - s (seconds)
|
||||
* - m (minutes)
|
||||
* - h (hours)
|
||||
* - d (days)
|
||||
*
|
||||
* Examples of valid durations:
|
||||
* - "300s"
|
||||
* - "5m"
|
||||
* - "1h30m"
|
||||
* - "2h45m30s"
|
||||
* - "1.5h"
|
||||
* - "7d"
|
||||
* - "2d12h"
|
||||
*
|
||||
* @param allowEmpty - Whether to allow empty strings (default: true)
|
||||
* @returns Yup string schema with duration validation
|
||||
*/
|
||||
export function durationValidation(allowEmpty = true) {
|
||||
// Regex pattern that matches duration format
|
||||
// Allows: number (with optional decimal) + unit, can be chained
|
||||
// Units: ns, us, µs, ms, s, m, h, d
|
||||
const durationRegex = /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h|d))+$/;
|
||||
|
||||
return string().test(
|
||||
'duration',
|
||||
'Invalid duration format. Use formats like 5m, 1h30m, 7d, or 300s',
|
||||
(value) => {
|
||||
// Empty string is valid if allowed
|
||||
if (allowEmpty && (!value || value.trim() === '')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empty string is invalid if not allowed
|
||||
if (!allowEmpty && (!value || value.trim() === '')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the format matches duration pattern
|
||||
return value ? durationRegex.test(value.trim()) : false;
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user