Skip to content

Commit cbffb7a

Browse files
committed
Support windows platforms
1 parent 828a074 commit cbffb7a

6 files changed

Lines changed: 144 additions & 13 deletions

File tree

packages/plugin-ecs-fargate/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ The Agent sidecar accepts custom metrics over DogStatsD: `DD_DOGSTATSD_ORIGIN_DE
4040

4141
Running the command twice is safe: the Agent container is matched by name, so an already instrumented task definition is reported as such and no revision is registered. Each revision the command registers is tagged `dd_sls_ci` with the version of `datadog-ci` that created it; upgrading the CLI does not on its own produce a new revision, since that tag is not part of the comparison.
4242

43+
#### Windows tasks
44+
45+
Windows task definitions are instrumented the same way, with three differences the command applies on its own after reading the task's `runtimePlatform`. The Agent runs the `-servercore` build of the image, published as a manifest list so ECS pulls the variant matching your Windows Server version. The Agent container is given `C:\` as its working directory, which it needs and its image does not set. And it gets no health check, because the Agent's probe is a shell script that only the Linux image ships, so a probe would report the Agent as permanently unhealthy rather than tell you anything; the command warns when it makes this choice.
46+
47+
If you pass `--agent-image` for a Windows task, it is used exactly as given, so point it at a `-servercore` tag: mirroring `public.ecr.aws/datadog/agent:latest` into your own registry gives you the Linux image, which will not start on Windows.
48+
4349
#### Deploying the new revision
4450

4551
Pass `--ecs-service` for each service that should run the revision the command just registered, and `--cluster` if those services are not in the `default` cluster. A service named by its full ARN already says which cluster it runs in, so `--cluster` can be left off; passing a `--cluster` that contradicts the ARN's cluster is an error, not a silent override. A run updates services in a single cluster, so ARNs naming more than one are reported too. Each service is matched to the task definition family it currently runs, so a run over several task definitions points each service at its own new revision, and a service already running the instrumented revision is left alone rather than redeployed. Updating a service starts an ECS deployment: the command returns as soon as ECS accepts it, and the rollout follows your service's deployment configuration.

packages/plugin-ecs-fargate/src/__tests__/fixtures.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const SERVICE_TAG: Tag = {key: 'service', value: MOCK_FAMILY}
2121
/** The tags every instrumented revision carries. */
2222
export const INSTRUMENTATION_TAGS: Tag[] = [SERVICE_TAG, CLI_VERSION_TAG]
2323

24-
/** No `agentImage`, so the transform picks the default build. */
24+
/** No `agentImage`, so the transform picks the default build for the task's platform. */
2525
export const MOCK_SETTINGS: InstrumentSettings = {
2626
site: 'datadoghq.com',
2727
apiKeySecretArn: MOCK_API_KEY_SECRET_ARN,
@@ -69,6 +69,15 @@ export const fargateTaskDefinition = ({
6969
...overrides,
7070
})
7171

72+
/**
73+
* A task definition that runs Windows containers, which the Agent sidecar is built differently for.
74+
*/
75+
export const windowsTaskDefinition = (overrides: Partial<TaskDefinition> = {}): TaskDefinition =>
76+
fargateTaskDefinition({
77+
runtimePlatform: {operatingSystemFamily: 'WINDOWS_SERVER_2022_CORE', cpuArchitecture: 'X86_64'},
78+
...overrides,
79+
})
80+
7281
/**
7382
* An ECS service as `DescribeServices` returns it, running the first revision of its family.
7483
*/

packages/plugin-ecs-fargate/src/__tests__/task-definition.test.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {ContainerDefinition} from '@aws-sdk/client-ecs'
1+
import type {ContainerDefinition, RuntimePlatform} from '@aws-sdk/client-ecs'
22

33
import {AGENT_IMAGE} from '@datadog/datadog-ci-base/helpers/serverless/constants'
44

@@ -14,6 +14,7 @@ import {
1414
MOCK_SETTINGS,
1515
SERVICE_TAG,
1616
fargateTaskDefinition,
17+
windowsTaskDefinition,
1718
} from './fixtures'
1819

1920
jest.mock('@datadog/datadog-ci-base/version', () => ({cliVersion: 'XXXX'}))
@@ -537,6 +538,68 @@ describe('instrumentTaskDefinition', () => {
537538
})
538539
})
539540

541+
describe('windows tasks', () => {
542+
test('runs the Windows build of the Agent image', () => {
543+
const {taskDefinition} = instrumentTaskDefinition(windowsTaskDefinition(), MOCK_SETTINGS)
544+
545+
expect(agentContainerOf(taskDefinition.containerDefinitions)?.image).toBe(`${AGENT_IMAGE}-servercore`)
546+
})
547+
548+
test('gives the Agent the working directory its Windows image leaves unset', () => {
549+
const {taskDefinition} = instrumentTaskDefinition(windowsTaskDefinition(), MOCK_SETTINGS)
550+
551+
expect(agentContainerOf(taskDefinition.containerDefinitions)?.workingDirectory).toBe('C:\\')
552+
})
553+
554+
test('adds no health check, since the Agent probe only ships in the Linux image', () => {
555+
const {taskDefinition, warnings} = instrumentTaskDefinition(windowsTaskDefinition(), MOCK_SETTINGS)
556+
557+
expect(agentContainerOf(taskDefinition.containerDefinitions)).not.toHaveProperty('healthCheck')
558+
expect(warnings).toContainEqual(expect.stringContaining('without a health check'))
559+
})
560+
561+
test('still runs an explicitly requested image', () => {
562+
const {taskDefinition} = instrumentTaskDefinition(windowsTaskDefinition(), {
563+
...MOCK_SETTINGS,
564+
agentImage: 'my-registry/agent:7.60.0-servercore',
565+
})
566+
567+
expect(agentContainerOf(taskDefinition.containerDefinitions)?.image).toBe('my-registry/agent:7.60.0-servercore')
568+
})
569+
570+
test('instruments the application containers as it would on Linux', () => {
571+
const {taskDefinition} = instrumentTaskDefinition(windowsTaskDefinition(), {
572+
...MOCK_SETTINGS,
573+
service: 'payments',
574+
})
575+
576+
const app = appContainerOf(taskDefinition.containerDefinitions)
577+
expect(envVarsOf(app)).toHaveProperty('DD_SERVICE', 'payments')
578+
expect(app?.dockerLabels).toStrictEqual({'com.datadoghq.tags.service': 'payments'})
579+
})
580+
581+
test('re-instrumenting produces an identical task definition', () => {
582+
const first = instrumentTaskDefinition(windowsTaskDefinition(), MOCK_SETTINGS)
583+
const described = windowsTaskDefinition({containerDefinitions: first.taskDefinition.containerDefinitions})
584+
585+
const second = instrumentTaskDefinition(described, MOCK_SETTINGS)
586+
587+
expect(second.taskDefinition).toStrictEqual(first.taskDefinition)
588+
})
589+
590+
test.each<[string, RuntimePlatform | undefined]>([
591+
['LINUX', {operatingSystemFamily: 'LINUX', cpuArchitecture: 'X86_64'}],
592+
['no runtime platform', undefined],
593+
])('builds the Linux Agent for a task declaring %s', (_, runtimePlatform) => {
594+
const {taskDefinition} = instrumentTaskDefinition(fargateTaskDefinition({runtimePlatform}), MOCK_SETTINGS)
595+
596+
const agent = agentContainerOf(taskDefinition.containerDefinitions)
597+
expect(agent?.image).toBe(AGENT_IMAGE)
598+
expect(agent?.healthCheck).toBeDefined()
599+
expect(agent).not.toHaveProperty('workingDirectory')
600+
})
601+
})
602+
540603
describe('validation', () => {
541604
test('rejects a task definition that is not awsvpc', () => {
542605
const original = fargateTaskDefinition({networkMode: 'bridge'})

packages/plugin-ecs-fargate/src/commands/instrument.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ export class PluginCommand extends EcsFargateInstrumentCommand {
235235

236236
/**
237237
* What the task definitions are instrumented with, leaving aside how the Agent gets its API key.
238+
*
239+
* The Agent image is left as the user gave it, absent included: the default depends on whether the
240+
* task runs Linux or Windows, which only the transform reading the task definition can tell.
238241
*/
239242
private buildSettings(config: EcsFargateConfigOptions): InstrumentSettings {
240243
return {

packages/plugin-ecs-fargate/src/constants.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,26 @@ export const LAUNCH_TYPE_FARGATE = 'FARGATE'
1717
*/
1818
export const AWSVPC_NETWORK_MODE = 'awsvpc'
1919

20+
/**
21+
* ECS spells the Windows operating system families with the `WINDOWS_SERVER` prefix (e.g.
22+
* `WINDOWS_SERVER_2019_CORE`, `WINDOWS_SERVER_2022_FULL`) Anything else, `LINUX` and
23+
* declaring no family at all included, runs Linux.
24+
*/
25+
export const WINDOWS_OS_FAMILY_PREFIX = 'WINDOWS_SERVER'
26+
2027
// Agent sidecar defaults
2128
export const AGENT_CONTAINER_NAME = 'datadog-agent'
2229

30+
/**
31+
* The tag suffix for the Windows build of the Agent image
32+
*/
33+
export const WINDOWS_AGENT_IMAGE_SUFFIX = '-servercore'
34+
35+
/**
36+
* The working directory the Agent requires on Windows, which its image does not set itself.
37+
*/
38+
export const WINDOWS_WORKING_DIRECTORY = 'C:\\'
39+
2340
/**
2441
* The task definition tag keys for the unified service tags, which name the same three concepts as
2542
* the `DD_SERVICE`, `DD_ENV`, and `DD_VERSION` environment variables the containers run with.
@@ -40,6 +57,9 @@ export const DOCKER_LABEL_VERSION = 'com.datadoghq.tags.version'
4057
/**
4158
* The Agent's own health probe. Shipping it means application containers can gate their startup on
4259
* the Agent being ready through a `dependsOn` HEALTHY condition.
60+
*
61+
* Linux only: `/probe.sh` is a shell script shipped in the Linux image, and the Windows image has
62+
* no equivalent on its `PATH`, so Windows tasks get no health check at all.
4363
*/
4464
export const AGENT_HEALTH_CHECK_COMMAND = ['CMD-SHELL', '/probe.sh']
4565
export const AGENT_HEALTH_CHECK_INTERVAL = 15

packages/plugin-ecs-fargate/src/task-definition.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,18 @@ import {
5454
READ_ONLY_TASK_DEFINITION_FIELDS,
5555
SERVICE_TAG_KEY,
5656
VERSION_TAG_KEY,
57+
WINDOWS_AGENT_IMAGE_SUFFIX,
58+
WINDOWS_OS_FAMILY_PREFIX,
59+
WINDOWS_WORKING_DIRECTORY,
5760
} from './constants'
5861

5962
/**
6063
* What the user asked for, resolved into the decisions the transform needs.
6164
*/
6265
export type InstrumentSettings = {
6366
/**
64-
* The Agent image to run. Absent leaves the choice to the transform, which picks the default
65-
* build.
67+
* The Agent image to run. Absent leaves the choice to the transform, which picks the default build
68+
* for the task's platform.
6669
*/
6770
agentImage?: string
6871
site: string
@@ -276,11 +279,26 @@ export type AgentContainerResult = {
276279
warnings: string[]
277280
}
278281

282+
/**
283+
* Whether the task runs Windows containers, which the Agent sidecar has to be built differently
284+
* for. A task definition that declares no `runtimePlatform`, or declares `LINUX`, runs Linux.
285+
*/
286+
const isWindowsTask = (taskDefinition: TaskDefinition): boolean =>
287+
taskDefinition.runtimePlatform?.operatingSystemFamily?.toUpperCase().startsWith(WINDOWS_OS_FAMILY_PREFIX) ?? false
288+
289+
/**
290+
* The Agent image to run; either the one specified by the user, or the default build for the task's platform.
291+
*/
292+
const agentImage = (settings: InstrumentSettings, windows: boolean): string =>
293+
settings.agentImage ?? (windows ? `${AGENT_IMAGE}${WINDOWS_AGENT_IMAGE_SUFFIX}` : AGENT_IMAGE)
294+
279295
/**
280296
* What the Agent sidecar is built from.
281297
*/
282298
type AgentContainerContext = {
283299
settings: InstrumentSettings
300+
/** Whether the task runs Windows containers. */
301+
windows: boolean
284302
/** The task definition family, used to name the service when the user did not. */
285303
family?: string
286304
/** The Agent container already on the task definition, if any. */
@@ -296,6 +314,7 @@ type AgentContainerContext = {
296314
*/
297315
const buildAgentContainer = ({
298316
settings,
317+
windows,
299318
family,
300319
existing,
301320
logConfiguration,
@@ -323,27 +342,37 @@ const buildAgentContainer = ({
323342
)
324343
}
325344

326-
const healthCheck = {
327-
command: [...AGENT_HEALTH_CHECK_COMMAND],
328-
interval: AGENT_HEALTH_CHECK_INTERVAL,
329-
timeout: AGENT_HEALTH_CHECK_TIMEOUT,
330-
retries: AGENT_HEALTH_CHECK_RETRIES,
331-
startPeriod: AGENT_HEALTH_CHECK_START_PERIOD,
332-
}
333-
if (existing?.healthCheck && !sortedEqual(existing.healthCheck, healthCheck)) {
345+
// The Agent's probe is a shell script that only exists in the Linux image, so a Windows task gets
346+
// no health check rather than one that can never pass.
347+
const healthCheck = windows
348+
? undefined
349+
: {
350+
command: [...AGENT_HEALTH_CHECK_COMMAND],
351+
interval: AGENT_HEALTH_CHECK_INTERVAL,
352+
timeout: AGENT_HEALTH_CHECK_TIMEOUT,
353+
retries: AGENT_HEALTH_CHECK_RETRIES,
354+
startPeriod: AGENT_HEALTH_CHECK_START_PERIOD,
355+
}
356+
if (windows) {
357+
warnings.push(
358+
`Leaving the ${AGENT_CONTAINER_NAME} container without a health check: the Agent's probe is a shell script that only its Linux image ships. Nothing will report whether the Agent is ready on this task.`
359+
)
360+
} else if (existing?.healthCheck && !sortedEqual(existing.healthCheck, healthCheck)) {
334361
warnings.push(`Replacing the health check on the ${AGENT_CONTAINER_NAME} container with the Agent's own probe.`)
335362
}
336363

