Пример #1
0
        private async Task <bool> ImportDependencies(ModuleManifest manifest, bool allowImport)
        {
            // discover module dependencies
            var dependencies = await _loader.DiscoverAllDependenciesAsync(manifest, checkExisting : false, allowImport, allowDependencyUpgrades : false);

            if (HasErrors)
            {
                return(false);
            }

            // copy all dependencies to deployment bucket that are missing or have a pre-release version
            foreach (var dependency in dependencies.Where(dependency => dependency.ModuleLocation.SourceBucketName != Settings.DeploymentBucketName))
            {
                var imported = false;

                // copy check-summed module artifacts (guaranteed immutable)
                foreach (var artifact in dependency.Manifest.Artifacts)
                {
                    imported = imported | await ImportArtifact(dependency.ModuleLocation.ModuleInfo.Origin, artifact);
                }

                // copy version manifest
                imported = imported | await ImportArtifact(dependency.ModuleLocation.ModuleInfo.Origin, dependency.ModuleLocation.ModuleInfo.VersionPath, replace : dependency.ModuleLocation.ModuleInfo.Version.IsPreRelease());

                // show message if any artifacts were imported
                if (imported)
                {
                    new ModelManifestLoader(Settings, manifest.GetFullName()).ResetCache(Settings.DeploymentBucketName, dependency.ModuleLocation.ModuleInfo);
                    Console.WriteLine($"=> Imported {dependency.ModuleLocation.ModuleInfo}");
                }
            }
            return(true);
        }
Пример #2
0
        private async Task <(bool Success, CloudFormationStack ExistingStack)> IsValidModuleUpdateAsync(string stackName, ModuleManifest manifest)
        {
            try {
                // check if the module was already deployed
                var describe = await Settings.CfnClient.DescribeStacksAsync(new DescribeStacksRequest {
                    StackName = stackName
                });

                // make sure the stack is in a stable state (not updating and not failed)
                var existing = describe.Stacks.FirstOrDefault();
                switch (existing?.StackStatus)
                {
                case null:
                case "CREATE_COMPLETE":
                case "ROLLBACK_COMPLETE":
                case "UPDATE_COMPLETE":
                case "UPDATE_ROLLBACK_COMPLETE":

                    // we're good to go
                    break;

                default:
                    LogError($"deployed module is not in a valid state; module deployment must be complete and successful (Status: {existing?.StackStatus})");
                    return(false, existing);
                }

                // validate existing module deployment
                var deployedOutputs = existing?.Outputs;
                var deployed        = deployedOutputs?.FirstOrDefault(output => output.OutputKey == "Module")?.OutputValue;
                if (!deployed.TryParseModuleDescriptor(
                        out string deployedOwner,
                        out string deployedName,
                        out VersionInfo deployedVersion,
                        out string _
                        ))
                {
                    LogError("unable to determine the name of the deployed module; use --force-deploy to proceed anyway");
                    return(false, existing);
                }
                var deployedFullName = $"{deployedOwner}.{deployedName}";
                if (deployedFullName != manifest.GetFullName())
                {
                    LogError($"deployed module name ({deployedFullName}) does not match {manifest.GetFullName()}; use --force-deploy to proceed anyway");
                    return(false, existing);
                }
                if (deployedVersion > manifest.GetVersion())
                {
                    LogError($"deployed module version (v{deployedVersion}) is newer than v{manifest.GetVersion()}; use --force-deploy to proceed anyway");
                    return(false, existing);
                }
                return(true, existing);
            } catch (AmazonCloudFormationException) {
                // stack doesn't exist
            }
            return(true, null);
        }
