src/Cli/func/Actions/LocalActions/InitAction.cs (539 lines of code) (raw):

// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Extensions; using Azure.Functions.Cli.Helpers; using Azure.Functions.Cli.Interfaces; using Azure.Functions.Cli.StacksApi; using Colors.Net; using Fclp; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Actions.LocalActions { [Action(Name = "init", HelpText = "Create a new Function App in the current folder. Initializes git repo.")] internal class InitAction : BaseAction { // Default to .NET 8 if the target framework is not specified private const string DefaultTargetFramework = Common.TargetFramework.Net8; private const string DefaultInProcTargetFramework = Common.TargetFramework.Net6; private readonly ITemplatesManager _templatesManager; private readonly ISecretsManager _secretsManager; internal static readonly Dictionary<Lazy<string>, Task<string>> FileToContentMap = new Dictionary<Lazy<string>, Task<string>> { { new Lazy<string>(() => ".gitignore"), StaticResources.GitIgnore } }; public InitAction(ITemplatesManager templatesManager, ISecretsManager secretsManager) { _templatesManager = templatesManager; _secretsManager = secretsManager; } public SourceControl SourceControl { get; set; } = SourceControl.Git; public bool InitSourceControl { get; set; } public bool InitDocker { get; set; } public bool InitDockerOnly { get; set; } public string WorkerRuntime { get; set; } public string FolderName { get; set; } = string.Empty; public bool Force { get; set; } public bool Csx { get; set; } public bool ExtensionBundle { get; set; } = true; public bool GeneratePythonDocumentation { get; set; } = true; public string Language { get; set; } public string TargetFramework { get; set; } public bool? ManagedDependencies { get; set; } public string ProgrammingModel { get; set; } public bool SkipNpmInstall { get; set; } = false; public WorkerRuntime ResolvedWorkerRuntime { get; set; } public string ResolvedLanguage { get; set; } public ProgrammingModel ResolvedProgrammingModel { get; set; } public override ICommandLineParserResult ParseArgs(string[] args) { Parser .Setup<bool>("source-control") .SetDefault(false) .WithDescription("Run git init. Default is false.") .Callback(f => InitSourceControl = f); Parser .Setup<string>("worker-runtime") .SetDefault(null) .WithDescription($"Runtime framework for the functions. Options are: {WorkerRuntimeLanguageHelper.AvailableWorkersRuntimeString}") .Callback(w => WorkerRuntime = w); Parser .Setup<bool>("force") .WithDescription("Force initializing") .Callback(f => Force = f); Parser .Setup<bool>("docker") .WithDescription("Create a Dockerfile based on the selected worker runtime") .Callback(f => InitDocker = f); Parser .Setup<bool>("docker-only") .WithDescription("Adds a Dockerfile to an existing function app project. Will prompt for worker-runtime if not specified or set in local.settings.json") .Callback(f => { InitDocker = f; InitDockerOnly = f; }); Parser .Setup<bool>("csx") .WithDescription("use csx dotnet functions") .Callback(f => Csx = f); Parser .Setup<string>("language") .SetDefault(null) .WithDescription("Initialize a language specific project. Currently supported when --worker-runtime set to node. Options are - \"typescript\" and \"javascript\"") .Callback(l => Language = l); Parser .Setup<string>("target-framework") .WithDescription($"Initialize a project with the given target framework moniker. Currently supported only when --worker-runtime set to dotnet-isolated or dotnet. Options are - {string.Join(", ", TargetFrameworkHelper.GetSupportedTargetFrameworks())}") .Callback(tf => TargetFramework = tf); Parser .Setup<bool>("managed-dependencies") .WithDescription("Installs managed dependencies. Currently, only the PowerShell worker runtime supports this functionality.") .Callback(f => ManagedDependencies = f); Parser .Setup<string>('m', "model") .WithDescription($"Selects the programming model for the function app. Note this flag is now only applicable to Python and JavaScript/TypeScript. Options are V1 and V2 for Python; V3 and V4 for JavaScript/TypeScript. Currently, the V2 and V4 programming models are in preview.") .Callback(m => ProgrammingModel = m); Parser .Setup<bool>("skip-npm-install") .WithDescription("Skips the npm installation phase when using V4 programming model for NodeJS") .Callback(skip => SkipNpmInstall = skip); Parser .Setup<bool>("no-bundle") .Callback(e => ExtensionBundle = !e); Parser .Setup<bool>("no-docs") .WithDescription("Do not create getting started documentation file. Currently supported when --worker-runtime set to python.") .Callback(d => GeneratePythonDocumentation = !d); if (args.Any() && !args.First().StartsWith("-")) { FolderName = args.First(); } return base.ParseArgs(args); } public override async Task RunAsync() { if (SourceControl != SourceControl.Git) { throw new Exception("Only Git is supported right now for vsc"); } if (!string.IsNullOrEmpty(FolderName)) { var folderPath = Path.Combine(Environment.CurrentDirectory, FolderName); FileSystemHelpers.EnsureDirectory(folderPath); Environment.CurrentDirectory = folderPath; } if (InitDockerOnly) { await InitDockerFileOnly(); } else { await InitFunctionAppProject(); } } private async Task InitDockerFileOnly() { await WriteDockerfile(GlobalCoreToolsSettings.CurrentWorkerRuntime, Language, TargetFramework, Csx); } private async Task InitFunctionAppProject() { if (Csx) { ResolvedWorkerRuntime = Helpers.WorkerRuntime.Dotnet; } else { (ResolvedWorkerRuntime, ResolvedLanguage) = ResolveWorkerRuntimeAndLanguage(WorkerRuntime, Language); // Order here is important: each language may have multiple runtimes, and each unique (language, worker-runtime) pair // may have its own programming model. Thus, we assume that ResolvedLanguage and ResolvedWorkerRuntime are properly set // before attempting to resolve the programming model. var supportedProgrammingModels = ProgrammingModelHelper.GetSupportedProgrammingModels(ResolvedWorkerRuntime); ResolvedProgrammingModel = ProgrammingModelHelper.ResolveProgrammingModel(ProgrammingModel, ResolvedWorkerRuntime, ResolvedLanguage); if (!supportedProgrammingModels.Contains(ResolvedProgrammingModel)) { throw new CliArgumentsException( $"The {ResolvedProgrammingModel.GetDisplayString()} programming model is not supported for worker runtime {ResolvedWorkerRuntime.GetDisplayString()}. Supported programming models for worker runtime {ResolvedWorkerRuntime.GetDisplayString()} are:\n{EnumerationHelper.Join("\n", supportedProgrammingModels)}"); } } TelemetryHelpers.AddCommandEventToDictionary(TelemetryCommandEvents, "WorkerRuntime", ResolvedWorkerRuntime.ToString()); ValidateTargetFramework(); if (WorkerRuntimeLanguageHelper.IsDotnet(ResolvedWorkerRuntime) && !Csx) { await ShowEolMessage(); await DotnetHelpers.DeployDotnetProject(Utilities.SanitizeLiteral(Path.GetFileName(Environment.CurrentDirectory), allowed: "-"), Force, ResolvedWorkerRuntime, TargetFramework); } else { bool managedDependenciesOption = ResolveManagedDependencies(ResolvedWorkerRuntime, ManagedDependencies); await InitLanguageSpecificArtifacts(ResolvedWorkerRuntime, ResolvedLanguage, ResolvedProgrammingModel, managedDependenciesOption, GeneratePythonDocumentation); await WriteFiles(); await WriteHostJson(ResolvedWorkerRuntime, managedDependenciesOption, ExtensionBundle); await WriteLocalSettingsJson(ResolvedWorkerRuntime, ResolvedProgrammingModel); } await WriteExtensionsJson(); if (InitSourceControl) { await SetupSourceControl(); } if (InitDocker) { await WriteDockerfile(ResolvedWorkerRuntime, ResolvedLanguage, TargetFramework, Csx); } if (!SkipNpmInstall) { await FetchPackages(ResolvedWorkerRuntime, ResolvedProgrammingModel); } else { ColoredConsole.Write(AdditionalInfoColor("You skipped \"npm install\". You must run \"npm install\" manually")); } } private static (WorkerRuntime WorkerRuntime, string WorkerLanguage) ResolveWorkerRuntimeAndLanguage(string workerRuntimeString, string languageString) { WorkerRuntime workerRuntime; string language; if (!string.IsNullOrEmpty(workerRuntimeString)) { workerRuntime = WorkerRuntimeLanguageHelper.NormalizeWorkerRuntime(workerRuntimeString); language = languageString ?? WorkerRuntimeLanguageHelper.NormalizeLanguage(workerRuntimeString); } else if (GlobalCoreToolsSettings.CurrentWorkerRuntimeOrNone == Helpers.WorkerRuntime.None) { SelectionMenuHelper.DisplaySelectionWizardPrompt("worker runtime"); IDictionary<WorkerRuntime, string> workerRuntimeToDisplayString = WorkerRuntimeLanguageHelper.GetWorkerToDisplayStrings(); string workerRuntimedisplay = SelectionMenuHelper.DisplaySelectionWizard(workerRuntimeToDisplayString.Values); workerRuntime = workerRuntimeToDisplayString.FirstOrDefault(wr => wr.Value.Equals(workerRuntimedisplay)).Key; ColoredConsole.WriteLine(TitleColor(WorkerRuntimeLanguageHelper.GetRuntimeMoniker(workerRuntime))); language = LanguageSelectionIfRelevant(workerRuntime); } else { workerRuntime = GlobalCoreToolsSettings.CurrentWorkerRuntime; language = GlobalCoreToolsSettings.CurrentLanguageOrNull ?? languageString ?? WorkerRuntimeLanguageHelper.NormalizeLanguage(WorkerRuntimeLanguageHelper.GetRuntimeMoniker(workerRuntime)); } return (workerRuntime, language); } private static string LanguageSelectionIfRelevant(WorkerRuntime workerRuntime) { if (workerRuntime == Helpers.WorkerRuntime.Node || workerRuntime == Helpers.WorkerRuntime.DotnetIsolated || workerRuntime == Helpers.WorkerRuntime.Dotnet) { if (WorkerRuntimeLanguageHelper.WorkerToSupportedLanguages.TryGetValue(workerRuntime, out IEnumerable<string> languages) && languages.Count() != 0) { SelectionMenuHelper.DisplaySelectionWizardPrompt("language"); var language = SelectionMenuHelper.DisplaySelectionWizard(languages); ColoredConsole.WriteLine(TitleColor(language)); return language; } } return string.Empty; } private static async Task InitLanguageSpecificArtifacts( WorkerRuntime workerRuntime, string language, ProgrammingModel programmingModel, bool managedDependenciesOption, bool generatePythonDocumentation = true) { switch (workerRuntime) { case Helpers.WorkerRuntime.Python: await PythonHelpers.SetupPythonProject(programmingModel, generatePythonDocumentation); break; case Helpers.WorkerRuntime.Powershell: await FileSystemHelpers.WriteFileIfNotExists("profile.ps1", await StaticResources.PowerShellProfilePs1); if (managedDependenciesOption) { var requirementsContent = await StaticResources.PowerShellRequirementsPsd1; bool majorVersionRetrievedSuccessfully = false; string guidance = null; try { var majorVersion = await PowerShellHelper.GetLatestAzModuleMajorVersion(); requirementsContent = Regex.Replace(requirementsContent, "MAJOR_VERSION", majorVersion); majorVersionRetrievedSuccessfully = true; } catch { guidance = "Uncomment the next line and replace the MAJOR_VERSION, e.g., 'Az' = '5.*'"; var warningMsg = "Failed to get Az module version. Edit the requirements.psd1 file when the powershellgallery.com is accessible."; ColoredConsole.WriteLine(WarningColor(warningMsg)); } if (majorVersionRetrievedSuccessfully) { guidance = Environment.NewLine + " # To use the Az module in your function app, please uncomment the line below."; } requirementsContent = Regex.Replace(requirementsContent, "GUIDANCE", guidance); await FileSystemHelpers.WriteFileIfNotExists("requirements.psd1", requirementsContent); } break; case Helpers.WorkerRuntime.Node: await NodeJSHelpers.SetupProject(programmingModel, language); break; } } private void ValidateTargetFramework() { if (string.IsNullOrEmpty(TargetFramework)) { if (ResolvedWorkerRuntime == Helpers.WorkerRuntime.DotnetIsolated) { TargetFramework = DefaultTargetFramework; } else if (ResolvedWorkerRuntime == Helpers.WorkerRuntime.Dotnet) { TargetFramework = DefaultInProcTargetFramework; } else { return; } } var supportedFrameworks = ResolvedWorkerRuntime == Helpers.WorkerRuntime.DotnetIsolated ? TargetFrameworkHelper.GetSupportedTargetFrameworks() : TargetFrameworkHelper.GetSupportedInProcTargetFrameworks(); if (!supportedFrameworks.Contains(TargetFramework, StringComparer.InvariantCultureIgnoreCase)) { throw new CliArgumentsException($"Unable to parse target framework {TargetFramework} for worker runtime {ResolvedWorkerRuntime.GetDisplayString()}. Valid options are {string.Join(", ", supportedFrameworks)}"); } else if (ResolvedWorkerRuntime != Helpers.WorkerRuntime.DotnetIsolated && ResolvedWorkerRuntime != Helpers.WorkerRuntime.Dotnet) { throw new CliArgumentsException("The --target-framework option is supported only when --worker-runtime is set to dotnet-isolated or dotnet"); } } private static async Task WriteLocalSettingsJson(WorkerRuntime workerRuntime, ProgrammingModel programmingModel) { var localSettingsJsonContent = await StaticResources.LocalSettingsJson; localSettingsJsonContent = localSettingsJsonContent.Replace($"{{{Constants.FunctionsWorkerRuntime}}}", WorkerRuntimeLanguageHelper.GetRuntimeMoniker(workerRuntime)); var storageConnectionStringValue = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Constants.StorageEmulatorConnectionString : string.Empty; localSettingsJsonContent = localSettingsJsonContent.Replace($"{{{Constants.AzureWebJobsStorage}}}", storageConnectionStringValue); if (workerRuntime == Helpers.WorkerRuntime.Powershell) { localSettingsJsonContent = AddLocalSetting(localSettingsJsonContent, Constants.FunctionsWorkerRuntimeVersion, Constants.PowerShellWorkerDefaultVersion); } await FileSystemHelpers.WriteFileIfNotExists("local.settings.json", localSettingsJsonContent); } private static async Task WriteDockerfile(WorkerRuntime workerRuntime, string language, string targetFramework, bool csx) { if (WorkerRuntimeLanguageHelper.IsDotnet(workerRuntime) && string.IsNullOrEmpty(targetFramework) && !csx) { var functionAppRoot = ScriptHostHelpers.GetFunctionAppRootDirectory(Environment.CurrentDirectory); if (functionAppRoot != null) { targetFramework = await DotnetHelpers.DetermineTargetFramework(functionAppRoot); } } if (workerRuntime == Helpers.WorkerRuntime.Dotnet) { if (csx) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileCsxDotNet); } else if (targetFramework == Common.TargetFramework.Net8) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileDotNet8); } else { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileDotNet); } } else if (workerRuntime == Helpers.WorkerRuntime.DotnetIsolated) { if (targetFramework == Common.TargetFramework.Net7) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileDotnet7Isolated); } else if (targetFramework == Common.TargetFramework.Net8) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileDotnet8Isolated); } else if (targetFramework == Common.TargetFramework.Net9) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileDotnet9Isolated); } else { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileDotnetIsolated); } } else if (workerRuntime == Helpers.WorkerRuntime.Node) { if (language == Constants.Languages.TypeScript) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileTypeScript); } else { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileJavaScript); } } else if (workerRuntime == Helpers.WorkerRuntime.Python) { await WritePythonDockerFile(); } else if (workerRuntime == Helpers.WorkerRuntime.Powershell) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfilePowershell72); } else if (workerRuntime == Helpers.WorkerRuntime.Custom) { await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await StaticResources.DockerfileCustom); } else if (workerRuntime == Helpers.WorkerRuntime.None) { throw new CliException("Can't find WorkerRuntime None"); } await FileSystemHelpers.WriteFileIfNotExists(".dockerignore", await StaticResources.DockerIgnoreFile); } private static async Task WritePythonDockerFile() { WorkerLanguageVersionInfo worker = await PythonHelpers.GetEnvironmentPythonVersion(); await FileSystemHelpers.WriteFileIfNotExists("Dockerfile", await PythonHelpers.GetDockerInitFileContent(worker)); } private static async Task WriteExtensionsJson() { var file = Path.Combine(Environment.CurrentDirectory, ".vscode", "extensions.json"); if (!FileSystemHelpers.DirectoryExists(Path.GetDirectoryName(file))) { FileSystemHelpers.CreateDirectory(Path.GetDirectoryName(file)); } await FileSystemHelpers.WriteFileIfNotExists(file, await StaticResources.VsCodeExtensionsJson); } private static async Task SetupSourceControl() { try { var checkGitRepoExe = new Executable("git", "rev-parse --git-dir"); var result = await checkGitRepoExe.RunAsync(); if (result != 0) { var exe = new Executable("git", $"init"); await exe.RunAsync(l => ColoredConsole.WriteLine(l), l => ColoredConsole.Error.WriteLine(l)); } else { ColoredConsole.WriteLine("Directory already a git repository."); } } catch (FileNotFoundException) { ColoredConsole.WriteLine(WarningColor("unable to find git on the path")); } } private static async Task WriteFiles() { foreach (var pair in FileToContentMap) { await FileSystemHelpers.WriteFileIfNotExists(pair.Key.Value, await pair.Value); } } private static bool ResolveManagedDependencies(WorkerRuntime workerRuntime, bool? managedDependenciesOption) { if (workerRuntime != Helpers.WorkerRuntime.Powershell) { if (managedDependenciesOption.HasValue) { throw new CliException("Managed dependencies is only supported for PowerShell."); } return false; } if (managedDependenciesOption.HasValue) { return managedDependenciesOption.Value; } return true; } private async Task WriteHostJson(WorkerRuntime workerRuntime, bool managedDependenciesOption, bool extensionBundle = true) { var hostJsonContent = await StaticResources.HostJson; if (workerRuntime == Helpers.WorkerRuntime.Powershell && managedDependenciesOption) { hostJsonContent = await hostJsonContent.AppendContent(Constants.ManagedDependencyConfigPropertyName, StaticResources.ManagedDependenciesConfig); } if (extensionBundle) { if (ResolvedProgrammingModel == Common.ProgrammingModel.V2 && ResolvedWorkerRuntime == Helpers.WorkerRuntime.Python) { hostJsonContent = await hostJsonContent.AppendContent(Constants.ExtensionBundleConfigPropertyName, StaticResources.BundleConfigPyStein); } else if (ResolvedProgrammingModel == Common.ProgrammingModel.V4 && ResolvedWorkerRuntime == Helpers.WorkerRuntime.Node) { hostJsonContent = await hostJsonContent.AppendContent(Constants.ExtensionBundleConfigPropertyName, StaticResources.BundleConfigNodeV4); } else { hostJsonContent = await hostJsonContent.AppendContent(Constants.ExtensionBundleConfigPropertyName, StaticResources.BundleConfig); } } if (workerRuntime == Helpers.WorkerRuntime.Custom) { hostJsonContent = await hostJsonContent.AppendContent(Constants.CustomHandlerPropertyName, StaticResources.CustomHandlerConfig); } await FileSystemHelpers.WriteFileIfNotExists(Constants.HostJsonFileName, hostJsonContent); } private static string AddLocalSetting(string localSettingsContent, string key, string value) { var localSettingsObj = JsonConvert.DeserializeObject<JObject>(localSettingsContent); if (localSettingsObj.TryGetValue("Values", StringComparison.OrdinalIgnoreCase, out var valuesContent)) { var values = valuesContent as JObject; values.Property(Constants.FunctionsWorkerRuntime).AddAfterSelf( new JProperty(key, value)); } return JsonConvert.SerializeObject(localSettingsObj, Formatting.Indented); } public async Task FetchPackages(WorkerRuntime workerRuntime, ProgrammingModel programmingModel) { if (workerRuntime == Helpers.WorkerRuntime.Node && programmingModel == Common.ProgrammingModel.V4) { try { await NpmHelper.Install(); } catch (Exception) { Console.Error.WriteLine(WarningColor("Warning: You must run \"npm install\" manually")); } } } private async Task ShowEolMessage() { try { if (!WorkerRuntimeLanguageHelper.IsDotnetIsolated(ResolvedWorkerRuntime) || TargetFramework == DefaultTargetFramework) { return; } var majorDotnetVersion = StacksApiHelper.GetMajorDotnetVersionFromDotnetVersionInProject(TargetFramework); if (majorDotnetVersion == null) { return; } var stacksContent = await StaticResources.StacksJson; var stacks = JsonConvert.DeserializeObject<FunctionsStacks>(stacksContent); var currentRuntimeSettings = stacks.GetRuntimeSettings(majorDotnetVersion.Value, out bool isLTS); if (currentRuntimeSettings == null) { return; } if (currentRuntimeSettings.IsDeprecated == true || currentRuntimeSettings.IsDeprecatedForRuntime == true) { var warningMessage = EolMessages.GetAfterEolCreateMessageDotNet(majorDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value); ColoredConsole.WriteLine(WarningColor(warningMessage)); } else if (StacksApiHelper.IsInNextSixMonths(currentRuntimeSettings.EndOfLifeDate)) { var warningMessage = EolMessages.GetEarlyEolCreateMessageForDotNet(majorDotnetVersion.ToString(), currentRuntimeSettings.EndOfLifeDate.Value); ColoredConsole.WriteLine(WarningColor(warningMessage)); } } catch (Exception) { // ignore. Failure to show the EOL message should not fail the init command. } } } }