public void NewCSharpFunction(
            Settings settings,
            string functionName,
            string rootNamespace,
            string framework,
            string workingDirectory,
            string moduleFile,
            int functionMemory,
            int functionTimeout,
            string projectDirectory,
            FunctionType functionType
            )
        {
            if (functionName == "Finalizer")
            {
                // always of type finalizer
                functionType    = FunctionType.Finalizer;
                functionTimeout = 900;
            }
            else if (functionType == FunctionType.Unknown)
            {
                // prompt for function type
                functionType = Enum.Parse <FunctionType>(settings.PromptChoice("Select function type", _functionTypes), ignoreCase: true);
            }

            // create function project
            var isNetCore31OrLater = VersionInfoCompatibility.IsNetCore3OrLater(framework);
            var projectFile        = Path.Combine(projectDirectory, functionName + ".csproj");
            var substitutions      = new Dictionary <string, string> {
                ["FRAMEWORK"]           = framework,
                ["ROOTNAMESPACE"]       = rootNamespace,
                ["LAMBDASHARP_VERSION"] = VersionInfoCompatibility.GetLambdaSharpAssemblyWildcardVersion(settings.ToolVersion, framework)
            };

            try {
                var projectContents = ReadResource($"NewCSharpFunction-{functionType}.xml", substitutions);
                File.WriteAllText(projectFile, projectContents);
                Console.WriteLine($"Created project file: {Path.GetRelativePath(Directory.GetCurrentDirectory(), projectFile)}");
            } catch (Exception e) {
                LogError($"unable to create project file '{projectFile}'", e);
                return;
            }

            // create function source code
            var functionFile     = Path.Combine(projectDirectory, "Function.cs");
            var functionContents = ReadResource($"NewCSharpFunction-{functionType}.txt", substitutions);

            try {
                File.WriteAllText(functionFile, functionContents);
                Console.WriteLine($"Created function file: {Path.GetRelativePath(Directory.GetCurrentDirectory(), functionFile)}");
            } catch (Exception e) {
                LogError($"unable to create function file '{functionFile}'", e);
                return;
            }
        }
