From 54490765184be3b38c5536d270b258fb95a2665a Mon Sep 17 00:00:00 2001 From: Ido Ran Date: Mon, 22 Jul 2013 22:48:16 +0300 Subject: [PATCH] Add CIDeployAppCommand which is a command to deploy application in CI environment. The command accept all the required parameters to run. When running inside CI env there is no console window so we use NullProgressBar to make sure we do not try to execute console command that is not allowed. Extract the user login logic into AccessTokenHelper static class. --- src/AppHarbor/AccessTokenHelper.cs | 34 +++++ src/AppHarbor/AppHarbor.csproj | 5 + src/AppHarbor/AppHarborInstaller.cs | 20 +++ src/AppHarbor/Commands/CIDeployAppCommand.cs | 147 +++++++++++++++++++ src/AppHarbor/Commands/DeployAppCommand.cs | 9 +- src/AppHarbor/Commands/LoginUserCommand.cs | 17 +-- src/AppHarbor/CompressionExtensions.cs | 3 +- src/AppHarbor/ConsoleProgressBar.cs | 2 +- src/AppHarbor/ConsoleWindowHelper.cs | 32 ++++ src/AppHarbor/IProgressBar.cs | 16 ++ src/AppHarbor/NullProgressBar.cs | 13 ++ 11 files changed, 275 insertions(+), 23 deletions(-) create mode 100644 src/AppHarbor/AccessTokenHelper.cs create mode 100644 src/AppHarbor/Commands/CIDeployAppCommand.cs create mode 100644 src/AppHarbor/ConsoleWindowHelper.cs create mode 100644 src/AppHarbor/IProgressBar.cs create mode 100644 src/AppHarbor/NullProgressBar.cs 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