Skip to content

Commit

Permalink
Zip push-to-deploy feature (projectkudu#2570)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickwalkmsft authored Oct 3, 2017
1 parent 1d5f917 commit 82791e9
Show file tree
Hide file tree
Showing 75 changed files with 1,680 additions and 610 deletions.
2 changes: 2 additions & 0 deletions Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public static class Constants
public const string WebRoot = "wwwroot";
public const string MappedSite = "/_app";
public const string RepositoryPath = "repository";
public const string ZipTempPath = "zipdeploy";
public const string ZipExtractPath = "extracted";

public const string LockPath = "locks";
public const string DeploymentLockFile = "deployments.lock";
Expand Down
40 changes: 40 additions & 0 deletions Kudu.Client/Deployment/RemotePushDeploymentManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.IO;
using System.Net;
using System.Net.Http;
using Kudu.Client.Infrastructure;
using System;
using System.Threading.Tasks;

namespace Kudu.Client.Deployment
{
public class RemotePushDeploymentManager : KuduRemoteClientBase
{
public RemotePushDeploymentManager(string serviceUrl, ICredentials credentials = null, HttpMessageHandler handler = null)
: base(serviceUrl, credentials, handler)
{
}

public async Task<HttpResponseMessage> PushDeployFromStream(Stream zipFile, bool doAsync = false)
{
using (var request = new HttpRequestMessage())
{
if (doAsync)
{
request.RequestUri = new Uri(Client.BaseAddress + "?isAsync=true");
}

request.Method = HttpMethod.Post;
request.Content = new StreamContent(zipFile);
return await Client.SendAsync(request);
}
}

public async Task<HttpResponseMessage> PushDeployFromFile(string path, bool doAsync = false)
{
using (var stream = File.OpenRead(path))
{
return await PushDeployFromStream(stream, doAsync);
}
}
}
}
1 change: 1 addition & 0 deletions Kudu.Client/Kudu.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Link>Properties\CommonAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Command\RemoteCommandExecutor.cs" />
<Compile Include="Deployment\RemotePushDeploymentManager.cs" />
<Compile Include="Deployment\RemoteFetchManager.cs" />
<Compile Include="Diagnostics\RemoteRuntimeManager.cs" />
<Compile Include="Diagnostics\RemoteLogFilesManager.cs" />
Expand Down
48 changes: 48 additions & 0 deletions Kudu.Contracts/Deployment/DeploymentInfoBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using Kudu.Core.SourceControl;
using Kudu.Contracts.Tracing;
using System.Threading.Tasks;

namespace Kudu.Core.Deployment
{
public abstract class DeploymentInfoBase
{
public delegate Task FetchDelegate(IRepository repository, DeploymentInfoBase deploymentInfo, string targetBranch, ILogger logger, ITracer tracer);

protected DeploymentInfoBase()
{
IsReusable = true;
AllowDeferredDeployment = true;
DoFullBuildByDefault = true;
}

public RepositoryType RepositoryType { get; set; }
public string RepositoryUrl { get; set; }
public string Deployer { get; set; }
public ChangeSet TargetChangeset { get; set; }
public bool IsReusable { get; set; }
// Allow deferred deployment via marker file mechanism.
public bool AllowDeferredDeployment { get; set; }
// indicating that this is a CI triggered by SCM provider
public bool IsContinuous { get; set; }
public FetchDelegate Fetch { get; set; }
public bool AllowDeploymentWhileScmDisabled { get; set; }

// this is only set by GenericHandler
// the RepositoryUrl can specify specific commitid to deploy
// for instance, http://github.com/kuduapps/hellokudu.git#<commitid>
public string CommitId { get; set; }

// Can set to false for deployments where we assume that the repository contains the entire
// built site, meaning we can skip app stack detection and simply use BasicBuilder
// (KuduSync only)
public bool DoFullBuildByDefault { get; set; }

public bool IsValid()
{
return !String.IsNullOrEmpty(Deployer);
}

public abstract IRepository GetRepository();
}
}
13 changes: 13 additions & 0 deletions Kudu.Contracts/Deployment/FetchDeploymentRequestResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Kudu.Core.Deployment
{
public enum FetchDeploymentRequestResult
{
Unknown = 0,
ForbiddenScmDisabled,
RunningAynschronously,
ConflictAutoSwapOngoing,
RanSynchronously,
Pending,
ConflictDeploymentInProgress
}
}
2 changes: 1 addition & 1 deletion Kudu.Contracts/Deployment/IDeploymentManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public interface IDeploymentManager
IEnumerable<LogEntry> GetLogEntryDetails(string id, string logId);

void Delete(string id);
Task DeployAsync(IRepository repository, ChangeSet changeSet, string deployer, bool clean, bool needFileUpdate = true);
Task DeployAsync(IRepository repository, ChangeSet changeSet, string deployer, bool clean, bool needFileUpdate = true, bool fullBuildByDefault = true);

/// <summary>
/// Creates a temporary deployment that is used as a placeholder until changeset details are available.
Expand Down
7 changes: 1 addition & 6 deletions Kudu.Contracts/Deployment/IDetailedLogger.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using Kudu.Core.Deployment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace Kudu.Core.Deployment
{
Expand Down
14 changes: 14 additions & 0 deletions Kudu.Contracts/Deployment/IFetchDeploymentManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Threading.Tasks;

namespace Kudu.Core.Deployment
{
public interface IFetchDeploymentManager
{
Task<FetchDeploymentRequestResult> FetchDeploy(
DeploymentInfoBase deployInfo,
bool asyncRequested,
Uri requestUri,
string targetBranch);
}
}
1 change: 1 addition & 0 deletions Kudu.Contracts/IEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public interface IEnvironment
string LocksPath { get; } // e.g. /site/locks
string SSHKeyPath { get; }
string TempPath { get; }
string ZipTempPath { get; } // e.g. ${TempPath}/zipdeploy
string ScriptPath { get; }
string NodeModulesPath { get; }
string LogFilesPath { get; } // e.g. /logfiles
Expand Down
28 changes: 28 additions & 0 deletions Kudu.Contracts/Infrastructure/StringUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,33 @@ public static bool IsTrueLike(string value)
{
return !String.IsNullOrEmpty(value) && (value == "1" || value.Equals(Boolean.TrueString, StringComparison.OrdinalIgnoreCase));
}

// Careful... returns "true" if the string value should be considered a boolean "false".
public static bool IsFalseLike(string value)
{
return !String.IsNullOrEmpty(value) && (value == "0" || value.Equals(Boolean.FalseString, StringComparison.OrdinalIgnoreCase));
}

// Like bool.TryParse but accepts "0" and "1" as well.
// This is for when there is no hard default and we need to see if the configured
// value is a confirmed "true" or "false" in order to override behavior
public static bool TryParseBoolean(string value, out bool result)
{
if (IsTrueLike(value))
{
result = true;
return true;
}
else if (IsFalseLike(value))
{
result = false;
return true;
}
else
{
result = false;
return false;
}
}
}
}
3 changes: 3 additions & 0 deletions Kudu.Contracts/Kudu.Contracts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,17 @@
<Compile Include="Commands\CommandResult.cs" />
<Compile Include="Commands\ICommandExecutor.cs" />
<Compile Include="Deployment\DeploymentFailedException.cs" />
<Compile Include="Deployment\DeploymentInfoBase.cs" />
<Compile Include="Deployment\DeployResult.cs" />
<Compile Include="Deployment\DeployStatus.cs" />
<Compile Include="Deployment\FetchDeploymentRequestResult.cs" />
<Compile Include="Deployment\IDeploymentEnvironment.cs" />
<Compile Include="Deployment\IDeploymentManager.cs" />
<Compile Include="Deployment\IDeploymentManagerFactory.cs" />
<Compile Include="Deployment\IDeploymentStatusManager.cs" />
<Compile Include="Deployment\IDeploymentStatusFile.cs" />
<Compile Include="Deployment\IDetailedLogger.cs" />
<Compile Include="Deployment\IFetchDeploymentManager.cs" />
<Compile Include="Deployment\ILogger.cs" />
<Compile Include="Deployment\LogEntry.cs" />
<Compile Include="Deployment\LogEntryType.cs" />
Expand Down
13 changes: 11 additions & 2 deletions Kudu.Contracts/Settings/DeploymentSettingsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public static string GetRepositoryPath(this IDeploymentSettingsManager settings)
}

