diff --git a/src/AppHarbor/AccessTokenHelper.cs b/src/AppHarbor/AccessTokenHelper.cs new file mode 100644 index 0000000..4f72d49 --- /dev/null +++ b/src/AppHarbor/AccessTokenHelper.cs @@ -0,0 +1,34 @@ +using RestSharp; +using RestSharp.Contrib; + +namespace AppHarbor +{ + class AccessTokenHelper + { + /// + /// Get access token. + /// + /// + /// + /// + public static string GetAccessToken(string username, string password) + { + //NOTE: Remove when merged into AppHarbor.NET library + var restClient = new RestClient("https://appharbor-token-client.apphb.com"); + var request = new RestRequest("/token", Method.POST); + + request.AddParameter("username", username); + request.AddParameter("password", password); + + var response = restClient.Execute(request); + var accessToken = HttpUtility.ParseQueryString(response.Content)["access_token"]; + + if (accessToken == null) + { + throw new CommandException("Couldn't log in. Try again"); + } + + return accessToken; + } + } +} diff --git a/src/AppHarbor/AppHarbor.csproj b/src/AppHarbor/AppHarbor.csproj index 3009e1a..fbc6616 100644 --- a/src/AppHarbor/AppHarbor.csproj +++ b/src/AppHarbor/AppHarbor.csproj @@ -70,6 +70,7 @@ + @@ -85,6 +86,7 @@ + @@ -102,6 +104,7 @@ + @@ -121,8 +124,10 @@ + + diff --git a/src/AppHarbor/AppHarborInstaller.cs b/src/AppHarbor/AppHarborInstaller.cs index d88e578..d84b60f 100644 --- a/src/AppHarbor/AppHarborInstaller.cs +++ b/src/AppHarbor/AppHarborInstaller.cs @@ -84,6 +84,26 @@ public void Install(IWindsorContainer container, IConfigurationStore store) container.Register(Component .For() .ImplementedBy()); + + RegisterProgressBar(container); + } + + private static void RegisterProgressBar(IWindsorContainer container) + { + if (ConsoleWindowHelper.HasConsoleWindow) + { + container.Register(Component + .For() + .ImplementedBy() + .LifeStyle.Transient); + } + else + { + container.Register(Component + .For() + .ImplementedBy() + .LifeStyle.Transient); + } } } } diff --git a/src/AppHarbor/Commands/CIDeployAppCommand.cs b/src/AppHarbor/Commands/CIDeployAppCommand.cs new file mode 100644 index 0000000..e3be599 --- /dev/null +++ b/src/AppHarbor/Commands/CIDeployAppCommand.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using Amazon.S3; +using Amazon.S3.Transfer; +using RestSharp; + +namespace AppHarbor.Commands +{ + [CommandHelp("Deploy application in CI environment", alias: "ci-deploy")] + public class CIDeployAppCommand : ApplicationCommand + { + private string _message; + private DirectoryInfo _sourceDirectory; + private string _username; + private string _password; + + private readonly IRestClient _restClient; + private readonly TextWriter _writer; + private readonly IProgressBar _progressBar; + + private readonly IList _excludedDirectories; + + public CIDeployAppCommand(IApplicationConfiguration applicationConfiguration, TextWriter writer, IProgressBar progressBar) + : base(applicationConfiguration) + { + _restClient = new RestClient("https://packageclient.apphb.com/"); + _writer = writer; + _progressBar = progressBar; + + _sourceDirectory = new DirectoryInfo(Directory.GetCurrentDirectory()); + OptionSet.Add("source-directory=", "Set source directory", x => _sourceDirectory = new DirectoryInfo(x)); + + _excludedDirectories = new List { ".git", ".hg" }; + OptionSet.Add("e|excluded-directory=", "Add excluded directory name", x => _excludedDirectories.Add(x)); + + OptionSet.Add("m|message=", "Specify commit message", x => _message = x); + + OptionSet.Add("u|user=", "Optional. Specify the user to use", x => _username = x); + OptionSet.Add("p|password=", "Optional. Specify the password of the user", x => _password = x); + } + + protected override void InnerExecute(string[] arguments) + { + _writer.WriteLine("Ensure login credentials..."); + string accessToken = GetAccessToken(); + _writer.WriteLine(); + + _writer.WriteLine("Getting upload credentials... "); + _writer.WriteLine(); + + var uploadCredentials = GetCredentials(); + + var temporaryFileName = Path.GetTempFileName(); + try + { + using (var packageStream = new FileStream(temporaryFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite)) + using (var gzipStream = new GZipStream(packageStream, CompressionMode.Compress, true)) + { + _sourceDirectory.ToTar(gzipStream, excludedDirectoryNames: _excludedDirectories.ToArray(), progressBar: _progressBar); + } + + using (var s3Client = new AmazonS3Client(uploadCredentials.GetSessionCredentials())) + using (var transferUtility = new TransferUtility(s3Client)) + { + var request = new TransferUtilityUploadRequest + { + FilePath = temporaryFileName, + BucketName = uploadCredentials.Bucket, + Key = uploadCredentials.ObjectKey, + Timeout = (int)TimeSpan.FromHours(2).TotalMilliseconds, + }; + + request.UploadProgressEvent += (object x, UploadProgressArgs y) => _progressBar + .Update("Uploading package", y.TransferredBytes, y.TotalBytes); + + transferUtility.Upload(request); + + Console.CursorTop++; + _writer.WriteLine(); + } + } + finally + { + File.Delete(temporaryFileName); + } + + TriggerAppHarborBuild(uploadCredentials, accessToken); + } + + private FederatedUploadCredentials GetCredentials() + { + var urlRequest = new RestRequest("applications/{slug}/uploadCredentials", Method.POST); + urlRequest.AddUrlSegment("slug", ApplicationId); + + var federatedCredentials = _restClient.Execute(urlRequest); + return federatedCredentials.Data; + } + + private void TriggerAppHarborBuild(FederatedUploadCredentials credentials, string accessToken) + { + _writer.WriteLine("The package will be deployed to application \"{0}\".", ApplicationId); + + if (string.IsNullOrEmpty(_message)) + { + _message = string.Format("CI Deployment at {0}", DateTime.Now); + } + + var request = new RestRequest("applications/{slug}/buildnotifications", Method.POST) + { + RequestFormat = DataFormat.Json + } + .AddUrlSegment("slug", ApplicationId) + .AddHeader("Authorization", string.Format("BEARER {0}", accessToken)) + .AddBody(new + { + Bucket = credentials.Bucket, + ObjectKey = credentials.ObjectKey, + CommitMessage = string.IsNullOrEmpty(_message) ? "Deployed from CLI" : _message, + }); + + _writer.WriteLine("Notifying AppHarbor."); + + var response = _restClient.Execute(request); + + if (response.StatusCode == HttpStatusCode.OK) + { + using (new ForegroundColor(ConsoleColor.Green)) + { + _writer.WriteLine("Deploying... Open application overview with `appharbor open`."); + } + } + } + + private string GetAccessToken() + { + // Request new access token using the specific + string accessToken = AccessTokenHelper.GetAccessToken(_username, _password); + _writer.WriteLine("Logged in with the username " + _username); + + return accessToken; + } + } +} diff --git a/src/AppHarbor/Commands/DeployAppCommand.cs b/src/AppHarbor/Commands/DeployAppCommand.cs index fa5cf3e..616558b 100644 --- a/src/AppHarbor/Commands/DeployAppCommand.cs +++ b/src/AppHarbor/Commands/DeployAppCommand.cs @@ -20,16 +20,18 @@ public class DeployAppCommand : ApplicationCommand private readonly IRestClient _restClient; private readonly TextReader _reader; private readonly TextWriter _writer; + private readonly IProgressBar _progressBar; private readonly IList _excludedDirectories; - public DeployAppCommand(IApplicationConfiguration applicationConfiguration, IAccessTokenConfiguration accessTokenConfiguration, TextReader reader, TextWriter writer) + public DeployAppCommand(IApplicationConfiguration applicationConfiguration, IAccessTokenConfiguration accessTokenConfiguration, TextReader reader, TextWriter writer, IProgressBar progressBar) : base(applicationConfiguration) { _accessToken = accessTokenConfiguration.GetAccessToken(); _restClient = new RestClient("https://packageclient.apphb.com/"); _reader = reader; _writer = writer; + _progressBar = progressBar; _sourceDirectory = new DirectoryInfo(Directory.GetCurrentDirectory()); OptionSet.Add("source-directory=", "Set source directory", x => _sourceDirectory = new DirectoryInfo(x)); @@ -53,7 +55,7 @@ protected override void InnerExecute(string[] arguments) using (var packageStream = new FileStream(temporaryFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite)) using (var gzipStream = new GZipStream(packageStream, CompressionMode.Compress, true)) { - _sourceDirectory.ToTar(gzipStream, excludedDirectoryNames: _excludedDirectories.ToArray()); + _sourceDirectory.ToTar(gzipStream, excludedDirectoryNames: _excludedDirectories.ToArray(), progressBar: _progressBar); } using (var s3Client = new AmazonS3Client(uploadCredentials.GetSessionCredentials())) @@ -67,8 +69,7 @@ protected override void InnerExecute(string[] arguments) Timeout = (int)TimeSpan.FromHours(2).TotalMilliseconds, }; - var progressBar = new MegaByteProgressBar(); - request.UploadProgressEvent += (object x, UploadProgressArgs y) => progressBar + request.UploadProgressEvent += (object x, UploadProgressArgs y) => _progressBar .Update("Uploading package", y.TransferredBytes, y.TotalBytes); transferUtility.Upload(request); diff --git a/src/AppHarbor/Commands/LoginUserCommand.cs b/src/AppHarbor/Commands/LoginUserCommand.cs index 2da2fdc..2f66dcf 100644 --- a/src/AppHarbor/Commands/LoginUserCommand.cs +++ b/src/AppHarbor/Commands/LoginUserCommand.cs @@ -41,22 +41,7 @@ protected override void InnerExecute(string[] arguments) public virtual string GetAccessToken(string username, string password) { - //NOTE: Remove when merged into AppHarbor.NET library - var restClient = new RestClient("https://appharbor-token-client.apphb.com"); - var request = new RestRequest("/token", Method.POST); - - request.AddParameter("username", username); - request.AddParameter("password", password); - - var response = restClient.Execute(request); - var accessToken = HttpUtility.ParseQueryString(response.Content)["access_token"]; - - if (accessToken == null) - { - throw new CommandException("Couldn't log in. Try again"); - } - - return accessToken; + return AccessTokenHelper.GetAccessToken(username, password); } } } diff --git a/src/AppHarbor/CompressionExtensions.cs b/src/AppHarbor/CompressionExtensions.cs index b7b8e84..37a42d6 100644 --- a/src/AppHarbor/CompressionExtensions.cs +++ b/src/AppHarbor/CompressionExtensions.cs @@ -7,7 +7,7 @@ namespace AppHarbor { public static class CompressionExtensions { - public static void ToTar(this DirectoryInfo sourceDirectory, Stream output, string[] excludedDirectoryNames) + public static void ToTar(this DirectoryInfo sourceDirectory, Stream output, string[] excludedDirectoryNames, IProgressBar progressBar) { var archive = TarArchive.CreateOutputTarArchive(output); @@ -19,7 +19,6 @@ public static void ToTar(this DirectoryInfo sourceDirectory, Stream output, stri var entriesCount = entries.Count(); - var progressBar = new MegaByteProgressBar(); for (var i = 0; i < entriesCount; i++) { archive.WriteEntry(entries[i], true); diff --git a/src/AppHarbor/ConsoleProgressBar.cs b/src/AppHarbor/ConsoleProgressBar.cs index 01a44cb..3eec3bf 100644 --- a/src/AppHarbor/ConsoleProgressBar.cs +++ b/src/AppHarbor/ConsoleProgressBar.cs @@ -4,7 +4,7 @@ namespace AppHarbor { - public abstract class ConsoleProgressBar + public abstract class ConsoleProgressBar : IProgressBar { private const char ProgressBarCharacter = '\u2592'; diff --git a/src/AppHarbor/ConsoleWindowHelper.cs b/src/AppHarbor/ConsoleWindowHelper.cs new file mode 100644 index 0000000..9ce9978 --- /dev/null +++ b/src/AppHarbor/ConsoleWindowHelper.cs @@ -0,0 +1,32 @@ +using System; + +namespace AppHarbor +{ + /// + /// Helper class for console window information. + /// + static class ConsoleWindowHelper + { + /// + /// Get indication if a console window is available or we run + /// without a window (in CI environment). + /// + public static bool HasConsoleWindow + { + get + { + bool hasConsoleWindow; + try + { + int w = Console.BufferWidth; + hasConsoleWindow = true; + } + catch (Exception) + { + hasConsoleWindow = false; + } + return hasConsoleWindow; + } + } + } +} diff --git a/src/AppHarbor/IProgressBar.cs b/src/AppHarbor/IProgressBar.cs new file mode 100644 index 0000000..1243aa2 --- /dev/null +++ b/src/AppHarbor/IProgressBar.cs @@ -0,0 +1,16 @@ +namespace AppHarbor +{ + /// + /// Represent a progress bar that show status updates. + /// + public interface IProgressBar + { + /// + /// Show update on the progress. + /// + /// + /// + /// + void Update(string message, long processedItems, long totalItems); + } +} diff --git a/src/AppHarbor/NullProgressBar.cs b/src/AppHarbor/NullProgressBar.cs new file mode 100644 index 0000000..fd7b20a --- /dev/null +++ b/src/AppHarbor/NullProgressBar.cs @@ -0,0 +1,13 @@ + +namespace AppHarbor +{ + /// + /// Reperesent a null progress bar which does nothing. + /// + public class NullProgressBar : IProgressBar + { + public void Update(string message, long processedItems, long totalItems) + { + } + } +} \ No newline at end of file