Exemple #2
0
        public bool ValidateLambdaSharpPackageReferences(VersionInfo toolVersion, Action <string>?logWarn, Action <string>?logError)
        {
            var success  = true;
            var includes = PackageReferences.Where(elem => elem.Attribute("Include")?.Value.StartsWith("LambdaSharp", StringComparison.Ordinal) ?? false);

            foreach (var include in includes)
            {
                var expectedVersion    = VersionInfoCompatibility.GetLambdaSharpAssemblyWildcardVersion(toolVersion, TargetFramework);
                var library            = include.Attribute("Include").Value;
                var libraryVersionText = include.Attribute("Version")?.Value;
                if (libraryVersionText == null)
                {
                    success = false;
                    logError?.Invoke($"csproj file is missing a version attribute in its assembly reference for {library} (expected version: '{expectedVersion}')");
                }
                else
                {
                    try {
                        if (!VersionInfoCompatibility.IsValidLambdaSharpAssemblyReferenceForToolVersion(toolVersion, TargetFramework, libraryVersionText, out var outdated))
                        {
                            // check if we're compiling a conditional package reference in contributor mode
                            if ((include.Attribute("Condition")?.Value != null) && (Environment.GetEnvironmentVariable("LAMBDASHARP") != null))
                            {
                                // show error as warning instead since this package reference will not be used anyway
                                logWarn?.Invoke($"csproj file contains a mismatched assembly reference for {library} (expected version: '{expectedVersion}', found: '{libraryVersionText}')");
                            }
                            else
                            {
                                success = false;
                                logError?.Invoke($"csproj file contains a mismatched assembly reference for {library} (expected version: '{expectedVersion}', found: '{libraryVersionText}')");
                            }
                        }
                        else if (outdated)
                        {
                            // show warning to updated dependency
                            logWarn?.Invoke($"csproj file contains outdated assembly reference for {library} (expected version: '{expectedVersion}', found: '{libraryVersionText}')");
                        }
                    } catch (VersionInfoCompatibilityUnsupportedFrameworkException) {
                        success = false;
                        logError?.Invoke($"csproj file targets unsupported framework '{TargetFramework}'");
                    } catch {
                        success = false;
                        logError?.Invoke($"csproj file contains an invalid wildcard version in its assembly reference for {library} (expected version: '{expectedVersion}', found: '{libraryVersionText}')");
                    }
                }
            }
            return(success);
        }
        //--- Methods ---
        public void Build(
            IFunction function,
            bool noCompile,
            bool noAssemblyValidation,
            string gitSha,
            string gitBranch,
            string buildConfiguration,
            bool forceBuild
            )
        {
            // collect sources with invoke methods
            var mappings = ExtractMappings(function);

            if (mappings == null)
            {
                // nothing to log since error was already logged
                return;
            }

            // check if a function package already exists
            if (!forceBuild)
            {
                var functionPackage = Provider.ExistingPackages.FirstOrDefault(p =>
                                                                               Path.GetFileName(p).StartsWith($"function_{Provider.ModuleFullName}_{function.LogicalId}_", StringComparison.Ordinal) &&
                                                                               p.EndsWith(".zip", StringComparison.Ordinal)
                                                                               );

                // to skip the build, we both need the function package and the function schema when mappings are present
                var schemaFile = Path.Combine(Provider.OutputDirectory, $"functionschema_{Provider.ModuleFullName}_{function.LogicalId}.json");
                if ((functionPackage != null) && (!mappings.Any() || File.Exists(schemaFile)))
                {
                    LogInfoVerbose($"=> Analyzing function {function.FullName} dependencies");

                    // find all files used to create the function package
                    var files = new HashSet <string>();
                    CSharpProjectFile.DiscoverDependencies(
                        files,
                        function.Project,
                        filePath => LogInfoVerbose($"... analyzing {filePath}"),
                        (message, exception) => LogError(message, exception)
                        );

                    // check if any of the files has been modified more recently than the function package
                    var functionPackageDate = File.GetLastWriteTime(functionPackage);
                    var file = files.FirstOrDefault(f => File.GetLastWriteTime(f) > functionPackageDate);
                    if (file == null)
                    {
                        var success = true;
                        if (mappings.Any())
                        {
                            // apply function schema to generate REST API and WebSocket models
                            try {
                                if (!ApplyInvocationSchemas(function, mappings, schemaFile, silent: true))
                                {
                                    success = false;

                                    // reset the mappings as the call to ApplyInvocationSchemas() may have modified them
                                    mappings = ExtractMappings(function);
                                    if (mappings == null)
                                    {
                                        // nothing to log since error was already logged
                                        return;
                                    }
                                }
                            } catch (Exception e) {
                                LogError("unable to read create-invoke-methods-schema output", e);
                                return;
                            }

                            // check if the mappings have changed by comparing the new data-structure to the one inside the zip file
                            if (success)
                            {
                                var newMappingsJson = JsonSerializer.Serialize(new ApiGatewayInvocationMappings {
                                    Mappings = mappings
                                }, _jsonOptions);
                                using (var zipArchive = ZipFile.Open(functionPackage, ZipArchiveMode.Read)) {
                                    var entry = zipArchive.Entries.FirstOrDefault(entry => entry.FullName == API_MAPPINGS);
                                    if (entry != null)
                                    {
                                        using (var stream = entry.Open())
                                            using (var reader = new StreamReader(stream)) {
                                                if (newMappingsJson != reader.ReadToEnd())
                                                {
                                                    // module mappings have change
                                                    success = false;
                                                    LogInfoVerbose($"... api mappings updated");
                                                }
                                            }
                                    }
                                    else
                                    {
                                        // we now have mappings and we didn't use to
                                        success = false;
                                        LogInfoVerbose($"... api mappings updated");
                                    }
                                }
                            }
                        }

                        // only skip compilation if we were able to apply the invocation schemas (or didn't have to)
                        if (success)
                        {
                            Provider.WriteLine($"=> Skipping function {Provider.InfoColor}{function.FullName}{Provider.ResetColor} (no changes found)");

                            // keep the existing package
                            Provider.ExistingPackages.Remove(functionPackage);

                            // set the module variable to the final package name
                            Provider.AddArtifact($"{function.FullName}::PackageName", functionPackage);
                            return;
                        }
                    }
                    else
                    {
                        LogInfoVerbose($"... change detected in {file}");
                    }
                }
            }
            else
            {
                LogInfoVerbose($"=> Analyzing function {function.FullName} dependencies");

                // find all files used to create the function package
                var files = new HashSet <string>();
                CSharpProjectFile.DiscoverDependencies(
                    files,
                    function.Project,
                    filePath => LogInfoVerbose($"... analyzing {filePath}"),
                    (message, exception) => LogError(message, exception)
                    );

                // loop over all project folders
                new CleanBuildFolders(BuildEventsConfig).Do(files);
            }

            // read settings from project file
            var projectFile = new CSharpProjectFile(function.Project);

            // compile function project
            var isReadyToRunSupported = VersionInfoCompatibility.IsReadyToRunSupported(projectFile.TargetFramework);
            var isAmazonLinux2        = Provider.IsAmazonLinux2();
            var isReadyToRun          = isReadyToRunSupported && isAmazonLinux2;
            var isSelfContained       = (projectFile.OutputType == "Exe") && (projectFile.AssemblyName == "bootstrap");
            var isTopLevelMain        = !isSelfContained && (projectFile.OutputType == "Exe");
            var readyToRunText        = isReadyToRun ? ", ReadyToRun" : "";
            var selfContained         = isSelfContained ? ", SelfContained" : "";

            Provider.WriteLine($"=> Building function {Provider.InfoColor}{function.FullName}{Provider.ResetColor} [{projectFile.TargetFramework}, {buildConfiguration}{readyToRunText}{selfContained}]");
            var projectDirectory = Path.Combine(Provider.WorkingDirectory, Path.GetFileNameWithoutExtension(function.Project));

            // check if the project contains an obsolete AWS Lambda Tools extension: <DotNetCliToolReference Include="Amazon.Lambda.Tools"/>
            if (projectFile.RemoveAmazonLambdaToolsReference())
            {
                LogWarn($"removing obsolete AWS Lambda Tools extension from {Path.GetRelativePath(Provider.WorkingDirectory, function.Project)}");
                projectFile.Save(function.Project);
            }

            // validate the project is using the most recent lambdasharp assembly references
            if (
                !noAssemblyValidation &&
                function.HasAssemblyValidation &&
                !projectFile.ValidateLambdaSharpPackageReferences(Provider.ToolVersion, LogWarn, LogError)
                )
            {
                return;
            }
            if (noCompile)
            {
                return;
            }

            // build project with AWS dotnet CLI lambda tool
            if (!DotNetPublish(projectFile.TargetFramework, buildConfiguration, projectDirectory, forceBuild, isReadyToRunSupported, isAmazonLinux2, isReadyToRun, isSelfContained, out var publishFolder))
            {
                // nothing to do; error was already reported
                return;
            }

            // building a function with top-level statements also creates an ELF file we don't need
            if (isTopLevelMain)
            {
                var elfBinary = Path.Combine(publishFolder, Path.GetFileNameWithoutExtension(function.Project));
                try {
                    File.Delete(elfBinary);
                } catch (Exception e) {
                    // no harm in leaving the file; report error as a warning
                    LogWarn($"Unable to delete unnecessary ELF binary at '{elfBinary}' (Error: {e})");
                }
            }

            // check if the assembly entry-point needs to be validated
            if (function.HasHandlerValidation)
            {
                if (isSelfContained || isTopLevelMain)
                {
                    // nothing to do
                }
                else
                {
                    // verify the function handler can be found in the compiled assembly
                    if (function.Handler != null)
                    {
                        if (!ValidateEntryPoint(
                                publishFolder,
                                function.Handler
                                ))
                        {
                            return;
                        }
                    }
                }
            }

            // add api mappings JSON file(s)
            if (mappings.Any())
            {
                // self-contained assemblies cannot be inspected
                if (isSelfContained)
                {
                    LogError("API Gateway mappings are not supported for self-contained Lambda functions");
                    return;
                }

                // create request/response schemas for invocation methods
                if (!LambdaSharpCreateInvocationSchemas(
                        function,
                        publishFolder,
                        projectFile.RootNamespace,
                        function.Handler,
                        mappings
                        ))
                {
                    LogError($"'{Provider.Lash} util create-invoke-methods-schema' command failed");
                    return;
                }

                // write api-mappings.json file to publish folder
                File.WriteAllText(Path.Combine(publishFolder, API_MAPPINGS), JsonSerializer.Serialize(new ApiGatewayInvocationMappings {
                    Mappings = mappings
                }, _jsonOptions));
            }

            // compute hash o publish folder
            string hash;

            using (var md5 = MD5.Create())
                using (var hashStream = new CryptoStream(Stream.Null, md5, CryptoStreamMode.Write)) {
                    foreach (var publishedFile in Directory.GetFiles(publishFolder, "*", SearchOption.AllDirectories).OrderBy(filePath => filePath))
                    {
                        // hash file path
                        var filePathBytes = Encoding.UTF8.GetBytes(Path.GetRelativePath(publishFolder, publishedFile).Replace('\\', '/'));
                        hashStream.Write(filePathBytes, 0, filePathBytes.Length);

                        // hash file contents
                        using (var stream = File.OpenRead(publishedFile)) {
                            stream.CopyTo(hashStream);
                        }
                    }
                    hashStream.FlushFinalBlock();
                    hash = md5.Hash !.ToHexString();
                }

            // genereate function package with hash
            var package = Path.Combine(Provider.OutputDirectory, $"function_{Provider.ModuleFullName}_{function.LogicalId}_{hash}.zip");

            if (Provider.ExistingPackages.Remove(package))
            {
                // remove old, existing package so we can create the new package in the same location (which also preserves the more recent build timestamp)
                File.Delete(package);
            }

            // write git-info.json file to publish folder
            File.WriteAllText(Path.Combine(publishFolder, GIT_INFO_FILE), JsonSerializer.Serialize(new ModuleManifestGitInfo {
                SHA    = gitSha,
                Branch = gitBranch
            }, _jsonOptions));

            // zip files in publishing folder
            new ZipTool(BuildEventsConfig).ZipFolderWithExecutable(package, publishFolder);

            // set the module variable to the final package name
            Provider.AddArtifact($"{function.FullName}::PackageName", package);
        }
        public async Task <ModuleLocation> ResolveInfoToLocationAsync(
            ModuleInfo moduleInfo,
            string originBucketName,
            ModuleManifestDependencyType dependencyType,
            bool allowImport,
            bool showError,
            bool allowCaching = false
            )
        {
            if (originBucketName == null)
            {
                throw new ArgumentNullException(nameof(originBucketName));
            }
            LogInfoVerbose($"... resolving module {moduleInfo}");
            var stopwatch = Stopwatch.StartNew();
            var cached    = false;

            try {
                // check if a cached manifest matches
                var cachedDirectory = Path.Combine(Settings.GetOriginCacheDirectory(moduleInfo));
                if (allowCaching && Settings.AllowCaching && Directory.Exists(cachedDirectory))
                {
                    var foundCached = Directory.GetFiles(cachedDirectory)
                                      .Select(found => VersionInfo.Parse(Path.GetFileName(found)))
                                      .Where(version => (moduleInfo.Version == null) || version.IsGreaterOrEqualThanVersion(moduleInfo.Version, strict: true));

                    // NOTE (2019-08-12, bjorg): unless the module is shared, we filter the list of found versions to
                    //  only contain versions that meet the module version constraint; for shared modules, we want to
                    //  keep the latest version that is compatible with the tool and is equal-or-greater than the
                    //  module version constraint.
                    if ((dependencyType != ModuleManifestDependencyType.Shared) && (moduleInfo.Version != null))
                    {
                        foundCached = foundCached.Where(version => version.MatchesConstraint(moduleInfo.Version)).ToList();
                    }

                    // attempt to identify the newest module version compatible with the tool
                    ModuleManifest manifest = null;
                    var            match    = VersionInfo.FindLatestMatchingVersion(foundCached, moduleInfo.Version, candidate => {
                        var candidateManifestText = File.ReadAllText(Path.Combine(Settings.GetOriginCacheDirectory(moduleInfo), candidate.ToString()));
                        manifest = JsonConvert.DeserializeObject <ModuleManifest>(candidateManifestText);

                        // check if module is compatible with this tool
                        return(VersionInfoCompatibility.IsModuleCoreVersionCompatibleWithToolVersion(manifest.CoreServicesVersion, Settings.ToolVersion));
                    });
                    if (manifest != null)
                    {
                        cached = true;

                        // TODO (2019-10-08, bjorg): what source bucket name should be used for cached manifests?
                        return(MakeModuleLocation(Settings.DeploymentBucketName, manifest));
                    }
                }

                // check if module can be found in the deployment bucket
                var result = await FindNewestModuleVersionAsync(Settings.DeploymentBucketName);

                // check if the origin bucket needs to be checked
                if (
                    allowImport &&
                    (Settings.DeploymentBucketName != originBucketName) &&
                    (

                        // no version has been found
                        (result.Version == null)

                        // no module version constraint was given; the ultimate floating version
                        || (moduleInfo.Version == null)

                        // the module version constraint is for a pre-release; we always prefer the origin version then
                        || moduleInfo.Version.IsPreRelease()

                        // the module version constraint is floating; we need to check if origin has a newer version
                        || !moduleInfo.Version.Minor.HasValue ||
                        !moduleInfo.Version.Patch.HasValue
                    )
                    )
                {
                    var originResult = await FindNewestModuleVersionAsync(originBucketName);

                    // check if module found at origin should be kept instead
                    if (
                        (originResult.Version != null) &&
                        (
                            (result.Version == null) ||
                            (moduleInfo.Version?.IsPreRelease() ?? false) ||
                            originResult.Version.IsGreaterThanVersion(result.Version)
                        )
                        )
                    {
                        result = originResult;
                    }
                }

                // check if a module was found
                if (result.Version == null)
                {
                    // could not find a matching version
                    var versionConstraint = (moduleInfo.Version != null)
                        ? $"v{moduleInfo.Version} or later"
                        : "any released version";
                    if (showError)
                    {
                        if (allowImport)
                        {
                            LogError($"could not find module '{moduleInfo}' ({versionConstraint})");
                        }
                        else
                        {
                            LogError($"missing module dependency must be imported explicitly '{moduleInfo}' ({versionConstraint})");
                        }
                    }
                    return(null);
                }
                LogInfoVerbose($"... selected module {moduleInfo.WithVersion(result.Version)} from {result.Origin}");

                // cache found version
                Directory.CreateDirectory(cachedDirectory);
                await File.WriteAllTextAsync(Path.Combine(cachedDirectory, result.Version.ToString()), JsonConvert.SerializeObject(result.Manifest));

                return(MakeModuleLocation(result.Origin, result.Manifest));
            } finally {
                LogInfoPerformance($"ResolveInfoToLocationAsync() for {moduleInfo}", stopwatch.Elapsed, cached);
            }

            async Task <(string Origin, VersionInfo Version, ModuleManifest Manifest)> FindNewestModuleVersionAsync(string bucketName)
            {
                // enumerate versions in bucket
                var found = await FindModuleVersionsAsync(bucketName);

                if (!found.Any())
                {
                    return(Origin : bucketName, Version : null, Manifest : null);
                }

                // NOTE (2019-08-12, bjorg): if the module is nested, we filter the list of found versions to
                //  only contain versions that meet the module version constraint; for shared modules, we want to
                //  keep the latest version that is compatible with the tool and is equal-or-greater than the
                //  module version constraint.
                if ((dependencyType == ModuleManifestDependencyType.Nested) && (moduleInfo.Version != null))
                {
                    found = found.Where(version => {
                        if (!version.MatchesConstraint(moduleInfo.Version))
                        {
                            LogInfoVerbose($"... rejected v{version}: does not match version constraint {moduleInfo.Version}");
                            return(false);
                        }
                        return(true);
                    }).ToList();
                }

                // attempt to identify the newest module version compatible with the tool
                ModuleManifest manifest = null;
                var            match    = VersionInfo.FindLatestMatchingVersion(found, moduleInfo.Version, candidateVersion => {
                    var candidateModuleInfo = new ModuleInfo(moduleInfo.Namespace, moduleInfo.Name, candidateVersion, moduleInfo.Origin);

                    // check if the module version is allowed by the build policy
                    if (!(Settings.BuildPolicy?.Modules?.Allow?.Contains(candidateModuleInfo.ToString()) ?? true))
                    {
                        LogInfoVerbose($"... rejected v{candidateVersion}: not allowed by build policy");
                        return(false);
                    }

                    // check if module is compatible with this tool
                    var candidateManifestText = GetS3ObjectContentsAsync(bucketName, candidateModuleInfo.VersionPath).GetAwaiter().GetResult();
                    manifest = JsonConvert.DeserializeObject <ModuleManifest>(candidateManifestText);
                    if (!VersionInfoCompatibility.IsModuleCoreVersionCompatibleWithToolVersion(manifest.CoreServicesVersion, Settings.ToolVersion))
                    {
                        LogInfoVerbose($"... rejected v{candidateVersion}: not compatible with tool version {Settings.ToolVersion}");
                        return(false);
                    }
                    return(true);
                });

                return(Origin : bucketName, Version : match, Manifest : manifest);
            }

            async Task <IEnumerable <VersionInfo> > FindModuleVersionsAsync(string bucketName)
            {
                // get bucket region specific S3 client
                var s3Client = await GetS3ClientByBucketNameAsync(bucketName);

                if (s3Client == null)
                {
                    // nothing to do; GetS3ClientByBucketName already emitted an error
                    return(new List <VersionInfo>());
                }

                // enumerate versions in bucket
                var versions = new List <VersionInfo>();
                var request  = new ListObjectsV2Request {
                    BucketName   = bucketName,
                    Prefix       = $"{moduleInfo.Origin ?? Settings.DeploymentBucketName}/{moduleInfo.Namespace}/{moduleInfo.Name}/",
                    Delimiter    = "/",
                    MaxKeys      = 100,
                    RequestPayer = RequestPayer.Requester
                };

                do
                {
                    try {
                        var response = await s3Client.ListObjectsV2Async(request);

                        versions.AddRange(response.S3Objects
                                          .Select(s3Object => s3Object.Key.Substring(request.Prefix.Length))
                                          .Select(found => VersionInfo.Parse(found))
                                          .Where(version => (moduleInfo.Version == null) || version.IsGreaterOrEqualThanVersion(moduleInfo.Version, strict: true))
                                          );
                        request.ContinuationToken = response.NextContinuationToken;
                    } catch (AmazonS3Exception e) when(e.Message == "Access Denied")
                    {
                        // show message that access was denied for this location
                        LogInfoVerbose($"... access denied to {bucketName} [{s3Client.Config.RegionEndpoint.SystemName}]");
                        return(versions);
                    }
                } while(request.ContinuationToken != null);
                LogInfoVerbose($"... found {versions.Count} version{((versions.Count == 1) ? "" : "s")} in {bucketName} [{s3Client.Config.RegionEndpoint.SystemName}]");
                return(versions);
            }

            ModuleLocation MakeModuleLocation(string sourceBucketName, ModuleManifest manifest)
            => new ModuleLocation(sourceBucketName, manifest.ModuleInfo, manifest.TemplateChecksum);
        }