Пример #3
0
        private async Task <(bool Success, CloudFormationStack ExistingStack)> IsValidModuleUpdateAsync(string stackName, ModuleManifest manifest, bool showError)
        {
            // check if the module was already deployed
            var existing = await Settings.CfnClient.GetStackAsync(stackName, LogError);

            if (existing.Stack == null)
            {
                return(existing.Success, existing.Stack);
            }

            // validate existing module deployment
            var deployed = existing.Stack?.GetModuleVersionText();

            if (!ModuleInfo.TryParse(deployed, out var deployedModuleInfo))
            {
                if (showError)
                {
                    LogError("unable to determine the name of the deployed module; use --force-deploy to proceed anyway");
                }
                return(false, existing.Stack);
            }
            if (deployedModuleInfo.FullName != manifest.GetFullName())
            {
                if (showError)
                {
                    LogError($"deployed module name ({deployedModuleInfo.FullName}) does not match {manifest.GetFullName()}; use --force-deploy to proceed anyway");
                }
                return(false, existing.Stack);
            }
            var versionComparison = deployedModuleInfo.Version.CompareToVersion(manifest.GetVersion());

            if (versionComparison > 0)
            {
                if (showError)
                {
                    LogError($"deployed module version (v{deployedModuleInfo.Version}) is newer than v{manifest.GetVersion()}; use --force-deploy to proceed anyway");
                }
                return(false, existing.Stack);
            }
            else if (versionComparison == null)
            {
                if (showError)
                {
                    LogError($"deployed module version (v{deployedModuleInfo.Version}) is not compatible with v{manifest.GetVersion()}; use --force-deploy to proceed anyway");
                }
                return(false, existing.Stack);
            }
            return(true, existing.Stack);
        }
Пример #4
0
        private List <CloudFormationParameter> PromptModuleParameters(
            ModuleManifest manifest,
            CloudFormationStack existing           = null,
            Dictionary <string, string> parameters = null,
            bool promptAll = false
            )
        {
            var stackParameters = new Dictionary <string, CloudFormationParameter>();

            // tentatively indicate to reuse previous parameter values
            if (existing != null)
            {
                foreach (var parameter in manifest.ParameterSections
                         .SelectMany(section => section.Parameters)
                         .Where(moduleParameter => existing.Parameters.Any(existingParameter => existingParameter.ParameterKey == moduleParameter.Name))
                         )
                {
                    stackParameters[parameter.Name] = new CloudFormationParameter {
                        ParameterKey     = parameter.Name,
                        UsePreviousValue = true
                    };
                }
            }

            // deployment bucket must always be set to match deployment tier in case it changed because LambdaSharp.Core was recreated
            stackParameters["DeploymentBucketName"] = new CloudFormationParameter {
                ParameterKey   = "DeploymentBucketName",
                ParameterValue = Settings.DeploymentBucketName
            };

            // add all provided parameters
            if (parameters != null)
            {
                foreach (var parameter in parameters)
                {
                    stackParameters[parameter.Key] = new CloudFormationParameter {
                        ParameterKey   = parameter.Key,
                        ParameterValue = parameter.Value
                    };
                }
            }

            // check if module requires any prompts
            if (manifest.GetAllParameters().Any(RequiresPrompt))
            {
                Console.WriteLine();
                Console.WriteLine($"Configuration for {manifest.GetFullName()} (v{manifest.GetVersion()})");

                // only list parameter sections that contain a parameter that requires a prompt
                foreach (var parameterGroup in manifest.ParameterSections.Where(group => group.Parameters.Any(RequiresPrompt)))
                {
                    Console.WriteLine();
                    Settings.PromptLabel(parameterGroup.Title.ToUpper());

                    // only prompt for required parameters
                    foreach (var parameter in parameterGroup.Parameters.Where(RequiresPrompt))
                    {
                        // check if parameter is multiple choice
                        string enteredValue;
                        if (parameter.AllowedValues?.Any() ?? false)
                        {
                            var message = parameter.Name;
                            if (parameter.Label != null)
                            {
                                message += $": {parameter.Label}";
                            }
                            enteredValue = Settings.PromptChoice(message, parameter.AllowedValues);
                        }
                        else
                        {
                            var constraints = new StringBuilder();
                            if ((parameter.MinValue != null) || (parameter.MaxValue != null))
                            {
                                // append value constraints
                                constraints.Append(" (");
                                if ((parameter.MinValue != null) && (parameter.MaxValue != null))
                                {
                                    constraints.Append($"Range: {parameter.MinValue.Value}..{parameter.MaxValue.Value}");
                                }
                                else if (parameter.MinValue != null)
                                {
                                    constraints.Append($"Mininum: {parameter.MinValue.Value}");
                                }
                                else if (parameter.MaxValue != null)
                                {
                                    constraints.Append($"Maximum: {parameter.MaxValue.Value}");
                                }
                                constraints.Append(")");
                            }
                            else if ((parameter.MinLength != null) || (parameter.MaxLength != null))
                            {
                                // append length constraints
                                constraints.Append(" (");
                                if ((parameter.MinLength != null) || (parameter.MaxLength != null))
                                {
                                    constraints.Append($"Length: {parameter.MinValue.Value}..{parameter.MaxValue.Value}");
                                }
                                else if (parameter.MinLength != null)
                                {
                                    constraints.Append($"Mininum Length: {parameter.MinLength.Value}");
                                }
                                else if (parameter.MaxLength != null)
                                {
                                    constraints.Append($"Maximum Length: {parameter.MaxLength.Value}");
                                }
                                constraints.Append(")");
                            }
                            var message = $"{parameter.Name} [{parameter.Type}]{constraints}";
                            if (parameter.Label != null)
                            {
                                message += $": {parameter.Label}";
                            }
                            enteredValue = Settings.PromptString(message, parameter.Default, parameter.AllowedPattern, parameter.ConstraintDescription) ?? "";
                        }
                        stackParameters[parameter.Name] = new CloudFormationParameter {
                            ParameterKey   = parameter.Name,
                            ParameterValue = enteredValue
                        };
                    }
                }
            }

            // check if LambdaSharp.Core services should be enabled by default
            if (
                (Settings.CoreServices == CoreServices.Enabled) &&
                manifest.GetAllParameters().Any(p => p.Name == "LambdaSharpCoreServices") &&
                !stackParameters.Any(p => p.Value.ParameterKey == "LambdaSharpCoreServices")
                )
            {
                stackParameters.Add("LambdaSharpCoreServices", new CloudFormationParameter {
                    ParameterKey   = "LambdaSharpCoreServices",
                    ParameterValue = Settings.CoreServices.ToString()
                });
            }
            return(stackParameters.Values.ToList());

            // local functions
            bool RequiresPrompt(ModuleManifestParameter parameter)
            {
                if (parameters?.ContainsKey(parameter.Name) == true)
                {
                    // no prompt since parameter is provided explicitly
                    return(false);
                }
                if (existing?.Parameters.Any(p => p.ParameterKey == parameter.Name) == true)
                {
                    // no prompt since we can reuse the previous parameter value
                    return(false);
                }
                if (!promptAll && (parameter.Default != null))
                {
                    // no prompt since parameter has a default value
                    return(false);
                }
                if (Settings.PromptsAsErrors)
                {
                    LogError($"{manifest.GetFullName()} requires value for parameter '{parameter.Name}'");
                    return(false);
                }
                return(true);
            }
        }
