Skip to content

Commit

Permalink
feature: Auto-completion from kubernetes context with longer string
Browse files Browse the repository at this point in the history
  • Loading branch information
Delawen committed Oct 18, 2023
1 parent e752cbb commit dfb7866
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import io.fabric8.kubernetes.client.KubernetesClient;

public class CamelOptionValuesCompletionsFuture implements Function<CamelCatalog, List<CompletionItem>> {

private static final Logger LOGGER = LoggerFactory.getLogger(CamelOptionValuesCompletionsFuture.class);

private static final String BOOLEAN_TYPE = "boolean";
Expand All @@ -60,19 +60,19 @@ public CamelOptionValuesCompletionsFuture(OptionParamValueURIInstance optionPara
@Override
public List<CompletionItem> apply(CamelCatalog camelCatalog) {
Optional<EndpointOptionModel> endpointModel = retrieveEndpointOptionModel(camelCatalog);
if(endpointModel.isPresent()) {
if (endpointModel.isPresent()) {
EndpointOptionModel endpointOptionModel = endpointModel.get();
List<String> enums = endpointOptionModel.getEnums();
if (enums != null && !enums.isEmpty()) {
return computeCompletionForEnums(enums);
} else if(BOOLEAN_TYPE.equals(endpointOptionModel.getType())) {
} else if (BOOLEAN_TYPE.equals(endpointOptionModel.getType())) {
CompletionItem trueItem = new CompletionItem(Boolean.TRUE.toString());
CompletionResolverUtils.applyTextEditToCompletionItem(optionParamValueURIInstance, trueItem);
CompletionItem falseItem = new CompletionItem(Boolean.FALSE.toString());
CompletionItem falseItem = new CompletionItem(Boolean.FALSE.toString());
CompletionResolverUtils.applyTextEditToCompletionItem(optionParamValueURIInstance, falseItem);
Stream<CompletionItem> values = Stream.of(trueItem, falseItem);
return values.filter(FilterPredicateUtils.matchesCompletionFilter(filterString)).collect(Collectors.toList());
} else if(optionParamValueURIInstance.getComponentName().startsWith("kubernetes-")
} else if (optionParamValueURIInstance.getComponentName().startsWith("kubernetes-")
&& "namespace".equals(optionParamValueURIInstance.getOptionParamURIInstance().getKey().getKeyName())) {
try (KubernetesClient client = KubernetesConfigManager.getInstance().getClient()) {
return client.namespaces().list().getItems().stream().map(namespace -> {
Expand All @@ -83,11 +83,11 @@ public List<CompletionItem> apply(CamelCatalog camelCatalog) {
} catch (ApiException e) {
LOGGER.error("Error while trying to provide completion for Kubernetes connected mode", e);
}
} else if("lang".equalsIgnoreCase(endpointOptionModel.getName())
} else if ("lang".equalsIgnoreCase(endpointOptionModel.getName())
&& optionParamValueURIInstance.getComponentName().startsWith("twitter-")) {
List<CompletionItem> items = new ArrayList<>();
//We will just rely on Java to be updated for this
for(String lang : Locale.getISOLanguages()) {
for (String lang : Locale.getISOLanguages()) {
CompletionItem langItem = new CompletionItem(lang);
CompletionResolverUtils.applyTextEditToCompletionItem(optionParamValueURIInstance, langItem);
items.add(langItem);
Expand All @@ -104,27 +104,58 @@ public List<CompletionItem> apply(CamelCatalog camelCatalog) {
}

private List<CompletionItem> getCompletionForKubernetes() {

final var value = optionParamValueURIInstance.getOptionParamURIInstance().getValue().getValueName();
final var interestingPosition =
optionParamValueURIInstance.getValueName().length() > filterString.length() ?
filterString.length() + 1 : filterString.length();
final var value = optionParamValueURIInstance.getValueName().substring(0, interestingPosition);
var kubernetesPlaceholders = new ArrayList<CompletionItem>();

if(StringUtils.startsWithIgnoreCase(value, "{{")) {

// Make sure we have the cursor after a {{
if (StringUtils.contains(value, "{{") &&
//Either it is not closed by }}
(!StringUtils.contains(value, "}}") ||
// or }} is after the cursor
(value.lastIndexOf("}}") < value.lastIndexOf("{{")))) {

//Get the string before the placeholder (starting with {{)
final var pre = value.substring(0, value.lastIndexOf("{{"));

// Now we get the string after the placeholder.
// This is tricky because we don't even know if the placeholder is already closed.
// interestingPosition is where the cursor is supposed to be
var lastPart = optionParamValueURIInstance.getValueName().substring(interestingPosition);

// The cursor is between {{ and }} because we found a }} after the cursor
if (lastPart.indexOf("}}") >= 0 &&
// This }} detected is closing the placeholder we are guessing
(lastPart.indexOf("}}") < lastPart.indexOf("{{")
//Or it is the only placeholder closing here, so it must be ours
|| lastPart.indexOf("{{") < 0)) {
//Let's get whatever is behind the detected }}
lastPart = lastPart.substring(lastPart.indexOf("}}") + 2);
}

final var post = lastPart;
try (KubernetesClient client = KubernetesConfigManager.getInstance().getClient()) {
if (client instanceof NamespacedKubernetesClient nsClient) {
kubernetesPlaceholders.addAll(nsClient.inAnyNamespace().secrets().list().getItems().stream().flatMap(element ->
element.getData().keySet().stream().map(k ->{
element.getData().keySet().stream().map(k -> {
CompletionItem item = new CompletionItem(
"{{secret:" + element.getMetadata().getName() + "/" + k + "}}");
item.setInsertText(pre + item.getLabel() + post);
item.setFilterText(pre + item.getLabel());
CompletionResolverUtils.applyTextEditToCompletionItem(optionParamValueURIInstance, item);
return item;
})).collect(Collectors.toList()));
kubernetesPlaceholders.addAll(nsClient.inAnyNamespace().configMaps().list().getItems().stream().flatMap(element ->
element.getData().keySet().stream().map(k ->{
CompletionItem item = new CompletionItem(
"{{configmap:" + element.getMetadata().getName() + "/" + k + "}}");
CompletionResolverUtils.applyTextEditToCompletionItem(optionParamValueURIInstance, item);
return item;
})).collect(Collectors.toList()));
element.getData().keySet().stream().map(k -> {
CompletionItem item = new CompletionItem(
"{{configmap:" + element.getMetadata().getName() + "/" + k + "}}");
item.setInsertText(pre + item.getLabel() + post);
item.setFilterText(pre + item.getLabel());
CompletionResolverUtils.applyTextEditToCompletionItem(optionParamValueURIInstance, item);
return item;
})).collect(Collectors.toList()));
}
} catch (Exception e) {
LOGGER.error("Error while trying to provide completion for Kubernetes connected mode", e);
Expand All @@ -136,7 +167,7 @@ private List<CompletionItem> getCompletionForKubernetes() {

private List<CompletionItem> computeCompletionForEnums(List<String> enums) {
List<CompletionItem> completionItems = new ArrayList<>();
for(String enumValue : enums) {
for (String enumValue : enums) {
CompletionItem item = new CompletionItem(enumValue);
CompletionResolverUtils.applyTextEditToCompletionItem(optionParamValueURIInstance, item);
completionItems.add(item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand Down Expand Up @@ -106,7 +106,7 @@ void testSecretCompletionWithNoSecrets() throws Exception {
@Test
void testSecretCompletionWithSeveralSecrets() throws Exception {
createNamespace("my-secrets-namespace");
createSecret("mySecrets", List.of("key","second"));
createSecret("mySecrets", List.of("key", "second"));
createSecret("myRealSecrets", List.of("another"));

List<CompletionItem> completions = getCompletionForSecrets();
Expand All @@ -127,7 +127,7 @@ void testConfigMapCompletion() throws Exception {
@Test
void testConfigMapAndSecretsCompletion() throws Exception {
createNamespace("my-secrets-namespace");
createSecret("mySecrets", List.of("key","second"));
createSecret("mySecrets", List.of("key", "second"));
createConfigMap("myMap", List.of("myKey1", "myKey2"));

List<CompletionItem> completions = getCompletionForPlaceholders();
Expand All @@ -138,6 +138,61 @@ void testConfigMapAndSecretsCompletion() throws Exception {
assertThat(completions.get(3).getLabel()).isEqualTo("{{configmap:myMap/myKey2}}");
}

@Test
void testKubernetesAutoCompletionMidValue() throws Exception {
createNamespace("my-secrets-namespace");
createConfigMap("myMap", List.of("myKey1", "myKey2"));

String camelUri = "pgevent:host:999/database/channel?user=something{{a}}afterwards";
String text = RouteTextBuilder.createXMLSpringRoute(camelUri);
CamelLanguageServer languageServer = initializeLanguageServer(text, ".xml");
Position position = new Position(0, RouteTextBuilder.XML_PREFIX_FROM.length() + camelUri.length() - 12);
List<CompletionItem> completions = getCompletionFor(languageServer, position).get().getLeft();

assertThat(completions).hasSize(2);
assertThat(completions.get(0).getLabel()).isEqualTo("{{configmap:myMap/myKey1}}");
assertThat(completions.get(0).getFilterText()).isEqualTo("something{{configmap:myMap/myKey1}}");
assertThat(completions.get(1).getLabel()).isEqualTo("{{configmap:myMap/myKey2}}");
assertThat(completions.get(1).getInsertText()).isEqualTo("something{{configmap:myMap/myKey2}}afterwards");
}

@Test
void testKubernetesAutoCompletionSeveralPlaceholders() throws Exception {
createNamespace("my-secrets-namespace");
createConfigMap("myMap", List.of("myKey1", "myKey2"));

String camelUri = "pgevent:host:999/database/channel?user={{secret:mySec/none}}something{{co}}afterwards" +
"{{secret:aa/BB}}";
String text = RouteTextBuilder.createXMLSpringRoute(camelUri);
CamelLanguageServer languageServer = initializeLanguageServer(text, ".xml");
Position position = new Position(0, RouteTextBuilder.XML_PREFIX_FROM.length() + camelUri.length() - 30);
List<CompletionItem> completions = getCompletionFor(languageServer, position).get().getLeft();

assertThat(completions).hasSize(2);
assertThat(completions.get(0).getLabel()).isEqualTo("{{configmap:myMap/myKey1}}");
assertThat(completions.get(0).getFilterText()).isEqualTo("{{secret:mySec/none}}something{{configmap:myMap/myKey1}}");
assertThat(completions.get(1).getLabel()).isEqualTo("{{configmap:myMap/myKey2}}");
assertThat(completions.get(1).getInsertText()).isEqualTo("{{secret:mySec/none}}something{{configmap:myMap/myKey2}}afterwards{{secret:aa/BB}}");
}

@Test
void testKubernetesAutoCompletionPlaceholdersNotClosed() throws Exception {
createNamespace("my-secrets-namespace");
createConfigMap("myMap", List.of("myKey1", "myKey2"));

String camelUri = "pgevent:host:999/database/channel?user=}}something{{a";
String text = RouteTextBuilder.createXMLSpringRoute(camelUri);
CamelLanguageServer languageServer = initializeLanguageServer(text, ".xml");
Position position = new Position(0, RouteTextBuilder.XML_PREFIX_FROM.length() + camelUri.length());
List<CompletionItem> completions = getCompletionFor(languageServer, position).get().getLeft();

assertThat(completions).hasSize(2);
assertThat(completions.get(0).getLabel()).isEqualTo("{{configmap:myMap/myKey1}}");
assertThat(completions.get(0).getFilterText()).isEqualTo("}}something{{configmap:myMap/myKey1}}");
assertThat(completions.get(1).getLabel()).isEqualTo("{{configmap:myMap/myKey2}}");
assertThat(completions.get(1).getInsertText()).isEqualTo("}}something{{configmap:myMap/myKey2}}");
}

private void createNamespace(String name) {
client.namespaces().resource(new NamespaceBuilder().withNewMetadata().withName(name).endMetadata().build()).create();
}
Expand Down

0 comments on commit dfb7866

Please sign in to comment.