This commit is contained in:
BoykoAlex
2017-07-19 17:56:24 -04:00
91 changed files with 3968 additions and 597 deletions

View File

@@ -79,6 +79,14 @@ resources:
secret_access_key: {{s3_secretkey}}
region_name: {{s3_region}}
regexp: sts4/vscode-extensions/snapshots/vscode-manifest-yaml-(.*).vsix
- name: s3-manifest-yaml-fatjar-snapshot
type: s3
source:
bucket: {{s3_bucket}}
access_key_id: {{s3_accesskey}}
secret_access_key: {{s3_secretkey}}
region_name: {{s3_region}}
regexp: sts4/fatjars/snapshots/manifest-yaml-language-server-(.*).jar
- name: snapshot-website
type: s3-multi
source:
@@ -232,10 +240,15 @@ jobs:
params:
text: |
Concourse ${BUILD_PIPELINE_NAME}/${BUILD_JOB_NAME}/${BUILD_NAME} has failed!
- put: s3-manifest-yaml-vsix-snapshot
params:
file: out/vscode-manifest-yaml-*.vsix
acl: public-read
- aggregate:
- put: s3-manifest-yaml-vsix-snapshot
params:
file: out/vscode-manifest-yaml-*.vsix
acl: public-read
- put: s3-manifest-yaml-fatjar-snapshot
params:
file: out/*.jar
acl: public-read
- name: trigger-rc-build
serial: true
plan:

View File

@@ -2,6 +2,7 @@
set -e
workdir=`pwd`
sources=$workdir/sts4/vscode-extensions/$extension_id
server_id=${extension_id#vscode-}
if [ -d "maven-cache" ]; then
echo "Prepopulating maven cache"
@@ -48,3 +49,4 @@ if [ "$dist_type" == release ]; then
fi
cp *.vsix $workdir/out
cp $workdir/sts4/headless-services/${server_id}-language-server/target/*.jar $workdir/out/${server_id}-language-server-${base_version}-${timestamp}.jar

View File

@@ -103,4 +103,5 @@ public class TypedProperty implements YTypedProperty {
public Deprecation getDeprecation() {
return deprecation;
}
}

View File

@@ -0,0 +1,59 @@
eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
sp_cleanup.add_default_serial_version_id=true
sp_cleanup.add_generated_serial_version_id=false
sp_cleanup.add_missing_annotations=true
sp_cleanup.add_missing_deprecated_annotations=true
sp_cleanup.add_missing_methods=false
sp_cleanup.add_missing_nls_tags=false
sp_cleanup.add_missing_override_annotations=true
sp_cleanup.add_missing_override_annotations_interface_methods=true
sp_cleanup.add_serial_version_id=false
sp_cleanup.always_use_blocks=true
sp_cleanup.always_use_parentheses_in_expressions=false
sp_cleanup.always_use_this_for_non_static_field_access=false
sp_cleanup.always_use_this_for_non_static_method_access=false
sp_cleanup.convert_functional_interfaces=false
sp_cleanup.convert_to_enhanced_for_loop=false
sp_cleanup.correct_indentation=false
sp_cleanup.format_source_code=false
sp_cleanup.format_source_code_changes_only=false
sp_cleanup.insert_inferred_type_arguments=false
sp_cleanup.make_local_variable_final=true
sp_cleanup.make_parameters_final=false
sp_cleanup.make_private_fields_final=true
sp_cleanup.make_type_abstract_if_missing_method=false
sp_cleanup.make_variable_declarations_final=false
sp_cleanup.never_use_blocks=false
sp_cleanup.never_use_parentheses_in_expressions=true
sp_cleanup.on_save_use_additional_actions=true
sp_cleanup.organize_imports=true
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
sp_cleanup.remove_private_constructors=true
sp_cleanup.remove_redundant_type_arguments=false
sp_cleanup.remove_trailing_whitespaces=true
sp_cleanup.remove_trailing_whitespaces_all=true
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
sp_cleanup.remove_unnecessary_casts=true
sp_cleanup.remove_unnecessary_nls_tags=false
sp_cleanup.remove_unused_imports=false
sp_cleanup.remove_unused_local_variables=false
sp_cleanup.remove_unused_private_fields=true
sp_cleanup.remove_unused_private_members=false
sp_cleanup.remove_unused_private_methods=true
sp_cleanup.remove_unused_private_types=true
sp_cleanup.sort_members=false
sp_cleanup.sort_members_all=false
sp_cleanup.use_anonymous_class_creation=false
sp_cleanup.use_blocks=false
sp_cleanup.use_blocks_only_for_return_and_throw=false
sp_cleanup.use_lambda=true
sp_cleanup.use_parentheses_in_expressions=false
sp_cleanup.use_this_for_non_static_field_access=false
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
sp_cleanup.use_this_for_non_static_method_access=false
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true

View File

@@ -45,6 +45,18 @@
<version>${dependencies.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mockito-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ide.vscode</groupId>
<artifactId>language-server-test-harness</artifactId>
<version>${dependencies.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems.MISSING_PROPERTY;
import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems.problem;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
public class BoshConstraints {
public static final Constraint SHA1_REQUIRED_FOR_HTTP_URL = new Constraint() {
@Override
public void verify(DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) {
NodeTuple urlProp = NodeUtil.getPropertyTuple(node, "url");
if (urlProp!=null) {
String url = NodeUtil.asScalar(urlProp.getValueNode());
if (url!=null && url.startsWith("http")) {
Node sha1 = NodeUtil.getProperty(node, "sha1");
if (sha1==null) {
problems.accept(problem(MISSING_PROPERTY, "'sha1' is required when the 'url' is http(s)", urlProp.getKeyNode()));
}
}
}
}
};
}

View File

@@ -10,30 +10,52 @@
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import org.springframework.ide.vscode.bosh.models.CachingModelProvider;
import org.springframework.ide.vscode.bosh.models.CloudConfigModel;
import org.springframework.ide.vscode.bosh.models.DynamicModelProvider;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.Renderables;
import org.springframework.ide.vscode.commons.util.ValueParsers;
import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YAtomicType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YBeanType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YContextSensitive;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.YTypedPropertyImpl;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil;
import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty;
import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema;
import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraints;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
/**
* @author Kris De Volder
*/
public class BoshDeploymentManifestSchema implements YamlSchema {
private final AbstractType TOPLEVEL_TYPE;
private final YBeanType V2_TOPLEVEL_TYPE;
private final YBeanType V1_TOPLEVEL_TYPE;
private final YContextSensitive TOPLEVEL_TYPE;
private final YTypeUtil TYPE_UTIL;
private static final ImmutableSet<String> DEPRECATED_V1_PROPS = ImmutableSet.of("resource_pools", "networks", "compilation", "jobs", "disk_pools", "cloud_provider");
private static final ImmutableSet<String> SHARED_V1_V2_PROPS = ImmutableSet.of("name", "director_uuid", "releases", "update", "properties");
private ImmutableList<YType> definitionTypes = null;
//Note: 'director_uuid' is also deprecated. But its treated separately since it is deprecated and ignored by V2 client no matter what (i.e. deprecated in both schemas)
public final YTypeFactory f = new YTypeFactory()
.enableTieredProposals(false);
.enableTieredProposals(false)
.suggestDeprecatedProperties(false);
public final YType t_string = f.yatomic("String");
public final YType t_ne_string = f.yatomic("String")
.parseWith(ValueParsers.NE_STRING);
@@ -49,45 +71,180 @@ public class BoshDeploymentManifestSchema implements YamlSchema {
public final YType t_strictly_pos_integer = f.yatomic("Strictly Positive Integer")
.parseWith(ValueParsers.integerAtLeast(1));
public final YType t_uuid = f.yatomic("UUID").parseWith(UUID::fromString);
public final YType t_integer_or_range = f.yatomic("Integer or Range")
.parseWith(BoshValueParsers.INTEGER_OR_RANGE);
private YType t_instance_group_name_def;
private YType t_stemcell_alias_def;
private YType t_stemcell_alias_ref;
private YType t_release_name_def;
private YType t_release_name_ref;
private YType t_var_name_def;
private final ASTTypeCache astTypes;
private DynamicModelProvider<CloudConfigModel> cloudConfigProvider;
public BoshDeploymentManifestSchema() {
public BoshDeploymentManifestSchema(ASTTypeCache astTypes, DynamicModelProvider<CloudConfigModel> cloudConfigProvider) {
this.astTypes = astTypes;
this.cloudConfigProvider = new CachingModelProvider<>(cloudConfigProvider);
TYPE_UTIL = f.TYPE_UTIL;
TOPLEVEL_TYPE = f.ybean("BoshDeploymentManifest");
addProp(TOPLEVEL_TYPE, "name", t_ne_string).isPrimary(true);
addProp(TOPLEVEL_TYPE, "director_uuid", t_uuid)
.isRequired(true);
V2_TOPLEVEL_TYPE = createV2Schema();
V1_TOPLEVEL_TYPE = createV1Schema(V2_TOPLEVEL_TYPE);
TOPLEVEL_TYPE = f.contextAware("DeploymenManifestV1orV2", (dc) -> {
boolean looksLikeV1 = dc.getDefinedProperties().stream().anyMatch(DEPRECATED_V1_PROPS::contains);
return looksLikeV1 ? V1_TOPLEVEL_TYPE : V2_TOPLEVEL_TYPE;
});
}
private YBeanType createV1Schema(AbstractType v2Schema) {
YBeanType v1Schema = f.ybean("DeploymentManifestV1");
Map<String, YTypedProperty> v2properties = v2Schema.getPropertiesMap();
ImmutableSet<String> v1Props = ImmutableSet.<String>builder()
.addAll(DEPRECATED_V1_PROPS)
.addAll(SHARED_V1_V2_PROPS)
.build();
for (String name : v1Props) {
YTypedProperty prop = v2properties.get(name);
Assert.isNotNull(prop);
v1Schema.addProperty(prop);
}
return v1Schema;
}
private YBeanType createV2Schema() {
YBeanType v2Schema = f.ybean("BoshDeploymentManifest");
addProp(v2Schema, "name", t_ne_string).isPrimary(true);
addProp(v2Schema, "director_uuid", t_uuid).isDeprecated(
"bosh v2 CLI no longer checks or requires director_uuid in the deployment manifest. " +
"To achieve similar safety make sure to give unique deployment names across environments."
);
t_instance_group_name_def = f.yatomic("InstanceGroupName")
.parseWith(ValueParsers.NE_STRING);
t_stemcell_alias_def = f.yatomic("StemcellAlias")
.parseWith(ValueParsers.NE_STRING);
t_stemcell_alias_ref = f.yenumFromDynamicValues("StemcellAlias", (dc) -> astTypes.getDefinedNames(dc, t_stemcell_alias_def));
t_release_name_def = f.yatomic("ReleaseName")
.parseWith(ValueParsers.NE_STRING);
t_release_name_ref = f.yenumFromDynamicValues("ReleaseName", (dc) -> astTypes.getDefinedNames(dc, t_release_name_def));
t_var_name_def = f.yatomic("VariableName")
.parseWith(ValueParsers.NE_STRING);
YAtomicType t_ip_address = f.yatomic("IPAddress"); //TODO: some kind of checking?
t_ip_address.parseWith(ValueParsers.NE_STRING);
YAtomicType t_url = f.yatomic("URL");
t_url.parseWith(BoshValueParsers.url("http", "https", "file"));
YAtomicType t_network_name = f.yatomic("NetworkName"); //TODO: resolve from 'cloud config' https://www.pivotaltracker.com/story/show/148712155
t_network_name.parseWith(ValueParsers.NE_STRING);
YAtomicType t_disk_type = f.yatomic("DiskType"); //TODO: resolve from 'cloud config' https://www.pivotaltracker.com/story/show/148704001
t_disk_type.parseWith(ValueParsers.NE_STRING);
YAtomicType t_vm_extension = f.yatomic("VMExtension"); //TODO: resolve dynamically from 'cloud config' ? https://www.pivotaltracker.com/story/show/148703877
t_vm_extension.parseWith(ValueParsers.NE_STRING);
YAtomicType t_vm_type = f.yenumFromDynamicValues("VMType", (dc) -> cloudConfigProvider.getModel(dc).getVMTypes());
YAtomicType t_az = f.yatomic("AvailabilityZone"); //TODO: resolve dynamically from 'cloud config': https://www.pivotaltracker.com/story/show/148704481
t_az.parseWith(ValueParsers.NE_STRING);
YBeanType t_network = f.ybean("Network");
addProp(t_network, "name", t_network_name).isRequired(true);
addProp(t_network, "static_ips", f.yseq(t_ip_address));
addProp(t_network, "default", f.yseq(t_ne_string)); //TODO: Can we determine the set of valid values? How?
YBeanType t_instance_group_env = f.ybean("InstanceGroupEnv");
addProp(t_instance_group_env, "bosh", t_params);
addProp(t_instance_group_env, "password", t_ne_string);
YAtomicType t_version = f.yatomic("Version");
t_version.addHints("latest");
t_version.parseWith(ValueParsers.NE_STRING);
YBeanType t_release = f.ybean("Release");
addProp(t_release, "name", t_ne_string).isPrimary(true);
addProp(t_release, "version", t_version).isRequired(true);
addProp(TOPLEVEL_TYPE, "releases", f.yseq(t_release)).isRequired(true);
addProp(t_release, "name", t_release_name_def).isPrimary(true);
addProp(t_release, "version", t_version);
//TODO: the checking here is just 'my best guess'. Unclarity remains:
// See: https://github.com/cloudfoundry/docs-bosh/issues/330
addProp(t_release, "url", t_url);
addProp(t_release, "sha1", t_ne_string);
addProp(v2Schema, "releases", f.yseq(t_release)).isRequired(true);
t_release.require(Constraints.requireAtLeastOneOf("url", "version"));
// ^^^^^^^ allthough docs seem to imply you shouldn't
// define both url and version it seems that bosh tolerates it.
t_release.require(BoshConstraints.SHA1_REQUIRED_FOR_HTTP_URL);
YBeanType t_stemcell = f.ybean("Stemcell");
addProp(t_stemcell, "alias", t_ne_string).isRequired(true);
addProp(t_stemcell, "alias", t_stemcell_alias_def).isRequired(true);
addProp(t_stemcell, "version", t_ne_string).isRequired(true);
addProp(t_stemcell, "name", t_ne_string);
addProp(t_stemcell, "os", t_ne_string);
t_stemcell.requireOneOf("name", "os");
addProp(TOPLEVEL_TYPE, "stemcells", f.yseq(t_stemcell)).isRequired(true);
addProp(v2Schema, "stemcells", f.yseq(t_stemcell)).isRequired(true);
YType t_update = t_params; //TODO: https://www.pivotaltracker.com/story/show/148627121
addProp(TOPLEVEL_TYPE, "update", t_update).isRequired(true);
YType t_instance_group = t_params; //TODO: https://www.pivotaltracker.com/story/show/148627211
addProp(TOPLEVEL_TYPE, "instance_groups", f.yseq(t_instance_group)).isRequired(true);
addProp(TOPLEVEL_TYPE, "properties", t_params).isDeprecated("Deprecated in favor of job level properties and links");
YType t_variable = t_params; //TODO: https://www.pivotaltracker.com/story/show/148627441
addProp(TOPLEVEL_TYPE, "variables", f.yseq(t_variable));
YBeanType t_update = f.ybean("Update");
addProp(t_update, "canaries", t_strictly_pos_integer).isRequired(true);
addProp(t_update, "max_in_flight", t_pos_integer).isRequired(true);
addProp(t_update, "canary_watch_time", t_integer_or_range).isRequired(true);
addProp(t_update, "update_watch_time", t_integer_or_range).isRequired(true);
addProp(t_update, "serial", t_boolean);
addProp(v2Schema, "update", t_update).isRequired(true);
addProp(TOPLEVEL_TYPE, "tags", t_params);
YBeanType t_job = f.ybean("Job");
addProp(t_job, "name", t_ne_string).isRequired(true);
addProp(t_job, "release", t_release_name_ref).isRequired(true);
addProp(t_job, "consumes", t_params);
addProp(t_job, "provides", t_params);
addProp(t_job, "properties", t_params);
YBeanType t_instance_group = f.ybean("InstanceGroup");
addProp(t_instance_group, "name", t_instance_group_name_def).isPrimary(true);
addProp(t_instance_group, "azs", f.yseq(t_az)).isRequired(true);
addProp(t_instance_group, "instances", t_pos_integer).isRequired(true); //Strictly positive? Or zero is okay?
addProp(t_instance_group, "jobs", f.yseq(t_job)).isRequired(true);
addProp(t_instance_group, "vm_type", t_vm_type).isRequired(true);
addProp(t_instance_group, "vm_extensions", f.yseq(t_vm_extension));
addProp(t_instance_group, "stemcell", t_stemcell_alias_ref).isRequired(true);
addProp(t_instance_group, "persistent_disk_type", t_disk_type);
addProp(t_instance_group, "networks", f.yseq(t_network)).isRequired(true);
YType t_update_override = f.ybean("UpdateOverrides", t_update.getProperties()
.stream()
.map((YTypedProperty prop) ->
f.yprop(prop).isRequired(false)
)
.toArray(sz -> new YTypedProperty[sz])
);
addProp(t_instance_group, "update", t_update_override);
YType t_migration = t_params; //TODO: https://www.pivotaltracker.com/story/show/148712595
addProp(t_instance_group, "migrated_from", f.yseq(t_migration));
addProp(t_instance_group, "lifecycle", f.yenum("WorkloadType", "service", "errand"));
addProp(t_instance_group, "properties", t_params).isDeprecated("Deprecated in favor of job level properties and links");
addProp(t_instance_group, "env", t_instance_group_env);
addProp(v2Schema, "instance_groups", f.yseq(t_instance_group)).isRequired(true);
addProp(v2Schema, "properties", t_params).isDeprecated("Deprecated in favor of job level properties and links");
YBeanType t_variable = f.ybean("Variable");
addProp(t_variable, "name", t_var_name_def).isPrimary(true);
addProp(t_variable, "type", f.yenum("VariableType", "certificate", "password", "rsa", "ssh")).isRequired(true);
addProp(t_variable, "options", t_params);
addProp(v2Schema, "variables", f.yseq(t_variable));
addProp(v2Schema, "tags", t_params);
for (String v1Prop : DEPRECATED_V1_PROPS) {
addProp(v2Schema, v1Prop, t_any).isDeprecated("Deprecated: '"+v1Prop+"' is a V1 schema property. Consider migrating your deployment manifest to V2");
}
for (YType defType : getDefinitionTypes()) {
v2Schema.require(Constraints.uniqueDefinition(this.astTypes, defType, YamlSchemaProblems.problemType("BOSH_DUPLICATE_"+defType)));
}
return v2Schema;
}
@Override
@@ -99,13 +256,13 @@ public class BoshDeploymentManifestSchema implements YamlSchema {
public YTypeUtil getTypeUtil() {
return TYPE_UTIL;
}
private YTypedPropertyImpl prop(AbstractType beanType, String name, YType type) {
YTypedPropertyImpl prop = f.yprop(name, type);
prop.setDescriptionProvider(descriptionFor(beanType, name));
return prop;
}
public static Renderable descriptionFor(YType owner, String propName) {
String typeName = owner.toString();
return Renderables.fromClasspath(BoshDeploymentManifestSchema.class, "/desc/"+typeName+"/"+propName);
@@ -121,4 +278,16 @@ public class BoshDeploymentManifestSchema implements YamlSchema {
return p;
}
public Collection<YType> getDefinitionTypes() {
if (definitionTypes==null) {
definitionTypes = ImmutableList.of(
t_instance_group_name_def,
t_stemcell_alias_def,
t_release_name_def,
t_var_name_def
);
}
return definitionTypes;
}
}

View File

@@ -10,6 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import org.springframework.ide.vscode.bosh.models.CloudConfigModel;
import org.springframework.ide.vscode.bosh.models.DynamicModelProvider;
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter;
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter.LazyCompletionResolver;
import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider;
@@ -28,8 +30,9 @@ import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngi
import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions;
import org.springframework.ide.vscode.commons.yaml.hover.YamlHoverInfoProvider;
import org.springframework.ide.vscode.commons.yaml.quickfix.YamlQuickfixes;
import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache;
import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlSymbolHandler;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaBasedReconcileEngine;
import org.springframework.ide.vscode.commons.yaml.schema.YamlSchema;
import org.springframework.ide.vscode.commons.yaml.structure.YamlStructureProvider;
import org.yaml.snakeyaml.Yaml;
@@ -39,11 +42,12 @@ public class BoshLanguageServer extends SimpleLanguageServer {
private final LazyCompletionResolver completionResolver = new LazyCompletionResolver(); //Set to null to disable lazy resolving
private final VscodeCompletionEngineAdapter completionEngine;
public BoshLanguageServer() {
public BoshLanguageServer(DynamicModelProvider<CloudConfigModel> cloudConfigProvider) {
super("vscode-bosh");
YamlASTProvider parser = new YamlParser(yaml);
SimpleTextDocumentService documents = getTextDocumentService();
YamlSchema schema = new BoshDeploymentManifestSchema();
ASTTypeCache astTypeCache = new ASTTypeCache();
BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(astTypeCache, cloudConfigProvider);
YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT;
YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema);
@@ -53,7 +57,9 @@ public class BoshLanguageServer extends SimpleLanguageServer {
HoverInfoProvider infoProvider = new YamlHoverInfoProvider(parser, structureProvider, contextProvider);
VscodeHoverEngine hoverEngine = new VscodeHoverEngineAdapter(this, infoProvider);
YamlQuickfixes quickfixes = new YamlQuickfixes(getQuickfixRegistry(), getTextDocumentService(), structureProvider);
IReconcileEngine engine = new YamlSchemaBasedReconcileEngine(parser, schema, quickfixes);
YamlSchemaBasedReconcileEngine engine = new YamlSchemaBasedReconcileEngine(parser, schema, quickfixes);
engine.setTypeCollector(astTypeCache);
documents.onDocumentSymbol(new TypeBasedYamlSymbolHandler(documents, astTypeCache, schema.getDefinitionTypes()));
documents.onDidChangeContent(params -> {
validateOnDocumentChange(engine, params.getDocument());

View File

@@ -0,0 +1,99 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import java.net.URI;
import org.springframework.ide.vscode.commons.util.ValueParseException;
import org.springframework.ide.vscode.commons.util.ValueParser;
import com.google.common.collect.ImmutableList;
public class BoshValueParsers {
public static ValueParser url(String... _schemes) {
return new ValueParser() {
private final ImmutableList<String> validSchemes = ImmutableList.copyOf(_schemes);
@Override
public Object parse(String s) throws Exception {
URI uri = new URI(s);
String scheme = uri.getScheme();
if (scheme==null) {
throw new ValueParseException(message());
} else if (!validSchemes.contains(scheme.toLowerCase())) {
int start = s.indexOf(scheme);
if (start>=0) {
int end = start + scheme.length();
throw new ValueParseException(message(), start, end);
} else {
// Trouble finding exact location of underlined region so underline whole url
throw new ValueParseException(message());
}
}
return uri;
}
private String message() {
return "Url scheme must be one of "+validSchemes;
}
};
}
public static final ValueParser INTEGER_OR_RANGE = new ValueParser() {
private int findDash(String s) throws ValueParseException {
int firstDash = s.indexOf('-');
if (firstDash<0) {
return firstDash; //no dash... and that's okay!
}
int secondDash = s.indexOf('-', firstDash+1);
if (secondDash>=0) {
//Only one dash is expected!
throw new ValueParseException("Should be either a Integer, or a range (of the form '<integer>-<integer>')");
}
return firstDash;
}
@Override
public Object parse(String s) throws Exception {
int dash = findDash(s);
if (dash>=0) {
int low, high;
//range
try {
low = Integer.parseInt(s.substring(0, dash));
} catch (Exception e) {
throw new ValueParseException("Should be a Integer", 0, dash);
}
try {
high = Integer.parseInt(s.substring(dash+1));
} catch (Exception e) {
throw new ValueParseException("Should be a Integer", dash+1, s.length());
}
if (low>high) {
throw new ValueParseException(low + " should be smaller than "+high);
}
} else {
//integer
try {
return Integer.parseInt(s);
} catch (Exception e) {
throw new ValueParseException("Should be either a Integer, or a range (of the form '<integer>-<integer>')");
}
}
return s;
}
};
}

View File

@@ -8,16 +8,15 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import java.io.IOException;
import org.springframework.ide.vscode.bosh.models.BoshCommandCloudConfigProvider;
import org.springframework.ide.vscode.commons.languageserver.LaunguageServerApp;
import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions;
public class Main {
public static void main(String[] args) throws IOException, InterruptedException {
LaunguageServerApp.start(() -> new BoshLanguageServer());
LaunguageServerApp.start(() -> new BoshLanguageServer(new BoshCommandCloudConfigProvider()));
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh.models;
import java.io.File;
import java.time.Duration;
import java.util.Collection;
import java.util.stream.Stream;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.CollectorUtil;
import org.springframework.ide.vscode.commons.util.ExternalCommand;
import org.springframework.ide.vscode.commons.util.ExternalProcess;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
import org.springframework.ide.vscode.commons.yaml.ast.YamlParser;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlTraversal;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.representer.Representer;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Concrete implementation of {@link CloudConfigProvider} that runs `bosh cloud-config` command
* with bosh cli and parses the output.
*
* @author Kris De Volder
*/
public class BoshCommandCloudConfigProvider implements DynamicModelProvider<CloudConfigModel> {
ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
final YamlParser yamlParser;
private Duration CMD_TIMEOUT = Duration.ofSeconds(10);
public BoshCommandCloudConfigProvider() {
Representer representer = new Representer();
representer.getPropertyUtils().setSkipMissingProperties(true);
yamlParser = new YamlParser(new Yaml());
}
/**
* For deserializing the output from bosh cloud-config command.
*/
public static class CloudConfigResponse {
private String[] blocks;
@JsonProperty("Blocks")
public String[] getBlocks() {
return blocks;
}
public void setBlocks(String[] blocks) {
this.blocks = blocks;
}
}
YamlTraversal VM_TYPE_NAMES = YamlPath.EMPTY
.thenAnyChild()
.thenValAt("vm_types")
.thenAnyChild()
.thenValAt("name");
@Override
public CloudConfigModel getModel(DynamicSchemaContext dc) throws Exception {
String out = executeBoshCloudConfigCommand();
CloudConfigResponse response = mapper.readValue(out, CloudConfigResponse.class);
String[] blocks = response.getBlocks();
Assert.isLegal(blocks!=null);
Assert.isLegal(blocks.length==1);
TextDocument doc = new TextDocument(null, LanguageId.BOSH_CLOUD_CONFIG);
doc.setText(blocks[0]);
YamlFileAST ast = yamlParser.getAST(doc);
return new CloudConfigModel() {
@Override
public Collection<String> getVMTypes() {
return VM_TYPE_NAMES.traverseAmbiguously(ast)
.flatMap(nameNode -> {
String name = NodeUtil.asScalar(nameNode);
return StringUtil.hasText(name)
? Stream.of(name)
: Stream.empty();
})
.collect(CollectorUtil.toMultiset());
}
};
}
/**
* Configure how long we wait for the command to fetch cloud config before
* raising timeout exception. (The command may block for long amounts of time
* of the director is unreachable on the network).
*/
public void setCommandTimeout(Duration duration) {
this.CMD_TIMEOUT = duration;
}
protected String executeBoshCloudConfigCommand() throws Exception {
ExternalCommand command = new ExternalCommand("bosh", "cloud-config", "--json");
ExternalProcess process = new ExternalProcess(new File(".").getAbsoluteFile(), command, true, CMD_TIMEOUT);
System.out.println("executeBoshCloudConfigCommand: "+process);
String out = process.getOut();
return out;
}
}

View File

@@ -0,0 +1,73 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh.models;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
* Wraps around a {@link DynamicModelProvider} to add a cache.
*/
public class CachingModelProvider<T> implements DynamicModelProvider<T> {
/**
* Special key to use when the actual key is null (because guava cache doesn't
* like null keys).
*/
private static final Object NULL_KEY = new Object();
private long timeout = 15;
private TimeUnit timeoutUnit = TimeUnit.SECONDS;
private Cache<Object, T> cache = createCache();
private final DynamicModelProvider<T> delegate;
public CachingModelProvider(DynamicModelProvider<T> delegate) {
this.delegate = delegate;
}
/**
* Function used to determine the caching key, given the current {@link DynamicSchemaContext}.
* <p>
* The default keyGetter ignores the context and just returns the same object all the time. This
* results in a cache that only keeps a single value (since there's only a single key ever used
* to store / find cache entries.
*/
private Function<DynamicSchemaContext, Object> keyGetter = (dc) -> "WHATEVER";
protected Cache<Object, T> createCache() {
return CacheBuilder.newBuilder()
.expireAfterWrite(timeout, timeoutUnit)
.build();
}
public CachingModelProvider<T> setTimeout(long timeout, TimeUnit unit) {
this.timeout = timeout;
this.timeoutUnit = unit;
return this;
}
@Override
public T getModel(DynamicSchemaContext dc) throws Exception {
Object key = keyGetter.apply(dc);
if (key==null) {
//guava cache doesn't like null key
key = NULL_KEY;
}
return cache.get(key, () -> delegate.getModel(dc));
}
}

View File

@@ -0,0 +1,20 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh.models;
import java.util.Collection;
/**
* Represents CloudConfig information as might be retrieved from bosh director.
*/
public interface CloudConfigModel {
Collection<String> getVMTypes();
}

View File

@@ -0,0 +1,10 @@
package org.springframework.ide.vscode.bosh.models;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
/**
* Responsible for somehow obtaining {@link CloudConfigModel} relative to a {@link DynamicSchemaContext}
*/
public interface DynamicModelProvider<T> {
T getModel(DynamicSchemaContext dc) throws Exception;
}

View File

@@ -0,0 +1 @@
*Required*. List of AZs associated with this instance group (should only be used when using [first class AZs](https://bosh.io/docs/azs.html) ). Example: `[z1, z2]`.

View File

@@ -0,0 +1 @@
*Optional*. Specifies advanced BOSH Agent configuration for each instance in the group.

View File

@@ -0,0 +1 @@
*Required*. The number of instances in this group. Each instance is a VM.

View File

@@ -0,0 +1 @@
*Required*. Specifies the name and release of jobs that will be installed on each instance.

View File

@@ -0,0 +1 @@
Specifies the kind of workload the instance group represents. Valid values are `service` and `errand`; defaults to `service`. A `service` runs indefinitely and restarts if it fails. An `errand` starts with a manual trigger and does not restart if it fails.

View File

@@ -0,0 +1 @@
*Optional*. Specific migration settings for this instance group. Use this to [rename and/or migrate instance groups](https://bosh.io/docs/migrated-from.html).

View File

@@ -0,0 +1 @@
*Required*. A unique name used to identify and reference the instance group.

View File

@@ -0,0 +1 @@
*Required*. Specifies the networks this instance requires.

View File

@@ -0,0 +1 @@
*Optional*. A valid disk type name from the cloud config. [Read more about persistent disks](https://bosh.io/docs/persistent-disks.html)

View File

@@ -0,0 +1 @@
*Optional*. Specifies instance group properties. Deprecated in favor of job level properties and links.

View File

@@ -0,0 +1 @@
*Required*. A valid stemcell alias from the Stemcells Block.

View File

@@ -0,0 +1 @@
*Optional*. Specific update settings for this instance group. Use this to override [global job update settings](https://bosh.io/docs/manifest-v2.html#update) on a per-instance-group basis.

View File

@@ -0,0 +1 @@
*Optional*. A valid list of VM extension names from the cloud config.

View File

@@ -0,0 +1 @@
*Required*. A valid VM type name from the cloud config.

View File

@@ -0,0 +1 @@
*Optional*. Crypted password for `vcap/root` user (will be placed into `/etc/shadow` on Linux).

View File

@@ -0,0 +1 @@
*Optional*. Links consumed by the job. [Read more about link configuration](https://bosh.io/docs/links.html#deployment)

View File

@@ -0,0 +1 @@
*Required*. The job name.

View File

@@ -0,0 +1 @@
*Optional*. Specifies job properties. Properties allow BOSH to configure jobs to a specific environment. `properties` defined in a Job block are accessible only to that job. Only properties specified here will be provided to the job.

View File

@@ -0,0 +1 @@
*Optional*. Links provided by the job. [Read more about link configuration](https://bosh.io/docs/links.html#deployment)

View File

@@ -0,0 +1 @@
*Required*. The release where the job exists

View File

@@ -0,0 +1 @@
*Optional*. Specifies which network components (DNS, Gateway) BOSH populates by default from this network. This property is required if more than one network is specified.

View File

@@ -0,0 +1 @@
*Required*. A valid network name from the cloud config.

View File

@@ -0,0 +1 @@
*Optional*. Array of IP addresses reserved for the instances on the network.

View File

@@ -0,0 +1 @@
The SHA1 of the release tarball. SHA1 is only required when using HTTP(s) URLs.

View File

@@ -0,0 +1 @@
URL of the release to use. URL may use the file protocol (`file://`) or HTTP(s) (`http(s)://`). File URLs can be absolute or relative to the current directory of `bosh-init` execution.

View File

@@ -0,0 +1 @@
*Required*. The number of [canary](https://bosh.io/docs/terminology.html#canary) instances.

View File

@@ -0,0 +1,6 @@
*Required*. Only applies to monit start operation.
- If the `canary_watch_time` is an integer, the Director sleeps for that many milliseconds, then checks whether the canary instances are healthy.
- If the `canary_watch_time` is a range (low-high), the Director:
- Waits for `low` milliseconds
- Waits until instances are healthy or `high` milliseconds have passed since instances started updating

View File

@@ -0,0 +1 @@
*Required*. The maximum number of non-canary instances to update in parallel.

View File

@@ -0,0 +1 @@
If disabled (set to `false`), instance groups will be deployed in parallel, otherwise - sequentially. Instances within a group will still follow `canary` and `max_in_flight` configuration. Defaults to `true`.

View File

@@ -0,0 +1,6 @@
*Required*. Only applies to monit start operation.
- If the `update_watch_time` is an integer, the Director sleeps for that many milliseconds, then checks whether the instances are healthy.
- If the `update_watch_time` is a range (low-high), the Director:
- Waits for `low` milliseconds
- Waits until instances are healthy or `high` milliseconds have passed since instances started updating

View File

@@ -0,0 +1 @@
*Required*. Unique name used to identify a variable. Example: `admin_password`

View File

@@ -0,0 +1 @@
*Optional*. Specifies generation options used for generating variable value if variable is not found. Example: `{is_ca: true, common_name: some-ca}`

View File

@@ -0,0 +1 @@
*Required*. Type of a variable. Currently supported variable types are `certificate`, `password`, `rsa`, and `ssh`. Example: `password`.

View File

@@ -0,0 +1,39 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider;
import org.springframework.ide.vscode.bosh.models.BoshCommandCloudConfigProvider;
import org.springframework.ide.vscode.bosh.models.CloudConfigModel;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import com.google.common.collect.ImmutableMultiset;
public class BoshCommandCloudConfigProviderTest {
public final MockCloudConfigProvider mockProvider = new MockCloudConfigProvider();
// For local testing only... in CI builds we don't have the means to use a real bosh director and cli.
// private BoshCommandCloudConfigProvider realProvider = new BoshCommandCloudConfigProvider();
@Test public void getVMTypes() throws Exception {
BoshCommandCloudConfigProvider provider = mockProvider;
DynamicSchemaContext dc = Mockito.mock(DynamicSchemaContext.class);
CloudConfigModel cloudConfig = provider.getModel(dc);
assertEquals(ImmutableMultiset.of("default", "large"), cloudConfig.getVMTypes());
}
}

View File

@@ -0,0 +1,923 @@
/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import static org.junit.Assert.assertEquals;
import static org.springframework.ide.vscode.languageserver.testharness.Editor.PLAIN_COMPLETION;
import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import org.eclipse.lsp4j.CompletionItem;
import org.junit.Before;
import org.junit.Test;
import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness;
public class BoshEditorTest {
LanguageServerHarness harness;
private MockCloudConfigProvider cloudConfigProvider = new MockCloudConfigProvider();
@Before public void setup() throws Exception {
harness = new LanguageServerHarness(() -> {
return new BoshLanguageServer(cloudConfigProvider)
.setMaxCompletions(100);
},
LanguageId.BOSH_DEPLOYMENT
);
harness.intialize(null);
}
@Test public void toplevelV2PropertyNamesKnown() throws Exception {
Editor editor = harness.newEditor(
"name: some-name\n" +
"director_uuid: cf8dc1fc-9c42-4ffc-96f1-fbad983a6ce6\n" +
"releases:\n" +
"- name: redis\n" +
" version: 12\n" +
"stemcells:\n" +
"- alias: default\n" +
" os: ubuntu-trusty\n" +
" version: 3421.11\n" +
"update:\n" +
" canaries: 1\n" +
" max_in_flight: 10\n" +
" canary_watch_time: 1000-30000\n" +
" update_watch_time: 1000-30000\n" +
"instance_groups:\n" +
"- name: redis-master\n" +
" instances: 1\n" +
" azs: [z1, z2]\n" +
" jobs:\n" +
" - name: redis-server\n" +
" release: redis\n" +
" properties:\n" +
" port: 3606\n" +
" vm_type: large\n" +
" vm_extensions: [public-lbs]\n" +
" stemcell: default\n" +
" persistent_disk_type: medium\n" +
" networks:\n" +
" - name: default\n" +
"- name: redis-slave\n" +
" instances: 2\n" +
" azs: [z1, z2]\n" +
" jobs:\n" +
" - name: redis-server\n" +
" release: redis\n" +
" properties: {}\n" +
" vm_type: large\n" +
" stemcell: default\n" +
" persistent_disk_type: large\n" +
" networks:\n" +
" - name: default\n" +
"variables:\n" +
"- name: admin_password\n" +
" type: password\n" +
"- name: default_ca\n" +
" type: certificate\n" +
" options:\n" +
" is_ca: true\n" +
" common_name: some-ca\n" +
"- name: director_ssl\n" +
" type: certificate\n" +
" options:\n" +
" ca: default_ca\n" +
" common_name: cc.cf.internal\n" +
" alternative_names: [cc.cf.internal]\n" +
"properties:\n" +
" a-property: the-value\n" +
"tags:\n" +
" project: cf\n" +
"blah: hoooo\n"
);
editor.assertProblems(
"director_uuid|bosh v2 CLI no longer checks or requires",
"properties|Deprecated in favor of job level properties and links",
"blah|Unknown property"
);
editor.assertHoverContains("name", "The name of the deployment");
editor.assertHoverContains("director_uuid", "This string must match the UUID of the currently targeted Director");
editor.assertHoverContains("releases", "The name and version of each release in the deployment");
editor.assertHoverContains("stemcells", "The name and version of each stemcell");
editor.assertHoverContains("update", "This specifies instance update properties");
editor.assertHoverContains("instance_groups", "Specifies the mapping between release [jobs](https://bosh.io/docs/terminology.html#job) and instance groups.");
editor.assertHoverContains("properties", 3, "Describes global properties. Deprecated");
editor.assertHoverContains("variables", "Describes variables");
editor.assertHoverContains("tags", "Specifies key value pairs to be sent to the CPI for VM tagging");
}
//@Ignore //For now... because not passing yet.
@Test public void reconcileCfManifest() throws Exception {
Editor editor = harness.newEditorFromClasspath("/workspace/cf-deployment-manifest.yml");
cloudConfigProvider.readWith(() -> {
throw new IOException("Couldn't contact the director");
});
editor.assertProblems(/*NONE*/);
}
@Test public void toplevelPropertyCompletions() throws Exception {
Editor editor = harness.newEditor(
"<*>"
);
editor.assertCompletions(
"name: <*>"
);
editor = harness.newEditor(
"name: blah\n" +
"<*>"
);
editor.assertCompletions(
"name: blah\n" +
"instance_groups:\n" +
"- name: <*>"
, // ============
"name: blah\n" +
"releases:\n" +
"- name: <*>"
, // ============
"name: blah\n" +
"stemcells:\n- <*>"
, // ============
"name: blah\n" +
"tags:\n <*>"
, // ============
"name: blah\n" +
"update:\n <*>"
, // ============
"name: blah\n" +
"variables:\n" +
"- name: <*>"
// Below completions are suppressed because they are deprecated
// , // ============
// "name: blah\n" +
// "director_uuid: <*>"
// , // ============
// "name: blah\n" +
// "properties:\n <*>"
);
}
@Test public void stemcellCompletions() throws Exception {
Editor editor = harness.newEditor(
"stemcells:\n" +
"- <*>"
);
editor.assertCompletions(
"stemcells:\n" +
"- alias: <*>"
, // ==========
"stemcells:\n" +
"- name: <*>"
, // ==========
"stemcells:\n" +
"- os: <*>"
, // ==========
"stemcells:\n" +
"- version: <*>"
);
editor = harness.newEditor(
"stemcells:\n" +
"- alias<*>"
);
}
@Test public void stemcellReconciling() throws Exception {
Editor editor = harness.newEditor(
"stemcells:\n" +
"- {}"
);
editor.assertProblems(
"-|One of [name, os] is required",
"-|[alias, version] are required",
"}|[instance_groups, name, releases, update] are required"
);
}
@Test public void releasesBlockCompletions() throws Exception {
Editor editor = harness.newEditor(
"releases:\n" +
"- <*>"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertCompletions(
"releases:\n" +
"- name: <*>"
);
editor = harness.newEditor(
"releases:\n" +
"- name: foo\n" +
" <*>"
);
editor.assertCompletions(PLAIN_COMPLETION,
"releases:\n" +
"- name: foo\n" +
" sha1: <*>"
, // ============
"releases:\n" +
"- name: foo\n" +
" url: <*>"
, // ============
"releases:\n" +
"- name: foo\n" +
" version: <*>"
);
}
@Test public void releasesAdvancedValidations() throws Exception {
Editor editor = harness.newEditor(
"releases:\n" +
"- name: some-release\n" +
" url: https://my.releases.com/funky.tar.gz\n" +
"- name: other-relase\n" +
" url: file:///root/releases/a-nice-file.tar.gz\n" +
"- name: bad-url\n" +
" url: proto://something.com\n" +
"#x"
);
editor.assertProblems(
"url|'sha1' is required when the 'url' is http(s)",
"proto|Url scheme must be one of [http, https, file]",
"x|are required"
);
}
@Test public void releasesBlockPropertyReconcileAndHovers() throws Exception {
Editor editor = harness.newEditor(
"releases:\n" +
"- name: some-release\n" +
" version: some-version\n" +
" url: https://my.releases.com/funky.tar.gz\n" +
" sha1: 440248a31253296b1626ad52886e58900730f32e\n" +
" woot: dunno\n"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(
"woot|Unknown property"
);
editor.assertHoverContains("name", "Name of a release used in the deployment");
editor.assertHoverContains("version", "The version of the release to use");
editor.assertHoverContains("url", "URL of the release to use");
editor.assertHoverContains("sha1", "The SHA1 of the release tarball");
editor = harness.newEditor(
"releases:\n" +
"- name: some-release\n" +
" version: <*>\n"
);
editor.assertCompletionLabels("latest");
}
@Test public void instanceGroupsCompletions() throws Exception {
Editor editor = harness.newEditor(
"instance_groups:\n" +
"- <*>"
);
editor.assertCompletions(
"instance_groups:\n" +
"- name: <*>"
);
editor = harness.newEditor(
"instance_groups:\n" +
"- name: foo-group\n" +
" <*>"
);
editor.assertCompletions(PLAIN_COMPLETION,
"instance_groups:\n" +
"- name: foo-group\n" +
" azs:\n" +
" - <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" env:\n" +
" <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" instances: <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" jobs:\n" +
" - <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" lifecycle: <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" migrated_from:\n" +
" - <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" networks:\n" +
" - name: <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" persistent_disk_type: <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" stemcell: <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" update:\n" +
" <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" vm_extensions:\n" +
" - <*>"
, // =============
"instance_groups:\n" +
"- name: foo-group\n" +
" vm_type: <*>"
// , // =============
// Not suggested because its deprecated:
// "instance_groups:\n" +
// "- name: foo-group\n" +
// " properties:\n" +
// " <*>"
);
}
@Test public void instanceGroupsHovers() throws Exception {
Editor editor = harness.newEditor(
"instance_groups:\n" +
"- name: redis-master\n" +
" properties: {}\n" +
" instances: 1\n" +
" azs: [z1, z2]\n" +
" jobs:\n" +
" - name: redis-server\n" +
" release: redis\n" +
" properties:\n" +
" port: 3606\n" +
" vm_type: medium\n" +
" vm_extensions: [public-lbs]\n" +
" stemcell: default\n" +
" persistent_disk_type: medium\n" +
" networks:\n" +
" - name: default\n" +
"\n" +
"- name: redis-slave\n" +
" instances: 2\n" +
" azs: [z1, z2]\n" +
" jobs:\n" +
" - name: redis-server\n" +
" release: redis\n" +
" properties: {}\n" +
" update:\n" +
" canaries: 2\n" +
" lifecycle: errand\n" +
" migrated_from: []\n" +
" env: {}\n" +
" vm_type: medium\n" +
" stemcell: default\n" +
" persistent_disk_type: medium\n" +
" networks:\n" +
" - name: default\n"
);
editor.assertHoverContains("name", "A unique name used to identify and reference the instance group.");
editor.assertHoverContains("azs", "List of AZs associated with this instance group");
editor.assertHoverContains("instances", "The number of instances in this group");
editor.assertHoverContains("jobs", "Specifies the name and release of jobs that will be installed on each instance.");
editor.assertHoverContains("vm_type", "A valid VM type name from the cloud config");
editor.assertHoverContains("vm_extensions", "A valid list of VM extension names from the cloud config");
editor.assertHoverContains("stemcell", "A valid stemcell alias from the Stemcells Block");
editor.assertHoverContains("persistent_disk_type", "A valid disk type name from the cloud config.");
editor.assertHoverContains("networks", "Specifies the networks this instance requires");
editor.assertHoverContains("update", "Specific update settings for this instance group");
editor.assertHoverContains("migrated_from", "Specific migration settings for this instance group.");
editor.assertHoverContains("lifecycle", "Specifies the kind of workload");
editor.assertHoverContains("properties", "Specifies instance group properties");
editor.assertHoverContains("env", "Specifies advanced BOSH Agent configuration");
}
@Test public void instanceGroups_job_hovers() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups:\n" +
"- name: foo\n" +
" jobs:\n" +
" - name: the-job\n" +
" release: the-jobs-release\n" +
" properties:\n" +
" blah: blah\n" +
" consumes:\n" +
" blah: blah \n" +
" provides:\n" +
" blah: blah\n"
);
editor.assertHoverContains("name", 3, "The job name");
editor.assertHoverContains("release", "The release where the job exists");
editor.assertHoverContains("consumes", "Links consumed by the job");
editor.assertHoverContains("provides", "Links provided by the job");
editor.assertHoverContains("properties", "Specifies job properties");
}
@Test public void instanceGroups_network_hovers() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups:\n" +
"- name: foo\n" +
" networks:\n" +
" - name: the-network\n" +
" static_ips: []\n" +
" default: []\n"
);
editor.assertHoverContains("name", 3, "A valid network name from the cloud config");
editor.assertHoverContains("static_ips", "Array of IP addresses");
editor.assertHoverContains("default", "Specifies which network components");
}
@Test public void instanceGroups_env_hovers() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups:\n" +
"- name: foo\n" +
" env:\n" +
" bosh: {}\n" +
" password: []\n"
);
editor.assertHoverContains("bosh", "no description");
editor.assertHoverContains("password", "Crypted password");
}
@Test public void updateBlockCompletions() throws Exception {
Editor editor = harness.newEditor(
"update:\n" +
" <*>"
);
editor.assertCompletions(PLAIN_COMPLETION,
"update:\n" +
" canaries: <*>"
, // =====
"update:\n" +
" canary_watch_time: <*>"
, // =====
"update:\n" +
" max_in_flight: <*>"
, // =====
"update:\n" +
" serial: <*>"
, // =====
"update:\n" +
" update_watch_time: <*>"
);
}
@Test public void updateBlockHovers() throws Exception {
Editor editor = harness.newEditor(
"update:\n" +
" canaries: 1\n" +
" max_in_flight: 10\n" +
" canary_watch_time: 1000-30000\n" +
" update_watch_time: 1000-30000\n" +
" serial: false"
);
editor.assertHoverContains("canaries", "The number of [canary]");
editor.assertHoverContains("max_in_flight", "maximum number of non-canary instances");
editor.assertHoverContains("canary_watch_time", "checks whether the canary instances");
editor.assertHoverContains("update_watch_time", "checks whether the instances");
editor.assertHoverContains("serial", "deployed in parallel");
}
@Test public void variablesBlockCompletions() throws Exception {
Editor editor = harness.newEditor(
"variables:\n" +
"- <*>"
);
editor.assertCompletions(
"variables:\n" +
"- name: <*>"
);
editor = harness.newEditor(
"variables:\n" +
"- name: foo\n" +
" <*>"
);
editor.assertCompletions(PLAIN_COMPLETION,
"variables:\n" +
"- name: foo\n" +
" options:\n" +
" <*>"
, // ===============
"variables:\n" +
"- name: foo\n" +
" type: <*>"
);
editor = harness.newEditor(
"variables:\n" +
"- name: foo\n" +
" type: <*>"
);
editor.assertCompletionLabels("certificate", "password", "rsa", "ssh");
}
@Test public void variablesBlockHovers() throws Exception {
Editor editor = harness.newEditor(
"variables:\n" +
"- name: admin_password\n" +
" type: password\n" +
"- name: default_ca\n" +
" type: certificate\n" +
" options:\n" +
" is_ca: true\n" +
" common_name: some-ca\n" +
"- name: director_ssl\n" +
" type: certificate\n" +
" options:\n" +
" ca: default_ca\n" +
" common_name: cc.cf.internal\n" +
" alternative_names: [cc.cf.internal]"
);
editor.assertHoverContains("name", "Unique name used to identify a variable");
editor.assertHoverContains("type", "Type of a variable");
editor.assertHoverContains("options", "Specifies generation options");
}
@Test public void tolerateV1Manifests() throws Exception {
Editor editor = harness.newEditor(
"---\n" +
"name: my-redis-deployment\n" +
"director_uuid: 1234abcd-5678-efab-9012-3456cdef7890\n" +
"\n" +
"releases:\n" +
"- {name: redis, version: 12}\n" +
"\n" +
"resource_pools:\n" +
"- name: redis-servers\n" +
" network: default\n" +
" stemcell:\n" +
" name: bosh-aws-xen-ubuntu-trusty-go_agent\n" +
" version: 2708\n" +
" cloud_properties:\n" +
" instance_type: m1.small\n" +
" availability_zone: us-east-1c\n" +
"\n" +
"disk_pools: []\n" +
"\n" +
"networks:\n" +
"- name: default\n" +
" type: manual\n" +
" subnets:\n" +
" - range: 10.10.0.0/24\n" +
" gateway: 10.10.0.1\n" +
" static:\n" +
" - 10.10.0.16 - 10.10.0.18\n" +
" reserved:\n" +
" - 10.10.0.2 - 10.10.0.15\n" +
" dns: [10.10.0.6]\n" +
" cloud_properties:\n" +
" subnet: subnet-d597b993\n" +
"\n" +
"compilation:\n" +
" workers: 2\n" +
" network: default\n" +
" reuse_compilation_vms: true\n" +
" cloud_properties:\n" +
" instance_type: c1.medium\n" +
" availability_zone: us-east-1c\n" +
"\n" +
"update:\n" +
" canaries: 1\n" +
" max_in_flight: 3\n" +
" canary_watch_time: 15000-30000\n" +
" update_watch_time: 15000-300000\n" +
"\n" +
"jobs:\n" +
"- name: redis-master\n" +
" instances: 1\n" +
" templates:\n" +
" - {name: redis-server, release: redis}\n" +
" persistent_disk: 10_240\n" +
" resource_pool: redis-servers\n" +
" networks:\n" +
" - name: default\n" +
"\n" +
"- name: redis-slave\n" +
" instances: 2\n" +
" templates:\n" +
" - {name: redis-server, release: redis}\n" +
" persistent_disk: 10_240\n" +
" resource_pool: redis-servers\n" +
" networks:\n" +
" - name: default\n" +
"\n" +
"properties:\n" +
" redis:\n" +
" max_connections: 10\n" +
"\n" +
"cloud_provider: {}"
);
editor.ignoreProblem(YamlSchemaProblems.DEPRECATED_PROPERTY);
editor.assertProblems(/*NONE*/);
editor = harness.newEditor(
"name: foo\n" +
"director_uuid: dca5480a-6b0e-11e7-907b-a6006ad3dba0\n" +
"networks: {}" //This makes it a V1 schema
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(
"director_uuid|bosh v2 CLI no longer checks or requires",
"networks|Deprecated: 'networks' is a V1 schema property"
);
}
@Test public void documentSymbols() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"variables:\n" +
"- name: blobstore_admin_users_password\n" +
" type: password\n" +
"- name: blobstore_secure_link_secret\n" +
" type: password\n" +
"stemcells:\n" +
"- alias: default\n" +
" os: ubuntu-trusty\n" +
" version: '3421.11'\n" +
"instance_groups:\n" +
"- name: foo-group\n" +
" networks:\n" +
" - name: the-network\n" +
" static_ips: []\n" +
" default: []\n" +
"- name: bar-group\n" +
" networks:\n" +
" - name: the-network\n" +
" static_ips: []\n" +
" default: []\n" +
"releases:\n" +
"- name: one-release\n" +
"- name: other-release\n"
);
editor.assertDocumentSymbols(
"default|StemcellAlias",
"foo-group|InstanceGroup",
"bar-group|InstanceGroup",
"one-release|Release",
"other-release|Release",
"blobstore_admin_users_password|Variable",
"blobstore_secure_link_secret|Variable"
);
}
@Test public void duplicateSymbolChecking() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"variables:\n" +
"- name: dup_var\n" +
" type: password\n" +
"- name: blobstore_admin_users_password\n" +
" type: password\n" +
"- name: blobstore_secure_link_secret\n" +
" type: password\n" +
"- name: dup_var\n" +
" type: ssh\n" +
"stemcells:\n" +
"- alias: default\n" +
" os: ubuntu-trusty\n" +
" version: '3421.11'\n" +
"- alias: dup_cell\n" +
" os: ubuntu-trusty\n" +
" version: '3421.11'\n" +
"- alias: dup_cell\n" +
" os: ubuntu-trusty\n" +
" version: '3421.11'\n" +
"instance_groups:\n" +
"- name: foo-group\n" +
" networks:\n" +
" - name: the-network\n" +
" static_ips: []\n" +
" default: []\n" +
"- name: bar-group\n" +
" networks:\n" +
" - name: the-network\n" +
" static_ips: []\n" +
" default: []\n" +
"- name: bar-group\n" +
" networks:\n" +
" - name: the-network\n" +
" static_ips: []\n" +
" default: []\n" +
"releases:\n" +
"- name: one-release\n" +
"- name: other-release\n" +
"- name: one-release\n"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(
"dup_var|Duplicate 'VariableName'",
"dup_var|Duplicate 'VariableName'",
"dup_cell|Duplicate 'StemcellAlias'",
"dup_cell|Duplicate 'StemcellAlias'",
"bar-group|Duplicate 'InstanceGroupName'",
"bar-group|Duplicate 'InstanceGroupName'",
"one-release|Duplicate 'ReleaseName'",
"one-release|Duplicate 'ReleaseName'"
);
}
@Test public void contentAssistReleaseReference() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" jobs:\n" +
" - release: <*>\n" +
"releases: \n" +
"- name: some-release\n" +
" url: https://release-hub.info/some-release.tar.gz?version=99.3.2\n" +
" sha1: asddsfsd\n" +
"- name: other-release\n" +
" url: https://release-hub.info/other-release.tar.gz?version=99.3.2\n" +
" sha1: asddsfsd\n"
);
editor.assertContextualCompletions("<*>"
, // ==>
"other-release<*>", "some-release<*>"
);
}
@Test public void reconcileReleaseReference() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" jobs:\n" +
" - release: some-release\n" +
"- name: some-other-server\n" +
" jobs:\n" +
" - release: bogus-release\n" +
"releases: \n" +
"- name: some-release\n" +
" url: https://release-hub.info/some-release.tar.gz?version=99.3.2\n" +
" sha1: asddsfsd\n" +
"- name: other-release\n" +
" url: https://release-hub.info/other-release.tar.gz?version=99.3.2\n" +
" sha1: asddsfsd\n"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(
"bogus-release|unknown 'ReleaseName'. Valid values are: [other-release, some-release]"
);
}
@Test public void contentAssistStemcellReference() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" stemcell: <*>\n" +
"stemcells:\n" +
"- alias: default\n" +
" os: ubuntu\n" +
" version: 1346.77.1\n" +
"- alias: windoze\n" +
" os: windows\n" +
" version: 678.9.1\n"
);
editor.assertContextualCompletions("<*>"
, // ==>
"default<*>", "windoze<*>"
);
}
@Test public void reconcileStemcellReference() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" stemcell: default\n" +
"- name: windoze-server\n" +
" stemcell: windoze\n" +
"- name: bad-server\n" +
" stemcell: bogus-stemcell\n" +
"stemcells:\n" +
"- alias: default\n" +
" os: ubuntu\n" +
" version: 1346.77.1\n" +
"- alias: windoze\n" +
" os: windows\n" +
" version: 678.9.1\n"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(
"bogus-stemcell|unknown 'StemcellAlias'. Valid values are: [default, windoze]"
);
}
@Test public void contentAssistVMtype() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" stemcell: windoze\n" +
" vm_type: <*>"
);
editor.assertContextualCompletions(
"<*>"
, // ==>
"default<*>",
"large<*>"
);
//Verify that the cache is working. Shouldn't read the cc provier more than once, evem for multiple CA requests.
editor.assertCompletionLabels("default", "large");
editor.assertCompletionLabels("default", "large");
assertEquals(1, cloudConfigProvider.getReadCount());
}
@Test public void reconcileVMtype() throws Exception {
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" vm_type: bogus-vm\n" +
"- name: other-server\n" +
" vm_type: large"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(
"bogus-vm|unknown 'VMType'. Valid values are: [default, large]"
);
assertEquals(1, cloudConfigProvider.getReadCount());
}
@Test public void reconcileVMTypeWhenCloudConfigUnavailable() throws Exception {
cloudConfigProvider.readWith(() -> null);
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" vm_type: bogus-vm\n" +
"- name: other-server\n" +
" vm_type: large"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(/*NONE*/); //Should not complain about unknown vm_types, if we can't determine what valid vm_types actually exist.
}
@Test public void reconcileVMTypeWhenCloudConfigThrows() throws Exception {
cloudConfigProvider.readWith(() -> { throw new TimeoutException("Reading cloud config timed out"); });
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" vm_type: bogus-vm\n" +
"- name: other-server\n" +
" vm_type: large"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(/*NONE*/); //Should not complain about unknown vm_types, if we can't determine what valid vm_types actually exist.
}
@Test public void contentAssistShowsWarningWhenCloudConfigThrows() throws Exception {
cloudConfigProvider.readWith(() -> {
throw new TimeoutException("Reading cloud config timed out");
});
Editor editor = harness.newEditor(
"name: foo\n" +
"instance_groups: \n" +
"- name: some-server\n" +
" vm_type: <*>"
);
CompletionItem completion = editor.assertCompletionLabels("TimeoutException").get(0);
completion = harness.resolveCompletionItem(completion);
assertContains("Reading cloud config timed out", completion.getDocumentation());
}
}

View File

@@ -8,8 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.concourse;
package org.springframework.ide.vscode.bosh;
import static org.assertj.core.api.Assertions.assertThat;
@@ -20,7 +19,7 @@ import java.nio.file.Paths;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.TextDocumentSyncKind;
import org.junit.Test;
import org.springframework.ide.vscode.bosh.BoshLanguageServer;
import org.springframework.ide.vscode.bosh.mocks.MockCloudConfigProvider;
import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness;
public class BoshLanguageServerTest {
@@ -31,7 +30,9 @@ public class BoshLanguageServerTest {
@Test
public void createAndInitializeServerWithWorkspace() throws Exception {
LanguageServerHarness harness = new LanguageServerHarness(() -> new BoshLanguageServer());
LanguageServerHarness harness = new LanguageServerHarness(() ->
new BoshLanguageServer(new MockCloudConfigProvider())
);
File workspaceRoot = getTestResource("/workspace/");
assertExpectedInitResult(harness.intialize(workspaceRoot));
}
@@ -39,7 +40,9 @@ public class BoshLanguageServerTest {
@Test
public void createAndInitializeServerWithoutWorkspace() throws Exception {
File workspaceRoot = null;
LanguageServerHarness harness = new LanguageServerHarness(() -> new BoshLanguageServer());
LanguageServerHarness harness = new LanguageServerHarness(() ->
new BoshLanguageServer(new MockCloudConfigProvider())
);
assertExpectedInitResult(harness.intialize(workspaceRoot));
}

View File

@@ -0,0 +1,95 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.springframework.ide.vscode.bosh.BoshValueParsers;
import org.springframework.ide.vscode.commons.util.ValueParseException;
import org.springframework.ide.vscode.commons.util.ValueParser;
public class BoshValueParserTest {
private static final String MARKER = "<*>";
@Test public void integerOrRangeOkay() throws Exception {
BoshValueParsers.INTEGER_OR_RANGE.parse("123");
BoshValueParsers.INTEGER_OR_RANGE.parse("123-456");
}
@Test public void integerOrRangeGarbage() throws Exception {
assertProblem(BoshValueParsers.INTEGER_OR_RANGE, "<*>garbage<*>", "Should be either a Integer, or a range (of the form '<integer>-<integer>')");
assertProblem(BoshValueParsers.INTEGER_OR_RANGE, "<*>123--456<*>", "Should be either a Integer, or a range (of the form '<integer>-<integer>')");
assertProblem(BoshValueParsers.INTEGER_OR_RANGE, "<*>garbage<*>-123", "Should be a Integer");
assertProblem(BoshValueParsers.INTEGER_OR_RANGE, "123-<*>garbage<*>", "Should be a Integer");
assertProblem(BoshValueParsers.INTEGER_OR_RANGE, "<*>123-122<*>", "123 should be smaller than 122");
}
@Test public void urlOkay() throws Exception {
ValueParser urlParser = BoshValueParsers.url("http", "https", "file");
urlParser.parse("http://foobar.com/munhings.tar.gz");
urlParser.parse("https://foobar.com/munhings.tar.gz");
urlParser.parse("hTTp://foobar.com/munhings.tar.gz");
urlParser.parse("HTTPS://foobar.com/munhings.tar.gz");
urlParser.parse("file://local/file");
urlParser.parse("file:///local/file");
urlParser.parse("FILE:///local/file");
}
@Test public void urlGarbage() throws Exception {
ValueParser urlParser = BoshValueParsers.url("http", "https", "file");
assertProblem(urlParser, "<*>woot<*>://foobar.com", "Url scheme must be one of [http, https, file]");
assertProblem(urlParser, "<*>wOOt<*>://foobar.com", "Url scheme must be one of [http, https, file]");
}
private void assertProblem(ValueParser parser, String input, String expectedMessage) throws Exception {
String unmarkedInput = input.replace(MARKER, "");
int firstMarker = input.indexOf(MARKER);
assertTrue(firstMarker>=0);
int secondMarker = input.indexOf(MARKER, firstMarker+1)-MARKER.length();
assertTrue(secondMarker>=firstMarker);
try {
parser.parse(unmarkedInput);
} catch (ValueParseException e) {
int startIndex = startIndex(unmarkedInput, e);
int endIndex = endIndex(unmarkedInput, e);
String markedInput =
unmarkedInput.substring(0, startIndex) +
MARKER +
unmarkedInput.substring(startIndex, endIndex) +
MARKER +
unmarkedInput.substring(endIndex);
assertEquals(input, markedInput);
assertEquals(expectedMessage, e.getMessage());
}
}
private int endIndex(String unmarkedInput, ValueParseException e) {
int i = e.getEndIndex();
if (i>=0) {
return i;
} else {
return unmarkedInput.length();
}
}
private int startIndex(String unarkedInput, ValueParseException e) {
int i = e.getStartIndex();
if (i>=0) {
return i;
} else {
return 0;
}
}
}

View File

@@ -0,0 +1,45 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.bosh.mocks;
import java.util.concurrent.Callable;
import org.springframework.ide.vscode.bosh.BoshCommandCloudConfigProviderTest;
import org.springframework.ide.vscode.bosh.models.BoshCommandCloudConfigProvider;
import org.springframework.ide.vscode.commons.util.IOUtil;
public final class MockCloudConfigProvider extends BoshCommandCloudConfigProvider {
static final String MOCK_DATA_RSRC = "/cmd-out/cloud-config.json";
private Callable<String> cloudConfigReader = () -> IOUtil.toString(BoshCommandCloudConfigProviderTest.class.getResourceAsStream(MOCK_DATA_RSRC));
private int readCount = 0;
/**
* Override with a 'fake' which just returns some mock data. That way we can unit-test
* without requiring a real bosh setup.
*/
@Override
protected String executeBoshCloudConfigCommand() throws Exception {
readCount++;
return cloudConfigReader.call();
}
public MockCloudConfigProvider readWith(Callable<String> reader) {
this.cloudConfigReader = reader;
return this;
}
public int getReadCount() {
return readCount;
}
}

View File

@@ -1,247 +0,0 @@
/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.concourse;
import org.junit.Before;
import org.junit.Test;
import org.springframework.ide.vscode.bosh.BoshLanguageServer;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness;
import static org.springframework.ide.vscode.languageserver.testharness.Editor.*;
public class BoshEditorTest {
LanguageServerHarness harness;
@Before public void setup() throws Exception {
harness = new LanguageServerHarness(() -> {
return new BoshLanguageServer()
.setMaxCompletions(100);
},
LanguageId.BOSH_DEPLOYMENT
);
harness.intialize(null);
}
@Test public void toplevelV2PropertyNamesKnown() throws Exception {
Editor editor = harness.newEditor(
"name: some-name\n" +
"director_uuid: cf8dc1fc-9c42-4ffc-96f1-fbad983a6ce6\n" +
"releases:\n" +
"- name: redis\n" +
" version: 12\n" +
"stemcells:\n" +
"- alias: default\n" +
" os: ubuntu-trusty\n" +
" version: 3421.11\n" +
"update:\n" +
" canaries: 1\n" +
" max_in_flight: 10\n" +
" canary_watch_time: 1000-30000\n" +
" update_watch_time: 1000-30000\n" +
"instance_groups:\n" +
"- name: redis-master\n" +
" instances: 1\n" +
" azs: [z1, z2]\n" +
" jobs:\n" +
" - name: redis-server\n" +
" release: redis\n" +
" properties:\n" +
" port: 3606\n" +
" vm_type: medium\n" +
" vm_extensions: [public-lbs]\n" +
" stemcell: default\n" +
" persistent_disk_type: medium\n" +
" networks:\n" +
" - name: default\n" +
"- name: redis-slave\n" +
" instances: 2\n" +
" azs: [z1, z2]\n" +
" jobs:\n" +
" - name: redis-server\n" +
" release: redis\n" +
" properties: {}\n" +
" vm_type: medium\n" +
" stemcell: default\n" +
" persistent_disk_type: medium\n" +
" networks:\n" +
" - name: default\n" +
"variables:\n" +
"- name: admin_password\n" +
" type: password\n" +
"- name: default_ca\n" +
" type: certificate\n" +
" options:\n" +
" is_ca: true\n" +
" common_name: some-ca\n" +
"- name: director_ssl\n" +
" type: certificate\n" +
" options:\n" +
" ca: default_ca\n" +
" common_name: cc.cf.internal\n" +
" alternative_names: [cc.cf.internal]\n" +
"properties:\n" +
" a-property: the-value\n" +
"tags:\n" +
" project: cf\n" +
"blah: hoooo\n"
);
editor.assertProblems(
"properties|Deprecated in favor of job level properties and links",
"blah|Unknown property"
);
editor.assertHoverContains("name", "The name of the deployment");
editor.assertHoverContains("director_uuid", "This string must match the UUID of the currently targeted Director");
editor.assertHoverContains("releases", "The name and version of each release in the deployment");
editor.assertHoverContains("stemcells", "The name and version of each stemcell");
editor.assertHoverContains("update", "This specifies instance update properties");
editor.assertHoverContains("instance_groups", "Specifies the mapping between release [jobs](https://bosh.io/docs/terminology.html#job) and instance groups.");
editor.assertHoverContains("properties", 3, "Describes global properties. Deprecated");
editor.assertHoverContains("variables", "Describes variables");
editor.assertHoverContains("tags", "Specifies key value pairs to be sent to the CPI for VM tagging");
}
@Test public void toplevelV2PropertyCompletions() throws Exception {
Editor editor = harness.newEditor(
"<*>"
);
editor.assertCompletions(
"name: <*>"
);
editor = harness.newEditor(
"name: blah\n" +
"<*>"
);
editor.assertCompletions(
"name: blah\n" +
"director_uuid: <*>"
, // ============
"name: blah\n" +
"instance_groups:\n- <*>"
, // ============
"name: blah\n" +
"releases:\n- <*>"
, // ============
"name: blah\n" +
"stemcells:\n- <*>"
, // ============
"name: blah\n" +
"tags:\n <*>"
, // ============
"name: blah\n" +
"update:\n <*>"
, // ============
"name: blah\n" +
"variables:\n- <*>"
, // ============
"name: blah\n" +
"properties:\n <*>"
);
}
@Test public void stemcellCompletions() throws Exception {
Editor editor = harness.newEditor(
"stemcells:\n" +
"- <*>"
);
editor.assertCompletions(
"stemcells:\n" +
"- alias: <*>"
, // ==========
"stemcells:\n" +
"- name: <*>"
, // ==========
"stemcells:\n" +
"- os: <*>"
, // ==========
"stemcells:\n" +
"- version: <*>"
);
editor = harness.newEditor(
"stemcells:\n" +
"- alias<*>"
);
}
@Test public void stemcellReconciling() throws Exception {
Editor editor = harness.newEditor(
"stemcells:\n" +
"- {}"
);
editor.assertProblems(
"-|One of [name, os] is required",
"-|[alias, version] are required",
"}|[director_uuid, instance_groups, name, releases, update] are required"
);
}
@Test public void releasesBlockCompletions() throws Exception {
Editor editor = harness.newEditor(
"releases:\n" +
"- <*>"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertCompletions(
"releases:\n" +
"- name: <*>"
);
editor = harness.newEditor(
"releases:\n" +
"- name: foo\n" +
" <*>"
);
editor.assertCompletions(PLAIN_COMPLETION,
"releases:\n" +
"- name: foo\n" +
" version: <*>"
);
}
@Test public void releasesBlockReconcileAndHovers() throws Exception {
Editor editor = harness.newEditor(
"releases:\n" +
"- name: some-release\n" +
" version: some-version\n" +
" woot: dunno\n"
);
editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY);
editor.assertProblems(
"woot|Unknown property"
);
editor.assertHoverContains("name", "Name of a release used in the deployment");
editor.assertHoverContains("version", "The version of the release to use");
editor = harness.newEditor(
"releases:\n" +
"- name: some-release\n" +
" version: <*>\n"
);
editor.assertCompletionLabels("latest");
}
@Test public void instanceGroupsCompletions() throws Exception {
Editor editor = harness.newEditor(
"instance_groups:\n" +
"- <*>"
);
}
}

View File

@@ -0,0 +1,10 @@
{
"Tables": null,
"Blocks": [
"azs:\n- cloud_properties:\n datacenters:\n - clusters:\n - AppFabric: {}\n name: z1\n- cloud_properties:\n datacenters:\n - clusters:\n - AppFabric: {}\n name: z2\n- cloud_properties:\n datacenters:\n - clusters:\n - AppFabric: {}\n name: z3\ncompilation:\n az: z1\n network: default\n reuse_compilation_vms: true\n vm_type: default\n workers: 5\ndisk_types:\n- disk_size: 3000\n name: default\n- disk_size: 50000\n name: large\nnetworks:\n- name: default\n subnets:\n - azs:\n - z1\n - z2\n - z3\n cloud_properties:\n name: VLAN 40 - AF\n dns:\n - 10.192.2.10\n - 8.8.8.8\n gateway: 10.194.4.1\n range: 10.194.4.0/23\n reserved:\n - 10.194.4.1-10.194.4.34\n - 10.194.4.40-10.194.5.255\n type: manual\nvm_types:\n- cloud_properties:\n cpu: 2\n disk: 3240\n ram: 1024\n name: default\n- cloud_properties:\n cpu: 2\n disk: 30240\n ram: 4096\n name: large\n"
],
"Lines": [
"Using environment '10.194.4.35' as client 'admin'",
"Succeeded"
]
}

View File

@@ -1,23 +0,0 @@
resources:
- name: sts4
type: git
source:
repository: https://github.com/kdvolder/somestuff
- name: other-repo
type: git
source:
repository: https://github.com/kdvolder/somestuff
- name: more-stuff
type: git
source:
repository: https://github.com/kdvolder/somestuff
jobs:
- name: job1
plan:
- get: sts4
- task: do-stuff
input_mapping:
task_input: bogus_input
repo: sts4
- get: bogus-get
- put: bogus-put

View File

@@ -1,115 +0,0 @@
##########################################################
resource_types:
- name: s3-multi
type: docker-image
source:
repository: kdvolder/s3-resource-simple
- name: slack-notification
type: docker-image
source:
repository: cfcommunity/slack-notification-resource
tag: latest
#########################################################
resources:
- name: docker-git
type: git
source:
uri: git@github.com:spring-projects/sts4.git
branch: {{branch}}
username: kdvolder
private_key: {{rsa_id}}
paths:
- concourse/docker
- name: sts4
type: git
source:
uri: git@github.com:spring-projects/sts4.git
branch: {{branch}}
private_key: {{rsa_id}}
- name: s3-boot-properties-vsix
type: s3
source:
bucket: {{s3_bucket}}
access_key_id: {{s3_accesskey}}
secret_access_key: {{s3_secretkey}}
region_name: {{s3_region}}
regexp: sts4/vscode-extensions/vscode-boot-properties-(.*).vsix
- name: s3-manifest-yaml-vsix
type: s3
source:
bucket: {{s3_bucket}}
access_key_id: {{s3_accesskey}}
secret_access_key: {{s3_secretkey}}
region_name: {{s3_region}}
regexp: sts4/vscode-extensions/vscode-manifest-yaml-(.*).vsix
- name: website
type: s3-multi
source:
bucket: {{s3_prod_bucket}}
access_key_id: {{s3_prod_accesskey}}
secret_access_key: {{s3_prod_secretkey}}
region_name: {{s3_region}}
path: snapshot/STS4/vscode-extensions
options:
- "--acl public-read"
- name: slack-notification
type: slack-notification
source:
url: https://hooks.slack.com/services/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
- name: docker-image
type: docker-image
source:
username: {{docker_hub_username}}
password: {{docker_hub_password}}
repository: kdvolder/sts4-build-env
########################################################################################
jobs:
- name: build-docker-image
serial: true
plan:
- get: docker-git
trigger: true
- put: docker-image
params:
build: docker-git/concourse/docker
get_params:
skip_download: true
- name: build-vsix
plan:
- get: sts4
trigger: true
- task: build-vscode-extensions
file: sts4/concourse/tasks/build-vscode-extensions.yml
on_success:
aggregate:
- put: s3-manifest-yaml-vsix
params:
file: vsix-files/vscode-manifest-yaml-*.vsix
acl: public-read
- put: s3-boot-properties-vsix
params:
file: vsix-files/vscode-boot-properties-*.vsix
acl: public-read
on_failure:
put: slack-notification
params:
channel: "@kdvolder"
text: |
Concourse ${BUILD_PIPELINE_NAME}/${BUILD_JOB_NAME}/${BUILD_NAME} has failed!
- name: build-website
plan:
- aggregate:
- get: sts4
- get: s3-manifest-yaml-vsix
passed:
- build-vsix
trigger: true
- get: s3-boot-properties-vsix
passed:
- build-vsix
trigger: true
- task: build-website
file: sts4/concourse/tasks/build-website.yml
- put: website
params:
path: website

View File

@@ -1,10 +0,0 @@
#!/bin/bash
set -e
workdir=`pwd`
cd sts4/vscode-extensions
./build-all.sh
cd $workdir
cp `find sts4/vscode-extensions -name "*.vsix"` vsix-files

View File

@@ -1,11 +0,0 @@
inputs:
- name: sts4
outputs:
- name: vsix-files
platform: linux
image_resource:
type: docker-image
source:
repository: kdvolder/sts4-build-env
run:
path: "sts4/concourse/tasks/build-vscode-extensions.sh"

View File

@@ -1,43 +0,0 @@
#!/bin/bash
set -e
workdir=`pwd`
sources=$workdir/sts4/eclipse-distribution/common/html
target=$workdir/website
#cp -r "${sources}/stylesheet.css" "$target"
#cp -r ${sources}/*.js "$target"
#cp s3-manifest-yaml-vsix/*.vsix "$target"
#cp s3-boot-properties-vsix/*.vsix "$target"
export vscode_manifest_yaml=$(basename s3-manifest-yaml-vsix/*.vsix)
echo "vscode_manifest_yaml=$vscode_manifest_yaml"
export vscode_boot_properties=$(basename s3-boot-properties-vsix/*.vsix)
echo "vscode_boot_properties=$vscode_boot_properties"
envsubst > "$target/vscode-extensions-snippet.html" << XXXXXX
<ul>
<li>Spring Boot Property Language Server:
<a href="http://s3-test.spring.io/sts4/vscode-extensions/${vscode_boot_properties}">${vscode_boot_properties}</a>
</li>
<li>Cloud Foundry Manifest Language Server:
<a href="http://s3-test.spring.io/sts4/vscode-extensions/${vscode_manifest_yaml}">${vscode_manifest_yaml}</a>
</li>
</ul>
XXXXXX
export vscode_snippet=`cat "$target/vscode-extensions-snippet.html"`
envsubst > "$target/vscode-extensions.html" << XXXXXX
<!DOCTYPE html>
<html>
<body>
<h1>STS4 Vscode Extensions</h1>
$vscode_snippet
</body>
</html>
XXXXXX
cat $target/vscode-extensions.html

View File

@@ -1,13 +0,0 @@
inputs:
- name: sts4
- name: s3-manifest-yaml-vsix
- name: s3-boot-properties-vsix
outputs:
- name: website
platform: linux
image_resource:
type: docker-image
source:
repository: kdvolder/sts4-build-env
run:
path: "sts4/concourse/tasks/build-website.sh"

View File

@@ -211,7 +211,7 @@ public class SpringBootApp {
if (env != null) {
JSONObject portsObject = env.getJSONObject("server.ports");
if (portsObject != null) {
String portValue = portsObject.getString("local.server.port");
String portValue = portsObject.get("local.server.port").toString();
return portValue;
}
}

View File

@@ -32,7 +32,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<version>${mockito-version}</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -15,6 +15,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import org.springframework.ide.vscode.commons.util.ExternalCommand;
import org.springframework.ide.vscode.commons.util.ExternalProcess;
@@ -28,7 +29,7 @@ public class MavenBuilder {
private List<String> properties;
public void execute() throws IOException, InterruptedException {
public void execute() throws IOException, InterruptedException, TimeoutException {
Path mvnwPath = System.getProperty("os.name").toLowerCase().startsWith("win") ? projectPath.resolve("mvnw.cmd")
: projectPath.resolve("mvnw");
mvnwPath.toFile().setExecutable(true);

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeoutException;
/**
* Encapsulates information about an 'external' command that can be run through the OS.
@@ -68,7 +69,7 @@ public class ExternalCommand {
* result of commands are logged to the console and if the command returns non
* 0 exit value an exception is thrown.
*/
public void exec(File workdir) throws IOException, InterruptedException {
public void exec(File workdir) throws IOException, InterruptedException, TimeoutException {
System.out.println(">>> exec: "+this);
ExternalProcess process = new ExternalProcess(workdir, this);
System.out.println(process);

View File

@@ -16,6 +16,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
@@ -113,13 +116,14 @@ public class ExternalProcess {
* Creates an external process and waits for it to terminate. The output and error streams
* will be read and forwarded to System.out and System.err
*/
public ExternalProcess(File workingDir, ExternalCommand cmd) throws IOException, InterruptedException {
this(workingDir, cmd, false);
public ExternalProcess(File workingDir, ExternalCommand cmd) throws IOException, InterruptedException, TimeoutException {
this(workingDir, cmd, false, null);
}
private void init(File workingDir, ExternalCommand cmd,
OutputStream outStream, OutputStream errStream) throws IOException,
InterruptedException {
OutputStream outStream, OutputStream errStream,
Duration timeout) throws IOException,
InterruptedException, TimeoutException {
this.cmd = cmd;
ProcessBuilder processBuilder = new ProcessBuilder(cmd.getProgramAndArgs());
processBuilder.directory(workingDir);
@@ -127,15 +131,33 @@ public class ExternalProcess {
process = processBuilder.start();
err = new StreamGobler(process.getErrorStream(), errStream);
out = new StreamGobler(process.getInputStream(), outStream);
exitValue = process.waitFor();
}
public ExternalProcess(File workingDir, ExternalCommand cmd, boolean captureStreams) throws IOException, InterruptedException {
if (captureStreams) {
init(workingDir, cmd, new ByteArrayOutputStream(), new ByteArrayOutputStream());
if (timeout==null) {
exitValue = process.waitFor();
} else {
init(workingDir, cmd, System.out, System.err);
if (process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
exitValue = process.exitValue();
} else {
process.destroy();
exitValue = 999; //Set some non-0 value as that is what some callers might use to determine 'failure' occurred.
throw new TimeoutException("Command timed out: "+this);
}
}
if (exitValue!=0) {
throw new IOException("Command execution failed:\n"+this);
}
}
public ExternalProcess(File workingDir, ExternalCommand cmd, boolean captureStreams, Duration timeout) throws IOException, InterruptedException, TimeoutException {
if (captureStreams) {
init(workingDir, cmd, new ByteArrayOutputStream(), new ByteArrayOutputStream(), timeout);
} else {
init(workingDir, cmd, System.out, System.err, timeout);
}
}
public ExternalProcess(File workingDir, ExternalCommand cmd, boolean captureStreams) throws IOException, InterruptedException, TimeoutException {
this(workingDir, cmd, captureStreams, null);
}
public String getOut() throws InterruptedException {
@@ -156,13 +178,13 @@ public class ExternalProcess {
result.append("exitValue = "+exitValue+"\n");
String strOut = getOut();
if (strOut!=null) {
result.append("\n------- System.out -------\n");
result.append("------- System.out -------\n");
result.append(strOut);
}
String strErr = getErr();
if (strErr!=null) {
result.append("\n------- System.err -------\n");
result.append(getOut());
result.append("------- System.err -------\n");
result.append(strErr);
}
result.append("<<<< ExternalProcess");
return result.toString();

View File

@@ -21,10 +21,12 @@ public class LanguageId {
public static final LanguageId PLAINTEXT = of("plaintext");
public static final LanguageId CONCOURSE_TASK = of("concourse-task-yaml");
public static final LanguageId CONCOURSE_PIPELINE = of("concourse-pipeline-yaml");
public static final LanguageId BOSH_DEPLOYMENT = of("bosh-deployment-manifest");
public static final LanguageId CF_MANIFEST = of("manifest-yaml");
public static final LanguageId JAVA = of("java");
public static final LanguageId YAML = of("yaml");
public static final LanguageId BOSH_DEPLOYMENT = of("bosh-deployment-manifest");
public static final LanguageId BOSH_CLOUD_CONFIG = of("bosh-cloud-config");
private final String id;

View File

@@ -0,0 +1,59 @@
eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
sp_cleanup.add_default_serial_version_id=true
sp_cleanup.add_generated_serial_version_id=false
sp_cleanup.add_missing_annotations=true
sp_cleanup.add_missing_deprecated_annotations=true
sp_cleanup.add_missing_methods=false
sp_cleanup.add_missing_nls_tags=false
sp_cleanup.add_missing_override_annotations=true
sp_cleanup.add_missing_override_annotations_interface_methods=true
sp_cleanup.add_serial_version_id=false
sp_cleanup.always_use_blocks=true
sp_cleanup.always_use_parentheses_in_expressions=false
sp_cleanup.always_use_this_for_non_static_field_access=false
sp_cleanup.always_use_this_for_non_static_method_access=false
sp_cleanup.convert_functional_interfaces=false
sp_cleanup.convert_to_enhanced_for_loop=false
sp_cleanup.correct_indentation=false
sp_cleanup.format_source_code=false
sp_cleanup.format_source_code_changes_only=false
sp_cleanup.insert_inferred_type_arguments=false
sp_cleanup.make_local_variable_final=true
sp_cleanup.make_parameters_final=false
sp_cleanup.make_private_fields_final=true
sp_cleanup.make_type_abstract_if_missing_method=false
sp_cleanup.make_variable_declarations_final=false
sp_cleanup.never_use_blocks=false
sp_cleanup.never_use_parentheses_in_expressions=true
sp_cleanup.on_save_use_additional_actions=true
sp_cleanup.organize_imports=false
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
sp_cleanup.remove_private_constructors=true
sp_cleanup.remove_redundant_type_arguments=false
sp_cleanup.remove_trailing_whitespaces=true
sp_cleanup.remove_trailing_whitespaces_all=true
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
sp_cleanup.remove_unnecessary_casts=true
sp_cleanup.remove_unnecessary_nls_tags=false
sp_cleanup.remove_unused_imports=false
sp_cleanup.remove_unused_local_variables=false
sp_cleanup.remove_unused_private_fields=true
sp_cleanup.remove_unused_private_members=false
sp_cleanup.remove_unused_private_methods=true
sp_cleanup.remove_unused_private_types=true
sp_cleanup.sort_members=false
sp_cleanup.sort_members_all=false
sp_cleanup.use_anonymous_class_creation=false
sp_cleanup.use_blocks=false
sp_cleanup.use_blocks_only_for_return_and_throw=false
sp_cleanup.use_lambda=true
sp_cleanup.use_parentheses_in_expressions=false
sp_cleanup.use_this_for_non_static_field_access=false
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
sp_cleanup.use_this_for_non_static_method_access=false
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true

View File

@@ -116,6 +116,18 @@ public class NodeUtil {
return null;
}
public static NodeTuple getPropertyTuple(Node node, String propName) {
if (node instanceof MappingNode) {
for (NodeTuple entry : ((MappingNode)node).getValue()) {
String key = NodeUtil.asScalar(entry.getKeyNode());
if (propName.equals(key)) {
return entry;
}
}
}
return null;
}
public static Node getProperty(Node node, String propName) {
if (node instanceof MappingNode) {
for (NodeTuple entry : ((MappingNode)node).getValue()) {

View File

@@ -54,7 +54,7 @@ public class AppendTextBuilder {
//ready to enter sequence element on next line
newline(text, indent);
text.append("- ");
singleRequiredProperty(typeUtil.getDomainType(type), indent+2, text);
singleMostImportantProperty(typeUtil.getDomainType(type), indent+2, text);
//Yes using 2 here instead of YamlIndentUtil.INDENT_BY is deliberate. It's the same value (now),
// but the 2 used here is the width of the "- " which should determine nested indent level for things to
// line up properly.
@@ -66,14 +66,18 @@ public class AppendTextBuilder {
}
}
private void singleRequiredProperty(YType type, int indent, StringBuilder text) {
private void singleMostImportantProperty(YType type, int indent, StringBuilder text) {
if (type!=null) {
YTypedProperty requireProp = Streams.getSingle(typeUtil.getProperties(type).stream()
.filter(p -> p.isRequired()));
if (requireProp!=null) {
text.append(requireProp.getName());
YTypedProperty singleProp = Streams.getSingle(typeUtil.getProperties(type).stream()
.filter(p -> p.isPrimary()));
if (singleProp==null) {
singleProp = Streams.getSingle(typeUtil.getProperties(type).stream()
.filter(p -> p.isRequired()));
}
if (singleProp!=null) {
text.append(singleProp.getName());
text.append(':');
build(requireProp.getType(), indent+YamlIndentUtil.INDENT_BY, text);
build(singleProp.getType(), indent+YamlIndentUtil.INDENT_BY, text);
}
}
}

View File

@@ -70,18 +70,18 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
this.type = relaxedType;
}
public YTypeAssistContext(YTypeAssistContext parent, YamlPath contextPath, YType YType, YTypeUtil typeUtil) {
public YTypeAssistContext(YTypeAssistContext parent, YamlPath contextPath, YType type, YTypeUtil typeUtil) {
super(parent.getDocument(), parent.documentSelector, contextPath);
this.parent = parent;
this.typeUtil = typeUtil;
this.type = typeUtil.inferMoreSpecificType(YType, getSchemaContext());
this.type = typeUtil.inferMoreSpecificType(type, getSchemaContext());
}
public YTypeAssistContext(TopLevelAssistContext parent, int documentSelector, YType type, YTypeUtil typeUtil) {
super(parent.getDocument(), documentSelector, YamlPath.EMPTY);
this.parent = parent;
this.typeUtil = typeUtil;
this.type = type;
this.type = typeUtil.inferMoreSpecificType(type, getSchemaContext());
}
@Override
@@ -114,9 +114,10 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
List<List<YTypedProperty>> tieredProperties = sortIntoTiers(allProperties);
Set<String> definedProps = dynamicCtxt.getDefinedProperties();
List<ICompletionProposal> proposals = new ArrayList<>();
boolean suggestDeprecated = typeUtil.suggestDeprecatedProperties();
for (List<YTypedProperty> thisTier : tieredProperties) {
List<YTypedProperty> undefinedProps = thisTier.stream()
.filter(p -> !definedProps.contains(p.getName()))
.filter(p -> !definedProps.contains(p.getName()) && (suggestDeprecated || !p.isDeprecated()))
.collect(Collectors.toList());
if (!undefinedProps.isEmpty()) {
for (YTypedProperty p : undefinedProps) {
@@ -215,7 +216,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
}
for (YValueHint value : values) {
double score = FuzzyMatcher.matchScore(query, value.getValue());
if (score!=0 && !value.equals(query)) {
if (score!=0 && value!=null && !query.equals(value.getValue())) {
int queryStart = offset-query.length();
DocumentEdits edits = new DocumentEdits(doc.getDocument());
edits.delete(queryStart, offset);
@@ -357,7 +358,7 @@ public class YTypeAssistContext extends AbstractYamlAssistContext {
}
return null;
}
protected Collection<ICompletionProposal> getDashedCompletions(YamlDocument doc, SNode current, int offset) {
try {
YamlAssistContext relaxed = relaxForDashes();

View File

@@ -8,8 +8,9 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.concourse;
package org.springframework.ide.vscode.commons.yaml.reconcile;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@@ -18,13 +19,21 @@ import java.util.Map.Entry;
import java.util.Set;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
import org.springframework.ide.vscode.commons.yaml.reconcile.ITypeCollector;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
import org.yaml.snakeyaml.nodes.Node;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Multimap;
/**
@@ -127,5 +136,33 @@ public class ASTTypeCache implements ITypeCollector {
return typeIndex.get(uri);
}
/**
* Use this astTypeCache to extract all defined names for a given type of definition.
*/
public Collection<String> getDefinedNames(DynamicSchemaContext dc, YType defType) {
IDocument doc = dc.getDocument();
if (doc!=null) {
String uri = doc.getUri();
if (uri!=null) {
NodeTypes typeMap = getNodeTypes(uri);
if (typeMap!=null) {
Collection<Node> nodes = typeMap.getNodes(defType);
if (nodes!=null) {
ImmutableSet.Builder<String> builder = ImmutableSortedSet.naturalOrder();
for (Node node : nodes) {
String name = NodeUtil.asScalar(node);
if (StringUtil.hasText(name)) {
builder.add(name);
}
}
return builder.build();
}
}
return ImmutableList.of();
}
}
return null;
}
}

View File

@@ -45,6 +45,7 @@ import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.commons.yaml.quickfix.YamlQuickfixes;
import org.springframework.ide.vscode.commons.yaml.schema.ASTDynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
import org.springframework.ide.vscode.commons.yaml.schema.YTypeUtil;
import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty;
@@ -129,11 +130,11 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
return allOf(ast, node);
}
private void reconcile(YamlFileAST ast, YamlPath path, Node parent, Node node, YType type) {
private void reconcile(YamlFileAST ast, YamlPath path, Node parent, Node node, YType _type) {
// IDocument doc = ast.getDocument();
if (type!=null) {
if (_type!=null) {
DynamicSchemaContext schemaContext = new ASTDynamicSchemaContext(ast, path, node);
type = typeUtil.inferMoreSpecificType(type, schemaContext);
YType type = typeUtil.inferMoreSpecificType(_type, schemaContext);
if (typeCollector!=null) {
typeCollector.accept(node, type);
}
@@ -190,19 +191,23 @@ public class SchemaBasedYamlASTReconciler implements YamlASTReconciler {
break;
case scalar:
if (typeUtil.isAtomic(type)) {
ValueParser parser = typeUtil.getValueParser(type, schemaContext);
if (parser!=null) {
try {
String value = NodeUtil.asScalar(node);
if (value!=null) {
parser.parse(value);
}
} catch (Exception e) {
ProblemType problemType = getProblemType(e);
DocumentRegion region = getRegion(e, ast.getDocument(), node);
String msg = getMessage(e);
valueParseError(type, region, msg, problemType, getValueReplacement(e));
}
SchemaContextAware<ValueParser> parserProvider = typeUtil.getValueParser(type);
if (parserProvider!=null) {
delayedConstraints.add(() -> {
parserProvider.safeWithContext(schemaContext).ifPresent(parser -> {
try {
String value = NodeUtil.asScalar(node);
if (value!=null) {
parser.parse(value);
}
} catch (Exception e) {
ProblemType problemType = getProblemType(e);
DocumentRegion region = getRegion(e, ast.getDocument(), node);
String msg = getMessage(e);
valueParseError(type, region, msg, problemType, getValueReplacement(e));
}
});
});
}
} else {
expectTypeButFoundScalar(type, node);

View File

@@ -8,7 +8,7 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.concourse;
package org.springframework.ide.vscode.commons.yaml.reconcile;
import java.util.Collection;
import java.util.List;
@@ -35,19 +35,20 @@ import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet;
/**
* Finds symbols in a concourse document. This relies on type information cached
* during reconcile and stored in the {@link ConcourseModel}. Therefore,
* this handler only works if invoked after a reconcile.
* Finds symbols in a yaml document based on type information cached
* during reconcile and stored in the {@link ASTTypeCache}. Because
* it relies on information computed during reconcile, this handler only
* works if it is invoked after a reconcile.
*
* @author Kris De Volder
*/
public class ConcourseDocumentSymbolHandler implements DocumentSymbolHandler {
public class TypeBasedYamlSymbolHandler implements DocumentSymbolHandler {
private ASTTypeCache astTypeCache;
private Set<YType> definitionTypes;
private SimpleTextDocumentService documents;
public ConcourseDocumentSymbolHandler(SimpleTextDocumentService documents, ASTTypeCache astTypeCache, Collection<YType> definitionTypes) {
public TypeBasedYamlSymbolHandler(SimpleTextDocumentService documents, ASTTypeCache astTypeCache, Collection<YType> definitionTypes) {
Assert.isTrue(!definitionTypes.isEmpty()); // If there's no interesting types then you are better of using DocumentSymbolHandler.NO_SYMBOLS
this.documents = documents;
this.astTypeCache = astTypeCache;

View File

@@ -114,7 +114,7 @@ public class YamlSchemaProblems {
return new ReconcileProblemImpl(problemType, msg, start, end-start);
}
public static ReconcileProblemImpl missingProperty(String msg, IDocument doc, Node parent, MappingNode map) {
public static ReconcileProblemImpl missingProperty(String msg, IDocument doc, Node parent, Node map) {
DocumentRegion underline = NodeUtil.region(doc, map);
if (parent instanceof MappingNode) {
for (NodeTuple prop : ((MappingNode) parent).getValue()) {

View File

@@ -10,6 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.schema;
import java.util.Optional;
/**
* Interface that can be implemented by something producing another
* component (of some type `T`) where the returned component needs to
@@ -19,7 +21,18 @@ package org.springframework.ide.vscode.commons.yaml.schema;
*/
@FunctionalInterface
public interface SchemaContextAware<T> {
T withContext(DynamicSchemaContext dc);
T withContext(DynamicSchemaContext dc) throws Exception;
/**
* Like `withContext' method, but swallows exceptions silently.
*/
default Optional<T> safeWithContext(DynamicSchemaContext dc) {
try {
return Optional.ofNullable(withContext(dc));
} catch (Exception e) {
return Optional.empty();
}
}
/**
* Convert a plain value into a {@link SchemaContextAware} that ignores the context and simply returns the value.

View File

@@ -24,6 +24,7 @@ import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileException;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReplacementQuickfix;
@@ -52,10 +53,8 @@ import reactor.core.publisher.Flux;
*/
public class YTypeFactory {
/**
* Configuration option for the type-based completion engine.
*/
private boolean enableTieredOptionalPropertyProposals = true;
private boolean suggestDeprecatedProperties = true;
private static class Deprecation {
final String errorMsg;
@@ -158,7 +157,7 @@ public class YTypeFactory {
* YTypeFactory
*/
public final YTypeUtil TYPE_UTIL = new YTypeUtil() {
@Override
public boolean isSequencable(YType type) {
return ((AbstractType)type).isSequenceable();
@@ -210,8 +209,8 @@ public class YTypeFactory {
}
@Override
public ValueParser getValueParser(YType type, DynamicSchemaContext dc) {
return ((AbstractType)type).getParser(dc);
public SchemaContextAware<ValueParser> getValueParser(YType type) {
return ((AbstractType)type).getParser();
}
@Override
@@ -239,6 +238,11 @@ public class YTypeFactory {
public boolean tieredOptionalPropertyProposals() {
return enableTieredOptionalPropertyProposals;
}
@Override
public boolean suggestDeprecatedProperties() {
return suggestDeprecatedProperties;
}
};
/////////////////////////////////////////////////////////////////////////////////////
@@ -253,6 +257,8 @@ public class YTypeFactory {
private List<YValueHint> hints = new ArrayList<>();
private Map<String, YTypedProperty> cachedPropertyMap;
private SchemaContextAware<Callable<Collection<YValueHint>>> hintProvider;
//TODO: SchemaContextAware now allows throwing exceptions so should be able to simplify the above to SchemaContextAware<Collection<YValueHint>>
private List<Constraint> constraints = new ArrayList<>(2);
private ISubCompletionEngine customContentAssistant = null;
@@ -263,7 +269,7 @@ public class YTypeFactory {
public ISubCompletionEngine getCustomContentAssistant() {
return customContentAssistant;
}
public AbstractType setCustomContentAssistant(ISubCompletionEngine customContentAssistant) {
this.customContentAssistant = customContentAssistant;
return this;
@@ -291,6 +297,7 @@ public class YTypeFactory {
}
public AbstractType addHintProvider(SchemaContextAware<Callable<Collection<YValueHint>>> hintProvider) {
//TODO: SchemaContextAware now allows throwing exceptions so should be able to simplify the above to SchemaContextAware<Collection<YValueHint>>
this.hintProvider = hintProvider;
return this;
}
@@ -402,8 +409,8 @@ public class YTypeFactory {
parseWith((DynamicSchemaContext dc) -> parser);
return this;
}
private ValueParser getParser(DynamicSchemaContext dc) {
return parser == null ? null : parser.withContext(dc);
private SchemaContextAware<ValueParser> getParser() {
return parser;
}
public AbstractType require(Constraint dynamicConstraint) {
@@ -444,10 +451,7 @@ public class YTypeFactory {
@Override
public YType inferMoreSpecificType(DynamicSchemaContext dc) {
if (dc!=null) {
YType inferred = typeGuesser.withContext(dc);
if (inferred!=null) {
return inferred;
}
return typeGuesser.safeWithContext(dc).orElse(this);
}
return this;
}
@@ -736,7 +740,7 @@ public class YTypeFactory {
}
public static class YTypedPropertyImpl implements YTypedProperty {
public static class YTypedPropertyImpl implements YTypedProperty, Cloneable {
final private String name;
final private YType type;
@@ -793,7 +797,7 @@ public class YTypeFactory {
this.isDeprecated = deprecationMessage!=null;
this.deprecationMessage = deprecationMessage;
}
@Override
public String getDeprecationMessage() {
return this.deprecationMessage;
@@ -804,7 +808,7 @@ public class YTypeFactory {
return this.isDeprecated;
}
public YTypedPropertyImpl isPrimary(boolean primary) {
this.isPrimary = primary;
this.isRequired = primary;
@@ -816,12 +820,19 @@ public class YTypeFactory {
this.isRequired = required;
return this;
}
@Override
public boolean isPrimary() {
return isPrimary;
}
public YTypedPropertyImpl copy() {
try {
return (YTypedPropertyImpl) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
}
public YAtomicType yatomic(String name) {
@@ -832,6 +843,10 @@ public class YTypeFactory {
return new YTypedPropertyImpl(name, type);
}
public YTypedPropertyImpl yprop(YTypedProperty prop) {
return ((YTypedPropertyImpl)prop).copy();
}
public YAtomicType yenumFromHints(String name, BiFunction<String, Collection<String>, String> errorMessageFormatter, SchemaContextAware<Collection<YValueHint>> values) {
YAtomicType t = yatomic(name);
t.addHintProvider((dc) -> () -> values.withContext(dc));
@@ -847,6 +862,17 @@ public class YTypeFactory {
return t;
}
public YAtomicType yenumFromDynamicValues(String name, SchemaContextAware<Collection<String>> values) {
return yenumFromHints(name,
//Error message formatter:
(parseString, validValues) -> "'"+parseString+"' is an unknown '"+name+"'. Valid values are: "+validValues,
//Hints provider:
(dc) ->
hints(values.withContext(dc)
)
);
}
public EnumTypeBuilder yenumBuilder(String name, String... values) {
return new EnumTypeBuilder(name, values);
}
@@ -916,4 +942,10 @@ public class YTypeFactory {
return this;
}
public YTypeFactory suggestDeprecatedProperties(boolean enable) {
this.suggestDeprecatedProperties = enable;
return this;
}
}

View File

@@ -33,7 +33,7 @@ public interface YTypeUtil {
YValueHint[] getHintValues(YType yType, DynamicSchemaContext dc) throws Exception;
String niceTypeName(YType type);
YType getKeyType(YType type);
ValueParser getValueParser(YType type, DynamicSchemaContext dc);
SchemaContextAware<ValueParser> getValueParser(YType type);
//TODO: only one of these two should be enough?
List<YTypedProperty> getProperties(YType type);
@@ -56,4 +56,10 @@ public interface YTypeUtil {
* suggested until required ones are all defined)
*/
boolean tieredOptionalPropertyProposals();
/**
* Config option for type-based completion engine. This enables/disables
* whether engine should generate proposals for deprecated properties (true),
* or suppress them (false).
*/
boolean suggestDeprecatedProperties();
}

View File

@@ -10,6 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.yaml.schema;
import java.util.Collection;
import org.springframework.ide.vscode.commons.util.IntegerRange;
/**

View File

@@ -11,18 +11,25 @@
package org.springframework.ide.vscode.commons.yaml.schema.constraints;
import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems.EXTRA_PROPERTY;
import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems.missingProperty;
import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems.*;
import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems.problem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache;
import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache.NodeTypes;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware;
@@ -31,7 +38,11 @@ import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multiset;
/**
* Various static methods for constructing/composing {@link Constraint}s.
@@ -39,7 +50,7 @@ import com.google.common.collect.ImmutableSet;
* @author Kris De Volder
*/
public class Constraints {
public static Constraint requireOneOf(String... properties) {
return new RequireOneOf(properties);
}
@@ -47,17 +58,28 @@ public class Constraints {
public static Constraint requireAtMostOneOf(String... properties) {
return new RequireOneOf(properties).allowFewer(true);
}
public static Constraint requireAtLeastOneOf(String... properties) {
return new RequireOneOf(properties).allowMultiple(true);
}
static private class RequireOneOf implements Constraint {
private final String[] _requiredProps;
private boolean allowFewer = false;
private boolean allowMultiple = false;
public RequireOneOf(String[] properties) {
Assert.isLegal(properties.length>1);
this._requiredProps = properties;
}
public Constraint allowMultiple(boolean b) {
this.allowMultiple = b;
return this;
}
public Constraint allowFewer(boolean b) {
this.allowFewer = b;
return this;
@@ -78,7 +100,7 @@ public class Constraints {
problems.accept(missingProperty(
"One of "+requiredProps+" is required for '"+type+"'", doc, parent, map));
}
} else if (foundPropsCount>1) {
} else if (foundPropsCount>1 && !allowMultiple) {
//Mark each of the found keys as a violation:
for (NodeTuple entry : map.getValue()) {
String key = NodeUtil.asScalar(entry.getKeyNode());
@@ -119,7 +141,57 @@ public class Constraints {
@Deprecated
public static Constraint schemaContextAware(SchemaContextAware<Constraint> dispatcher) {
return (DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) -> {
dispatcher.withContext(dc).verify(dc, parent, node, type, problems);
dispatcher.safeWithContext(dc).ifPresent((constraint) -> constraint.verify(dc, parent, node, type, problems));
};
}
public static Constraint mutuallyExclusive(String p1, String p2) {
return (DynamicSchemaContext dc, Node parent, Node node, YType type, IProblemCollector problems) -> {
if (node instanceof MappingNode) {
MappingNode map = (MappingNode) node;
Set<String> defined = dc.getDefinedProperties();
if (defined.contains(p1) && defined.contains(p2)) {
for (NodeTuple tup : map.getValue()) {
Node keyNode = tup.getKeyNode();
String key = NodeUtil.asScalar(keyNode);
if (p1.equals(key) || p1.equals(key)) {
problems.accept(problem(EXTRA_PROPERTY,
"Only one of '"+p1+"' and '"+p2+"' should be defined for '"+type+"'", keyNode
));
}
}
}
}
};
}
/**
* Check that all nodes of a given type, across the AST represent unique names.
*/
public static Constraint uniqueDefinition(ASTTypeCache astTypes, YType defType, ProblemType problemType) {
return (DynamicSchemaContext dc, Node parent, Node _ignored_node, YType type, IProblemCollector problems) -> {
NodeTypes nodeTypes = astTypes.getNodeTypes(dc.getDocument().getUri());
if (nodeTypes!=null) {
Collection<Node> nodes = nodeTypes.getNodes(defType);
if (nodes!=null && !nodes.isEmpty()) {
Multimap<String, Node> name2nodes = ArrayListMultimap.create();
for (Node node : nodes) {
String name = NodeUtil.asScalar(node);
if (StringUtil.hasText(name)) {
name2nodes.put(name, node);
}
}
for (String name : name2nodes.keys()) {
Collection<Node> nodesForName = name2nodes.get(name);
if (nodesForName.size()>1) {
for (Node duplicateNode : nodesForName) {
problems.accept(YamlSchemaProblems.problem(problemType, "Duplicate '"+defType+"'", duplicateNode));
}
}
}
}
}
};
}
}

View File

@@ -326,7 +326,7 @@ public class Editor {
return completions;
}
public void assertCompletionLabels(String... expectedLabels) throws Exception {
public List<CompletionItem> assertCompletionLabels(String... expectedLabels) throws Exception {
StringBuilder expect = new StringBuilder();
StringBuilder actual = new StringBuilder();
for (String label : expectedLabels) {
@@ -334,11 +334,13 @@ public class Editor {
expect.append("\n");
}
for (CompletionItem completion : getCompletions()) {
List<CompletionItem> completions;
for (CompletionItem completion : completions = getCompletions()) {
actual.append(completion.getLabel());
actual.append("\n");
}
assertEquals(expect.toString(), actual.toString());
return completions;
}
public void assertContainsCompletions(String... expectTextAfter) throws Exception {
@@ -383,6 +385,32 @@ public class Editor {
}
}
public void assertContextualCompletions(LanguageId language, Predicate<CompletionItem> isInteresting, String textBefore, String... textAfter) throws Exception {
Editor editor = harness.newEditor(language, this.getText());
editor.reconcile(); //this ensures the conText is parsed and its AST is cached (will be used for
//dynamic CA when the conText + textBefore is not parsable.
textBefore = replaceSelection(textBefore);
textAfter = Arrays.stream(textAfter)
.map((String t) -> replaceSelection(t))
.collect(Collectors.toList()).toArray(new String[0]);
editor.setText(textBefore);
editor.assertCompletions(isInteresting, textAfter);
}
public void assertContextualCompletions(String textBefore, String... textAfter) throws Exception {
assertContextualCompletions(getLanguageId(), (x) -> true, textBefore, textAfter);
}
private String replaceSelection(String replacement) {
try {
String text = getRawText();
return text.substring(0, selectionStart) + replacement + text.substring(selectionEnd);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void apply(CompletionItem completion) throws Exception {
completion = harness.resolveCompletionItem(completion);
TextEdit edit = completion.getTextEdit();

View File

@@ -14,8 +14,11 @@ package org.springframework.ide.vscode.languageserver.testharness;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.ArrayList;
@@ -80,6 +83,7 @@ import org.springframework.ide.vscode.commons.languageserver.util.LanguageServer
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.IOUtil;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
@@ -569,4 +573,16 @@ public class LanguageServerHarness {
}
};
}
/**
* Create a new editor and populate contents from a file found on the (test) classpath.
*/
public Editor newEditorFromClasspath(String resourcePath) throws Exception {
try (InputStream is = LanguageServerHarness.class.getResourceAsStream(resourcePath)) {
if (is==null) {
fail("Couldn't find the resource: "+resourcePath);
}
return newEditor(IOUtil.toString(is));
}
}
}

View File

@@ -23,6 +23,7 @@
</modules>
<repositories>
<!-- Note: avoid using this repo, it can block project builds for long time.
<repository>
<id>lsp4j-snapshots-repo</id>
<name>lsp4j-snapshots-repo</name>
@@ -34,6 +35,7 @@
<enabled>true</enabled>
</snapshots>
</repository>
-->
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
@@ -75,9 +77,10 @@
<assertj-version>3.5.2</assertj-version>
<slf4j-version>1.7.22</slf4j-version>
<guava-version>19.0</guava-version>
<mockito-version>1.10.19</mockito-version>
<jackson-2-version>2.5.0</jackson-2-version>
<jersey-2-version>2.10</jersey-2-version>
<lsp4j-version>0.2.0-SNAPSHOT</lsp4j-version>
<lsp4j-version>0.2.1</lsp4j-version>
<!-- NOTE: Reactor version must match version used by the CF client -->
<reactor-version>3.0.5.RELEASE</reactor-version>
<reactor-netty>0.6.0.RELEASE</reactor-netty>

View File

@@ -23,6 +23,7 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil;
import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
import org.yaml.snakeyaml.nodes.Node;

View File

@@ -34,6 +34,7 @@ import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngi
import org.springframework.ide.vscode.commons.yaml.completion.YamlCompletionEngineOptions;
import org.springframework.ide.vscode.commons.yaml.hover.YamlHoverInfoProvider;
import org.springframework.ide.vscode.commons.yaml.quickfix.YamlQuickfixes;
import org.springframework.ide.vscode.commons.yaml.reconcile.TypeBasedYamlSymbolHandler;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaBasedReconcileEngine;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
@@ -74,7 +75,7 @@ public class ConcourseLanguageServer extends SimpleLanguageServer {
reconcileEngine.setTypeCollector(models.getAstTypeCache());
this.symbolHandler = CollectionUtil.hasElements(definitionTypes)
? new ConcourseDocumentSymbolHandler(documents, models.getAstTypeCache(), definitionTypes)
? new TypeBasedYamlSymbolHandler(documents, models.getAstTypeCache(), definitionTypes)
: DocumentSymbolHandler.NO_SYMBOLS;
}

View File

@@ -40,7 +40,9 @@ import org.springframework.ide.vscode.commons.yaml.path.NodeCursor;
import org.springframework.ide.vscode.commons.yaml.path.YamlPath;
import org.springframework.ide.vscode.commons.yaml.path.YamlPathSegment;
import org.springframework.ide.vscode.commons.yaml.path.YamlTraversal;
import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache;
import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems;
import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache.NodeTypes;
import org.springframework.ide.vscode.commons.yaml.schema.BasicYValueHint;
import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
@@ -51,7 +53,6 @@ import org.springframework.ide.vscode.commons.yaml.schema.constraints.Constraint
import org.springframework.ide.vscode.commons.yaml.schema.YTypedProperty;
import org.springframework.ide.vscode.commons.yaml.schema.YValueHint;
import org.springframework.ide.vscode.commons.yaml.util.Streams;
import org.springframework.ide.vscode.concourse.ASTTypeCache.NodeTypes;
import org.springframework.ide.vscode.concourse.util.StaleFallbackCache;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.YAMLException;

View File

@@ -473,7 +473,7 @@ public class ConcourseEditorTest {
editor = harness.newEditor("jo<*>");
editor.assertCompletions(
"jobs:\n"+
"- <*>"
"- name: <*>"
);
}
@@ -486,19 +486,19 @@ public class ConcourseEditorTest {
"- name: <*>"
, // --------------
"jobs:\n" +
"- <*>"
"- name: <*>"
, // ---------------
"resource_types:\n" +
"- <*>"
"- name: <*>"
, // ---------------
"resources:\n"+
"- <*>"
"- name: <*>"
);
editor = harness.newEditor("rety<*>");
editor.assertCompletions(
"resource_types:\n" +
"- <*>"
"- name: <*>"
);
}

View File

@@ -57,7 +57,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<version>${mockito-version}</version>
<scope>test</scope>
</dependency>
<dependency>