From 3f215fdd8239c8a32614d84c5b2dab518dbc819c Mon Sep 17 00:00:00 2001 From: "Muthuramalingam, Brinda Santh" Date: Fri, 11 Jan 2019 11:30:40 -0500 Subject: Add blueprint Kotlin script support. Change-Id: I92b8e9850ea07c7f671e7aada9770713854534c3 Issue-ID: CCSDK-941 Signed-off-by: Muthuramalingam, Brinda Santh --- components/core/pom.xml | 5 ++ .../core/script/BluePrintScriptConfiguration.kt | 86 ++++++++++++++++++++ .../core/script/BluePrintScriptService.kt | 78 ++++++++++++++++++ .../core/script/BlueprintScriptingHost.kt | 93 ++++++++++++++++++++++ .../services/javax.script.ScriptEngineFactory | 17 ++++ .../core/script/BluePrintScriptServiceTest.kt | 49 ++++++++++++ .../core/utils/JacksonReactorUtilsTest.kt | 52 ------------ .../scripts/SampleBlueprintFunctionNode.kts | 44 ++++++++++ .../artifact_type/artifact-script-kotlin.json | 8 ++ .../starter-type/node_type/source-capability.json | 3 +- .../node_type/tosca.nodes.component.Kotlin.json | 5 ++ components/parent/pom.xml | 45 +++++++++-- 12 files changed, 427 insertions(+), 58 deletions(-) create mode 100644 components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptConfiguration.kt create mode 100644 components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptService.kt create mode 100644 components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BlueprintScriptingHost.kt create mode 100644 components/core/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory create mode 100644 components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptServiceTest.kt delete mode 100644 components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/utils/JacksonReactorUtilsTest.kt create mode 100644 components/core/src/test/resources/scripts/SampleBlueprintFunctionNode.kts create mode 100644 components/model-catalog/definition-type/starter-type/artifact_type/artifact-script-kotlin.json create mode 100644 components/model-catalog/definition-type/starter-type/node_type/tosca.nodes.component.Kotlin.json (limited to 'components') diff --git a/components/core/pom.xml b/components/core/pom.xml index 84063fd5..f33146b6 100644 --- a/components/core/pom.xml +++ b/components/core/pom.xml @@ -63,6 +63,11 @@ mockk test + + org.jetbrains.kotlinx + kotlinx-coroutines-test + test + diff --git a/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptConfiguration.kt b/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptConfiguration.kt new file mode 100644 index 00000000..f7bfb857 --- /dev/null +++ b/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptConfiguration.kt @@ -0,0 +1,86 @@ +/* + * Copyright © 2017-2018 AT&T Intellectual Property. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.apps.controllerblueprints.core.script + +import org.jetbrains.kotlin.script.util.LocalFilesResolver +import java.io.File +import kotlin.script.dependencies.ScriptContents +import kotlin.script.dependencies.ScriptDependenciesResolver +import kotlin.script.experimental.annotations.KotlinScript +import kotlin.script.experimental.api.* +import kotlin.script.experimental.jvm.JvmDependency +import kotlin.script.experimental.jvm.dependenciesFromCurrentContext +import kotlin.script.experimental.jvm.jvm + + +@KotlinScript(fileExtension = "kts", + compilationConfiguration = ComponentScriptConfiguration::class) +abstract class ComponentScript { + +} + +object ComponentScriptConfiguration : ScriptCompilationConfiguration( + { + // defaultImports(DependsOn::class, Repository::class) + jvm { + dependenciesFromCurrentContext( + wholeClasspath = true + ) + } +// refineConfiguration { +// onAnnotations(DependsOn::class, Repository::class, handler = ::configureLocalFileDepsOnAnnotations) +// } + } +) + + +private val resolver = LocalFilesResolver() + +fun configureLocalFileDepsOnAnnotations(context: ScriptConfigurationRefinementContext): + ResultWithDiagnostics { + + val annotations = context.collectedData?.get(ScriptCollectedData.foundAnnotations)?.takeIf { it.isNotEmpty() } + ?: return context.compilationConfiguration.asSuccess() + + val scriptContents = object : ScriptContents { + override val annotations: Iterable = annotations + override val file: File? = null + override val text: CharSequence? = null + } + + val diagnostics = arrayListOf() + + fun report(severity: ScriptDependenciesResolver.ReportSeverity, message: String, position: ScriptContents.Position?) { + //TODO + } + + return try { + val newDepsFromResolver = resolver.resolve(scriptContents, emptyMap(), ::report, null).get() + ?: return context.compilationConfiguration.asSuccess(diagnostics) + + val resolvedClasspath = newDepsFromResolver.classpath.toList().takeIf { it.isNotEmpty() } + ?: return context.compilationConfiguration.asSuccess(diagnostics) + + ScriptCompilationConfiguration(context.compilationConfiguration) { + dependencies.append(JvmDependency(resolvedClasspath)) + + }.asSuccess(diagnostics) + + } catch (e: Throwable) { + ResultWithDiagnostics.Failure(*diagnostics.toTypedArray(), e.asDiagnostics()) + } +} \ No newline at end of file diff --git a/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptService.kt b/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptService.kt new file mode 100644 index 00000000..8ae09151 --- /dev/null +++ b/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptService.kt @@ -0,0 +1,78 @@ +/* + * Copyright © 2017-2018 AT&T Intellectual Property. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.apps.controllerblueprints.core.script + +import org.onap.ccsdk.apps.controllerblueprints.core.BluePrintProcessorException +import java.io.File +import java.io.InputStream +import java.io.Reader +import javax.script.ScriptEngineManager +import kotlin.script.experimental.api.ResultValue +import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.resultOrNull +import kotlin.script.experimental.host.toScriptSource +import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate + + +open class BluePrintScriptService(classLoader: ClassLoader? = Thread.currentThread().contextClassLoader) { + + /** + * Get the Script Class instance + */ + inline fun scriptClassNewInstance(scriptFile: File, scriptClassName: String): T { + + val compilationConfiguration = createJvmCompilationConfigurationFromTemplate() + + val scriptEvaluator = BluePrintScriptEvaluator(scriptClassName) + + val evalResponse = BlueprintScriptingHost(scriptEvaluator).eval(scriptFile.toScriptSource(), compilationConfiguration, + null) + + when (evalResponse) { + is ResultWithDiagnostics.Success -> { + val returnValue = evalResponse.resultOrNull()?.returnValue as ResultValue.Value + return returnValue.value.castOrError() + } + is ResultWithDiagnostics.Failure -> { + throw BluePrintProcessorException(evalResponse.reports.joinToString("\n")) + } + else -> { + throw BluePrintProcessorException("Failed to process script ${scriptFile.absolutePath}") + } + } + + } + + val engine = ScriptEngineManager(classLoader).getEngineByExtension("kts") + + inline fun safeEval(evaluation: () -> R?) = try { + evaluation() + } catch (e: Exception) { + throw BluePrintProcessorException("Cannot load script", e) + } + + inline fun Any?.castOrError() = takeIf { it is T }?.let { it as T } + ?: throw IllegalArgumentException("Cannot cast $this to expected type ${T::class}") + + inline fun load(script: String): T = safeEval { engine.eval(script) }.castOrError() + + inline fun load(reader: Reader): T = safeEval { engine.eval(reader) }.castOrError() + + inline fun load(inputStream: InputStream): T = load(inputStream.reader()) + + inline fun loadAll(vararg inputStream: InputStream): List = inputStream.map(::load) +} diff --git a/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BlueprintScriptingHost.kt b/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BlueprintScriptingHost.kt new file mode 100644 index 00000000..bda20a44 --- /dev/null +++ b/components/core/src/main/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BlueprintScriptingHost.kt @@ -0,0 +1,93 @@ +/* + * Copyright © 2017-2018 AT&T Intellectual Property. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.apps.controllerblueprints.core.script + +import org.onap.ccsdk.apps.controllerblueprints.core.BluePrintProcessorException +import org.slf4j.LoggerFactory +import kotlin.script.experimental.api.* +import kotlin.script.experimental.host.BasicScriptingHost +import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration +import kotlin.script.experimental.jvmhost.JvmScriptCompiler + +val defaultBlueprintScriptCompiler = JvmScriptCompiler(defaultJvmScriptingHostConfiguration) + +open class BlueprintScriptingHost(evaluator: ScriptEvaluator +) : BasicScriptingHost(defaultBlueprintScriptCompiler, evaluator) { + + override fun eval( + script: SourceCode, + scriptCompilationConfiguration: ScriptCompilationConfiguration, + configuration: ScriptEvaluationConfiguration? + ): ResultWithDiagnostics = + + runInCoroutineContext { + + compiler(script, scriptCompilationConfiguration) + .onSuccess { + evaluator(it, configuration) + } + } +} + + +open class BluePrintScriptEvaluator(private val scriptClassName: String) : ScriptEvaluator { + + val log = LoggerFactory.getLogger(BluePrintScriptEvaluator::class.java)!! + + override suspend operator fun invoke( + compiledScript: CompiledScript<*>, + scriptEvaluationConfiguration: ScriptEvaluationConfiguration? + ): ResultWithDiagnostics = + try { + val res = compiledScript.getClass(scriptEvaluationConfiguration) + when (res) { + is ResultWithDiagnostics.Failure -> res + is ResultWithDiagnostics.Success -> { + val scriptClass = res.value + val args = ArrayList() + scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.providedProperties)?.forEach { + args.add(it.value) + } + scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.implicitReceivers)?.let { + args.addAll(it) + } + scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.constructorArgs)?.let { + args.addAll(it) + } + + val completeScriptClass = "Script\$$scriptClassName" + log.info("Searching for class type($completeScriptClass)") + /** + * Search for Class Name + */ + val instanceClass = scriptClass.java.classes + .single { it.name == completeScriptClass } + //.single { it.name == "Script\$SampleBlueprintsFunctionNode" } + + + val instance = instanceClass.newInstance() + ?: throw BluePrintProcessorException("failed to create instance from the script") + + ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Value(completeScriptClass, + instance, instance.javaClass.typeName), + scriptEvaluationConfiguration)) + } + } + } catch (e: Throwable) { + ResultWithDiagnostics.Failure(e.asDiagnostics("Error evaluating script")) + } +} \ No newline at end of file diff --git a/components/core/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory b/components/core/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory new file mode 100644 index 00000000..89838f44 --- /dev/null +++ b/components/core/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory @@ -0,0 +1,17 @@ +# +# Copyright © 2017-2018 AT&T Intellectual Property. +# +# Licensed 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory \ No newline at end of file diff --git a/components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptServiceTest.kt b/components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptServiceTest.kt new file mode 100644 index 00000000..5c5ad3ba --- /dev/null +++ b/components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/script/BluePrintScriptServiceTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright © 2017-2018 AT&T Intellectual Property. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.onap.ccsdk.apps.controllerblueprints.core.script + +import org.junit.Ignore +import org.junit.Test +import org.onap.ccsdk.apps.controllerblueprints.core.interfaces.BlueprintFunctionNode +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class BluePrintScriptServiceTest { + + @Test + fun `invoke script`() { + val scriptContent = "11 + 11" + val value = BluePrintScriptService() + .load(scriptContent) + assertEquals(22, value, "failed to execute command") + } + + @Test + @Ignore + fun `invoke script component node`() { + + //println(classpathFromClasspathProperty()?.joinToString("\n")) + + val scriptFile = File("src/test/resources/scripts/SampleBlueprintFunctionNode.kts") + + val functionNode = BluePrintScriptService() + .scriptClassNewInstance>(scriptFile, + "SampleBlueprintFunctionNode") + assertNotNull(functionNode, "failed to get instance from script") + } +} \ No newline at end of file diff --git a/components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/utils/JacksonReactorUtilsTest.kt b/components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/utils/JacksonReactorUtilsTest.kt deleted file mode 100644 index ad55c776..00000000 --- a/components/core/src/test/kotlin/org/onap/ccsdk/apps/controllerblueprints/core/utils/JacksonReactorUtilsTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright © 2017-2018 AT&T Intellectual Property. - * - * Licensed 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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.onap.ccsdk.apps.controllerblueprints.core.utils - -import com.att.eelf.configuration.EELFLogger -import com.att.eelf.configuration.EELFManager -import org.junit.Test -import org.onap.ccsdk.apps.controllerblueprints.core.BluePrintException -import org.onap.ccsdk.apps.controllerblueprints.core.data.ServiceTemplate -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -@Deprecated("Reactor will be replacecd by coroutines by default.") -class JacksonReactorUtilsTest { - private val log: EELFLogger = EELFManager.getInstance().getLogger(this::class.toString()) - @Test - fun testReadValues() { - - val serviceTemplate = JacksonReactorUtils.readValueFromFile("./../model-catalog/blueprint-model/starter-blueprint/baseconfiguration/Definitions/activation-blueprint.json", - ServiceTemplate::class.java).block() - - assertNotNull(serviceTemplate, "Failed to simple transform Service Template") - assertEquals(true, serviceTemplate is ServiceTemplate, "failed to get Service Template instance") - - val jsonContent = JacksonReactorUtils.getJson(serviceTemplate, true).block() - assertNotNull(jsonContent, "Failed to get json content") - - val jsonNode = JacksonReactorUtils.jsonNodeFromFile("./../model-catalog/blueprint-model/starter-blueprint/baseconfiguration/Definitions/activation-blueprint.json") - .block() - assertNotNull(jsonContent, "Failed to get json Node") - } - - @Test(expected = BluePrintException::class) - fun testReadValuesFailure() { - JacksonReactorUtils.jsonNodeFromFile("load/blueprints/not-found.json") - .block() - } -} \ No newline at end of file diff --git a/components/core/src/test/resources/scripts/SampleBlueprintFunctionNode.kts b/components/core/src/test/resources/scripts/SampleBlueprintFunctionNode.kts new file mode 100644 index 00000000..44cc957d --- /dev/null +++ b/components/core/src/test/resources/scripts/SampleBlueprintFunctionNode.kts @@ -0,0 +1,44 @@ +/* + * Copyright © 2017-2018 AT&T Intellectual Property. + * + * Licensed 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.onap.ccsdk.apps.controllerblueprints.core.interfaces.BlueprintFunctionNode + +open class SampleBlueprintFunctionNode : BlueprintFunctionNode{ + + override fun getName(): String { + return "Kotlin-Script-Function-Node" + } + + override fun prepareRequest(executionRequest: String): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun process(executionRequest: String) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun recover(runtimeException: RuntimeException, executionRequest: String) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun prepareResponse(): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun apply(t: String): String { + return "$t-status" + } +} \ No newline at end of file diff --git a/components/model-catalog/definition-type/starter-type/artifact_type/artifact-script-kotlin.json b/components/model-catalog/definition-type/starter-type/artifact_type/artifact-script-kotlin.json new file mode 100644 index 00000000..cf19ac61 --- /dev/null +++ b/components/model-catalog/definition-type/starter-type/artifact_type/artifact-script-kotlin.json @@ -0,0 +1,8 @@ +{ + "description": "Kotlin Script file", + "version": "1.0.0", + "file_ext": [ + "kts" + ], + "derived_from": "tosca.artifacts.Implementation" +} \ No newline at end of file diff --git a/components/model-catalog/definition-type/starter-type/node_type/source-capability.json b/components/model-catalog/definition-type/starter-type/node_type/source-capability.json index 241b6995..e4eb90e7 100644 --- a/components/model-catalog/definition-type/starter-type/node_type/source-capability.json +++ b/components/model-catalog/definition-type/starter-type/node_type/source-capability.json @@ -5,10 +5,11 @@ "type": { "required": true, "type": "string", - "default": "JAVA-COMPONENT", + "default": "KOTLIN-COMPONENT", "constraints": [ { "valid_values": [ + "KOTLIN-COMPONENT", "JAVA-COMPONENT", "JYTHON-COMPONENT" ] diff --git a/components/model-catalog/definition-type/starter-type/node_type/tosca.nodes.component.Kotlin.json b/components/model-catalog/definition-type/starter-type/node_type/tosca.nodes.component.Kotlin.json new file mode 100644 index 00000000..381ed59e --- /dev/null +++ b/components/model-catalog/definition-type/starter-type/node_type/tosca.nodes.component.Kotlin.json @@ -0,0 +1,5 @@ +{ + "description": "This is Kotlin Component", + "version": "1.0.0", + "derived_from": "tosca.nodes.Root" +} \ No newline at end of file diff --git a/components/parent/pom.xml b/components/parent/pom.xml index c9da403c..8ecb2c1a 100644 --- a/components/parent/pom.xml +++ b/components/parent/pom.xml @@ -31,13 +31,13 @@ 2.1.1.RELEASE 5.1.3.RELEASE - 1.3.10 - 1.3.10 - 1.0.1 - 1.16.1 + 1.3.11 + 1.3.11 + 1.1.0 + 1.17.1 3.6.1 1.0.0 - 26.0-jre + 27.0.1-jre 2.9.2 1.4.197 1.2.2 @@ -77,6 +77,7 @@ ${springfox.swagger2.version} + org.apache.commons commons-lang3 @@ -114,6 +115,26 @@ kotlin-stdlib ${kotlin.version} + + org.jetbrains.kotlin + kotlin-scripting-jvm-host + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-compiler-embeddable + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-script-util + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-script-runtime + ${kotlin.version} + org.jetbrains.kotlinx kotlinx-coroutines-core @@ -201,6 +222,12 @@ ${kotlin.version} test + + org.jetbrains.kotlinx + kotlinx-coroutines-test + ${kotlin.couroutines.version} + test + io.grpc grpc-testing @@ -255,6 +282,10 @@ org.jetbrains.kotlin kotlin-stdlib + + org.jetbrains.kotlin + kotlin-script-util + org.jetbrains.kotlin kotlin-stdlib-jdk8 @@ -271,6 +302,10 @@ com.fasterxml.jackson.module jackson-module-kotlin + + org.jetbrains.kotlin + kotlin-scripting-jvm-host + io.grpc -- cgit 1.2.3-korg