Beispiel #1
0
        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);
            }
        }
        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);
            }
        }