Example #1
0
        /// <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));
        }
Example #5
0
        /// <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));
            }
        }