Exemple #5
0
        public async Task <ModuleLocation> ResolveInfoToLocationAsync(
            ModuleInfo moduleInfo,
            string bucketName,
            ModuleManifestDependencyType dependencyType,
            bool allowImport,
            bool showError
            )
        {
            if (bucketName == null)
            {
                throw new ArgumentNullException(nameof(bucketName));
            }
            LogInfoVerbose($"... resolving module {moduleInfo}");
            StartLogPerformance($"ResolveInfoToLocationAsync() for {moduleInfo}");
            var cached = false;

            try {
                // check if module can be found in the deployment bucket
                var result = await FindNewestModuleVersionInBucketAsync(Settings.DeploymentBucketName);

                // check if the origin bucket needs to be checked
                if (
                    allowImport &&
                    (Settings.DeploymentBucketName != bucketName) &&
                    (

                        // no version has been found
                        (result.Version == null)

                        // no module version constraint was given; the ultimate floating version
                        || (moduleInfo.Version == null)

                        // the module version constraint is for a pre-release; we always prefer the origin version then
                        || moduleInfo.Version.IsPreRelease()

                        // the module version constraint is floating; we need to check if origin has a newer version
                        || !moduleInfo.Version.Minor.HasValue ||
                        !moduleInfo.Version.Patch.HasValue
                    )
                    )
                {
                    var originResult = await FindNewestModuleVersionInBucketAsync(bucketName);

                    // check if module found at origin should be kept instead
                    if (
                        (originResult.Version != null) &&
                        (
                            (result.Version == null) ||
                            (moduleInfo.Version?.IsPreRelease() ?? false) ||
                            originResult.Version.IsGreaterThanVersion(result.Version)
                        )
                        )
                    {
                        result = originResult;
                    }
                }

                // check if a module was found
                if (result.Version == null)
                {
                    // could not find a matching version
                    var versionConstraint = (moduleInfo.Version != null)
                        ? $"v{moduleInfo.Version} or later"
                        : "any released version";
                    if (showError)
                    {
                        if (allowImport)
                        {
                            LogError($"could not find module '{moduleInfo}' ({versionConstraint})");
                        }
                        else
                        {
                            LogError($"missing module dependency must be imported explicitly '{moduleInfo}' ({versionConstraint})");
                        }
                    }
                    return(null);
                }
                LogInfoVerbose($"... selected module {moduleInfo.WithVersion(result.Version)} from {result.Origin}");
                return(MakeModuleLocation(result.Origin, result.Manifest));
            } finally {
                StopLogPerformance(cached);
            }

            async Task <(string Origin, VersionInfo Version, ModuleManifest Manifest)> FindNewestModuleVersionInBucketAsync(string bucketName)
            {
                StartLogPerformance($"FindNewestModuleVersionInBucketAsync() for s3://{bucketName}");
                try {
                    // enumerate versions in bucket
                    var found = await FindModuleVersionsInBucketAsync(bucketName);

                    if (!found.Any())
                    {
                        return(Origin : bucketName, Version : null, Manifest : null);
                    }

                    // NOTE (2019-08-12, bjorg): if the module is nested, we filter the list of found versions to
                    //  only contain versions that meet the module version constraint; for shared modules, we want to
                    //  keep the latest version that is compatible with the tool and is equal-or-greater than the
                    //  module version constraint.
                    if ((dependencyType == ModuleManifestDependencyType.Nested) && (moduleInfo.Version != null))
                    {
                        found = found.Where(version => {
                            if (!version.MatchesConstraint(moduleInfo.Version))
                            {
                                LogInfoVerbose($"... rejected v{version}: does not match version constraint {moduleInfo.Version}");
                                return(false);
                            }
                            return(true);
                        }).ToList();
                    }

                    // attempt to identify the newest module version compatible with the tool
                    ModuleManifest manifest = null;
                    var            match    = VersionInfo.FindLatestMatchingVersion(found, moduleInfo.Version, candidateVersion => {
                        var candidateModuleInfo = new ModuleInfo(moduleInfo.Namespace, moduleInfo.Name, candidateVersion, moduleInfo.Origin);

                        // check if the module version is allowed by the build policy
                        if (!(Settings.BuildPolicy?.Modules?.Allow?.Contains(candidateModuleInfo.ToString()) ?? true))
                        {
                            LogInfoVerbose($"... rejected v{candidateVersion}: not allowed by build policy");
                            return(false);
                        }

                        // load module manifest
                        var(candidateManifest, candidateManifestErrorReason) = LoadManifestFromLocationAsync(new ModuleLocation(bucketName, candidateModuleInfo, "<MISSING>")).GetAwaiter().GetResult();
                        if (candidateManifest == null)
                        {
                            LogInfoVerbose($"... rejected v{candidateVersion}: {candidateManifestErrorReason}");
                            return(false);
                        }

                        // check if module is compatible with this tool
                        if (!VersionInfoCompatibility.IsModuleCoreVersionCompatibleWithToolVersion(candidateManifest.CoreServicesVersion, Settings.ToolVersion))
                        {
                            LogInfoVerbose($"... rejected v{candidateVersion}: not compatible with tool version {Settings.ToolVersion}");
                            return(false);
                        }

                        // keep this manifest
                        manifest = candidateManifest;
                        return(true);
                    });
                    return(Origin : bucketName, Version : match, Manifest : manifest);
                } finally {
                    StopLogPerformance();
                }
            }

            async Task <IEnumerable <VersionInfo> > FindModuleVersionsInBucketAsync(string bucketName)
            {
                StartLogPerformance($"FindModuleVersionsInBucketAsync() for s3://{bucketName}");
                var cached = false;

                try {
                    var moduleOrigin            = moduleInfo.Origin ?? Settings.DeploymentBucketName;
                    List <VersionInfo> versions = null;
                    string             region   = null;

                    // check if a cached version exists
                    string cachedManifestVersionsFilePath = null;
                    if (!Settings.ForceRefresh)
                    {
                        var cachedManifestFolder = GetCachedManifestDirectory(bucketName, moduleOrigin, moduleInfo.Namespace, moduleInfo.Name);
                        if (cachedManifestFolder != null)
                        {
                            cachedManifestVersionsFilePath = Path.Combine(cachedManifestFolder, "versions.json");
                            if (
                                File.Exists(cachedManifestVersionsFilePath) &&
                                (File.GetLastWriteTimeUtc(cachedManifestVersionsFilePath).Add(Settings.CachedManifestListingExpiration) > DateTime.UtcNow)
                                )
                            {
                                cached = true;
                                var cachedManifestVersions = JsonSerializer.Deserialize <ModuleManifestVersions>(File.ReadAllText(cachedManifestVersionsFilePath), Settings.JsonSerializerOptions);
                                region   = cachedManifestVersions.Region;
                                versions = cachedManifestVersions.Versions;
                            }
                        }
                    }

                    // check if data needs to be fetched from S3 bucket
                    if (versions == null)
                    {
                        // get bucket region specific S3 client
                        var s3Client = await GetS3ClientByBucketNameAsync(bucketName);

                        if (s3Client == null)
                        {
                            // nothing to do; GetS3ClientByBucketName already emitted an error
                            return(new List <VersionInfo>());
                        }

                        // enumerate versions in bucket
                        versions = new List <VersionInfo>();
                        region   = s3Client.Config.RegionEndpoint.SystemName;
                        var request = new ListObjectsV2Request {
                            BucketName   = bucketName,
                            Prefix       = $"{moduleOrigin}/{moduleInfo.Namespace}/{moduleInfo.Name}/",
                            Delimiter    = "/",
                            MaxKeys      = 100,
                            RequestPayer = RequestPayer.Requester
                        };
                        do
                        {
                            try {
                                var response = await s3Client.ListObjectsV2Async(request);

                                versions.AddRange(response.S3Objects
                                                  .Select(s3Object => s3Object.Key.Substring(request.Prefix.Length))
                                                  .Select(found => VersionInfo.Parse(found))
                                                  );
                                request.ContinuationToken = response.NextContinuationToken;
                            } catch (AmazonS3Exception e) when(e.Message == "Access Denied")
                            {
                                // show message that access was denied for this location
                                LogInfoVerbose($"... access denied to {bucketName} [{s3Client.Config.RegionEndpoint.SystemName}]");
                                return(Enumerable.Empty <VersionInfo>());
                            }
                        } while(request.ContinuationToken != null);

                        // cache module versions listing
                        if (cachedManifestVersionsFilePath != null)
                        {
                            try {
                                File.WriteAllText(cachedManifestVersionsFilePath, JsonSerializer.Serialize(new ModuleManifestVersions {
                                    Region   = region,
                                    Versions = versions
                                }, Settings.JsonSerializerOptions));
                            } catch {
                                // nothing to do
                            }
                        }
                    }

                    // filter list down to matching versions
                    versions = versions.Where(version => (moduleInfo.Version == null) || version.IsGreaterOrEqualThanVersion(moduleInfo.Version, strict: true)).ToList();
                    LogInfoVerbose($"... found {versions.Count} version{((versions.Count == 1) ? "" : "s")} in {bucketName} [{region}]");
                    return(versions);
                } finally {
                    StopLogPerformance(cached);
                }
            }

            ModuleLocation MakeModuleLocation(string sourceBucketName, ModuleManifest manifest)
            => new ModuleLocation(sourceBucketName, manifest.ModuleInfo, manifest.TemplateChecksum);
        }
        public void NewCSharpFunction(
            Settings settings,
            string functionName,
            string rootNamespace,
            string framework,
            string workingDirectory,
            string moduleFile,
            int functionMemory,
            int functionTimeout,
            string projectDirectory,
            FunctionType functionType
            )
        {
            if (functionName == "Finalizer")
            {
                // always of type finalizer
                functionType    = FunctionType.Finalizer;
                functionTimeout = 900;
            }
            else if (functionType == FunctionType.Unknown)
            {
                // prompt for function type
                functionType = Enum.Parse <FunctionType>(settings.PromptChoice("Select function type", _functionTypes), ignoreCase: true);
            }

            // fetch resource names for this function type
            var frameworkFolder           = framework.Replace(".", "");
            var sourceResourceNamesPrefix = $"{frameworkFolder}.NewCSharpFunction-{functionType}.";
            var sourceResourceNames       = GetResourceNames(sourceResourceNamesPrefix);

            if (sourceResourceNames.Length == 0)
            {
                LogError("function type is not supported for selected framework");
                return;
            }

            // create files for the project
            var substitutions = new Dictionary <string, string> {
                ["FRAMEWORK"]           = framework,
                ["ROOTNAMESPACE"]       = rootNamespace,
                ["LAMBDASHARP_VERSION"] = VersionInfoCompatibility.GetLambdaSharpAssemblyWildcardVersion(settings.ToolVersion, framework)
            };
            var projectSourceResourceName = $"{sourceResourceNamesPrefix}xml";

            foreach (var sourceResourceName in sourceResourceNames)
            {
                var sourceContents = ReadResource(sourceResourceName, substitutions);
                if (sourceResourceName == projectSourceResourceName)
                {
                    // create function project
                    var projectFile = Path.Combine(projectDirectory, functionName + ".csproj");
                    try {
                        File.WriteAllText(projectFile, sourceContents);
                        Console.WriteLine($"Created project file: {Path.GetRelativePath(Directory.GetCurrentDirectory(), projectFile)}");
                    } catch (Exception e) {
                        LogError($"unable to create project file '{projectFile}'", e);
                        return;
                    }
                }
                else
                {
                    // create source file
                    var otherFile = Path.Combine(projectDirectory, Path.GetFileNameWithoutExtension(sourceResourceName.Substring(sourceResourceNamesPrefix.Length)).Replace("_", "."));
                    try {
                        File.WriteAllText(otherFile, sourceContents);
                        Console.WriteLine($"Created file: {Path.GetRelativePath(Directory.GetCurrentDirectory(), otherFile)}");
                    } catch (Exception e) {
                        LogError($"unable to create file '{otherFile}'", e);
                        return;
                    }
                }
            }
        }
        protected async Task <bool> PopulateDeploymentTierSettingsAsync(
            Settings settings,
            bool requireBucketName   = true,
            bool requireCoreServices = true,
            bool requireVersionCheck = true,
            bool optional            = false,
            bool force        = false,
            bool allowCaching = false
            )
        {
            var stopwatch = System.Diagnostics.Stopwatch.StartNew();
            var cached    = false;

            try {
                if (
                    (settings.DeploymentBucketName == null) ||
                    (settings.TierVersion == null) ||
                    force
                    )
                {
                    var cachedDeploymentTierSettings = Path.Combine(Settings.AwsProfileCacheDirectory, $"{settings.TierPrefix}tier.json");
                    if (!force && allowCaching && Settings.AllowCaching && File.Exists(cachedDeploymentTierSettings))
                    {
                        var cachedInfo = JsonConvert.DeserializeObject <CachedDeploymentTierSettingsInfo>(await File.ReadAllTextAsync(cachedDeploymentTierSettings));

                        // initialize settings
                        settings.DeploymentBucketName = cachedInfo.DeploymentBucketName;
                        settings.LoggingBucketName    = cachedInfo.LoggingBucketName;
                        settings.TierVersion          = cachedInfo.TierVersion;
                        settings.CoreServices         = cachedInfo.CoreServices;
                        cached = true;
                        return(true);
                    }

                    // attempt to find an existing core module
                    var stackName = $"{settings.TierPrefix}LambdaSharp-Core";
                    var existing  = await settings.CfnClient.GetStackAsync(stackName, LogError);

                    if (existing.Stack == null)
                    {
                        if (!optional)
                        {
                            LogError($"LambdaSharp tier {settings.TierName} does not exist", new LambdaSharpDeploymentTierSetupException(settings.TierName));
                        }
                        return(false);
                    }

                    // validate module information
                    var result             = true;
                    var tierModuleInfoText = existing.Stack?.GetModuleVersionText();
                    if (tierModuleInfoText == null)
                    {
                        if (!optional && result)
                        {
                            LogError($"Could not find LambdaSharp tier information for {stackName}");
                        }
                        result = false;
                    }

                    // read deployment S3 bucket name
                    var tierModuleDeploymentBucketArnParts = GetStackOutput("DeploymentBucket")?.Split(':');
                    if ((tierModuleDeploymentBucketArnParts == null) && requireBucketName)
                    {
                        if (!optional && result)
                        {
                            LogError("could not find 'DeploymentBucket' output value for deployment tier settings", new LambdaSharpDeploymentTierOutOfDateException(settings.TierName));
                        }
                        result = false;
                    }
                    if (tierModuleDeploymentBucketArnParts != null)
                    {
                        if ((tierModuleDeploymentBucketArnParts.Length != 6) || (tierModuleDeploymentBucketArnParts[0] != "arn") || (tierModuleDeploymentBucketArnParts[1] != "aws") || (tierModuleDeploymentBucketArnParts[2] != "s3"))
                        {
                            LogError("invalid value 'DeploymentBucket' output value for deployment tier settings", new LambdaSharpDeploymentTierOutOfDateException(settings.TierName));
                            result = false;
                            tierModuleDeploymentBucketArnParts = null;
                        }
                    }
                    var deploymentBucketName = tierModuleDeploymentBucketArnParts?[5];

                    // read logging S3 bucket name
                    var tierModuleLoggingBucketArnParts = GetStackOutput("LoggingBucket")?.Split(':');
                    if (tierModuleLoggingBucketArnParts != null)
                    {
                        if ((tierModuleLoggingBucketArnParts.Length != 6) || (tierModuleLoggingBucketArnParts[0] != "arn") || (tierModuleLoggingBucketArnParts[1] != "aws") || (tierModuleLoggingBucketArnParts[2] != "s3"))
                        {
                            LogError("invalid value 'LoggingBucket' output value for deployment tier settings", new LambdaSharpDeploymentTierOutOfDateException(settings.TierName));
                            result = false;
                            tierModuleLoggingBucketArnParts = null;
                        }
                    }
                    var loggingBucketName = tierModuleLoggingBucketArnParts?[5];

                    // do some sanity checks
                    if (
                        !ModuleInfo.TryParse(tierModuleInfoText, out var tierModuleInfo) ||
                        (tierModuleInfo.Namespace != "LambdaSharp") ||
                        (tierModuleInfo.Name != "Core")
                        )
                    {
                        LogError("LambdaSharp tier is not configured propertly", new LambdaSharpDeploymentTierSetupException(settings.TierName));
                        result = false;
                    }

                    // check if tier and tool versions are compatible
                    if (
                        !optional &&
                        (tierModuleInfo != null) &&
                        requireVersionCheck &&
                        !VersionInfoCompatibility.IsTierVersionCompatibleWithToolVersion(tierModuleInfo.Version, settings.ToolVersion)
                        )
                    {
                        var tierToToolVersionComparison = VersionInfoCompatibility.CompareTierVersionToToolVersion(tierModuleInfo.Version, settings.ToolVersion);
                        if (tierToToolVersionComparison < 0)
                        {
                            LogError($"LambdaSharp tier is not up to date (tool: {settings.ToolVersion}, tier: {tierModuleInfo.Version})", new LambdaSharpDeploymentTierOutOfDateException(settings.TierName));
                            result = false;
                        }
                        else if (tierToToolVersionComparison > 0)
                        {
                            // tier is newer; we expect the tier to be backwards compatible by exposing the same resources as before
                        }
                        else
                        {
                            LogError($"LambdaSharp tool is not compatible (tool: {settings.ToolVersion}, tier: {tierModuleInfo.Version})", new LambdaSharpToolOutOfDateException(tierModuleInfo.Version));
                            result = false;
                        }
                    }

                    // read tier mode
                    var coreServicesModeText = GetStackOutput("CoreServices");
                    if (!Enum.TryParse <CoreServices>(coreServicesModeText, true, out var coreServicesMode) && requireCoreServices)
                    {
                        if (!optional && result)
                        {
                            LogError("unable to parse CoreServices output value from stack");
                        }
                        result = false;
                    }

                    // initialize settings
                    settings.DeploymentBucketName = deploymentBucketName;
                    settings.LoggingBucketName    = loggingBucketName;
                    settings.TierVersion          = tierModuleInfo?.Version;
                    settings.CoreServices         = coreServicesMode;

                    // cache deployment tier settings
                    if (allowCaching && Settings.AllowCaching)
                    {
                        Directory.CreateDirectory(Path.GetDirectoryName(cachedDeploymentTierSettings));
                        await File.WriteAllTextAsync(cachedDeploymentTierSettings, JsonConvert.SerializeObject(new CachedDeploymentTierSettingsInfo {
                            DeploymentBucketName = settings.DeploymentBucketName,
                            TierVersion          = settings.TierVersion,
                            CoreServices         = settings.CoreServices
                        }));
                    }
                    return(result);

                    // local functions
                    string GetStackOutput(string key) => existing.Stack?.Outputs.FirstOrDefault(output => output.OutputKey == key)?.OutputValue;
                }
                return(true);
            } finally {
                Settings.LogInfoPerformance($"PopulateDeploymentTierSettingsAsync() for '{settings.TierName}'", stopwatch.Elapsed, cached);
            }
        }
        //--- Methods --
        public void Register(CommandLineApplication app)
        {
            app.Command("init", cmd => {
                cmd.HelpOption();
                cmd.Description = "Create or update a LambdaSharp deployment tier";

                // init options
                var protectStackOption         = cmd.Option("--protect", "(optional) Enable termination protection for the CloudFormation stack", CommandOptionType.NoValue);
                var enableXRayTracingOption    = cmd.Option("--xray[:<LEVEL>]", "(optional) Enable service-call tracing with AWS X-Ray for all resources in module  (0=Disabled, 1=RootModule, 2=AllModules; RootModule if LEVEL is omitted)", CommandOptionType.SingleOrNoValue);
                var versionOption              = cmd.Option("--version <VERSION>", "(optional) Specify version for LambdaSharp.Core module (default: same as CLI version)", CommandOptionType.SingleValue);
                var parametersFileOption       = cmd.Option("--parameters <FILE>", "(optional) Specify source filename for module parameters (default: none)", CommandOptionType.SingleValue);
                var forceDeployOption          = cmd.Option("--force-deploy", "(optional) Force module deployment", CommandOptionType.NoValue);
                var quickStartOption           = cmd.Option("--quick-start", "(optional, create-only) Use safe defaults for quickly setting up a LambdaSharp deployment tier.", CommandOptionType.NoValue);
                var coreServicesOption         = cmd.Option("--core-services <VALUE>", "(optional, create-only) Select if LambdaSharp.Core services should be enabled or not (either Enabled or Disabled, default prompts)", CommandOptionType.SingleValue);
                var existingS3BucketNameOption = cmd.Option("--existing-s3-bucket-name <NAME>", "(optional, create-only) Existing S3 bucket name for module deployments (blank value creates new bucket)", CommandOptionType.SingleValue);
                var localOption               = cmd.Option("--local <PATH>", "(optional) Provide a path to a local check-out of the LambdaSharp modules (default: LAMBDASHARP environment variable)", CommandOptionType.SingleValue);
                var usePublishedOption        = cmd.Option("--use-published", "(optional) Force the init command to use the published LambdaSharp modules", CommandOptionType.NoValue);
                var promptAllParametersOption = cmd.Option("--prompt-all", "(optional) Prompt for all missing parameters values (default: only prompt for missing parameters with no default value)", CommandOptionType.NoValue);
                var allowUpgradeOption        = cmd.Option("--allow-upgrade", "(optional) Allow upgrading LambdaSharp.Core across major releases (default: prompt)", CommandOptionType.NoValue);
                var skipApiGatewayCheckOption = cmd.Option("--skip-apigateway-check", "(optional) Skip API Gateway role check", CommandOptionType.NoValue);
                var initSettingsCallback      = CreateSettingsInitializer(cmd);
                AddStandardCommandOptions(cmd);
                cmd.OnExecute(async() => {
                    ExecuteCommandActions(cmd);

                    // check if .aws/credentials file needs to be created
                    if (
                        !File.Exists(CredentialsFilePath) &&
                        (
                            (Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") == null) ||
                            (Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY") == null)
                        )
                        )
                    {
                        var tmpSettings = new Settings(Version);

                        // prompt for AWS credentials information
                        Console.WriteLine();
                        tmpSettings.PromptLabel("Create AWS profile");
                        var accessKey       = tmpSettings.PromptString("Enter the AWS Access Key ID");
                        var secretAccessKey = tmpSettings.PromptString("Enter the AWS Secret Access Key");
                        var region          = tmpSettings.PromptString("Enter the AWS region", "us-east-1");

                        // create credentials file
                        var credentialsTemplate = ReadResource("credentials.txt", new Dictionary <string, string> {
                            ["REGION"]          = region,
                            ["ACCESSKEY"]       = accessKey,
                            ["SECRETACCESSKEY"] = secretAccessKey
                        });
                        try {
                            if (!Directory.Exists(CredentialsFolder))
                            {
                                Directory.CreateDirectory(CredentialsFolder);
                            }
                            File.WriteAllText(CredentialsFilePath, credentialsTemplate);
                        } catch {
                            LogError("unable to create .aws/credentials file");
                            return;
                        }
                    }

                    // initialize settings
                    var settings = await initSettingsCallback();
                    if (settings == null)
                    {
                        return;
                    }

                    // check x-ray settings
                    if (!TryParseEnumOption(enableXRayTracingOption, XRayTracingLevel.Disabled, XRayTracingLevel.RootModule, out var xRayTracingLevel))
                    {
                        // NOTE (2018-08-04, bjorg): no need to add an error message since it's already added by 'TryParseEnumOption'
                        return;
                    }
                    if (!TryParseEnumOption(coreServicesOption, CoreServices.Undefined, CoreServices.Undefined, out var coreServices))
                    {
                        // NOTE (2018-08-04, bjorg): no need to add an error message since it's already added by 'TryParseEnumOption'
                        return;
                    }

                    // set initialization parameters
                    var existingS3BucketName = existingS3BucketNameOption.Value();
                    if (quickStartOption.HasValue())
                    {
                        // check if --core-services was specified, if not default to 'disabled'
                        coreServices = coreServicesOption.HasValue()
                            ? coreServices
                            : CoreServices.Disabled;

                        // check if --existing-s3-bucket-name was specified, if not default to ""
                        existingS3BucketName = existingS3BucketNameOption.HasValue()
                            ? existingS3BucketNameOption.Value()
                            : "";
                    }

                    // begin tier initialization
                    await Init(
                        settings,
                        allowDataLoos: true,
                        protectStackOption.HasValue(),
                        forceDeployOption.HasValue(),
                        versionOption.HasValue() ? VersionInfo.Parse(versionOption.Value()) : VersionInfoCompatibility.GetCoreServicesReferenceVersion(Version),
                        usePublishedOption.HasValue()
                            ? null
                            : (localOption.Value() ?? Environment.GetEnvironmentVariable("LAMBDASHARP")),
                        parametersFileOption.Value(),
                        promptAllParametersOption.HasValue(),
                        xRayTracingLevel,
                        coreServices,
                        existingS3BucketName,
                        allowUpgradeOption.HasValue(),
                        skipApiGatewayCheckOption.HasValue(),
                        quickStartOption.HasValue()
                        );
                });
            });
        }
        public async Task <bool> Init(
            Settings settings,
            bool allowDataLoos,
            bool protectStack,
            bool forceDeploy,
            VersionInfo version,
            string lambdaSharpPath,
            string parametersFilename,
            bool promptAllParameters,
            XRayTracingLevel xRayTracingLevel,
            CoreServices coreServices,
            string existingS3BucketName,
            bool allowUpgrade,
            bool skipApiGatewayCheck,
            bool quickStart
            )
        {
            // NOTE (2019-08-15, bjorg): the deployment tier initialization must support the following scenarios:
            //  1. New deployment tier
            //  2. Updating an existing tier with any configuration changes
            //  3. Upgrading an existing tier to enable LambdaSharp.Core services
            //  4. Downgrading an existing tier to disable LambdaSharp.Core services

            // read the current deployment tier settings if possible
            await PopulateDeploymentTierSettingsAsync(
                settings,

                // bucket name and core services settings may be missing for deployment tier v0.6 or earlier
                requireBucketName : false,
                requireCoreServices : false,

                // version is more explicitly checked below
                requireVersionCheck : false,

                // deployment tier may not exist yet
                optional : true
                );

            if (HasErrors)
            {
                return(false);
            }

            // check if a new installation is required
            var createNewTier                 = (settings.TierVersion == null);
            var updateExistingTier            = forceDeploy;
            var disableCoreServicesForUpgrade = false;

            if (!createNewTier && !updateExistingTier)
            {
                // check if core services state was not specified
                if (coreServices == CoreServices.Undefined)
                {
                    // inherit current state
                    coreServices = settings.CoreServices;
                }

                // always upgrade Bootstrap state to Enabled, since the former is a transitional state only
                if (coreServices == CoreServices.Bootstrap)
                {
                    coreServices = CoreServices.Enabled;
                }

                // determine if the deployment tier needs to be updated
                var tierToToolVersionComparison = VersionInfoCompatibility.CompareTierVersionToToolVersion(settings.TierVersion, settings.ToolVersion);
                if (tierToToolVersionComparison == 0)
                {
                    // versions are identical; nothing to do, unless we're forced to update
                    updateExistingTier = forceDeploy

                                         // it's a pre-release, which always needs to be updated
                                         || settings.ToolVersion.IsPreRelease()

                                         // we're running in contributor mode, which means new binaries may be built
                                         || (lambdaSharpPath != null)

                                         // deployment tier is running core services state is different from requested state
                                         || (settings.CoreServices != coreServices) ||
                                         await IsNewerCoreModuleVersionAvailable();

                    // local functions
                    async Task <bool> IsNewerCoreModuleVersionAvailable()
                    {
                        var coreModuleInfo           = new ModuleInfo("LambdaSharp", "Core", settings.CoreServicesReferenceVersion, "lambdasharp");
                        var latestCoreModuleLocation = await new ModelManifestLoader(settings, "").ResolveInfoToLocationAsync(
                            coreModuleInfo,
                            coreModuleInfo.Origin,
                            ModuleManifestDependencyType.Shared,
                            allowImport: true,
                            showError: false
                            );
                        var latestCoreModuleVersion = latestCoreModuleLocation?.ModuleInfo.Version;

                        return(settings.TierVersion.IsLessThanVersion(latestCoreModuleVersion));
                    }
                }
                else if (tierToToolVersionComparison > 0)
                {
                    // tier is newer; tool needs to get updated
                    LogError($"LambdaSharp tool is out of date (tool: {settings.ToolVersion}, tier: {settings.TierVersion})", new LambdaSharpToolOutOfDateException(settings.TierVersion));
                    return(false);
                }
                else if (tierToToolVersionComparison < 0)
                {
                    // tier is older; let's only upgrade it if requested
                    updateExistingTier = true;

                    // tool version is more recent; if it's a minor update, proceed without prompting, otherwise ask user to confirm upgrade
                    if (!VersionInfoCompatibility.IsTierVersionCompatibleWithToolVersion(settings.TierVersion, settings.ToolVersion))
                    {
                        // update requires core service to be replaced
                        disableCoreServicesForUpgrade = true;

                        // check if an interactive confirmation prompt is required
                        if (!allowUpgrade)
                        {
                            Console.WriteLine($"LambdaSharp Tier is out of date");
                            updateExistingTier = settings.PromptYesNo($"Do you want to upgrade LambdaSharp Tier '{settings.TierName}' from v{settings.TierVersion} to v{settings.CoreServicesReferenceVersion}?", defaultAnswer: false);
                        }
                    }
                    if (!updateExistingTier)
                    {
                        return(false);
                    }
                }
                else if (!forceDeploy)
                {
                    LogError($"LambdaSharp tool is not compatible (tool: {settings.ToolVersion}, tier: {settings.TierVersion}); use --force-deploy to proceed anyway");
                    return(false);
                }
                else
                {
                    // force deploy it is!
                    updateExistingTier = true;
                }
            }

            // check if deployment tier with disabled core services needs to be created/updated
            Dictionary <string, string> parameters = null;
            var tierCommand = new CliTierCommand();
            var updated     = false;

            if (
                createNewTier ||
                (updateExistingTier && (

                     // deployment tier doesn't have core services (pre-0.7); so the bootstrap stack needs to be installed first
                     (settings.CoreServices == CoreServices.Undefined)

                     // deployment tier core services need to be disabled
                     || (coreServices == CoreServices.Disabled)

                     // we have to disable core services to upgrade
                     || disableCoreServicesForUpgrade
                     ))
                )
            {
                // deploy bootstrap stack with disabled core services
                if (!await DeployCoreServicesDisabledTemplate())
                {
                    return(false);
                }
                updated = true;
            }

            // check if API Gateway role needs to be set or updated
            if (!skipApiGatewayCheck)
            {
                await CheckApiGatewayRole(settings);
            }
            if (HasErrors)
            {
                return(false);
            }

            // standard modules
            var standardModules = new List <string> {
                "LambdaSharp.Core"
            };

            // check if the module must be built and published first (only applicable when running lash in contributor mode)
            var buildPublishDeployCommand = new CliBuildPublishDeployCommand();

            if (lambdaSharpPath != null)
            {
                Console.WriteLine($"Building LambdaSharp modules");

                // attempt to parse the tool version from environment variables
                if (!VersionInfo.TryParse(Environment.GetEnvironmentVariable("LAMBDASHARP_VERSION"), out var moduleVersion))
                {
                    LogError("unable to parse module version from LAMBDASHARP_VERSION");
                    return(false);
                }

                // gather list of module to build and publish
                standardModules.AddRange(
                    Directory.GetDirectories(Path.Combine(lambdaSharpPath, "Modules"))
                    .Where(directory => File.Exists(Path.Combine(directory, "Module.yml")))
                    .Select(directory => Path.GetFileName(directory))
                    .Where(module => module != "LambdaSharp.Core")
                    );
                foreach (var module in standardModules)
                {
                    var moduleSource = Path.Combine(lambdaSharpPath, "Modules", module, "Module.yml");
                    settings.WorkingDirectory = Path.GetDirectoryName(moduleSource);
                    settings.OutputDirectory  = Path.Combine(settings.WorkingDirectory, "bin");

                    // build local module
                    if (!await buildPublishDeployCommand.BuildStepAsync(
                            settings,
                            Path.Combine(settings.OutputDirectory, "cloudformation.json"),
                            noAssemblyValidation: true,
                            noPackageBuild: false,
                            gitSha: new GitTool(BuildEventsConfig).GetGitShaValue(settings.WorkingDirectory, showWarningOnFailure: false),
                            gitBranch: new GitTool(BuildEventsConfig).GetGitBranch(settings.WorkingDirectory, showWarningOnFailure: false),
                            buildConfiguration: "Release",
                            selector: null,
                            moduleSource: moduleSource,
                            moduleVersion: moduleVersion,
                            forceBuild: true,
                            buildPolicy: null
                            ))
                    {
                        return(false);
                    }

                    // publish module
                    var moduleReference = await buildPublishDeployCommand.PublishStepAsync(settings, forcePublish : true, moduleOrigin : "lambdasharp");

                    if (moduleReference == null)
                    {
                        return(false);
                    }
                }
            }

            // check if core services do not need to be updated further
            if (coreServices == CoreServices.Disabled)
            {
                if (!updated)
                {
                    Console.WriteLine();
                    Console.WriteLine("Core Services disabled. No update required");
                }
                return(true);
            }

            // check if operating services need to be installed/updated
            if (createNewTier)
            {
                Console.WriteLine();
                Console.WriteLine($"Creating new deployment tier '{settings.TierName}'");
            }
            else if (updateExistingTier)
            {
                Console.WriteLine();
                Console.WriteLine($"Updating deployment tier '{settings.TierName}'");
            }
            else
            {
                if (!updated)
                {
                    Console.WriteLine();
                    Console.WriteLine("No update required");
                }
                return(true);
            }

            // read parameters if they haven't been read yet
            if (parameters == null)
            {
                if (parametersFilename != null)
                {
                    parameters = CliBuildPublishDeployCommand.ReadInputParametersFiles(settings, parametersFilename);
                    if (HasErrors)
                    {
                        return(false);
                    }
                }
                else
                {
                    parameters = new Dictionary <string, string>();
                }
            }
            if (quickStart)
            {
                // set all LambdaSharp.Core parameters to their default input values
                if (!parameters.ContainsKey("DeadLetterQueue"))
                {
                    parameters["DeadLetterQueue"] = "";
                }
                if (!parameters.ContainsKey("LoggingFirehoseStream"))
                {
                    parameters["LoggingFirehoseStream"] = "";
                }
                if (!parameters.ContainsKey("CoreSecretsKey"))
                {
                    parameters["CoreSecretsKey"] = "";
                }
                if (!parameters.ContainsKey("LoggingStreamRole"))
                {
                    parameters["LoggingStreamRole"] = "";
                }
                if (!parameters.ContainsKey("LoggingBucket"))
                {
                    parameters["LoggingBucket"] = "";
                }
            }

            // deploy LambdaSharp module
            foreach (var module in standardModules)
            {
                var isLambdaSharpCoreModule = (module == "LambdaSharp.Core");

                // explicitly import the LambdaSharp module (if it wasn't built locally)
                if (lambdaSharpPath == null)
                {
                    if (!await buildPublishDeployCommand.ImportStepAsync(
                            settings,
                            ModuleInfo.Parse($"{module}:{version}@lambdasharp"),
                            forcePublish: true
                            ))
                    {
                        return(false);
                    }
                }

                // deploy module
                if (!await buildPublishDeployCommand.DeployStepAsync(
                        settings,
                        dryRun: null,
                        moduleReference: $"{module}:{version}@lambdasharp",
                        instanceName: null,
                        allowDataLoos: allowDataLoos,
                        protectStack: protectStack,
                        parameters: parameters,
                        forceDeploy: forceDeploy,
                        promptAllParameters: promptAllParameters,
                        xRayTracingLevel: xRayTracingLevel,
                        deployOnlyIfExists: !isLambdaSharpCoreModule,
                        allowDependencyUpgrades: true
                        ))
                {
                    return(false);
                }

                // reset tier version if core module was deployed; this will force the tier settings to be refetched
                if (isLambdaSharpCoreModule)
                {
                    await PopulateDeploymentTierSettingsAsync(settings, force : true);
                }
            }

            // check if core services need to be enabled for deployed modules
            if (settings.CoreServices == CoreServices.Enabled)
            {
                await tierCommand.UpdateCoreServicesAsync(settings, enabled : true, showModules : false);
            }
            return(!HasErrors);

            // local function
            async Task <bool> DeployCoreServicesDisabledTemplate()
            {
                // initialize stack with seed CloudFormation template
                var template = ReadResource("LambdaSharpCore.yml", new Dictionary <string, string> {
                    ["CORE-VERSION"] = settings.CoreServicesReferenceVersion.ToString(),
                    ["TOOL-VERSION"] = settings.ToolVersion.ToString(),
                    ["CHECKSUM"]     = settings.ToolVersion.ToString().ToMD5Hash()
                });

                // check if bootstrap template is being updated or installed
                if (createNewTier)
                {
                    Console.WriteLine($"Creating LambdaSharp tier '{settings.TierName}'");
                }
                else
                {
                    Console.WriteLine($"Updating LambdaSharp tier '{settings.TierName}'");
                }

                // create lambdasharp CLI bootstrap stack
                var stackName = $"{settings.TierPrefix}LambdaSharp-Core";

                parameters = (parametersFilename != null)
                    ? CliBuildPublishDeployCommand.ReadInputParametersFiles(settings, parametersFilename)
                    : new Dictionary <string, string>();
                if (HasErrors)
                {
                    return(false);
                }
                var bootstrapParameters = new Dictionary <string, string>(parameters)
                {
                    ["TierName"] = settings.Tier
                };

                // check if command line options were provided to set template parameters
                if ((coreServices == CoreServices.Enabled) || (coreServices == CoreServices.Disabled))
                {
                    bootstrapParameters["CoreServices"] = coreServices.ToString();
                }
                if (existingS3BucketName != null)
                {
                    bootstrapParameters["ExistingDeploymentBucket"] = (
                        string.IsNullOrEmpty(existingS3BucketName) ||
                        existingS3BucketName.StartsWith("arn:", StringComparison.Ordinal)
                        )
                        ? existingS3BucketName
                        : "arn:aws:s3:::" + existingS3BucketName;
                }

                // prompt for missing parameters
                var templateParameters = await PromptMissingTemplateParameters(
                    settings,
                    stackName,
                    bootstrapParameters,
                    template,
                    quickStart
                    );

                if (coreServices == CoreServices.Undefined)
                {
                    // determine wanted core services state from template parameters
                    var coreServicesValue = templateParameters.First(parameter => parameter.ParameterKey == "CoreServices")?.ParameterValue;
                    if (!Enum.TryParse <CoreServices>(coreServicesValue, ignoreCase: true, out coreServices))
                    {
                        LogError($"unable to parse CoreServices value from template parameters (found: '{coreServicesValue}')");
                        return(false);
                    }
                }
                if (HasErrors)
                {
                    return(false);
                }

                // disable core services in all deployed modules
                if (!createNewTier)
                {
                    await tierCommand.UpdateCoreServicesAsync(settings, enabled : false, showModules : false);

                    if (HasErrors)
                    {
                        return(false);
                    }
                }

                // create/update cloudformation stack
                if (createNewTier)
                {
                    Console.WriteLine($"=> Stack creation initiated for {Settings.InfoColor}{stackName}{Settings.ResetColor}");
                    var response = await settings.CfnClient.CreateStackAsync(new CreateStackRequest {
                        StackName    = stackName,
                        Capabilities = new List <string> {
                        },
                        OnFailure    = OnFailure.DELETE,
                        Parameters   = templateParameters,
                        EnableTerminationProtection = protectStack,
                        TemplateBody = template,
                        Tags         = settings.GetCloudFormationStackTags("LambdaSharp.Core", stackName)
                    });

                    var created = await settings.CfnClient.TrackStackUpdateAsync(stackName, response.StackId, mostRecentStackEventId : null, logError : LogError);

                    if (created.Success)
                    {
                        Console.WriteLine("=> Stack creation finished");
                    }
                    else
                    {
                        Console.WriteLine("=> Stack creation FAILED");
                        return(false);
                    }
                }
                else
                {
                    Console.WriteLine();
                    Console.WriteLine($"=> Stack update initiated for {Settings.InfoColor}{stackName}{Settings.ResetColor}");
                    try {
                        var mostRecentStackEventId = await settings.CfnClient.GetMostRecentStackEventIdAsync(stackName);

                        var response = await settings.CfnClient.UpdateStackAsync(new UpdateStackRequest {
                            StackName    = stackName,
                            Capabilities = new List <string> {
                            },
                            Parameters   = templateParameters,
                            TemplateBody = template,
                            Tags         = settings.GetCloudFormationStackTags("LambdaSharp.Core", stackName)
                        });

                        var created = await settings.CfnClient.TrackStackUpdateAsync(stackName, response.StackId, mostRecentStackEventId, logError : LogError);

                        if (created.Success)
                        {
                            Console.WriteLine("=> Stack update finished");
                        }
                        else
                        {
                            Console.WriteLine("=> Stack update FAILED");
                            return(false);
                        }
                    } catch (AmazonCloudFormationException e) when(e.Message == "No updates are to be performed.")
                    {
                        // this error is thrown when no required updates where found
                        Console.WriteLine("=> No stack update required");
                    }
                }
                await PopulateDeploymentTierSettingsAsync(settings, force : true);

                return(!HasErrors);
            }
        }