/// <summary> /// This method stages the application into the <paramref name="stageDirectory"/> by invoking the WebPublish target /// present in all Web projects. It publishes to the staging directory by using the FileSystem method. /// </summary> private static async Task <bool> CreateAppBundleAsync( IParsedProject project, string stageDirectory, IToolsPathProvider toolsPathProvider, Action <string> outputAction) { var arguments = $@"""{project.FullPath}""" + " " + "/p:Configuration=Release " + "/p:Platform=AnyCPU " + "/t:WebPublish " + "/p:WebPublishMethod=FileSystem " + "/p:DeleteExistingFiles=True " + $@"/p:publishUrl=""{stageDirectory}"""; outputAction($"msbuild.exe {arguments}"); bool result = await ProcessUtils.RunCommandAsync(toolsPathProvider.GetMsbuildPath(), arguments, (o, e) => outputAction(e.Line)); // We perform this check here because it is not required to have gcloud installed in order to deploy // ASP.NET 4.x apps to GCE VMs. Therefore nothing would have checked for the presence of gcloud before // getting here. var gcloudValidation = await GCloudWrapper.ValidateGCloudAsync(); Debug.WriteLineIf(!gcloudValidation.IsValid, "Skipping creating context, gcloud is not installed."); if (gcloudValidation.IsValid) { await GCloudWrapper.GenerateSourceContext(project.DirectoryPath, stageDirectory); } return(result); }
/// <summary> /// Creates an app bundle by publishing it to the given directory. /// </summary> /// <param name="project">The project.</param> /// <param name="stageDirectory">The directory to which to publish.</param> /// <param name="pathsProvider">The provider for paths.</param> /// <param name="outputAction">The callback to call with output from the command.</param> /// <param name="configuration">The name of the configuration to publish.</param> public static async Task <bool> CreateAppBundleAsync( IParsedProject project, string stageDirectory, IToolsPathProvider pathsProvider, Action <string> outputAction, string configuration) { var arguments = $"publish -o \"{stageDirectory}\" -c {configuration}"; var externalTools = pathsProvider.GetExternalToolsPath(); var workingDir = project.DirectoryPath; var env = new Dictionary <string, string> { { "PATH", $"{Environment.GetEnvironmentVariable("PATH")};{externalTools}" } }; Debug.WriteLine($"Using tools from {externalTools}"); Debug.WriteLine($"Setting working directory to {workingDir}"); Directory.CreateDirectory(stageDirectory); outputAction($"dotnet {arguments}"); bool result = await ProcessUtils.Default.RunCommandAsync( file : pathsProvider.GetDotnetPath(), args : arguments, workingDir : workingDir, handler : (o, e) => outputAction(e.Line), environment : env); await GCloudWrapper.GenerateSourceContextAsync(project.DirectoryPath, stageDirectory); return(result); }
public NetCoreAppUtils(Lazy <IProcessService> processService, Lazy <IFileSystem> fileSystem, Lazy <IGCloudWrapper> gcloudWrapper, Lazy <IEnvironment> environment) { _processService = processService; _fileSystem = fileSystem; _gcloudWrapper = gcloudWrapper; _environment = environment; _toolsPathProvider = VsVersionUtils.ToolsPathProvider; }
public void TestGetToolsPathProvider_GetsVs15Provider() { PackageMock.Setup(p => p.VsVersion).Returns(VsVersionUtils.VisualStudio2017Version); IToolsPathProvider result = VsVersionUtils.GetToolsPathProvider(); Assert.IsInstanceOfType(result, typeof(GoogleCloudExtension.VsVersion.VS15.ToolsPathProvider)); }
/// <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> private async Task <AppEngineFlexDeploymentResult> PublishProjectAsync( IParsedProject project, DeploymentOptions options, IProgress <double> progress, IToolsPathProvider toolsPathProvider, Action <string> outputAction) { string stageDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(stageDirectory); progress.Report(0.1); using (new Disposable(() => CommonUtils.Cleanup(stageDirectory))) { // Wait for the bundle creation operation to finish, updating progress as it goes. Task <bool> createAppBundleTask = NetCoreAppUtils.CreateAppBundleAsync(project, stageDirectory, toolsPathProvider, outputAction, options.Configuration); if (!await ProgressHelper.UpdateProgress(createAppBundleTask, progress, from: 0.1, to: 0.3)) { Debug.WriteLine("Failed to create app bundle."); return(null); } string runtime = ConfigurationService.GetAppEngineRuntime(project); ConfigurationService.CopyOrCreateAppYaml(project, stageDirectory, options.Service); if (runtime == AppEngineConfiguration.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. Task <bool> deployTask = DeployAppBundleAsync( stageDirectory: stageDirectory, version: options.Version, 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); string service = options.Service ?? ConfigurationService.GetAppEngineService(project); return(new AppEngineFlexDeploymentResult( projectId: options.Context.ProjectId, service: service, version: options.Version, promoted: options.Promote)); } }
/// <summary> /// This method publishes the app to the VM using the <paramref name="publishSettingsPath"/> to find the publish /// settings to use to do so. /// </summary> private static Task <bool> DeployAppAsync( string stageDirectory, string publishSettingsPath, IToolsPathProvider toolsPathProvider, Action <string> outputAction) { var arguments = "-verb:sync " + $@"-source:contentPath=""{stageDirectory}"" " + $@"-dest:contentPath=""Default Web Site"",publishSettings=""{publishSettingsPath}"" " + "-allowUntrusted"; outputAction($"msdeploy.exe {arguments}"); return(ProcessUtils.RunCommandAsync(toolsPathProvider.GetMsdeployPath(), arguments, (o, e) => outputAction(e.Line))); }
/// <summary> /// Publishes an ASP.NET 4.x project to the given GCE <seealso cref="Instance"/>. /// </summary> /// <param name="project">The project to deploy.</param> /// <param name="targetInstance">The instance to deploy.</param> /// <param name="credentials">The Windows credentials to use to deploy to the <paramref name="targetInstance"/>.</param> /// <param name="progress">The progress indicator.</param> /// <param name="toolsPathProvider">Povides the path to the publishing tools.</param> /// <param name="outputAction">The action to call with lines of output.</param> public static async Task <bool> PublishProjectAsync( IParsedProject project, Instance targetInstance, WindowsInstanceCredentials credentials, IProgress <double> progress, IToolsPathProvider toolsPathProvider, Action <string> outputAction) { var stagingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(stagingDirectory); progress.Report(0.1); var publishSettingsPath = Path.GetTempFileName(); var publishSettingsContent = targetInstance.GeneratePublishSettings(credentials.User, credentials.Password); File.WriteAllText(publishSettingsPath, publishSettingsContent); using (var cleanup = new Disposable(() => Cleanup(publishSettingsPath, stagingDirectory))) { // Wait for the bundle operation to finish and update the progress in the mean time to show progress. if (!await ProgressHelper.UpdateProgress( CreateAppBundleAsync(project, stagingDirectory, toolsPathProvider, outputAction), progress, from: 0.1, to: 0.5)) { return(false); } progress.Report(0.6); // Update for the deploy operation to finish and update the progress as it goes. if (!await ProgressHelper.UpdateProgress( DeployAppAsync(stagingDirectory, publishSettingsPath, toolsPathProvider, outputAction), progress, from: 0.6, to: 0.9)) { return(false); } progress.Report(1); } return(true); }
/// <summary> /// This method stages the application into the <paramref name="stageDirectory"/> by invoking the WebPublish target /// present in all Web projects. It publishes to the staging directory by using the FileSystem method. /// </summary> private static async Task <bool> CreateAppBundleAsync( IParsedProject project, string stageDirectory, IToolsPathProvider toolsPathProvider, Action <string> outputAction) { var arguments = $@"""{project.FullPath}""" + " " + "/p:Configuration=Release " + "/p:Platform=AnyCPU " + "/t:WebPublish " + "/p:WebPublishMethod=FileSystem " + "/p:DeleteExistingFiles=True " + $@"/p:publishUrl=""{stageDirectory}"""; outputAction($"msbuild.exe {arguments}"); bool result = await ProcessUtils.RunCommandAsync(toolsPathProvider.GetMsbuildPath(), arguments, (o, e) => outputAction(e.Line)); await GCloudWrapper.GenerateSourceContext(project.DirectoryPath, stageDirectory); return(result); }
/// <summary> /// Publishes the ASP.NET Core app using the <paramref name="options"/> to produce the right deployment /// and service (if needed). /// </summary> /// <param name="project">The 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="toolsPathProvider">Provides the path to the publish tools.</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( 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))) { var appRootPath = Path.Combine(stageDirectory, "app"); var buildFilePath = Path.Combine(stageDirectory, "cloudbuild.yaml"); if (!await ProgressHelper.UpdateProgress( NetCoreAppUtils.CreateAppBundleAsync(project, appRootPath, toolsPathProvider, outputAction), progress, from: 0.1, to: 0.3)) { Debug.WriteLine("Failed to create app bundle."); return(null); } NetCoreAppUtils.CopyOrCreateDockerfile(project, 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)); } }