in src/Bicep.LangServer/Handlers/ImportKubernetesManifestHandler.cs [24:239]
public record ImportKubernetesManifestRequest(string ManifestFilePath)
: IRequest<ImportKubernetesManifestResponse>;
public record ImportKubernetesManifestResponse(string? BicepFilePath);
public class ImportKubernetesManifestHandler(
ILanguageServerFacade server,
ITelemetryProvider telemetryProvider,
IConfigurationManager configurationManager) : IJsonRpcRequestHandler<ImportKubernetesManifestRequest, ImportKubernetesManifestResponse>
{
private readonly TelemetryAndErrorHandlingHelper<ImportKubernetesManifestResponse> helper = new(server.Window, telemetryProvider);
public Task<ImportKubernetesManifestResponse> Handle(ImportKubernetesManifestRequest request, CancellationToken cancellationToken)
=> helper.ExecuteWithTelemetryAndErrorHandling(async () =>
{
var bicepFilePath = Path.ChangeExtension(request.ManifestFilePath, ".bicep");
var manifestContents = await File.ReadAllTextAsync(request.ManifestFilePath);
var bicepContents = this.Decompile(bicepFilePath, manifestContents, this.helper);
await File.WriteAllTextAsync(bicepFilePath, bicepContents, cancellationToken);
return new(new(bicepFilePath), BicepTelemetryEvent.ImportKubernetesManifestSuccess());
});
public string Decompile(string bicepFilePath, string manifestContents, TelemetryAndErrorHandlingHelper<ImportKubernetesManifestResponse> telemetryHelper)
{
var declarations = new List<SyntaxBase>
{
new ParameterDeclarationSyntax(
[
SyntaxFactory.CreateDecorator("secure"),
SyntaxFactory.NewlineToken,
],
SyntaxFactory.ParameterKeywordToken,
SyntaxFactory.CreateIdentifierWithTrailingSpace("kubeConfig"),
new VariableAccessSyntax(new(SyntaxFactory.CreateIdentifierToken("string"))),
null),
new ExtensionDeclarationSyntax(
[],
SyntaxFactory.ExtensionKeywordToken,
SyntaxFactory.CreateIdentifierWithTrailingSpace(K8sNamespaceType.BuiltInName),
new ExtensionWithClauseSyntax(
SyntaxFactory.CreateIdentifierToken(LanguageConstants.WithKeyword),
SyntaxFactory.CreateObject(
[
SyntaxFactory.CreateObjectProperty("namespace", SyntaxFactory.CreateStringLiteral("default")),
SyntaxFactory.CreateObjectProperty("kubeConfig", SyntaxFactory.CreateIdentifier("kubeConfig"))
])),
asClause: SyntaxFactory.EmptySkippedTrivia)
};
try
{
var reader = new StringReader(manifestContents);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
foreach (var yamlDocument in yamlStream.Documents)
{
var syntax = ProcessResourceYaml(yamlDocument, telemetryHelper);
declarations.Add(syntax);
}
}
catch (Exception ex)
{
Trace.TraceError("Exception deserializing manifest: {0}", ex);
throw telemetryHelper.CreateException(
$"Failed to deserialize kubernetes manifest YAML: {ex.Message}",
BicepTelemetryEvent.ImportKubernetesManifestFailure("DeserializeYamlFailed"),
new ImportKubernetesManifestResponse(null));
}
var program = new ProgramSyntax(
declarations.SelectMany(x => new SyntaxBase[] { x, SyntaxFactory.DoubleNewlineToken }),
SyntaxFactory.CreateToken(TokenType.EndOfFile));
var configuration = configurationManager.GetConfiguration(PathHelper.FilePathToFileUrl(bicepFilePath));
var printerOptions = configuration.Formatting.Data;
var printerContext = PrettyPrinterV2Context.Create(printerOptions, EmptyDiagnosticLookup.Instance, EmptyDiagnosticLookup.Instance);
return PrettyPrinterV2.Print(program, printerContext);
}
private static ResourceDeclarationSyntax ProcessResourceYaml(YamlDocument yamlDocument, TelemetryAndErrorHandlingHelper<ImportKubernetesManifestResponse> telemetryHelper)
{
if (yamlDocument.RootNode is not YamlMappingNode rootNode)
{
throw new YamlException(yamlDocument.RootNode.Start, yamlDocument.RootNode.End, $"Expected dictionary node.");
}
var kindKey = rootNode.Children.Keys.FirstOrDefault(x => x is YamlScalarNode scalar && scalar.Value == "kind");
var apiVersionKey = rootNode.Children.Keys.FirstOrDefault(x => x is YamlScalarNode scalar && scalar.Value == "apiVersion");
if (kindKey is null || apiVersionKey is null)
{
throw new YamlException(rootNode.Start, rootNode.End, $"Failed to find 'kind' and 'apiVersion' keys for resource declaration.");
}
if (rootNode.Children[kindKey] is not YamlScalarNode kindNode)
{
throw new YamlException(kindKey.Start, kindKey.End, "Unable to process 'kind' for resource declaration.");
}
if (rootNode.Children[apiVersionKey] is not YamlScalarNode apiVersionNode)
{
throw new YamlException(apiVersionKey.Start, apiVersionKey.End, "Unable to process 'apiVersion' for resource declaration.");
}
var (type, apiVersion) = apiVersionNode.Value.LastIndexOf('/') switch
{
-1 => ($"core/{kindNode.Value}", apiVersionNode.Value),
int x => ($"{apiVersionNode.Value.Substring(0, x)}/{kindNode.Value}", apiVersionNode.Value.Substring(x + 1)),
};
var filteredChildren = rootNode.Children.Where(x => x.Key != kindKey && x.Key != apiVersionKey);
var resourceBody = ConvertObjectChildren(filteredChildren);
var symbolName = GetResourceSymbolName(type, resourceBody);
return new ResourceDeclarationSyntax(
[],
SyntaxFactory.ResourceKeywordToken,
SyntaxFactory.CreateIdentifierWithTrailingSpace(symbolName),
SyntaxFactory.CreateStringLiteral($"{type}@{apiVersion}"),
null,
SyntaxFactory.AssignmentToken,
[],
resourceBody);
}
private static string GetResourceSymbolName(string type, SyntaxBase resourceBody)
{
var identifier = type;
if ((resourceBody as ObjectSyntax)?.TryGetPropertyByNameRecursive("metadata", "name") is { } nameProperty &&
(nameProperty.Value as StringSyntax)?.TryGetLiteralValue() is { } nameString)
{
identifier = $"{type}_{nameString}";
}
var identifierBuilder = new StringBuilder();
var capitalizeNext = false;
for (var i = 0; i < identifier.Length; i++)
{
var c = identifier[i];
var isValidChar =
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(i > 0 && c >= '0' && c <= '9') ||
(i > 0 && c == '_');
if (capitalizeNext && (c >= 'a' && c <= 'z'))
{
// ASCII codes for lc and uppercase chars are 32 apart.
// Subtract 32 from the ASCII code to convert to uppercase.
c -= (char)32;
}
if (isValidChar)
{
identifierBuilder.Append(c);
}
capitalizeNext = !isValidChar;
}
return identifierBuilder.ToString();
}
private static SyntaxBase ConvertValue(YamlNode value)
{
switch (value)
{
case YamlMappingNode dictValue:
return ConvertObjectChildren(dictValue.Children);
case YamlSequenceNode listValue:
var items = listValue.Children.Select(ConvertValue);
return SyntaxFactory.CreateArray(items);
case YamlScalarNode scalarNode:
if (scalarNode.Style == SharpYaml.ScalarStyle.Plain)
{
// If the user hasn't provided quotes, there's no way to differentiate between strings, ints & bools. We have to guess...
if (bool.TryParse(scalarNode.Value, out var boolVal))
{
return SyntaxFactory.CreateBooleanLiteral(boolVal);
}
if (long.TryParse(scalarNode.Value, out var longVal))
{
return SyntaxFactory.CreatePositiveOrNegativeInteger(longVal);
}
}
return SyntaxFactory.CreateStringLiteral(scalarNode.Value);
default:
throw new InvalidOperationException($"Unsupported type {value.GetType()}");
}
}
private static ObjectSyntax ConvertObjectChildren(IEnumerable<KeyValuePair<YamlNode, YamlNode>> children)
{
var objectProperties = new List<ObjectPropertySyntax>();
foreach (var kvp in children)
{
if (kvp.Key is not YamlScalarNode keyNode)
{
throw new InvalidOperationException($"Unsupported object key {kvp.Key.GetType()}");
}
var objectProperty = SyntaxFactory.CreateObjectProperty(keyNode.Value, ConvertValue(kvp.Value));
objectProperties.Add(objectProperty);
}
return SyntaxFactory.CreateObject(objectProperties);
}
}