File: Tasks\OneDeploy\OneDeploy.cs
Web Access
Project: src\src\sdk\src\WebSdk\Publish\Tasks\Microsoft.NET.Sdk.Publish.Tasks.csproj (Microsoft.NET.Sdk.Publish.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Framework;
using Microsoft.NET.Sdk.Common;
using Microsoft.NET.Sdk.Publish.Tasks.Properties;

namespace Microsoft.NET.Sdk.Publish.Tasks.OneDeploy;

/// <summary>
/// 'OneDeploy' publish task.
/// </summary>
public partial class OneDeploy : Task
{
    private const string DefaultRequestContentType = "application/zip";
    private const string UserAgentName = "websdk";
    private const string OneDeployApiPath = "api/publish";
    private const string OneDeployQueryParam = "RemoteBuild=false";

    private readonly ITaskLogger _taskLogger;

    public OneDeploy()
    {
        // logger is enabled by default
        _taskLogger = new TaskLogger(Log, true);
    }

    // Test constructor
    internal OneDeploy(ITaskLogger taskLogger)
    {
        _taskLogger = taskLogger;
    }

    [Required]
    public string? FileToPublishPath { get; set; }

    public string? PublishUrl { get; set; }

    [Required]
    public string? Username { get; set; }

    public string? Password { get; set; }

    [Required]
    public string? UserAgentVersion { get; set; }

    /// <inheritdoc/>
    public override bool Execute()
    {
        var deployTask = OneDeployAsync(FileToPublishPath, Username, Password, PublishUrl, UserAgentVersion, WebJobName, WebJobType);
        deployTask.Wait();

        return deployTask.Result;
    }

    public async Task<bool> OneDeployAsync(
        string? fileToPublishPath,
        string? username,
        string? password,
        string? url,
        string? userAgentVersion,
        string? webjobName = null,
        string? webjobType = null,
        CancellationToken cancellationToken = default)
    {
        using DefaultHttpClient httpClient = new();

        var userAgent = $"{UserAgentName}/{userAgentVersion}";
        var deployStatusService = new OneDeployStatusService(_taskLogger);

        return await OneDeployAsync(
            fileToPublishPath, username, password, url, userAgent, webjobName, webjobType, httpClient, deployStatusService, cancellationToken);
    }

    internal async Task<bool> OneDeployAsync(
        string? fileToPublishPath,
        string? username,
        string? password,
        string? url,
        string userAgent,
        string? webjobName,
        string? webjobType,
        IHttpClient httpClient,
        IDeploymentStatusService<DeploymentResponse> deploymentStatusService,
        CancellationToken cancellationToken = default)
    {
        // missing credentials
        if (string.IsNullOrEmpty(password) && !GetCredentialsFromTask(out username, out password))
        {
            _taskLogger.LogError(Resources.ONEDEPLOY_FailedToRetrieveCredentials);
            return false;
        }

        // missing file to publish
        if (!File.Exists(fileToPublishPath))
        {
            _taskLogger.LogError(Resources.ONEDEPLOY_FileToPublish_NotFound);
            return false;
        }

        // 'PublishUrl' must be valid
        if (!GetPublishUrl(url, webjobName, webjobType, out var oneDeployPublishUri) || oneDeployPublishUri is null)
        {
            _taskLogger.LogError(string.Format(Resources.ONEDEPLOY_InvalidPublishUrl, url));
            return false;
        }

        _taskLogger.LogMessage(
            MessageImportance.High,
            string.Format(Resources.ONEDEPLOY_PublishingOneDeploy, fileToPublishPath, oneDeployPublishUri.ToString()));

        var fileToPublishStream = File.OpenRead(fileToPublishPath);

        // push package to target instance
        var response = await DeployAsync(
            httpClient, oneDeployPublishUri, username, password, userAgent, fileToPublishPath, webjobName, webjobType, fileToPublishStream, cancellationToken);

        // if push failed, finish operation
        if (!(response?.IsResponseSuccessful() ?? false))
        {
            var responseText = response is null ? string.Empty : await response.GetTextResponseAsync(cancellationToken);

            var errorMessage = !string.IsNullOrEmpty(responseText)
                ? string.Format(Resources.ONEDEPLOY_FailedDeployRequest_With_ResponseText, oneDeployPublishUri, response?.StatusCode, responseText)
                : string.Format(Resources.ONEDEPLOY_FailedDeployRequest, oneDeployPublishUri, response?.StatusCode);

            _taskLogger.LogError(errorMessage);
            return false;
        }

        _taskLogger.LogMessage(string.Format(Resources.ONEDEPLOY_Uploaded, fileToPublishPath));

        // monitor deployment (if available)
        var deploymentUrl = response.GetHeader("Location").FirstOrDefault();
        if (!string.IsNullOrEmpty(deploymentUrl) && Uri.TryCreate(deploymentUrl, UriKind.Absolute, out var _))
        {
            var deploymentResponse = await deploymentStatusService.PollDeploymentAsync(httpClient, deploymentUrl, username, password, userAgent, cancellationToken);

            if (deploymentResponse?.IsSuccessfulResponse() ?? false)
            {
                _taskLogger.LogMessage(MessageImportance.High, Resources.ONEDEPLOY_Success);
                return true;
            }

            if (deploymentResponse?.IsFailedResponse() ?? true)
            {
                _taskLogger.LogError(string.Format(Resources.ONEDEPLOY_FailedWithLogs,
                    fileToPublishPath,
                    oneDeployPublishUri,
                    deploymentResponse?.Status ?? DeploymentStatus.Unknown,
                    deploymentResponse?.GetLogUrlWithId()));

                return false;
            }
        }

        // no follow-up deployment response (as in WebJobs); assume deployment worked
        _taskLogger.LogMessage(MessageImportance.High, Resources.ONEDEPLOY_Success);
        return true;
    }

    private bool GetCredentialsFromTask(out string user, out string password)
    {
        VSHostObject hostObj = new(HostObject, Log);
        if (hostObj.TryGetCredentials() is (string u, string p))
        {
            user = u;
            password = p;

            return true;
        }

        user = string.Empty;
        password = string.Empty;

        return false;
    }

    private Task<IHttpResponse?> DeployAsync(
        IHttpClient httpClient,
        Uri publishUri,
        string? username,
        string? password,
        string? userAgent,
        string? fileToPublish,
        string? webjobName,
        string? webjobType,
        FileStream fileToPublishStream,
        CancellationToken cancellationToken)
    {
        return IsWebJobProject(webjobName, webjobType)
           ? DeployWebJobAsync(httpClient, publishUri, username, password, userAgent, fileToPublish, fileToPublishStream, cancellationToken)
           : DefaultDeployAsync(httpClient, publishUri, username, password, userAgent, fileToPublishStream, cancellationToken);
    }

    private bool GetPublishUrl(string? publishUrl, string? webjobName, string? webjobType, out Uri? publishUri)
    {
        publishUri = null;

        if (publishUrl is null || publishUrl.Length == 0 ||
            !Uri.TryCreate(publishUrl, UriKind.Absolute, out var _))
        {
            return false;
        }

        return IsWebJobProject(webjobName, webjobType)
            ? GetWebJobPublishUri(publishUrl, webjobName, webjobType, out publishUri)
            : GetDefaultPublishUri(publishUrl, out publishUri);
    }

    private async Task<IHttpResponse?> DefaultDeployAsync(
        IHttpClient httpClient,
        Uri publishUri,
        string? username,
        string? password,
        string? userAgent,
        FileStream fileToPublishStream,
        CancellationToken cancellationToken)
    {
        var response = await httpClient.PostRequestAsync(
            publishUri, username, password, DefaultRequestContentType, userAgent, Encoding.UTF8, fileToPublishStream);

        return response;
    }

    private bool GetDefaultPublishUri(string publishUrl, out Uri publishUri)
    {
        publishUri = new UriBuilder(publishUrl)
        {
            Path = OneDeployApiPath,
            Query = OneDeployQueryParam,
            Port = -1
        }.Uri;

        return Uri.TryCreate(publishUri.ToString(), UriKind.Absolute, out var _);
    }
}