Пример #5
0
        private List <CloudFormationParameter> PromptModuleParameters(
            ModuleManifest manifest,
            CloudFormationStack existing           = null,
            Dictionary <string, string> parameters = null,
            bool promptAll       = false,
            bool promptsAsErrors = false
            )
        {
            var stackParameters = new Dictionary <string, CloudFormationParameter>();

            // tentatively indicate to reuse previous parameter values
            if (existing != null)
            {
                foreach (var parameter in manifest.ParameterSections
                         .SelectMany(section => section.Parameters)
                         .Where(moduleParameter => existing.Parameters.Any(existingParameter => existingParameter.ParameterKey == moduleParameter.Name))
                         )
                {
                    stackParameters[parameter.Name] = new CloudFormationParameter {
                        ParameterKey     = parameter.Name,
                        UsePreviousValue = true
                    };
                }
            }

            // add all provided parameters
            if (parameters != null)
            {
                foreach (var parameter in parameters)
                {
                    stackParameters[parameter.Key] = new CloudFormationParameter {
                        ParameterKey   = parameter.Key,
                        ParameterValue = parameter.Value
                    };
                }
            }

            // check if module requires any prompts
            if (manifest.GetAllParameters().Any(RequiresPrompt))
            {
                Console.WriteLine();
                Console.WriteLine($"Configuration for {manifest.GetFullName()} (v{manifest.GetVersion()})");

                // only list parameter sections that contain a parameter that requires a prompt
                foreach (var parameterGroup in manifest.ParameterSections.Where(group => group.Parameters.Any(RequiresPrompt)))
                {
                    Console.WriteLine();
                    Console.WriteLine($"*** {parameterGroup.Title.ToUpper()} ***");

                    // only prompt for required parameters
                    foreach (var parameter in parameterGroup.Parameters.Where(RequiresPrompt))
                    {
                        var enteredValue = PromptString(parameter, parameter.Default) ?? "";
                        stackParameters[parameter.Name] = new CloudFormationParameter {
                            ParameterKey   = parameter.Name,
                            ParameterValue = enteredValue
                        };
                    }
                }
            }
            return(stackParameters.Values.ToList());

            // local functions
            bool RequiresPrompt(ModuleManifestParameter parameter)
            {
                if (parameters?.ContainsKey(parameter.Name) == true)
                {
                    // no prompt since parameter is provided explicitly
                    return(false);
                }
                if (existing?.Parameters.Any(p => p.ParameterKey == parameter.Name) == true)
                {
                    // no prompt since we can reuse the previous parameter value
                    return(false);
                }
                if (!promptAll && (parameter.Default != null))
                {
                    // no prompt since parameter has a default value
                    return(false);
                }
                if (promptsAsErrors)
                {
                    LogError($"{manifest.GetFullName()} requires value for parameter '{parameter.Name}'");
                    return(false);
                }
                return(true);
            }

            string PromptString(ModuleManifestParameter parameter, string defaultValue = null)
            {
                var prompt = $"|=> {parameter.Name} [{parameter.Type}]:";

                if (parameter.Label != null)
                {
                    prompt += $" {parameter.Label}:";
                }
                if (!string.IsNullOrEmpty(defaultValue))
                {
                    prompt = $"{prompt} [{defaultValue}]";
                }
                Console.Write(prompt);
                Console.Write(' ');
                SetCursorVisible(true);
                var resp = Console.ReadLine();

                SetCursorVisible(false);
                if (!string.IsNullOrEmpty(resp))
                {
                    return(resp);
                }
                return(defaultValue);

                // local functions
                void SetCursorVisible(bool visible)
                {
                    try {
                        Console.CursorVisible = visible;
                    } catch { }
                }
            }
        }
