/// <summary> /// Publishes the ASP.NET Core app using the <paramref name="options"/> to produce the right deployment /// and service (if needed). /// </summary> /// <param name="projectPath">The full path to the project.json file of the startup project.</param> /// <param name="options">The options to use for the deployment.</param> /// <param name="progress">The progress interface for progress notifications.</param> /// <param name="outputAction">The output callback to invoke for output from the process.</param> /// <returns>Returns a <seealso cref="GkeDeploymentResult"/> if the deployment succeeded null otherwise.</returns> public static async Task <GkeDeploymentResult> PublishProjectAsync( string projectPath, DeploymentOptions options, IProgress <double> progress, Action <string> outputAction) { if (!File.Exists(projectPath)) { Debug.WriteLine($"Cannot find {projectPath}, not a valid project."); return(null); } var stageDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(stageDirectory); progress.Report(0.1); using (var cleanup = new Disposable(() => CommonUtils.Cleanup(stageDirectory))) { var appRootPath = Path.Combine(stageDirectory, "app"); var buildFilePath = Path.Combine(stageDirectory, "cloudbuild.yaml"); if (!await ProgressHelper.UpdateProgress( NetCoreAppUtils.CreateAppBundleAsync(projectPath, appRootPath, outputAction), progress, from: 0.1, to: 0.3)) { Debug.WriteLine("Failed to create app bundle."); return(null); } NetCoreAppUtils.CopyOrCreateDockerfile(projectPath, appRootPath); var image = CloudBuilderUtils.CreateBuildFile( project: options.GCloudContext.ProjectId, imageName: options.DeploymentName, imageVersion: options.DeploymentVersion, buildFilePath: buildFilePath); if (!await ProgressHelper.UpdateProgress( GCloudWrapper.BuildContainerAsync(buildFilePath, appRootPath, outputAction, options.GCloudContext), progress, from: 0.4, to: 0.7)) { Debug.WriteLine("Failed to build container."); return(null); } progress.Report(0.7); string publicIpAddress = null; string clusterIpAddress = null; bool deploymentUpdated = false; bool deploymentScaled = false; bool serviceExposed = false; bool serviceUpdated = false; bool serviceDeleted = false; // Create or update the deployment. var deployments = await KubectlWrapper.GetDeploymentsAsync(options.KubectlContext); var deployment = deployments?.FirstOrDefault(x => x.Metadata.Name == options.DeploymentName); if (deployment == null) { Debug.WriteLine($"Creating new deployment {options.DeploymentName}"); if (!await KubectlWrapper.CreateDeploymentAsync( name: options.DeploymentName, imageTag: image, replicas: options.Replicas, outputAction: outputAction, context: options.KubectlContext)) { Debug.WriteLine($"Failed to create deployment {options.DeploymentName}"); return(null); } progress.Report(0.8); } else { Debug.WriteLine($"Updating existing deployment {options.DeploymentName}"); if (!await KubectlWrapper.UpdateDeploymentImageAsync( options.DeploymentName, image, outputAction, options.KubectlContext)) { Debug.WriteLine($"Failed to update deployemnt {options.DeploymentName}"); return(null); } deploymentUpdated = true; // If the deployment already exists but the replicas number requested is not the // same as the existing number we will scale up/down the deployment. if (deployment.Spec.Replicas != options.Replicas) { Debug.WriteLine($"Updating the replicas for the deployment."); if (!await KubectlWrapper.ScaleDeploymentAsync( options.DeploymentName, options.Replicas, outputAction, options.KubectlContext)) { Debug.WriteLine($"Failed to scale up deployment {options.DeploymentName}"); return(null); } deploymentScaled = true; } } // Expose the service if requested and it is not already exposed. var services = await KubectlWrapper.GetServicesAsync(options.KubectlContext); var service = services?.FirstOrDefault(x => x.Metadata.Name == options.DeploymentName); if (options.ExposeService) { var requestedType = options.ExposePublicService ? GkeServiceSpec.LoadBalancerType : GkeServiceSpec.ClusterIpType; if (service != null && service?.Spec?.Type != requestedType) { Debug.WriteLine($"The existing service is {service?.Spec?.Type} the requested is {requestedType}"); if (!await KubectlWrapper.DeleteServiceAsync(options.DeploymentName, outputAction, options.KubectlContext)) { Debug.WriteLine($"Failed to delete serive {options.DeploymentName}"); } service = null; // Now the service is gone, needs to be re-created with the new options. serviceUpdated = true; } if (service == null) { // The service needs to be exposed but it wasn't. Expose a new service here. if (!await KubectlWrapper.ExposeServiceAsync( options.DeploymentName, options.ExposePublicService, outputAction, options.KubectlContext)) { Debug.WriteLine($"Failed to expose service {options.DeploymentName}"); return(null); } clusterIpAddress = await WaitForServiceClusterIpAddressAsync(options.DeploymentName, options.KubectlContext); if (options.ExposePublicService) { publicIpAddress = await WaitForServicePublicIpAddressAsync( options.DeploymentName, options.WaitingForServiceIpCallback, options.KubectlContext); } serviceExposed = true; } } else { // The user doesn't want a service exposed. if (service != null) { if (!await KubectlWrapper.DeleteServiceAsync(options.DeploymentName, outputAction, options.KubectlContext)) { Debug.WriteLine($"Failed to delete service {options.DeploymentName}"); return(null); } } serviceDeleted = true; } return(new GkeDeploymentResult( publicIpAddress: publicIpAddress, privateIpAddress: clusterIpAddress, serviceExposed: serviceExposed, serviceUpdated: serviceUpdated, serviceDeleted: serviceDeleted, deploymentUpdated: deploymentUpdated, deploymentScaled: deploymentScaled)); } }
/// <summary> /// Publishes the ASP.NET Core project to App Engine Flex. /// </summary> /// <param name="project">The project to deploy.</param> /// <param name="options">The <seealso cref="DeploymentOptions"/> to use.</param> /// <param name="progress">The progress indicator.</param> /// <param name="toolsPathProvider">The tools path provider to use.</param> /// <param name="outputAction">The action to call with lines from the command output.</param> public static async Task <AppEngineFlexDeploymentResult> PublishProjectAsync( IParsedProject project, DeploymentOptions options, IProgress <double> progress, IToolsPathProvider toolsPathProvider, Action <string> outputAction) { var stageDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(stageDirectory); progress.Report(0.1); using (var cleanup = new Disposable(() => CommonUtils.Cleanup(stageDirectory))) { // Wait for the bundle creation operation to finish, updating progress as it goes. if (!await ProgressHelper.UpdateProgress( NetCoreAppUtils.CreateAppBundleAsync(project, stageDirectory, toolsPathProvider, outputAction), progress, from: 0.1, to: 0.3)) { Debug.WriteLine("Failed to create app bundle."); return(null); } var runtime = GetAppEngineRuntime(project); CopyOrCreateAppYaml(project, stageDirectory); if (runtime == CustomRuntime) { Debug.WriteLine($"Copying Docker file to {stageDirectory} with custom runtime."); NetCoreAppUtils.CopyOrCreateDockerfile(project, stageDirectory); } else { Debug.WriteLine($"Detected runtime {runtime}"); } progress.Report(0.4); // Deploy to app engine, this is where most of the time is going to be spent. Wait for // the operation to finish, update the progress as it goes. var effectiveVersion = options.Version ?? GetDefaultVersion(); var deployTask = DeployAppBundleAsync( stageDirectory: stageDirectory, version: effectiveVersion, promote: options.Promote, context: options.Context, outputAction: outputAction); if (!await ProgressHelper.UpdateProgress(deployTask, progress, 0.6, 0.9)) { Debug.WriteLine("Failed to deploy bundle."); return(null); } progress.Report(1.0); var service = GetAppEngineService(project); return(new AppEngineFlexDeploymentResult( projectId: options.Context.ProjectId, service: service, version: effectiveVersion, promoted: options.Promote)); } }