From a22533410cb195e9cdbf1cfbe84c7785acccdb0f Mon Sep 17 00:00:00 2001 From: Gayan Gunapala <1393675+chathsuom@users.noreply.github.com> Date: Fri, 29 Jan 2021 12:28:22 +1100 Subject: [PATCH] capacity providers (#211) * capacity providers * capacity providers * capacity providers * capacity providers * PR cmments * added assumedRoleArn --- README.md | 1 + .../jenkins/plugins/amazonecs/ECSService.java | 14 ++- .../plugins/amazonecs/ECSTaskTemplate.java | 117 +++++++++++++++++- .../pipeline/ECSTaskTemplateStep.java | 22 ++++ .../ECSTaskTemplateStepExecution.java | 2 + .../amazonecs/ECSTaskTemplate/config.jelly | 24 ++++ .../help-defaultCapacityProvider.html | 26 ++++ .../plugins/amazonecs/ECSCloudTest.java | 2 + .../plugins/amazonecs/ECSSlaveTest.java | 2 + .../amazonecs/ECSTaskTemplateTest.java | 12 +- .../ECSTaskTemplateStepExecutionTest.java | 4 + 11 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/help-defaultCapacityProvider.html diff --git a/README.md b/README.md index bafea34c..e586b713 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ TaskRole: Resource: "*" - Action: - "ecs:ListContainerInstances" + - "ecs:DescribeClusters" Effect: Allow Resource: - !Sub "arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:cluster/" diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSService.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSService.java index 70c5d40c..70519e1e 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSService.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSService.java @@ -340,8 +340,8 @@ TaskDefinition registerTemplate(final String cloudName, final ECSTaskTemplate te if (template.isFargate()) { request - .withRequiresCompatibilities(template.getLaunchType()) - .withNetworkMode("awsvpc") + .withRequiresCompatibilities(LaunchType.FARGATE.toString()) + .withNetworkMode(NetworkMode.Awsvpc.toString()) .withMemory(String.valueOf(template.getMemoryConstraint())) .withCpu(String.valueOf(template.getCpu())); } @@ -439,7 +439,6 @@ RunTaskResult runEcsTask(final ECSSlave agent, final ECSTaskTemplate template, S RunTaskRequest req = new RunTaskRequest() .withTaskDefinition(taskDefinition.getTaskDefinitionArn()) - .withLaunchType(LaunchType.fromValue(template.getLaunchType())) .withOverrides(new TaskOverride() .withContainerOverrides(new ContainerOverride() .withName(agentContainerName) @@ -448,8 +447,13 @@ RunTaskResult runEcsTask(final ECSSlave agent, final ECSTaskTemplate template, S .withEnvironment(envNodeSecret))) .withPlacementStrategy(template.getPlacementStrategyEntries()) .withCluster(clusterArn); - - if (template.getLaunchType() != null && template.getLaunchType().equals("FARGATE")) { + if ( ! template.getDefaultCapacityProvider() && template.getCapacityProviderStrategies() == null ) { + req.withLaunchType(LaunchType.fromValue(template.getLaunchType())); + } + if ( ! template.getDefaultCapacityProvider() && template.getCapacityProviderStrategies() != null ) { + req.withCapacityProviderStrategy(template.getCapacityProviderStrategyEntries()); + } + if (template.isFargate()) { req.withPlatformVersion(template.getPlatformVersion()); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java index f1e414e4..4cd3b886 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate.java @@ -25,12 +25,14 @@ package com.cloudbees.jenkins.plugins.amazonecs; +import com.amazonaws.services.ecs.AmazonECS; import com.amazonaws.services.ecs.model.AwsVpcConfiguration; import com.amazonaws.services.ecs.model.ContainerDefinition; import com.amazonaws.services.ecs.model.HostEntry; import com.amazonaws.services.ecs.model.HostVolumeProperties; import com.amazonaws.services.ecs.model.KeyValuePair; import com.amazonaws.services.ecs.model.LaunchType; +import com.amazonaws.services.ecs.model.CapacityProviderStrategyItem; import com.amazonaws.services.ecs.model.LinuxParameters; import com.amazonaws.services.ecs.model.MountPoint; import com.amazonaws.services.ecs.model.NetworkMode; @@ -40,8 +42,12 @@ import com.amazonaws.services.ecs.model.RegisterTaskDefinitionRequest; import com.amazonaws.services.ecs.model.RepositoryCredentials; import com.amazonaws.services.ecs.model.Volume; +import com.amazonaws.services.ecs.model.DescribeClustersRequest; +import com.amazonaws.services.ecs.model.DescribeClustersResult; +import com.amazonaws.services.ecs.model.Cluster; import static com.google.common.base.Strings.isNullOrEmpty; import hudson.Extension; +import hudson.RelativePath; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.model.Label; @@ -71,6 +77,7 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.Collections; /** * @author Nicolas De Loof @@ -238,6 +245,12 @@ public int getMemoryConstraint() { */ private final String launchType; + /** + * Use default capacity provider will omit launch types and capacity strategies + * + */ + private boolean defaultCapacityProvider; + /** * Task network mode */ @@ -269,7 +282,7 @@ public int getMemoryConstraint() { private List extraHosts; private List portMappings; private List placementStrategies; - + private List capacityProviderStrategies; /** @@ -301,7 +314,9 @@ public ECSTaskTemplate(String templateName, @Nullable String dynamicTaskDefinitionOverride, String image, @Nullable final String repositoryCredentials, - String launchType, + @Nullable String launchType, + boolean defaultCapacityProvider, + @Nullable List capacityProviderStrategies, String networkMode, @Nullable String remoteFSRoot, boolean uniqueRemoteFSRoot, @@ -349,6 +364,8 @@ public ECSTaskTemplate(String templateName, this.memoryReservation = memoryReservation; this.cpu = cpu; this.launchType = launchType; + this.defaultCapacityProvider = defaultCapacityProvider; + this.capacityProviderStrategies = capacityProviderStrategies; this.networkMode = networkMode; this.subnets = subnets; this.securityGroups = securityGroups; @@ -425,6 +442,15 @@ public void setDnsSearchDomains(String dnsSearchDomains) { } public boolean isFargate() { + if (!this.defaultCapacityProvider && this.capacityProviderStrategies != null && ! this.capacityProviderStrategies.isEmpty()) { + for (CapacityProviderStrategyEntry capacityProviderStrategy : this.capacityProviderStrategies) { + String provider = capacityProviderStrategy.provider; + if (provider.contains(LaunchType.FARGATE.toString())) { + return true; + } + } + return false; + } return StringUtils.trimToNull(this.launchType) != null && launchType.equals(LaunchType.FARGATE.toString()); } @@ -489,6 +515,10 @@ public boolean getAssignPublicIp() { return assignPublicIp; } + public boolean getDefaultCapacityProvider() { + return defaultCapacityProvider; + } + public String getDnsSearchDomains() { return dnsSearchDomains; } @@ -613,6 +643,10 @@ public List getPlacementStrategies() { return placementStrategies; } + public List getCapacityProviderStrategies() { + return capacityProviderStrategies; + } + /** * This merge does not take an into consideration the child intentionally setting empty values for parameters like "entrypoint" - in fact * it's not uncommon to override the entrypoint of a container and set it to blank so you can use your own entrypoint as part of the command. @@ -631,6 +665,7 @@ public ECSTaskTemplate merge(ECSTaskTemplate parent) { String image = isNullOrEmpty(this.image) ? parent.getImage() : this.image; String repositoryCredentials = isNullOrEmpty(this.repositoryCredentials) ? parent.getRepositoryCredentials() : this.repositoryCredentials; String launchType = isNullOrEmpty(this.launchType) ? parent.getLaunchType() : this.launchType; + boolean defaultCapacityProvider = this.defaultCapacityProvider ? this.defaultCapacityProvider : parent.getDefaultCapacityProvider(); String networkMode = isNullOrEmpty(this.networkMode) ? parent.getNetworkMode() : this.networkMode; String remoteFSRoot = isNullOrEmpty(this.remoteFSRoot) ? parent.getRemoteFSRoot() : this.remoteFSRoot; @@ -660,6 +695,7 @@ public ECSTaskTemplate merge(ECSTaskTemplate parent) { List mountPoints = isEmpty(this.mountPoints) ? parent.getMountPoints() : this.mountPoints; List portMappings = isEmpty(this.portMappings) ? parent.getPortMappings() : this.portMappings; List placementStrategies = isEmpty(this.placementStrategies) ? parent.getPlacementStrategies() : this.placementStrategies; + List capacityProviderStrategies = isEmpty(this.capacityProviderStrategies) ? parent.getCapacityProviderStrategies() : this.capacityProviderStrategies; String executionRole = isNullOrEmpty(this.executionRole) ? parent.getExecutionRole() : this.executionRole; String taskrole = isNullOrEmpty(this.taskrole) ? parent.getTaskrole() : this.taskrole; @@ -671,6 +707,8 @@ public ECSTaskTemplate merge(ECSTaskTemplate parent) { image, repositoryCredentials, launchType, + defaultCapacityProvider, + capacityProviderStrategies, networkMode, remoteFSRoot, uniqueRemoteFSRoot, @@ -802,6 +840,22 @@ Collection getPlacementStrategyEntries() { return placements; } + Collection getCapacityProviderStrategyEntries() { + if (null == capacityProviderStrategies || capacityProviderStrategies.isEmpty()) + return null; + Collection stragies = new ArrayList(); + for (CapacityProviderStrategyEntry capacityProviderStrategy : this.capacityProviderStrategies) { + String provider = capacityProviderStrategy.provider; + int base = capacityProviderStrategy.base; + int weight = capacityProviderStrategy.weight; + + stragies.add(new CapacityProviderStrategyItem().withCapacityProvider(provider) + .withWeight(weight) + .withBase(base)); + } + return stragies; + } + public static class EnvironmentEntry extends AbstractDescribableImpl implements Serializable { private static final long serialVersionUID = 4195862080979262875L; public String name, value; @@ -957,6 +1011,57 @@ public FormValidation doCheckField(@QueryParameter("field") String field, @Query } } + public static class CapacityProviderStrategyEntry extends AbstractDescribableImpl implements Serializable { + //private static final long serialVersionUID = 4195862080979262875L; + public String provider; + public int base, weight; + + @DataBoundConstructor + public CapacityProviderStrategyEntry(String provider, int base, int weight) { + this.base = base; + this.weight = weight; + this.provider = provider; + } + + @Override + public String toString() { + return "CapacityProviderStrategyEntry{" + provider + "base: " + base + "weight: " + weight + "}"; + } + + @Extension + public static class DescriptorImpl extends Descriptor { + public ListBoxModel doFillProviderItems( + @RelativePath("../..") @QueryParameter String credentialsId, + @RelativePath("../..") @QueryParameter String assumedRoleArn, + @RelativePath("../..") @QueryParameter String regionName, + @RelativePath("../..") @QueryParameter String cluster + ){ + ECSService ecsService = new ECSService(credentialsId, assumedRoleArn, regionName); + final AmazonECS client = ecsService.getAmazonECSClient(); + final List allClusters = new ArrayList(); + DescribeClustersResult result = client.describeClusters(new DescribeClustersRequest().withClusters(cluster)); + allClusters.addAll(result.getClusters()); + final ListBoxModel options = new ListBoxModel(); + for ( Cluster c : allClusters) { + List item = c.getCapacityProviders(); + Collections.sort(item); + for (String provider : item) { + options.add(provider); + } + } + return options; + } + @Override + public String getDisplayName() { + return "CapacityProviderStrategyEntry"; + } + + public FormValidation doCheckField(@QueryParameter("base") int base, @QueryParameter("weight") int weight, @QueryParameter("provider") String provider) throws IOException, ServletException { + return FormValidation.ok(); + } + } + } + public Set getLabelSet() { return Label.parse(label); } @@ -1142,6 +1247,12 @@ public boolean equals(Object o) { if (launchType != null ? !launchType.equals(that.launchType) : that.launchType != null) { return false; } + if (defaultCapacityProvider != that.defaultCapacityProvider) { + return false; + } + if (capacityProviderStrategies != null ? !capacityProviderStrategies.equals(that.capacityProviderStrategies) : that.capacityProviderStrategies != null) { + return false; + } if (networkMode != null ? !networkMode.equals(that.networkMode) : that.networkMode != null) { return false; } @@ -1192,6 +1303,8 @@ public int hashCode() { result = 31 * result + (jvmArgs != null ? jvmArgs.hashCode() : 0); result = 31 * result + (mountPoints != null ? mountPoints.hashCode() : 0); result = 31 * result + (launchType != null ? launchType.hashCode() : 0); + result = 31 * result + (defaultCapacityProvider ? 1 : 0); + result = 31 * result + (capacityProviderStrategies != null ? capacityProviderStrategies.hashCode() : 0); result = 31 * result + (networkMode != null ? networkMode.hashCode() : 0); result = 31 * result + (privileged ? 1 : 0); result = 31 * result + (uniqueRemoteFSRoot ? 1 : 0); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStep.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStep.java index 79a91a6c..aad2a393 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStep.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStep.java @@ -30,6 +30,7 @@ import com.cloudbees.jenkins.plugins.amazonecs.ECSTaskTemplate.MountPointEntry; import com.cloudbees.jenkins.plugins.amazonecs.ECSTaskTemplate.PortMappingEntry; import com.cloudbees.jenkins.plugins.amazonecs.ECSTaskTemplate.PlacementStrategyEntry; +import com.cloudbees.jenkins.plugins.amazonecs.ECSTaskTemplate.CapacityProviderStrategyEntry; import com.google.common.collect.ImmutableSet; import hudson.model.Run; import java.util.logging.Level; @@ -50,6 +51,7 @@ public class ECSTaskTemplateStep extends Step implements Serializable { private String repositoryCredentials; private String image; private String launchType; + private boolean defaultCapacityProvider; private String networkMode; private String remoteFSRoot; private boolean uniqueRemoteFSRoot; @@ -73,6 +75,7 @@ public class ECSTaskTemplateStep extends Step implements Serializable { private List mountPoints; private List portMappings; private List placementStrategies; + private List capacityProviderStrategies; private List overrides; @@ -235,6 +238,15 @@ public boolean getAssignPublicIp() { return assignPublicIp; } + @DataBoundSetter + public void setDefaultCapacityProvider(boolean defaultCapacityProvider) { + this.defaultCapacityProvider = defaultCapacityProvider; + } + + public boolean getDefaultCapacityProvider() { + return defaultCapacityProvider; + } + @DataBoundSetter public void setPrivileged(boolean privileged) { this.privileged = privileged; @@ -343,6 +355,16 @@ public void setPlacementStrategies(List placementStrateg this.placementStrategies = placementStrategies; } + public List getCapacityProviderStrategies() { + return capacityProviderStrategies; + } + + @DataBoundSetter + public void setCapacityProviderStrategy(List capacityProviderStrategies) { + this.capacityProviderStrategies = capacityProviderStrategies; + } + + @DataBoundSetter public void setOverrides(List overrides) { this.overrides = overrides; diff --git a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecution.java b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecution.java index e139f4d4..60b8df4f 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecution.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecution.java @@ -61,6 +61,8 @@ public boolean start() throws Exception { step.getImage(), step.getRepositoryCredentials(), step.getLaunchType(), + step.getDefaultCapacityProvider(), + step.getCapacityProviderStrategies(), step.getNetworkMode(), step.getRemoteFSRoot(), step.getUniqueRemoteFSRoot(), diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/config.jelly index d0648ae1..32a976ca 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/config.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/config.jelly @@ -43,6 +43,30 @@ + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/help-defaultCapacityProvider.html b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/help-defaultCapacityProvider.html new file mode 100644 index 00000000..7f67d9be --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplate/help-defaultCapacityProvider.html @@ -0,0 +1,26 @@ + +When this is enabled, Launch type and Capacity provider strategy will be ignored. +However, value selected in Launch Type will be used to set Require Compatibilities. diff --git a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloudTest.java b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloudTest.java index c2970ead..ebfc1d9f 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloudTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSCloudTest.java @@ -149,6 +149,8 @@ private ECSTaskTemplate getTaskTemplate(String templateName, String label) { "image", "repositoryCredentials", "launchType", + false, + null, "networkMode", "remoteFSRoot", false, diff --git a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlaveTest.java b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlaveTest.java index aa563fa2..65ac1ca7 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlaveTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSSlaveTest.java @@ -81,6 +81,8 @@ private ECSTaskTemplate getTaskTemplate() { "image", "repositoryCredentials", "launchType", + false, + null, "networkMode", "remoteFSRoot", false, diff --git a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplateTest.java b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplateTest.java index e6fc3732..90ae87f6 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplateTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/ECSTaskTemplateTest.java @@ -10,7 +10,7 @@ public class ECSTaskTemplateTest { ECSTaskTemplate getParent() { return new ECSTaskTemplate( "parent-name", "parent-label", - null, null, "parent-image", "parent-repository-credentials", "FARGATE", "parent-network-mode", "parent-remoteFSRoot", + null, null, "parent-image", "parent-repository-credentials", "FARGATE", false, null, "parent-network-mode", "parent-remoteFSRoot", false, null, 0, 0, 0, null, null, false, false, "parent-containerUser", null, null, null, null, null, null, null, null, null, 0); } @@ -18,7 +18,7 @@ ECSTaskTemplate getParent() { ECSTaskTemplate getChild(String parent) { return new ECSTaskTemplate( "child-name", "child-label", - null, null, "child-image", "child-repository-credentials", "EC2", "child-network-mode", "child-remoteFSRoot", + null, null, "child-image", "child-repository-credentials", "EC2", false, null, "child-network-mode", "child-remoteFSRoot", false, null, 0, 0, 0, null, null, false, false, "child-containerUser", null, null, null, null, null, null, null, null, parent, 0); } @@ -31,7 +31,7 @@ public void shouldMerge() throws Exception { ECSTaskTemplate expected = new ECSTaskTemplate( "child-name", "child-label", - null, null, "child-image", "child-repository-credentials", "EC2", "child-network-mode", "child-remoteFSRoot", + null, null, "child-image", "child-repository-credentials", "EC2", false, null, "child-network-mode", "child-remoteFSRoot", false, null, 0, 0, 0, null, null, false, false, "child-containerUser", null, null, null, null, null, null, null, null, null, 0); @@ -48,7 +48,7 @@ public void shouldReturnSettingsFromParent() throws Exception { ECSTaskTemplate expected = new ECSTaskTemplate( "child-name", "child-label", - null, null, "child-image", "child-repository-credentials", "EC2", "child-network-mode", "child-remoteFSRoot", + null, null, "child-image", "child-repository-credentials", "EC2", false, null, "child-network-mode", "child-remoteFSRoot", false, null, 0, 0, 0, null, null, false, false, "child-containerUser", null, null, null, null, null, null, null, null, null, 0); @@ -64,7 +64,7 @@ public void shouldReturnChildIfNoParent() throws Exception { ECSTaskTemplate expected = new ECSTaskTemplate( "child-name", "child-label", - null, null, "child-image", "child-repository-credentials", "EC2", "child-network-mode", "child-remoteFSRoot", + null, null, "child-image", "child-repository-credentials", "EC2", false, null, "child-network-mode", "child-remoteFSRoot", false, null, 0, 0, 0, null, null, false, false, "child-containerUser", null, null, null, null, null, null, null, null, null, 0); @@ -82,7 +82,7 @@ public void shouldOverrideEntrypoint() { ECSTaskTemplate expected = new ECSTaskTemplate( "child-name", "child-label", - null, null, "child-image", "child-repository-credentials", "EC2", "child-network-mode", "child-remoteFSRoot", + null, null, "child-image", "child-repository-credentials", "EC2", false, null, "child-network-mode", "child-remoteFSRoot", false, null, 0, 0, 0, null, null, false, false, "child-containerUser", null, null, null, null, null, null, null, null, null, 0); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecutionTest.java b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecutionTest.java index da905823..045c088a 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecutionTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/amazonecs/pipeline/ECSTaskTemplateStepExecutionTest.java @@ -71,6 +71,8 @@ public void testMerge() throws Exception { "image-override", UUID.randomUUID().toString(), UUID.randomUUID().toString(), + false, + null, UUID.randomUUID().toString(), UUID.randomUUID().toString(), false, @@ -146,6 +148,8 @@ private ECSTaskTemplate getTaskTemplate(String templateName, String label) { "image", "repositoryCredentials", "launchType", + false, + null, "networkMode", "remoteFSRoot", false,