// in case of no repository, we will default to webroot (preferring inplace).
if (settings.IsNullRepository())
if (settings.NoRepository())
{
return Constants.WebRoot;
}
Expand All @@ -171,7 +171,7 @@ public static string GetTargetPath(this IDeploymentSettingsManager settings)
return null;
}

public static bool IsNullRepository(this IDeploymentSettingsManager settings)
public static bool NoRepository(this IDeploymentSettingsManager settings)
{
return settings.GetValue(SettingsKeys.NoRepository) == "1";
}
Expand Down Expand Up @@ -233,5 +233,14 @@ public static bool RestartAppContainerOnGitDeploy(this IDeploymentSettingsManage
// Default is true
return value == null || StringUtils.IsTrueLike(value);
}

public static bool DoBuildDuringDeployment(this IDeploymentSettingsManager settings)
{
string value = settings.GetValue(SettingsKeys.DoBuildDuringDeployment);

// A default value should be set on a per-deployment basis depending on the context, but
// returning true by default here as an indicator of generally expected behavior
return value == null || StringUtils.IsTrueLike(value);
}
}
}
1 change: 1 addition & 0 deletions Kudu.Contracts/Settings/SettingsKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ public static class SettingsKeys
public const string MaxRandomDelayInSec = "SCM_MAX_RANDOM_START_DELAY";
public const string DockerCiEnabled = "DOCKER_ENABLE_CI";
public const string LinuxRestartAppContainerAfterDeployment = "SCM_RESTART_APP_CONTAINER_AFTER_DEPLOYMENT";
public const string DoBuildDuringDeployment = "SCM_DO_BUILD_DURING_DEPLOYMENT";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,48 @@
using Kudu.Contracts.Settings;
using Kudu.Contracts.SourceControl;
using Kudu.Contracts.Tracing;
using Kudu.Core;
using Kudu.Core.Deployment;
using Kudu.Core.Infrastructure;
using Kudu.Core.SourceControl;
using Kudu.Services.ServiceHookHandlers;
using Moq;
using Xunit;
using Kudu.Services.ServiceHookHandlers;