337364
const container = removeUndefinedValues({
338365
...existing,
339366
name: AGENT_CONTAINER_NAME,
340-
image: settings.agentImage ?? AGENT_IMAGE,
367+
image: agentImage(settings, windows),
341368
// The Agent must not be able to take the task down: a crashed Agent should cost telemetry, not
342369
// availability.
343370
essential: false,
344371
environment: toEnvironment(getAgentEnvVars(settings, family), inheritedEnvironment),
345372
secrets,
346373
healthCheck,
374+
// The Windows Agent image leaves the working directory unset, and the Agent needs one.
375+
workingDirectory: windows ? WINDOWS_WORKING_DIRECTORY : existing?.workingDirectory,
347376
// Without one, a Fargate container's output goes nowhere, which would leave an Agent that
348377
// cannot pull its image or reach Datadog impossible to diagnose.
349378
logConfiguration: existing?.logConfiguration ?? logConfiguration,
@@ -429,6 +458,7 @@ export const instrumentTaskDefinition = (
429458
const existingAgent = containers.find((container) => container.name === AGENT_CONTAINER_NAME)
430459
const {container: agentContainer, warnings} = buildAgentContainer({
431460
settings,
461+
windows: isWindowsTask(taskDefinition),
432462
family,
433463
existing: existingAgent,
434464
logConfiguration: borrowedLogConfiguration(containers),

0 commit comments

Comments
 (0)