-
Notifications
You must be signed in to change notification settings - Fork 267
/
config.ts
1066 lines (944 loc) · 34.5 KB
/
config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (C) 2018-2023 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import {
artifactsTargetDescription,
envVarRegex,
joi,
joiPrimitive,
joiSparseArray,
joiStringMap,
joiUserIdentifier,
Primitive,
PrimitiveMap,
createSchema,
ActionReference,
} from "../../config/common"
import { ArtifactSpec } from "../../config/validation"
import { ingressHostnameSchema, linkUrlSchema } from "../../types/service"
import { DEFAULT_PORT_PROTOCOL } from "../../constants"
import { dedent, deline } from "../../util/string"
import { syncGuideLink } from "../kubernetes/sync"
import { k8sDeploymentTimeoutSchema, runCacheResultSchema } from "../kubernetes/config"
import { localModeGuideLink } from "../kubernetes/local-mode"
import { BuildAction, BuildActionConfig } from "../../actions/build"
import { DeployAction, DeployActionConfig } from "../../actions/deploy"
import { TestAction, TestActionConfig } from "../../actions/test"
import { RunAction, RunActionConfig } from "../../actions/run"
import { memoize } from "lodash"
import Joi from "@hapi/joi"
export const defaultDockerfileName = "Dockerfile"
export const defaultContainerLimits: ServiceLimitSpec = {
cpu: 1000, // = 1000 millicpu = 1 CPU
memory: 1024, // = 1024MB = 1GB
}
export const defaultContainerResources: ContainerResourcesSpec = {
cpu: {
min: 10,
max: 1000,
},
memory: {
min: 90, // This is the minimum in some clusters.
max: 1024,
},
}
export interface ContainerIngressSpec {
annotations: Annotations
linkUrl?: string
hostname?: string
path: string
port: string
}
export type ServicePortProtocol = "TCP" | "UDP"
export interface ServicePortSpec {
name: string
protocol: ServicePortProtocol
containerPort: number
localPort?: number
// Defaults to containerPort
servicePort: number
hostPort?: number
nodePort?: number | true
}
export interface ContainerVolumeSpecBase {
name: string
containerPath: string
hostPath?: string
}
export interface ContainerVolumeSpec extends ContainerVolumeSpecBase {
action?: ActionReference<"Deploy">
}
export interface ServiceHealthCheckSpec {
httpGet?: {
path: string
port: string
scheme?: "HTTP" | "HTTPS"
}
command?: string[]
tcpPort?: string
readinessTimeoutSeconds?: number
livenessTimeoutSeconds?: number
}
/**
* DEPRECATED: Use {@link ContainerResourcesSpec} instead.
*/
export interface ServiceLimitSpec {
cpu: number
memory: number
}
export interface ContainerResourcesSpec {
cpu: {
min: number
max: number | null
}
memory: {
min: number
max: number | null
}
}
interface Annotations {
[name: string]: string
}
const deploymentStrategies = ["RollingUpdate", "Recreate"] as const
export type DeploymentStrategy = (typeof deploymentStrategies)[number]
export const defaultDeploymentStrategy: DeploymentStrategy = "RollingUpdate"
export const commandExample = ["/bin/sh", "-c"]
export type SyncMode =
| "one-way"
| "one-way-safe"
| "one-way-replica"
| "one-way-reverse"
| "one-way-replica-reverse"
| "two-way"
| "two-way-safe"
| "two-way-resolved"
export const defaultSyncMode: SyncMode = "one-way-safe"
export interface DevModeSyncOptions {
mode?: SyncMode
exclude?: string[]
defaultFileMode?: number
defaultDirectoryMode?: number
defaultOwner?: number | string
defaultGroup?: number | string
}
export interface DevModeSyncSpec extends DevModeSyncOptions {
source: string
target: string
}
const permissionsDocs =
"See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information."
const ownerDocs =
"Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information."
export const syncExcludeSchema = memoize(() =>
joi
.array()
.items(joi.posixPath().allowGlobs().subPathOnly())
.description(
dedent`
Specify a list of POSIX-style paths or glob patterns that should be excluded from the sync.
\`.git\` directories and \`.garden\` directories are always ignored.
`
)
.example(["dist/**/*", "*.log"])
)
export const syncModeSchema = memoize(() =>
joi
.string()
.allow(
"one-way",
"one-way-safe",
"one-way-replica",
"one-way-reverse",
"one-way-replica-reverse",
"two-way",
"two-way-safe",
"two-way-resolved"
)
.only()
.default(defaultSyncMode)
.description(
`The sync mode to use for the given paths. See the [Code Synchronization guide](${syncGuideLink}) for details.`
)
)
export const syncDefaultFileModeSchema = memoize(() =>
joi
.number()
.min(0)
.max(777)
.description(
"The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user read/write). " +
permissionsDocs
)
)
export const syncDefaultDirectoryModeSchema = memoize(() =>
joi
.number()
.min(0)
.max(777)
.description(
"The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 (user read/write). " +
permissionsDocs
)
)
export const syncDefaultOwnerSchema = memoize(() =>
joi
.alternatives(joi.number().integer(), joi.string())
.description("Set the default owner of files and directories at the target. " + ownerDocs)
)
export const syncDefaultGroupSchema = memoize(() =>
joi
.alternatives(joi.number().integer(), joi.string())
.description("Set the default group on files and directories at the target. " + ownerDocs)
)
export const syncTargetPathSchema = memoize(() =>
joi
.posixPath()
.absoluteOnly()
.required()
.invalid("/")
.description(
deline`
POSIX-style absolute path to sync to inside the container. The root path (i.e. "/") is not allowed.
`
)
.example("/app/src")
)
const containerSyncSchema = createSchema({
name: "container-sync",
keys: () => ({
source: joi
.string()
.default(".")
.description(
deline`
POSIX-style or Windows path of the directory to sync to the target. Defaults to the config's directory if no value is provided.
`
)
.example("src"),
target: syncTargetPathSchema(),
exclude: syncExcludeSchema(),
mode: syncModeSchema(),
defaultFileMode: syncDefaultFileModeSchema(),
defaultDirectoryMode: syncDefaultDirectoryModeSchema(),
defaultOwner: syncDefaultOwnerSchema(),
defaultGroup: syncDefaultGroupSchema(),
}),
})
export interface ContainerSyncSpec {
args?: string[]
command?: string[]
paths: DevModeSyncSpec[]
}
export const containerSyncPathSchema = createSchema({
name: "container-sync-path",
description: dedent`
Specifies which files or directories to sync to which paths inside the running containers of the service when it's in sync mode, and overrides for the container command and/or arguments.
Sync is enabled e.g. by setting the \`--sync\` flag on the \`garden deploy\` command.
See the [Code Synchronization guide](${syncGuideLink}) for more information.
`,
keys: () => ({
args: joi
.sparseArray()
.items(joi.string())
.description("Override the default container arguments when in sync mode."),
command: joi
.sparseArray()
.items(joi.string())
.description("Override the default container command (i.e. entrypoint) when in sync mode."),
paths: joi
.array()
.items(containerSyncSchema())
.description("Specify one or more source files or directories to automatically sync with the running container."),
}),
rename: [["sync", "paths"]],
})
const defaultLocalModeRestartDelayMsec = 1000
const defaultLocalModeMaxRestarts = Number.POSITIVE_INFINITY
export interface LocalModeRestartSpec {
delayMsec: number
max: number
}
export const localModeRestartSchema = createSchema({
name: "local-mode-restart",
description: `Specifies restarting policy for the local application. By default, the local application will be restarting infinitely with ${defaultLocalModeRestartDelayMsec}ms between attempts.`,
keys: () => ({
delayMsec: joi
.number()
.integer()
.greater(-1)
.optional()
.default(defaultLocalModeRestartDelayMsec)
.description(
`Delay in milliseconds between the local application restart attempts. The default value is ${defaultLocalModeRestartDelayMsec}ms.`
),
max: joi
.number()
.integer()
.greater(-1)
.optional()
.default(defaultLocalModeMaxRestarts)
.allow(defaultLocalModeMaxRestarts)
.description("Max number of the local application restarts. Unlimited by default."),
}),
options: { presence: "optional" },
default: {
delayMsec: defaultLocalModeRestartDelayMsec,
max: defaultLocalModeMaxRestarts,
},
})
export interface LocalModePortsSpec {
local: number
remote: number
}
export const localModePortsSchema = createSchema({
name: "local-mode-port",
keys: () => ({
local: joi
.number()
.integer()
.greater(0)
.optional()
.description("The local port to be used for reverse port-forward."),
remote: joi
.number()
.integer()
.greater(0)
.optional()
.description("The remote port to be used for reverse port-forward."),
}),
})
export interface ContainerLocalModeSpec {
ports: LocalModePortsSpec[]
command?: string[]
restart: LocalModeRestartSpec
}
export const containerLocalModeSchema = createSchema({
name: "container-local-mode",
description: dedent`
[EXPERIMENTAL] Configures the local application which will send and receive network requests instead of the target resource.
The target service will be replaced by a proxy container which runs an SSH server to proxy requests.
Reverse port-forwarding will be automatically configured to route traffic to the local service and back.
Local mode is enabled by setting the \`--local\` option on the \`garden deploy\` command.
Local mode always takes the precedence over sync mode if there are any conflicting service names.
Health checks are disabled for services running in local mode.
See the [Local Mode guide](${localModeGuideLink}) for more information.
Note! This feature is still experimental. Some incompatible changes can be made until the first non-experimental release.
`,
keys: () => ({
ports: joi
.array()
.items(localModePortsSchema())
.description("The reverse port-forwards configuration for the local application."),
command: joi
.sparseArray()
.optional()
.items(joi.string())
.description(
"The command to run the local application. If not present, then the local application should be started manually."
),
restart: localModeRestartSchema(),
}),
})
const annotationsSchema = memoize(() =>
joiStringMap(joi.string())
.example({ "nginx.ingress.kubernetes.io/proxy-body-size": "0" })
.default(() => ({}))
)
export interface EnvSecretRef {
secretRef: {
name: string
key?: string
}
}
const secretRefSchema = createSchema({
name: "container-secret-ref",
description:
"A reference to a secret, that should be applied to the environment variable. " +
"Note that this secret must already be defined in the provider.",
keys: () => ({
secretRef: joi.object().keys({
name: joi.string().required().description("The name of the secret to refer to."),
key: joi
.string()
.description("The key to read from in the referenced secret. May be required for some providers."),
}),
}),
})
export interface ContainerEnvVars {
[key: string]: Primitive | EnvSecretRef
}
export const containerEnvVarsSchema = memoize(() =>
joi
.object()
.pattern(envVarRegex, joi.alternatives(joiPrimitive(), secretRefSchema()))
.default(() => ({}))
.unknown(false)
.description(
"Key/value map of environment variables. Keys must be valid POSIX environment variable names " +
"(must not start with `GARDEN`) and values must be primitives or references to secrets."
)
.example([
{
MY_VAR: "some-value",
MY_SECRET_VAR: { secretRef: { name: "my-secret", key: "some-key" } },
},
{},
])
)
const ingressSchema = createSchema({
name: "container-ingress",
keys: () => ({
annotations: annotationsSchema().description(
"Annotations to attach to the ingress (Note: May not be applicable to all providers)"
),
hostname: ingressHostnameSchema(),
linkUrl: linkUrlSchema(),
path: joi.string().default("/").description("The path which should be routed to the service."),
port: joi
.string()
.required()
.description("The name of the container port where the specified paths should be routed."),
}),
})
const healthCheckSchema = createSchema({
name: "container-health-check",
keys: () => ({
httpGet: joi
.object()
.keys({
path: joi
.string()
.uri(<any>{ relativeOnly: true })
.required()
.description("The path of the service's health check endpoint."),
port: joi
.string()
.required()
.description("The name of the port where the service's health check endpoint should be available."),
scheme: joi.string().allow("HTTP", "HTTPS").default("HTTP"),
})
.description("Set this to check the service's health by making an HTTP request."),
command: joi
.sparseArray()
.items(joi.string())
.description("Set this to check the service's health by running a command in its container."),
tcpPort: joi
.string()
.description("Set this to check the service's health by checking if this TCP port is accepting connections."),
readinessTimeoutSeconds: joi
.number()
.min(1)
.default(3)
.description("The maximum number of seconds to wait until the readiness check counts as failed."),
livenessTimeoutSeconds: joi
.number()
.min(1)
.default(3)
.description("The maximum number of seconds to wait until the liveness check counts as failed."),
}),
xor: [["httpGet", "command", "tcpPort"]],
})
const limitsSchema = createSchema({
name: "container-limits",
keys: () => ({
cpu: joi
.number()
.min(10)
.description("The maximum amount of CPU the service can use, in millicpus (i.e. 1000 = 1 CPU)")
.meta({ deprecated: true }),
memory: joi
.number()
.min(64)
.description("The maximum amount of RAM the service can use, in megabytes (i.e. 1024 = 1 GB)")
.meta({ deprecated: true }),
}),
})
export const containerCpuSchema = () =>
joi.object().keys({
min: joi.number().default(defaultContainerResources.cpu.min).description(deline`
The minimum amount of CPU the container needs to be available for it to be deployed, in millicpus
(i.e. 1000 = 1 CPU)
`),
max: joi.number().default(defaultContainerResources.cpu.max).min(defaultContainerResources.cpu.min).allow(null)
.description(deline`
The maximum amount of CPU the container can use, in millicpus (i.e. 1000 = 1 CPU).
If set to null will result in no limit being set.
`),
})
export const containerMemorySchema = createSchema({
name: "container-memory",
keys: () => ({
min: joi.number().default(defaultContainerResources.memory.min).description(deline`
The minimum amount of RAM the container needs to be available for it to be deployed, in megabytes
(i.e. 1024 = 1 GB)
`),
max: joi.number().default(defaultContainerResources.memory.max).allow(null).min(64).description(deline`
The maximum amount of RAM the container can use, in megabytes (i.e. 1024 = 1 GB)
If set to null will result in no limit being set.
`),
}),
})
export const portSchema = createSchema({
name: "container-port",
keys: () => ({
name: joiUserIdentifier()
.required()
.description("The name of the port (used when referencing the port elsewhere in the service configuration)."),
protocol: joi.string().allow("TCP", "UDP").default(DEFAULT_PORT_PROTOCOL).description("The protocol of the port."),
containerPort: joi.number().required().example(8080).description(deline`
The port exposed on the container by the running process. This will also be the default value
for \`servicePort\`.
This is the port you would expose in your Dockerfile and that your process listens on.
This is commonly a non-priviledged port like 8080 for security reasons.
The service port maps to the container port:
\`servicePort:80 -> containerPort:8080 -> process:8080\``),
localPort: joi
.number()
.example(10080)
.description(
dedent`
Specify a preferred local port to attach to when creating a port-forward to the service port. If this port is
busy, a warning will be shown and an alternative port chosen.
`
),
servicePort: joi
.number()
.default((context) => context.containerPort)
.example(80).description(deline`
The port exposed on the service.
Defaults to \`containerPort\` if not specified.
This is the port you use when calling a service from another service within the cluster.
For example, if your service name is my-service and the service port is 8090,
you would call it with: http://my-service:8090/some-endpoint.
It is common to use port 80, the default port number, so that you can call the service
directly with http://my-service/some-endpoint.
The service port maps to the container port:
\`servicePort:80 -> containerPort:8080 -> process:8080\``),
hostPort: joi.number().meta({ deprecated: true }),
nodePort: joi.number().allow(true).description(deline`
Set this to expose the service on the specified port on the host node (may not be supported by all providers).
Set to \`true\` to have the cluster pick a port automatically, which is most often advisable if the cluster is
shared by multiple users.
This allows you to call the service from the outside by the node's IP address
and the port number set in this field.
`),
}),
})
export const volumeSchemaBase = createSchema({
name: "container-volume-base",
keys: () => ({
name: joiUserIdentifier().required().description("The name of the allocated volume."),
containerPath: joi
.posixPath()
.required()
.description("The path where the volume should be mounted in the container."),
hostPath: joi
.posixPath()
.description(
dedent`
_NOTE: Usage of hostPath is generally discouraged, since it doesn't work reliably across different platforms and providers. Some providers may not support it at all._
A local path or path on the node that's running the container, to mount in the container, relative to the config source directory (or absolute).
`
)
.example("/some/dir"),
}),
})
const volumeSchema = createSchema({
name: "container-volume",
extend: volumeSchemaBase,
keys: () => ({
// TODO-0.13.0: remove when kubernetes-container type is ready, better to swap out with raw k8s references
action: joi
.actionReference()
.kind("Deploy")
.name("base-volume")
.description(
dedent`
The action reference to a _volume Deploy action_ that should be mounted at \`containerPath\`. The supported action types are \`persistentvolumeclaim\` and \`configmap\`.
Note: Make sure to pay attention to the supported \`accessModes\` of the referenced volume. Unless it supports the ReadWriteMany access mode, you'll need to make sure it is not configured to be mounted by multiple services at the same time. Refer to the documentation of the module type in question to learn more.
`
),
}),
oxor: [["hostPath", "action"]],
})
export function getContainerVolumesSchema(schema: Joi.ObjectSchema) {
return joiSparseArray(schema).unique("name").description(dedent`
List of volumes that should be mounted when starting the container.
Note: If neither \`hostPath\` nor \`action\` is specified,
an empty ephemeral volume is created and mounted when deploying the container.
`)
}
const containerPrivilegedSchema = memoize(() =>
joi
.boolean()
.optional()
.description(
`If true, run the main container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false.`
)
)
const containerAddCapabilitiesSchema = memoize(() =>
joi.sparseArray().items(joi.string()).optional().description(`POSIX capabilities to add when running the container.`)
)
const containerDropCapabilitiesSchema = memoize(() =>
joi
.sparseArray()
.items(joi.string())
.optional()
.description(`POSIX capabilities to remove when running the container.`)
)
interface ContainerCommonRuntimeSpec {
args: string[]
command?: string[]
env: PrimitiveMap
limits?: ServiceLimitSpec
cpu: ContainerResourcesSpec["cpu"]
memory: ContainerResourcesSpec["memory"]
privileged?: boolean
addCapabilities?: string[]
dropCapabilities?: string[]
}
// Passed to ContainerServiceSpec
export interface ContainerCommonDeploySpec extends ContainerCommonRuntimeSpec {
annotations: Annotations
daemon: boolean
sync?: ContainerSyncSpec
localMode?: ContainerLocalModeSpec
ingresses: ContainerIngressSpec[]
healthCheck?: ServiceHealthCheckSpec
timeout?: number
ports: ServicePortSpec[]
replicas?: number
tty?: boolean
deploymentStrategy: DeploymentStrategy
}
export interface ContainerDeploySpec extends ContainerCommonDeploySpec {
volumes: ContainerVolumeSpec[]
image?: string
}
export type ContainerDeployActionConfig = DeployActionConfig<"container", ContainerDeploySpec>
export interface ContainerDeployOutputs {
deployedImageId: string
}
export const containerDeployOutputsSchema = createSchema({
name: "container-deploy-outputs",
keys: () => ({
deployedImageId: joi.string().required().description("The ID of the image that was deployed."),
}),
})
export type ContainerDeployAction = DeployAction<ContainerDeployActionConfig, ContainerDeployOutputs>
const containerCommonRuntimeSchemaKeys = memoize(() => ({
command: joi
.sparseArray()
.items(joi.string().allow(""))
.description("The command/entrypoint to run the container with.")
.example(commandExample),
args: joi
.sparseArray()
.items(joi.string().allow(""))
.description("The arguments (on top of the `command`, i.e. entrypoint) to run the container with.")
.example(["npm", "start"]),
env: containerEnvVarsSchema(),
cpu: containerCpuSchema().default(defaultContainerResources.cpu),
memory: containerMemorySchema().default(defaultContainerResources.memory),
volumes: getContainerVolumesSchema(volumeSchema()),
privileged: containerPrivilegedSchema(),
addCapabilities: containerAddCapabilitiesSchema(),
dropCapabilities: containerDropCapabilitiesSchema(),
tty: joi
.boolean()
.default(false)
.description(
"Specify if containers in this action have TTY support enabled (which implies having stdin support enabled)."
),
deploymentStrategy: joi
.string()
.default(defaultDeploymentStrategy)
.valid(...deploymentStrategies)
.description("Specifies the container's deployment strategy."),
}))
const containerImageSchema = memoize(() =>
joi.string().allow(false, null).empty([false, null]).description(deline`
Specify an image ID to deploy. Should be a valid Docker image identifier. Required if no \`build\` is specified.
`)
)
export const containerDeploySchemaKeys = memoize(() => ({
...containerCommonRuntimeSchemaKeys(),
annotations: annotationsSchema().description(
dedent`
Annotations to attach to the service _(note: May not be applicable to all providers)_.
When using the Kubernetes provider, these annotations are applied to both Service and Pod resources. You can generally specify the annotations intended for both Pods or Services here, and the ones that don't apply on either side will be ignored (i.e. if you put a Service annotation here, it'll also appear on Pod specs but will be safely ignored there, and vice versa).
`
),
daemon: joi.boolean().default(false).description(deline`
Whether to run the service as a daemon (to ensure exactly one instance runs per node).
May not be supported by all providers.
`),
sync: containerSyncPathSchema(),
localMode: containerLocalModeSchema(),
image: containerImageSchema(),
ingresses: joiSparseArray(ingressSchema())
.description("List of ingress endpoints that the service exposes.")
.example([{ path: "/api", port: "http" }]),
healthCheck: healthCheckSchema().description("Specify how the service's health should be checked after deploying."),
// TODO: remove in 0.14, keeping around to avoid config failures
hotReload: joi.any().meta({ internal: true }),
timeout: k8sDeploymentTimeoutSchema(),
limits: limitsSchema()
.description("Specify resource limits for the service.")
.meta({ deprecated: "Please use the `cpu` and `memory` fields instead." }),
ports: joiSparseArray(portSchema()).unique("name").description("List of ports that the service container exposes."),
replicas: joi.number().integer().description(deline`
The number of instances of the service to deploy.
Defaults to 3 for environments configured with \`production: true\`, otherwise 1.
Note: This setting may be overridden or ignored in some cases. For example, when running with \`daemon: true\` or if the provider doesn't support multiple replicas.
`),
}))
export const containerDeploySchema = createSchema({
name: "container-deploy",
keys: containerDeploySchemaKeys,
rename: [["devMode", "sync"]],
meta: { name: "container-deploy" },
})
export interface ContainerRegistryConfig {
hostname: string
port?: number
namespace: string
insecure: boolean
}
export const containerRegistryConfigSchema = createSchema({
name: "container-registry-config",
keys: () => ({
hostname: joi
.string()
.required()
.description("The hostname (and optionally port, if not the default port) of the registry.")
.example("gcr.io"),
port: joi.number().integer().description("The port where the registry listens on, if not the default."),
namespace: joi
.string()
.default("_")
.description(
"The registry namespace. Will be placed between hostname and image name, like so: <hostname>/<namespace>/<image name>"
)
.example("my-project"),
insecure: joi
.boolean()
.default(false)
.description("Set to true to allow insecure connections to the registry (without SSL)."),
}),
})
// TEST //
export const artifactsDescription = dedent`
Specify artifacts to copy out of the container after the run. The artifacts are stored locally under
the \`.garden/artifacts\` directory.
`
export const containerArtifactSchema = createSchema({
name: "container-artifact",
keys: () => ({
source: joi
.posixPath()
.allowGlobs()
.absoluteOnly()
.required()
.description("A POSIX-style path or glob to copy. Must be an absolute path. May contain wildcards.")
.example("/output/**/*"),
target: joi
.posixPath()
.relativeOnly()
.subPathOnly()
.default(".")
.description(artifactsTargetDescription)
.example("outputs/foo/"),
}),
})
const artifactsSchema = memoize(() =>
joi
.array()
.items(containerArtifactSchema())
.description(
deline`
${artifactsDescription}\n
Note: Depending on the provider, this may require the container image to include \`sh\` \`tar\`, in order
to enable the file transfer.
`
)
.example([{ source: "/report/**/*" }])
)
export interface ContainerTestOutputs {
log: string
}
export const containerTestOutputSchema = createSchema({
name: "container-test-output",
keys: () => ({
log: joi
.string()
.allow("")
.default("")
.description(
"The full log output from the executed action. (Pro-tip: Make it machine readable so it can be parsed by dependants)"
),
}),
})
export interface ContainerTestActionSpec extends ContainerCommonRuntimeSpec {
artifacts: ArtifactSpec[]
image?: string
volumes: ContainerVolumeSpec[]
}
export type ContainerTestActionConfig = TestActionConfig<"container", ContainerTestActionSpec>
export type ContainerTestAction = TestAction<ContainerTestActionConfig, ContainerTestOutputs>
export const containerTestSpecKeys = memoize(() => ({
...containerCommonRuntimeSchemaKeys(),
artifacts: artifactsSchema(),
image: containerImageSchema(),
}))
export const containerTestActionSchema = createSchema({
name: "container:Test",
keys: containerTestSpecKeys,
})
// RUN //
export interface ContainerRunOutputs extends ContainerTestOutputs {}
export const containerRunOutputSchema = () => containerTestOutputSchema()
export interface ContainerRunActionSpec extends ContainerTestActionSpec {
cacheResult: boolean
}
export type ContainerRunActionConfig = RunActionConfig<"container", ContainerRunActionSpec>
export type ContainerRunAction = RunAction<ContainerRunActionConfig, ContainerRunOutputs>
export const containerRunSpecKeys = memoize(() => ({
...containerTestSpecKeys(),
cacheResult: runCacheResultSchema(),
}))
export const containerRunActionSchema = createSchema({
name: "container:Run",
keys: containerRunSpecKeys,
})
// BUILD //
export interface ContainerBuildOutputs {
"localImageName": string
"localImageId": string
"deploymentImageName": string
"deploymentImageId": string
// Aliases, for backwards compatibility.
// TODO: remove in 0.14
"local-image-name": string
"local-image-id": string
"deployment-image-name": string
"deployment-image-id": string
}
export const containerBuildOutputSchemaKeys = memoize(() => ({
"localImageName": joi
.string()
.required()
.description("The name of the image (without tag/version) that the Build uses for local builds and deployments.")
.example("my-build"),
"localImageId": joi
.string()
.required()
.description("The full ID of the image (incl. tag/version) that the Build uses for local builds and deployments.")
.example("my-build:v-abf3f8dca"),
"deploymentImageName": joi
.string()
.required()
.description("The name of the image (without tag/version) that the Build will use during deployment.")
.example("my-deployment-registry.io/my-org/my-build"),
"deploymentImageId": joi
.string()
.required()
.description("The full ID of the image (incl. tag/version) that the Build will use during deployment.")
.example("my-deployment-registry.io/my-org/my-build:v-abf3f8dca"),
// Aliases
"local-image-name": joi.string().required().description("Alias for localImageName, for backward compatibility."),
"local-image-id": joi.string().required().description("Alias for localImageId, for backward compatibility."),
"deployment-image-name": joi
.string()
.required()
.description("Alias for deploymentImageName, for backward compatibility."),
"deployment-image-id": joi
.string()
.required()
.description("Alias for deploymentImageId, for backward compatibility."),
}))
export const containerBuildOutputsSchema = createSchema({
name: "container:Build:outputs",
keys: containerBuildOutputSchemaKeys,
})
export interface ContainerBuildActionSpec {
buildArgs: PrimitiveMap
dockerfile: string
extraFlags: string[]
localId?: string
publishId?: string
targetStage?: string
}
export type ContainerBuildActionConfig = BuildActionConfig<"container", ContainerBuildActionSpec>
export type ContainerBuildAction = BuildAction<ContainerBuildActionConfig, ContainerBuildOutputs>
export const containerBuildSpecKeys = memoize(() => ({
localId: joi
.string()
.allow(false, null)
.empty([false, null])
.description(