namespace Kudu.Services.Test
namespace Kudu.Core.Test
{
public class FetchHandlerFacts
public class FetchDeploymentManagerFacts
{
[Theory]
[MemberData("Scenarios")]
public async Task FetchHandlerBasicTests(IScenario scenario)
public async Task FetchDeploymentManagerBasicTests(IScenario scenario)
{
// Arrange
var siteRoot = @"x:\vdir0\site";
var deploymentManager = new MockDeploymentManager();
var fileSystem = GetFileSystem(siteRoot, scenario.WriteTimeUTCs.ToArray());
var environment = GetEnvironment(siteRoot);
var serviceHookHandler = GetServiceHookHandler();
var repositoryFactory = GetRepositoryFactory();
var handler = CreateFetchHandler(deploymentManager: deploymentManager,
var handler = CreateFetchDeploymentManager(deploymentManager: deploymentManager,
fileSystem: fileSystem.Object,
serviceHookHandler: serviceHookHandler.Object,
repositoryFactory: repositoryFactory.Object,
environment: environment.Object);

// Test
await handler.PerformDeployment(new DeploymentInfo
await handler.PerformDeployment(new DeploymentInfo(repositoryFactory.Object)
{
IsReusable = scenario.IsReusable,
Handler = serviceHookHandler.Object,
Fetch = FakeFetch,
TargetChangeset = GetChangeSet()
});

// Assert
Assert.Equal(scenario.DeployCount, deploymentManager.DeployCount);
}

public static Task FakeFetch(IRepository repository, DeploymentInfoBase deploymentInfo, string targetBranch, ILogger logger, ITracer tracer)
{
return Task.FromResult(0);
}

public static IEnumerable<object[]> Scenarios
{
get
Expand Down Expand Up @@ -87,25 +88,23 @@ public class NotReusableConcurrentScenario : IScenario
public int DeployCount { get { return 1; } }
}

private FetchHandler CreateFetchHandler(ITracer tracer = null,
private FetchDeploymentManager CreateFetchDeploymentManager(ITracer tracer = null,
IDeploymentManager deploymentManager = null,
IDeploymentSettingsManager settings = null,
IDeploymentStatusManager status = null,
IOperationLock deploymentLock = null,
IEnvironment environment = null,
IServiceHookHandler serviceHookHandler = null,
IRepositoryFactory repositoryFactory = null,
IFileSystem fileSystem = null)
{
FileSystemHelpers.Instance = fileSystem ?? Mock.Of<IFileSystem>();
return new FetchHandler(tracer ?? Mock.Of<ITracer>(),
deploymentManager ?? Mock.Of<IDeploymentManager>(),
settings ?? Mock.Of<IDeploymentSettingsManager>(),
status ?? Mock.Of<IDeploymentStatusManager>(),
deploymentLock ?? Mock.Of<IOperationLock>(),
environment ?? Mock.Of<IEnvironment>(),
new[] { serviceHookHandler ?? Mock.Of<IServiceHookHandler>() },
repositoryFactory ?? Mock.Of<IRepositoryFactory>());

return new FetchDeploymentManager(
settings ?? Mock.Of<IDeploymentSettingsManager>(),
environment ?? Mock.Of<IEnvironment>(),
tracer ?? Mock.Of<ITracer>(),
deploymentLock ?? Mock.Of<IOperationLock>(),
deploymentManager ?? Mock.Of<IDeploymentManager>(),
status ?? Mock.Of<IDeploymentStatusManager>());
}

private Mock<IFileSystem> GetFileSystem(string siteRoot, params DateTime[] writeTimeUtcs)
Expand All @@ -126,14 +125,6 @@ private Mock<IFileSystem> GetFileSystem(string siteRoot, params DateTime[] write
return fileSystem;
}

private Mock<IServiceHookHandler> GetServiceHookHandler()
{
var handler = new Mock<IServiceHookHandler>();
handler.Setup(h => h.Fetch(It.IsAny<IRepository>(), It.IsAny<DeploymentInfo>(), It.IsAny<string>(), It.IsAny<ILogger>(), It.IsAny<ITracer>()))
.Returns(Task.FromResult(0));
return handler;
}

private static ChangeSet GetChangeSet()
{
return new ChangeSet(Guid.NewGuid().ToString(), null, null, null, DateTimeOffset.Now);
Expand Down Expand Up @@ -189,7 +180,7 @@ public void Delete(string id)
throw new NotImplementedException();
}

public Task DeployAsync(IRepository repository, ChangeSet changeSet, string deployer, bool clean, bool needFileUpdate = true)
public Task DeployAsync(IRepository repository, ChangeSet changeSet, string deployer, bool clean, bool needFileUpdate = true, bool fullBuildByDefault = true)
{
++DeployCount;
return Task.FromResult(1);
Expand Down
Loading

0 comments on commit 82791e9

Please sign in to comment.