/// <summary> /// Determine the action to be done for the local path, like building a .NET Core package, then uploading the /// package to S3. The S3 key is returned to be updated in the template. /// </summary> /// <param name="templateDirectory"></param> /// <param name="field"></param> /// <returns></returns> /// <exception cref="LambdaToolsException"></exception> private async Task <UpdateResourceResults> ProcessUpdatableResourceAsync(string templateDirectory, IUpdateResourceField field, string[] args) { UpdateResourceResults results; var localPath = field.GetLocalPath(); if (!Path.IsPathRooted(localPath)) { localPath = Path.Combine(templateDirectory, localPath); } bool deleteArchiveAfterUploaded = false; // Uploading a single file as the code for the resource. If the single file is not a zip file then zip the file first. if (File.Exists(localPath)) { if (field.IsCode && !string.Equals(Path.GetExtension(localPath), ".zip", StringComparison.OrdinalIgnoreCase)) { this.Logger.WriteLine($"Creating zip archive for {localPath} file"); results = new UpdateResourceResults { ZipArchivePath = GenerateOutputZipFilename(field) }; LambdaPackager.BundleFiles(results.ZipArchivePath, Path.GetDirectoryName(localPath), new string[] { localPath }, this.Logger); } else { results = new UpdateResourceResults { ZipArchivePath = localPath }; } } // If IsCode is false then the local path needs to point to a file and not a directory. When IsCode is true // it can point either to a file or a directory. else if (!field.IsCode && !File.Exists(localPath)) { throw new LambdaToolsException($"File that the field {field.Resource.Name}/{field.Name} is pointing to doesn't exist", LambdaToolsException.LambdaErrorCode.ServerlessTemplateMissingLocalPath); } else if (!Directory.Exists(localPath)) { throw new LambdaToolsException($"Directory that the field {field.Resource.Name}/{field.Name} is pointing doesn't exist", LambdaToolsException.LambdaErrorCode.ServerlessTemplateMissingLocalPath); } // To maintain compatibility if the field is point to current directory or not set at all but a prepackaged zip archive is given // then use it as the package source. else if (IsCurrentDirectory(field.GetLocalPath()) && !string.IsNullOrEmpty(this.DefaultOptions.Package)) { results = new UpdateResourceResults { ZipArchivePath = this.DefaultOptions.Package }; } else if (field.IsCode) { // If the function is image upload then run the .NET tools to handle running // docker build even if the current folder is not a .NET project. The .NET // could be in a sub folder or be a self contained Docker build. if (IsDotnetProjectDirectory(localPath) || field.Resource.UploadType == CodeUploadType.Image) { results = await PackageDotnetProjectAsync(field, localPath, args); } else { results = new UpdateResourceResults { ZipArchivePath = GenerateOutputZipFilename(field) }; LambdaPackager.BundleDirectory(results.ZipArchivePath, localPath, false, this.Logger); } deleteArchiveAfterUploaded = true; } else { throw new LambdaToolsException($"Unable to determine package action for the field {field.Resource.Name}/{field.Name}", LambdaToolsException.LambdaErrorCode.ServerlessTemplateUnknownActionForLocalPath); } if (!string.IsNullOrEmpty(results.ZipArchivePath)) { string s3Key; using (var stream = File.OpenRead(results.ZipArchivePath)) { s3Key = await Utilities.UploadToS3Async(this.Logger, this.S3Client, this.S3Bucket, this.S3Prefix, Path.GetFileName(results.ZipArchivePath), stream); results.S3Key = s3Key; } // Now that the temp zip file is uploaded to S3 clean up by deleting the temp file. if (deleteArchiveAfterUploaded) { try { File.Delete(results.ZipArchivePath); } catch (Exception e) { this.Logger?.WriteLine($"Warning: Unable to delete temporary archive, {results.ZipArchivePath}, after uploading to S3: {e.Message}"); } } } return(results); }
public static int Execute(CreateLocalLayerOptions opts) { Console.WriteLine($"Creating runtime package store from manifest: {opts.Manifest}"); if (opts.EnableOptimization) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { throw new Exception($"Package optimization is only possible on Amazon Linux. To use this feature execute the command in an Amazon Linux environment."); } else { Console.WriteLine("Warning: Package optimization has been enabled. Be sure to run this on an Amazon Linux environment or the optimization might not be compatbile with the Lambda runtime."); } } if (!File.Exists(opts.Manifest)) { throw new Exception($"Can not find package manifest {opts.Manifest}. Make sure to point to a file not a directory."); } var tempDirectoryName = $"{opts.StoreName}-{DateTime.UtcNow.Ticks}".ToLower(); var tempRootPath = Path.Combine(Path.GetTempPath(), tempDirectoryName); var storeOutputDirectory = Path.Combine(tempRootPath, Common.Constants.DEFAULT_LAYER_OPT_DIRECTORY); var convertResult = ManifestUtilities.ConvertManifestToSdkManifest(opts.Manifest); if (convertResult.ShouldDelete) { Console.WriteLine("Converted ASP.NET Core project file to temporary package manifest file."); } var cliWrapper = new LambdaDotNetCLIWrapper(Directory.GetCurrentDirectory()); var storeResult = cliWrapper.Store( !string.IsNullOrEmpty(opts.ProjectLocation) ? opts.ProjectLocation : Directory.GetCurrentDirectory(), storeOutputDirectory, opts.TargetFramework, convertResult.PackageManifest, opts.EnableOptimization); if (storeResult.exitCode != 0) { throw new Exception($"Error executing the 'dotnet store' command"); } var manifest = ProjectUtilities.FindArtifactOutput(storeResult.filePath); //string[] files = Directory.GetFiles(storeResult.filePath, "artifact.xml", SearchOption.AllDirectories); var updatedContent = File.ReadAllText(manifest); var manifestPath = Path.Combine(Directory.GetCurrentDirectory(), $"{opts.StoreName}.xml"); File.WriteAllText(manifestPath, updatedContent); Console.WriteLine($"Created package manifest file ({manifestPath})"); if (convertResult.ShouldDelete) { File.Delete(convertResult.PackageManifest); } var zipPath = Path.Combine(Directory.GetCurrentDirectory(), $"{opts.StoreName}.zip"); if (File.Exists(zipPath)) { File.Delete(zipPath); } LambdaPackager.BundleDirectory(zipPath, tempRootPath, false); return(0); }
private async Task <CreateLayerZipFileResult> CreateRuntimePackageStoreLayerZipFile(string layerName, string s3Prefix) { var targetFramework = this.GetStringValueOrDefault(this.TargetFramework, CommonDefinedCommandOptions.ARGUMENT_FRAMEWORK, true); var projectLocation = this.GetStringValueOrDefault(this.ProjectLocation, CommonDefinedCommandOptions.ARGUMENT_PROJECT_LOCATION, false); var enableOptimization = this.GetBoolValueOrDefault(this.EnablePackageOptimization, LambdaDefinedCommandOptions.ARGUMENT_ENABLE_PACKAGE_OPTIMIZATION, false).GetValueOrDefault(); if (string.Equals(targetFramework, "netcoreapp3.1")) { var version = DotNetCLIWrapper.GetSdkVersion(); // .NET SDK 3.1 versions less then 3.1.400 have an issue throwing NullReferenceExceptions when pruning packages out with the manifest. // https://github.com/dotnet/sdk/issues/10973 if (version < Version.Parse("3.1.400")) { var message = $"Publishing runtime package store layers targeting .NET Core 3.1 requires at least version 3.1.400 of the .NET SDK. Current version installed is {version}."; throw new LambdaToolsException(message, LambdaToolsException.LambdaErrorCode.DisabledSupportForNET31Layers); } } #if NETCORE if (enableOptimization) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { throw new LambdaToolsException($"Package optimization is only possible on Amazon Linux. To use this feature execute the command in an Amazon Linux environment.", LambdaToolsException.LambdaErrorCode.UnsupportedOptimizationPlatform); } else { this.Logger.WriteLine("Warning: Package optimization has been enabled. Be sure to run this on an Amazon Linux environment or the optimization might not be compatbile with the Lambda runtime."); } } #else // This is the case the code is run in the AWS Toolkit for Visual Studio which will never run on Amazon Linux. enableOptimization = false; #endif // This is the manifest that list the NuGet packages via <PackageReference> elements in the msbuild project file. var packageManifest = this.GetStringValueOrDefault(this.PackageManifest, LambdaDefinedCommandOptions.ARGUMENT_PACKAGE_MANIFEST, false); // If this is null attempt to use the current directory. This is likely if the intent is to make a // layer from the current Lambda project. if (string.IsNullOrEmpty(packageManifest)) { packageManifest = Utilities.DetermineProjectLocation(this.WorkingDirectory, projectLocation); } // If this is a directory look to see if there is a single csproj of fsproj in the directory in use that. // This is to make it easy to make a layer in the current directory of a Lambda function. if (Directory.Exists(packageManifest)) { var files = Directory.GetFiles(packageManifest, "*.csproj"); if (files.Length == 1) { packageManifest = Path.Combine(packageManifest, files[0]); } else if (files.Length == 0) { files = Directory.GetFiles(packageManifest, "*.fsproj"); if (files.Length == 1) { packageManifest = Path.Combine(packageManifest, files[0]); } } } if (!File.Exists(packageManifest)) { throw new LambdaToolsException($"Can not find package manifest {packageManifest}. Make sure to point to a file not a directory.", LambdaToolsException.LambdaErrorCode.LayerPackageManifestNotFound); } // Create second subdirectory so that when the directory is zipped the sub directory is retained in the zip file. // The sub directory will be created in the /opt directory in the Lambda environment. var tempDirectoryName = $"{layerName}-{DateTime.UtcNow.Ticks}".ToLower(); var optDirectory = this.GetStringValueOrDefault(this.OptDirectory, LambdaDefinedCommandOptions.ARGUMENT_OPT_DIRECTORY, false); if (string.IsNullOrEmpty(optDirectory)) { optDirectory = LambdaConstants.DEFAULT_LAYER_OPT_DIRECTORY; } var tempRootPath = Path.Combine(Path.GetTempPath(), tempDirectoryName); var storeOutputDirectory = Path.Combine(tempRootPath, optDirectory); { var convertResult = LambdaUtilities.ConvertManifestToSdkManifest(targetFramework, packageManifest); if (convertResult.ShouldDelete) { this.Logger?.WriteLine("Converted ASP.NET Core project file to temporary package manifest file."); } var architecture = this.GetStringValueOrDefault(this.Architecture, LambdaDefinedCommandOptions.ARGUMENT_FUNCTION_ARCHITECTURE, false); var cliWrapper = new LambdaDotNetCLIWrapper(this.Logger, this.WorkingDirectory); if (cliWrapper.Store(defaults: this.DefaultConfig, projectLocation: projectLocation, outputLocation: storeOutputDirectory, targetFramework: targetFramework, packageManifest: convertResult.PackageManifest, architecture: architecture, enableOptimization: enableOptimization) != 0) { throw new LambdaToolsException($"Error executing the 'dotnet store' command", LambdaToolsException.LambdaErrorCode.StoreCommandError); } if (convertResult.ShouldDelete) { File.Delete(convertResult.PackageManifest); } } // The artifact.xml file is generated by the "dotnet store" command that lists the packages that were added to the store. // It is required during a "dotnet publish" so the NuGet packages in the store will be filtered out. var artifactXmlPath = Path.Combine(storeOutputDirectory, "x64", targetFramework, "artifact.xml"); if (!File.Exists(artifactXmlPath)) { throw new LambdaToolsException($"Failed to find artifact.xml file in created local store.", LambdaToolsException.LambdaErrorCode.FailedToFindArtifactZip); } this.Logger.WriteLine($"Uploading runtime package store manifest to S3"); var s3Key = await UploadFile(artifactXmlPath, $"{s3Prefix}artifact.xml"); this.Logger.WriteLine($"Create zip file of runtime package store directory"); var zipPath = Path.Combine(Path.GetTempPath(), $"{layerName}-{DateTime.UtcNow.Ticks}.zip"); if (File.Exists(zipPath)) { File.Delete(zipPath); } LambdaPackager.BundleDirectory(zipPath, tempRootPath, false, this.Logger); var result = new CreateLayerZipFileResult { ZipFile = zipPath, LayerDirectory = optDirectory }; var s3Bucket = this.GetStringValueOrDefault(this.S3Bucket, LambdaDefinedCommandOptions.ARGUMENT_S3_BUCKET, true); // Set the description field to the our JSON layer manifest file so when the tooling is used // to create a package of a Lambda function in the future the artifact.xml file can be used during "dotnet publish". result.Description = GeneratorRuntimePackageManifestLayerDescription(optDirectory, s3Bucket, s3Key, enableOptimization); var compatibleRuntime = LambdaUtilities.DetermineLambdaRuntimeFromTargetFramework(targetFramework); if (!string.IsNullOrEmpty(compatibleRuntime)) { result.CompatibleRuntimes = new List <string>() { compatibleRuntime }; } return(result); }
/// <summary> /// Determine the action to be done for the local path, like building a .NET Core package, then uploading the /// package to S3. The S3 key is returned to be updated in the template. /// </summary> /// <param name="templateDirectory"></param> /// <param name="field"></param> /// <returns></returns> /// <exception cref="LambdaToolsException"></exception> private async Task <string> ProcessUpdatableResourceAsync(string templateDirectory, IUpdateResourceField field) { var localPath = field.GetLocalPath(); if (!Path.IsPathRooted(localPath)) { localPath = Path.Combine(templateDirectory, localPath); } bool deleteArchiveAfterUploaded = false; string zipArchivePath; if (File.Exists(localPath)) { if (field.IsCode && !string.Equals(Path.GetExtension(localPath), ".zip", StringComparison.OrdinalIgnoreCase)) { this.Logger.WriteLine($"Creating zip archive for {localPath} file"); zipArchivePath = GenerateOutputZipFilename(field); LambdaPackager.BundleFiles(zipArchivePath, Path.GetDirectoryName(localPath), new string[] { localPath }, this.Logger); } else { zipArchivePath = localPath; } } // If IsCode is false then the local path needs to point to a file and not a directory. When IsCode is true // it can point either to a file or a directory. else if (!field.IsCode && !File.Exists(localPath)) { throw new LambdaToolsException($"File that the field {field.Resource.Name}/{field.Name} is pointing to doesn't exist", LambdaToolsException.LambdaErrorCode.ServerlessTemplateMissingLocalPath); } else if (!Directory.Exists(localPath)) { throw new LambdaToolsException($"Directory that the field {field.Resource.Name}/{field.Name} is pointing doesn't exist", LambdaToolsException.LambdaErrorCode.ServerlessTemplateMissingLocalPath); } // To maintain compatibility if the field is point to current directory or not set at all but a prepackaged zip archive is given // then use it as the package source. else if (IsCurrentDirectory(field.GetLocalPath()) && !string.IsNullOrEmpty(this.DefaultOptions.Package)) { zipArchivePath = this.DefaultOptions.Package; } else if (field.IsCode) { if (IsDotnetProjectDirectory(localPath)) { zipArchivePath = await PackageDotnetProjectAsync(field, localPath); } else { zipArchivePath = GenerateOutputZipFilename(field); LambdaPackager.BundleDirectory(zipArchivePath, localPath, false, this.Logger); } deleteArchiveAfterUploaded = true; } else { throw new LambdaToolsException($"Unable to determine package action for the field {field.Resource.Name}/{field.Name}", LambdaToolsException.LambdaErrorCode.ServerlessTemplateUnknownActionForLocalPath); } string s3Key; using (var stream = File.OpenRead(zipArchivePath)) { s3Key = await Utilities.UploadToS3Async(this.Logger, this.S3Client, this.S3Bucket, this.S3Prefix, Path.GetFileName(zipArchivePath), stream); } // Now that the temp zip file is uploaded to S3 clean up by deleting the temp file. if (deleteArchiveAfterUploaded) { try { File.Delete(zipArchivePath); } catch (Exception e) { this.Logger?.WriteLine($"Warning: Unable to delete temporary archive, {zipArchivePath}, after uploading to S3: {e.Message}"); } } return(s3Key); }