Пример #6
0
        //--- Methods ---
        public async Task <string> PublishAsync(ModuleManifest manifest, bool forcePublish)
        {
            Console.WriteLine($"Publishing module: {manifest.GetFullName()}");
            _forcePublish    = forcePublish;
            _changesDetected = false;

            // verify that all files referenced by manifest exist (NOTE: source file was already checked)
            foreach (var file in manifest.Assets)
            {
                var filepath = Path.Combine(Settings.OutputDirectory, file);
                if (!File.Exists(filepath))
                {
                    LogError($"could not find: '{filepath}'");
                }
            }
            if (Settings.HasErrors)
            {
                return(null);
            }

            // verify that manifest is either a pre-release or its version has not been published yet
            if (!manifest.Module.TryParseModuleDescriptor(
                    out string moduleOwner,
                    out string moduleName,
                    out VersionInfo moduleVersion,
                    out string _
                    ))
            {
                throw new ApplicationException("invalid module info");
            }
            var destinationKey = $"{moduleOwner}/Modules/{moduleName}/Versions/{moduleVersion}/cloudformation.json";

            // check if we want to always publish, regardless of version or detected changes
            if (!forcePublish)
            {
                // check if a manifest already exists for this version
                var existingManifest = await new ModelManifestLoader(Settings, "cloudformation.json").LoadFromS3Async(Settings.DeploymentBucketName, destinationKey, errorIfMissing: false);
                if (existingManifest != null)
                {
                    if (existingManifest.Hash == manifest.Hash)
                    {
                        // manifest matches, nothing further to do
                        Console.WriteLine($"=> No changes found to publish");
                        return($"s3://{Settings.DeploymentBucketName}/{destinationKey}");
                    }
                    else if (!moduleVersion.IsPreRelease)
                    {
                        // don't allow publishing over an existing, stable version
                        LogError($"{moduleOwner}.{moduleName} (v{moduleVersion}) is already published; use --force-publish to proceed anyway");
                        return(null);
                    }
                }
            }

            // upload assets
            for (var i = 0; i < manifest.Assets.Count; ++i)
            {
                manifest.Assets[i] = await UploadPackageAsync(manifest, manifest.Assets[i], "asset");
            }

            // upload CloudFormation template
            var template = await UploadTemplateFileAsync(manifest, "template");

            // store copy of cloudformation template under version number
            await Settings.S3Client.CopyObjectAsync(new CopyObjectRequest {
                SourceBucket      = Settings.DeploymentBucketName,
                SourceKey         = template,
                DestinationBucket = Settings.DeploymentBucketName,
                DestinationKey    = destinationKey,
                ContentType       = "application/json"
            });

            if (!_changesDetected)
            {
                // NOTE: this message should never appear since we already do a similar check earlier
                Console.WriteLine($"=> No changes found to upload");
            }
            return($"s3://{Settings.DeploymentBucketName}/{destinationKey}");
        }