From a8600cd2f8a702a6107db500e6e762512047a785 Mon Sep 17 00:00:00 2001 From: Xiaomin Wu Date: Tue, 3 Feb 2015 11:58:37 -0800 Subject: [PATCH] Added simple GUI for ArmClient --- .../Contracts/TenantDetails.cs | 2 +- .../Contracts/VerifiedDomain.cs | 2 +- ARMClient.Authentication/Utilities/Utils.cs | 98 ++++- ARMClient.Console/Program.cs | 64 +-- ARMClient.sln | 6 + ArmClient.Gui/App.config | 8 + ArmClient.Gui/App.xaml | 8 + ArmClient.Gui/App.xaml.cs | 17 + ArmClient.Gui/ArmClient.Gui.csproj | 137 +++++++ ArmClient.Gui/GuiPersistentAuthHelper.cs | 19 + ArmClient.Gui/MainWindow.xaml | 44 ++ ArmClient.Gui/MainWindow.xaml.cs | 386 ++++++++++++++++++ ArmClient.Gui/Models/ConfigSettingFactory.cs | 88 ++++ ArmClient.Gui/Models/ConfigSettings.cs | 91 +++++ ArmClient.Gui/Properties/AssemblyInfo.cs | 55 +++ .../Properties/Resources.Designer.cs | 71 ++++ ArmClient.Gui/Properties/Resources.resx | 117 ++++++ ArmClient.Gui/Properties/Settings.Designer.cs | 30 ++ ArmClient.Gui/Properties/Settings.settings | 7 + ArmClient.Gui/Utils/HttpLoggingHandler.cs | 92 +++++ ArmClient.Gui/Utils/Logger.cs | 49 +++ ArmClient.Gui/config.json | 129 ++++++ ArmClient.Gui/packages.config | 6 + 23 files changed, 1462 insertions(+), 64 deletions(-) create mode 100644 ArmClient.Gui/App.config create mode 100644 ArmClient.Gui/App.xaml create mode 100644 ArmClient.Gui/App.xaml.cs create mode 100644 ArmClient.Gui/ArmClient.Gui.csproj create mode 100644 ArmClient.Gui/GuiPersistentAuthHelper.cs create mode 100644 ArmClient.Gui/MainWindow.xaml create mode 100644 ArmClient.Gui/MainWindow.xaml.cs create mode 100644 ArmClient.Gui/Models/ConfigSettingFactory.cs create mode 100644 ArmClient.Gui/Models/ConfigSettings.cs create mode 100644 ArmClient.Gui/Properties/AssemblyInfo.cs create mode 100644 ArmClient.Gui/Properties/Resources.Designer.cs create mode 100644 ArmClient.Gui/Properties/Resources.resx create mode 100644 ArmClient.Gui/Properties/Settings.Designer.cs create mode 100644 ArmClient.Gui/Properties/Settings.settings create mode 100644 ArmClient.Gui/Utils/HttpLoggingHandler.cs create mode 100644 ArmClient.Gui/Utils/Logger.cs create mode 100644 ArmClient.Gui/config.json create mode 100644 ArmClient.Gui/packages.config diff --git a/ARMClient.Authentication/Contracts/TenantDetails.cs b/ARMClient.Authentication/Contracts/TenantDetails.cs index ad64562..34abcbb 100644 --- a/ARMClient.Authentication/Contracts/TenantDetails.cs +++ b/ARMClient.Authentication/Contracts/TenantDetails.cs @@ -1,7 +1,7 @@  namespace ARMClient.Authentication.Contracts { - internal class TenantDetails + public class TenantDetails { public string objectId { get; set; } public string displayName { get; set; } diff --git a/ARMClient.Authentication/Contracts/VerifiedDomain.cs b/ARMClient.Authentication/Contracts/VerifiedDomain.cs index 986cca2..4aae0b1 100644 --- a/ARMClient.Authentication/Contracts/VerifiedDomain.cs +++ b/ARMClient.Authentication/Contracts/VerifiedDomain.cs @@ -1,7 +1,7 @@  namespace ARMClient.Authentication.Contracts { - internal class VerifiedDomain + public class VerifiedDomain { public bool @default { get; set; } public string name { get; set; } diff --git a/ARMClient.Authentication/Utilities/Utils.cs b/ARMClient.Authentication/Utilities/Utils.cs index 91f51f5..51da162 100644 --- a/ARMClient.Authentication/Utilities/Utils.cs +++ b/ARMClient.Authentication/Utilities/Utils.cs @@ -1,5 +1,10 @@ -using System; +using ARMClient.Authentication.AADAuthentication; +using ARMClient.Authentication.Contracts; +using System; using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; namespace ARMClient.Authentication.Utilities { @@ -31,5 +36,96 @@ namespace ARMClient.Authentication.Utilities System.Diagnostics.Trace.WriteLine(message); } } + + public static async Task HttpInvoke(Uri uri, TokenCacheInfo cacheInfo, string verb, DelegatingHandler handler, HttpContent content) + { + using (var client = new HttpClient(handler)) + { + client.DefaultRequestHeaders.Add("Authorization", cacheInfo.CreateAuthorizationHeader()); + client.DefaultRequestHeaders.Add("User-Agent", Constants.UserAgent.Value); + client.DefaultRequestHeaders.Add("Accept", Constants.JsonContentType); + + if (Utils.IsRdfe(uri)) + { + client.DefaultRequestHeaders.Add("x-ms-version", "2013-10-01"); + } + + client.DefaultRequestHeaders.Add("x-ms-request-id", Guid.NewGuid().ToString()); + + HttpResponseMessage response = null; + if (String.Equals(verb, "get", StringComparison.OrdinalIgnoreCase)) + { + response = await client.GetAsync(uri); + } + else if (String.Equals(verb, "delete", StringComparison.OrdinalIgnoreCase)) + { + response = await client.DeleteAsync(uri); + } + else if (String.Equals(verb, "post", StringComparison.OrdinalIgnoreCase)) + { + response = await client.PostAsync(uri, content); + } + else if (String.Equals(verb, "put", StringComparison.OrdinalIgnoreCase)) + { + response = await client.PutAsync(uri, content); + } + else + { + throw new InvalidOperationException(String.Format("Invalid http verb {0}!", verb)); + } + + using (response) + { + if (response.IsSuccessStatusCode) + { + return 0; + } + + return (-1) * (int)response.StatusCode; + } + } + } + + public static Uri EnsureAbsoluteUri(string path, PersistentAuthHelper persistentAuthHelper) + { + Uri ret; + if (Uri.TryCreate(path, UriKind.Absolute, out ret)) + { + return ret; + } + + var env = persistentAuthHelper.IsCacheValid() ? persistentAuthHelper.AzureEnvironments : AzureEnvironments.Prod; + var parts = path.Split(new[] { '/', '?' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length <= 0 + || String.Equals(parts[0], "tenants", StringComparison.OrdinalIgnoreCase) + || String.Equals(parts[0], "subscriptions", StringComparison.OrdinalIgnoreCase) + || String.Equals(parts[0], "providers", StringComparison.OrdinalIgnoreCase)) + { + return new Uri(new Uri(ARMClient.Authentication.Constants.CSMUrls[(int)env]), path); + } + + Guid guid; + if (Guid.TryParse(parts[0], out guid)) + { + if (path.Length > 1 && String.Equals(parts[1], "services", StringComparison.OrdinalIgnoreCase)) + { + return new Uri(new Uri(ARMClient.Authentication.Constants.RdfeUrls[(int)env]), path); + } + } + + return new Uri(new Uri(ARMClient.Authentication.Constants.AADGraphUrls[(int)env]), path); + } + + public static bool IsRdfe(Uri uri) + { + var host = uri.Host; + return Constants.RdfeUrls.Any(url => url.IndexOf(host, StringComparison.OrdinalIgnoreCase) > 0); + } + + public static bool IsGraphApi(Uri uri) + { + var host = uri.Host; + return Constants.AADGraphUrls.Any(url => url.IndexOf(host, StringComparison.OrdinalIgnoreCase) > 0); + } } } diff --git a/ARMClient.Console/Program.cs b/ARMClient.Console/Program.cs index fd9ca4d..6a4695b 100644 --- a/ARMClient.Console/Program.cs +++ b/ARMClient.Console/Program.cs @@ -41,7 +41,6 @@ namespace ARMClient else if (String.Equals(verb, "listcache", StringComparison.OrdinalIgnoreCase)) { _parameters.ThrowIfUnknown(); - EnsureTokenCache(persistentAuthHelper); foreach (var line in persistentAuthHelper.DumpTokenCache()) @@ -53,7 +52,6 @@ namespace ARMClient else if (String.Equals(verb, "clearcache", StringComparison.OrdinalIgnoreCase)) { _parameters.ThrowIfUnknown(); - persistentAuthHelper.ClearTokenCache(); return 0; } @@ -87,7 +85,6 @@ namespace ARMClient { var tenantId = _parameters.Get(1, keyName: "tenant"); EnsureGuidFormat(tenantId); - var appId = _parameters.Get(2, keyName: "appId"); EnsureGuidFormat(appId); @@ -339,51 +336,8 @@ namespace ARMClient static async Task HttpInvoke(Uri uri, TokenCacheInfo cacheInfo, string verb, bool verbose, HttpContent content) { - using (var client = new HttpClient(new HttpLoggingHandler(new HttpClientHandler(), verbose))) - { - client.DefaultRequestHeaders.Add("Authorization", cacheInfo.CreateAuthorizationHeader()); - client.DefaultRequestHeaders.Add("User-Agent", Constants.UserAgent.Value); - client.DefaultRequestHeaders.Add("Accept", Constants.JsonContentType); - - if (IsRdfe(uri)) - { - client.DefaultRequestHeaders.Add("x-ms-version", "2013-10-01"); - } - - client.DefaultRequestHeaders.Add("x-ms-request-id", Guid.NewGuid().ToString()); - - HttpResponseMessage response = null; - if (String.Equals(verb, "get", StringComparison.OrdinalIgnoreCase)) - { - response = await client.GetAsync(uri); - } - else if (String.Equals(verb, "delete", StringComparison.OrdinalIgnoreCase)) - { - response = await client.DeleteAsync(uri); - } - else if (String.Equals(verb, "post", StringComparison.OrdinalIgnoreCase)) - { - response = await client.PostAsync(uri, content); - } - else if (String.Equals(verb, "put", StringComparison.OrdinalIgnoreCase)) - { - response = await client.PutAsync(uri, content); - } - else - { - throw new InvalidOperationException(String.Format("Invalid http verb {0}!", verb)); - } - - using (response) - { - if (response.IsSuccessStatusCode) - { - return 0; - } - - return (-1) * (int)response.StatusCode; - } - } + var logginerHandler = new HttpLoggingHandler(new HttpClientHandler(), verbose); + return await Utils.HttpInvoke(uri, cacheInfo, verb, logginerHandler, content); } //http://stackoverflow.com/questions/4810841/how-can-i-pretty-print-json-using-javascript @@ -438,24 +392,12 @@ namespace ARMClient } } - static bool IsRdfe(Uri uri) - { - var host = uri.Host; - return Constants.RdfeUrls.Any(url => url.IndexOf(host, StringComparison.OrdinalIgnoreCase) > 0); - } - - static bool IsGraphApi(Uri uri) - { - var host = uri.Host; - return Constants.AADGraphUrls.Any(url => url.IndexOf(host, StringComparison.OrdinalIgnoreCase) > 0); - } - static string GetTenantOrSubscription(Uri uri) { try { var paths = uri.AbsolutePath.Split(new[] { '/', '?' }, StringSplitOptions.RemoveEmptyEntries); - if (IsGraphApi(uri)) + if (Utils.IsGraphApi(uri)) { return Guid.Parse(paths[0]).ToString(); } diff --git a/ARMClient.sln b/ARMClient.sln index 6115671..20db4f2 100644 --- a/ARMClient.sln +++ b/ARMClient.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARMClient.Authentication", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARMClient.Library.Runner", "ARMClient.Library.Runner\ARMClient.Library.Runner.csproj", "{CFCAF2B0-6844-419E-9739-50528D35D2B1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArmClient.Gui", "ArmClient.Gui\ArmClient.Gui.csproj", "{E3542A27-0F79-4B24-AC34-8DD7B62A46B9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {CFCAF2B0-6844-419E-9739-50528D35D2B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {CFCAF2B0-6844-419E-9739-50528D35D2B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {CFCAF2B0-6844-419E-9739-50528D35D2B1}.Release|Any CPU.Build.0 = Release|Any CPU + {E3542A27-0F79-4B24-AC34-8DD7B62A46B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3542A27-0F79-4B24-AC34-8DD7B62A46B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3542A27-0F79-4B24-AC34-8DD7B62A46B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3542A27-0F79-4B24-AC34-8DD7B62A46B9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ArmClient.Gui/App.config b/ArmClient.Gui/App.config new file mode 100644 index 0000000..4c704ea --- /dev/null +++ b/ArmClient.Gui/App.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ArmClient.Gui/App.xaml b/ArmClient.Gui/App.xaml new file mode 100644 index 0000000..68f9291 --- /dev/null +++ b/ArmClient.Gui/App.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/ArmClient.Gui/App.xaml.cs b/ArmClient.Gui/App.xaml.cs new file mode 100644 index 0000000..1ff7b59 --- /dev/null +++ b/ArmClient.Gui/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace ArmGuiClient +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/ArmClient.Gui/ArmClient.Gui.csproj b/ArmClient.Gui/ArmClient.Gui.csproj new file mode 100644 index 0000000..e30e8ea --- /dev/null +++ b/ArmClient.Gui/ArmClient.Gui.csproj @@ -0,0 +1,137 @@ + + + + + Debug + AnyCPU + {E3542A27-0F79-4B24-AC34-8DD7B62A46B9} + WinExe + Properties + ArmGuiClient + ArmGuiClient + v4.5.1 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll + + + False + ..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll + + + + + + False + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + + + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + MainWindow.xaml + Code + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + Always + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + {2824485e-0ead-4f72-b897-ffdaff346528} + ARMClient.Authentication + + + + + \ No newline at end of file diff --git a/ArmClient.Gui/GuiPersistentAuthHelper.cs b/ArmClient.Gui/GuiPersistentAuthHelper.cs new file mode 100644 index 0000000..58b680d --- /dev/null +++ b/ArmClient.Gui/GuiPersistentAuthHelper.cs @@ -0,0 +1,19 @@ +using ARMClient.Authentication.AADAuthentication; +using ARMClient.Authentication.Contracts; +using System.Collections.Generic; + +namespace ArmGuiClient +{ + public class GuiPersistentAuthHelper : PersistentAuthHelper + { + public GuiPersistentAuthHelper(AzureEnvironments azureEnvironment = AzureEnvironments.Prod) + : base(azureEnvironment) + { + } + + public Dictionary GetTenants() + { + return this.TenantStorage.GetCache(); + } + } +} diff --git a/ArmClient.Gui/MainWindow.xaml b/ArmClient.Gui/MainWindow.xaml new file mode 100644 index 0000000..a9e13aa --- /dev/null +++ b/ArmClient.Gui/MainWindow.xaml @@ -0,0 +1,44 @@ + + + + + + 2014-11-01 + + + + + + + + + + + + diff --git a/ArmClient.Gui/MainWindow.xaml.cs b/ArmClient.Gui/MainWindow.xaml.cs new file mode 100644 index 0000000..db45bf7 --- /dev/null +++ b/ArmClient.Gui/MainWindow.xaml.cs @@ -0,0 +1,386 @@ +using ARMClient.Authentication; +using ARMClient.Authentication.Contracts; +using ArmGuiClient.Models; +using ArmGuiClient.Utils; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using AuthUtils = ARMClient.Authentication.Utilities.Utils; + +namespace ArmGuiClient +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + private static readonly string _tmpPayloadFile = System.IO.Path.Combine(Environment.CurrentDirectory, "ArmGuiClient.Payload.json"); + private GuiPersistentAuthHelper _authHelper; + + public MainWindow() + { + InitializeComponent(); + this.Dispatcher.ShutdownStarted += this.OnApplicationShutdown; + Logger.Init(this.OutputRTB); + ConfigSettingFactory.Init(); + + try + { + this._authHelper = new GuiPersistentAuthHelper(ConfigSettingFactory.ConfigSettings.GetAzureEnvironments()); + this.InitUI(); + if (this.CheckIsLogin()) + { + this.PopulateTenant(); + } + } + catch (Exception ex) + { + Logger.ErrorLn("{0} {1}", ex.Message, ex.StackTrace); + this.ExecuteBtn.IsEnabled = false; + } + } + + private void InitUI() + { + // populate api version + if (ConfigSettingFactory.ConfigSettings.ApiVersions == null + || ConfigSettingFactory.ConfigSettings.ApiVersions.Length == 0) + { + Logger.ErrorLn("Missing api version from config.json"); + return; + } + this.ApiVersionCB.Items.Clear(); + + foreach (var str in ConfigSettingFactory.ConfigSettings.ApiVersions) + { + ApiVersionCB.Items.Add(new ComboBoxItem() + { + Content = str + }); + } + this.ApiVersionCB.SelectedIndex = 0; + + // populate actions + if (ConfigSettingFactory.ConfigSettings.Actioins == null + || ConfigSettingFactory.ConfigSettings.Actioins.Length == 0) + { + Logger.ErrorLn("No action define in config.json"); + return; + } + this.ActionCB.Items.Clear(); + foreach (var item in ConfigSettingFactory.ConfigSettings.Actioins) + { + this.ActionCB.Items.Add(new ComboBoxItem() + { + Content = item.Name + }); + } + this.ActionCB.SelectedIndex = 0; + this.OutputRTB.Document.Blocks.Clear(); + + // bind keyboard short cuts + var editPayloadCommand = new RoutedCommand(); + editPayloadCommand.InputGestures.Add(new KeyGesture(Key.W, ModifierKeys.Control, "Ctrl + E")); + this.CommandBindings.Add(new CommandBinding(editPayloadCommand, this.ExecuteEditPayloadCommand)); + + var executeArmCommand = new RoutedCommand(); + executeArmCommand.InputGestures.Add(new KeyGesture(Key.Enter, ModifierKeys.Control, "Ctrl + Enter")); + this.CommandBindings.Add(new CommandBinding(executeArmCommand, this.ExecuteRunArmRequestCommand)); + + var editConfigCommand = new RoutedCommand(); + editConfigCommand.InputGestures.Add(new KeyGesture(Key.P, ModifierKeys.Control, "Ctrl + P")); + this.CommandBindings.Add(new CommandBinding(editConfigCommand, this.ExecuteEditConfigCommand)); + } + + private void PopulateParamsUI(ConfigActioin action) + { + this.ParamLV.Items.Clear(); + double textboxWidth = this.ParamLV.Width * 0.7; + if (action == null || action.Params == null || action.Params.Length == 0) + { + return; + } + + for (int i = 0; i < action.Params.Length; i++) + { + ActionParam param = action.Params[i]; + + StackPanel sp = new StackPanel(); + sp.Orientation = Orientation.Vertical; + + Label label = new Label(); + label.Content = param.Name + (param.Required ? "*" : string.Empty); + + TextBox textbox = new TextBox(); + textbox.Name = param.PlaceHolder; + textbox.Width = textboxWidth; + textbox.KeyUp += new KeyEventHandler((object sender, KeyEventArgs e) => + { + this.UpdateCmdText(); + }); + + sp.Children.Add(label); + sp.Children.Add(textbox); + + this.ParamLV.Items.Add(sp); + } + } + + private bool CheckIsLogin() + { + if (this._authHelper != null && this._authHelper.IsCacheValid()) + { + this.LoginBtn.IsEnabled = false; + this.LogoutBtn.IsEnabled = true; + this.ExecuteBtn.IsEnabled = true; + return true; + } + else + { + this.LoginBtn.IsEnabled = true; + this.LogoutBtn.IsEnabled = false; + this.ExecuteBtn.IsEnabled = false; + this.TenantCB.ItemsSource = new object[0]; + this.SubscriptionCB.ItemsSource = new object[0]; + return false; + } + } + + private void UpdateCmdText() + { + ConfigActioin action = this.GetSelectedAction(); + if (action == null) + { + return; + } + + string cmd = action.Template; + foreach (var item in this.ParamLV.Items) + { + StackPanel wrapperPanel = item as StackPanel; + if (wrapperPanel != null && wrapperPanel.Children.Count == 2) + { + TextBox tb = wrapperPanel.Children[1] as TextBox; + if (tb != null && !string.IsNullOrWhiteSpace(tb.Text)) + { + cmd = cmd.Replace("{" + tb.Name + "}", tb.Text); + } + } + } + + // api version + ComboBoxItem apiItem = this.ApiVersionCB.SelectedValue as ComboBoxItem; + cmd = cmd.Replace("{apiVersion}", apiItem.Content as string); + + // subscription + string subscriptionId = this.SubscriptionCB.SelectedValue as string; + if (!string.IsNullOrWhiteSpace(subscriptionId)) + { + cmd = cmd.Replace("{subscription}", subscriptionId); + } + + this.CmdText.Text = cmd; + } + + private void PopulateTenant() + { + Dictionary tenants = this._authHelper.GetTenants(); + this.TenantCB.ItemsSource = tenants.Values; + this.TenantCB.DisplayMemberPath = "displayName"; + this.TenantCB.SelectedValuePath = "tenantId"; + Logger.InfoLn("{0} tenant found.", tenants.Count); + if (tenants.Count > 0) + { + this.TenantCB.SelectedIndex = 0; + } + } + + private ConfigActioin GetSelectedAction() + { + if (this.ActionCB == null) + { + return null; + } + + int actionIdx = this.ActionCB.SelectedIndex; + + if (actionIdx > -1) + { + return ConfigSettingFactory.ConfigSettings.Actioins[actionIdx]; + } + else + { + return null; + } + } + + private async Task RunArmRequest() + { + try + { + this.ExecuteBtn.IsEnabled = false; + string path = this.CmdText.Text; + string subscriptionId = this.SubscriptionCB.SelectedValue as string; + ConfigActioin action = this.GetSelectedAction(); + Uri uri = AuthUtils.EnsureAbsoluteUri(path, this._authHelper); + var cacheInfo = await this._authHelper.GetToken(subscriptionId, null); + var handler = new HttpLoggingHandler(new HttpClientHandler(), ConfigSettingFactory.ConfigSettings.Verbose); + HttpContent payload = null; + if (!string.Equals("get", action.HttpMethod, StringComparison.OrdinalIgnoreCase) + && !string.Equals("delete", action.HttpMethod, StringComparison.OrdinalIgnoreCase)) + { + payload = new StringContent(File.ReadAllText(_tmpPayloadFile), Encoding.UTF8, Constants.JsonContentType); + } + + await AuthUtils.HttpInvoke(uri, cacheInfo, action.HttpMethod, handler, payload); + } + catch (Exception ex) + { + Logger.ErrorLn("{0} {1}", ex.Message, ex.StackTrace); + } + finally + { + this.ExecuteBtn.IsEnabled = true; + } + } + + private void InvokeEditorToEditPayload() + { + try + { + ConfigActioin action = this.GetSelectedAction(); + if (string.Equals("get", action.HttpMethod, StringComparison.OrdinalIgnoreCase) || + string.Equals("delete", action.HttpMethod, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + string payload = action.Payload; + File.WriteAllText(_tmpPayloadFile, string.IsNullOrWhiteSpace(payload) ? "" : payload); + Process.Start(ConfigSettingFactory.ConfigSettings.Editor, _tmpPayloadFile); + Logger.InfoLn("Editing payload in {0} (Ctrl + W)", _tmpPayloadFile); + } + catch (Exception ex) + { + Logger.ErrorLn("Editor: '{0}'", ConfigSettingFactory.ConfigSettings.Editor); + Logger.ErrorLn("{0} {1}", ex.Message, ex.StackTrace); + } + } + + private async void LoginBtn_Click(object sender, RoutedEventArgs e) + { + await this._authHelper.AcquireTokens(); + if (this.CheckIsLogin()) + { + this.PopulateTenant(); + } + } + + private void ActionCB_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + ConfigActioin action = this.GetSelectedAction(); + this.PopulateParamsUI(action); + this.UpdateCmdText(); + + if (ConfigSettingFactory.ConfigSettings.AutoPromptEditor) + { + this.InvokeEditorToEditPayload(); + } + else + { + Logger.WarnLn("Ctrl + W to edit payload."); + } + } + + private void TenantCB_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + string selectedTenantId = this.TenantCB.SelectedValue as string; + + if (!string.IsNullOrWhiteSpace(selectedTenantId)) + { + Dictionary tenants = this._authHelper.GetTenants(); + + TenantCacheInfo tenant = tenants[selectedTenantId]; + this.SubscriptionCB.ItemsSource = tenant.subscriptions; + this.SubscriptionCB.DisplayMemberPath = "displayName"; + this.SubscriptionCB.SelectedValuePath = "subscriptionId"; + + if (tenant.subscriptions.Length > 0) + { + this.SubscriptionCB.SelectedIndex = 0; + } + + Logger.InfoLn("{0} subscription found under tenant '{1}'", tenant.subscriptions.Length, tenant.displayName); + } + + this.UpdateCmdText(); + } + + private void SubscriptionCB_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + this.UpdateCmdText(); + } + + private void LogoutBtn_Click(object sender, RoutedEventArgs e) + { + this._authHelper.ClearTokenCache(); + this.CheckIsLogin(); + Logger.WarnLn("Goodbye!"); + } + + private async void ExecuteBtn_Click(object sender, RoutedEventArgs e) + { + await this.RunArmRequest(); + } + + private void ApiVersionCB_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + this.UpdateCmdText(); + } + + private void ExecuteEditPayloadCommand(object sender, ExecutedRoutedEventArgs e) + { + ConfigActioin action = this.GetSelectedAction(); + if (string.Equals("get", action.HttpMethod, StringComparison.OrdinalIgnoreCase) || + string.Equals("delete", action.HttpMethod, StringComparison.OrdinalIgnoreCase)) + { + Logger.WarnLn("{0} request doesn`t require any payload", action.HttpMethod); + return; + } + + this.InvokeEditorToEditPayload(); + } + + private async void ExecuteRunArmRequestCommand(object sender, ExecutedRoutedEventArgs e) + { + await this.RunArmRequest(); + } + + private void ExecuteEditConfigCommand(object sender, ExecutedRoutedEventArgs e) + { + try + { + Process.Start(ConfigSettingFactory.ConfigSettings.Editor, ConfigSettingFactory.ConfigFilePath); + Logger.InfoLn("Editing {0} (Ctrl + P)", ConfigSettingFactory.ConfigFilePath); + } + catch (Exception ex) + { + Logger.ErrorLn("Editor: '{0}'", ConfigSettingFactory.ConfigSettings.Editor); + Logger.ErrorLn("Expecting Config.json in: '{0}'", ConfigSettingFactory.ConfigFilePath); + Logger.ErrorLn("{0} {1}", ex.Message, ex.StackTrace); + } + } + + private void OnApplicationShutdown(object sender, EventArgs arg) + { + ConfigSettingFactory.Shutdown(); + } + } +} diff --git a/ArmClient.Gui/Models/ConfigSettingFactory.cs b/ArmClient.Gui/Models/ConfigSettingFactory.cs new file mode 100644 index 0000000..d625018 --- /dev/null +++ b/ArmClient.Gui/Models/ConfigSettingFactory.cs @@ -0,0 +1,88 @@ + +using ArmGuiClient.Utils; +using System; +using System.IO; +using System.Timers; +using System.Web.Script.Serialization; +using System.Windows; +namespace ArmGuiClient.Models +{ + internal class ConfigSettingFactory + { + public static readonly string ConfigFilePath = System.IO.Path.Combine(Environment.CurrentDirectory, "config.json"); + private static ConfigSettings _settingInstance; + private static FileSystemWatcher _configWatcher; + private static Timer _refreshTimer; + + public static void Init() + { + _configWatcher = new FileSystemWatcher(); + _configWatcher.Path = System.IO.Path.GetDirectoryName(ConfigFilePath); + _configWatcher.Filter = System.IO.Path.GetFileName(ConfigFilePath); + _configWatcher.NotifyFilter = NotifyFilters.LastWrite; + _configWatcher.Changed += new FileSystemEventHandler(AutoUpdateConfigOnChanged); + _configWatcher.EnableRaisingEvents = true; + + // https://msdn.microsoft.com/en-us/library/xcc1t119(v=vs.71).aspx + // FileSystemWatcher changed event will be raised multiple time + // delay action by 1 seconds to wait till all events have been raised + _refreshTimer = new Timer(); + _refreshTimer.AutoReset = false; + _refreshTimer.Interval = TimeSpan.FromSeconds(1).TotalMilliseconds; + _refreshTimer.Elapsed += (object sender, ElapsedEventArgs e) => + { + try + { + _settingInstance = GetConfigSettings(); + Logger.InfoLn("Changes are detected from config.json. Setting updated."); + } + catch (Exception ex) + { + Logger.ErrorLn("Changes are detected from config.json. {0} {1}", ex.Message, ex.StackTrace); + } + }; + } + + public static void Shutdown() + { + if (_configWatcher != null) + { + _configWatcher.Dispose(); + } + + if (_refreshTimer != null) + { + _refreshTimer.Dispose(); + } + } + + public static ConfigSettings ConfigSettings + { + get + { + if (_settingInstance == null) + { + _settingInstance = GetConfigSettings(); + } + + return _settingInstance; + } + } + + public static void Refresh() + { + _refreshTimer.Start(); + } + + private static ConfigSettings GetConfigSettings() + { + var ser = new JavaScriptSerializer(); + return ser.Deserialize(File.ReadAllText(ConfigFilePath)); + } + + private static void AutoUpdateConfigOnChanged(object source, FileSystemEventArgs e) + { + Refresh(); + } + } +} diff --git a/ArmClient.Gui/Models/ConfigSettings.cs b/ArmClient.Gui/Models/ConfigSettings.cs new file mode 100644 index 0000000..4c999f2 --- /dev/null +++ b/ArmClient.Gui/Models/ConfigSettings.cs @@ -0,0 +1,91 @@ +using ARMClient.Authentication.Contracts; +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; + +namespace ArmGuiClient.Models +{ + public class ActionParam + { + public string Name { get; set; } + public string PlaceHolder { get; set; } + + public bool Required { get; set; } + } + + public class ConfigActioin + { + private static readonly JavaScriptSerializer _ser = new JavaScriptSerializer(); + private dynamic _payload; + + public string Name { get; set; } + public string Template { get; set; } + + public ActionParam[] Params { get; set; } + + public string HttpMethod { get; set; } + public dynamic Payload + { + get + { + if (this._payload == null) + { + return ""; + } + + return _ser.Serialize(this._payload); + } + + set + { + this._payload = value; + } + } + } + + public class ConfigSettings + { + private string _editor; + + public string TargetEnvironment { get; set; } + + public bool Verbose { get; set; } + + public string Editor + { + get + { + if (string.IsNullOrWhiteSpace(this._editor)) + { + this._editor = @"%windir%\system32\notepad.exe"; + } + + return Environment.ExpandEnvironmentVariables(this._editor); + } + + set + { + this._editor = value; + } + } + + public string[] ApiVersions { get; set; } + + public bool AutoPromptEditor { get; set; } + + public Dictionary DefaultValues { get; set; } + + public ConfigActioin[] Actioins { get; set; } + + public AzureEnvironments GetAzureEnvironments() + { + AzureEnvironments env; + if (Enum.TryParse(this.TargetEnvironment, true, out env)) + { + return env; + } + + return AzureEnvironments.Prod; + } + } +} diff --git a/ArmClient.Gui/Properties/AssemblyInfo.cs b/ArmClient.Gui/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a5cfe4e --- /dev/null +++ b/ArmClient.Gui/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ArmGuiClient")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Hewlett-Packard Company")] +[assembly: AssemblyProduct("ArmGuiClient")] +[assembly: AssemblyCopyright("Copyright © Hewlett-Packard Company 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ArmClient.Gui/Properties/Resources.Designer.cs b/ArmClient.Gui/Properties/Resources.Designer.cs new file mode 100644 index 0000000..82c0e99 --- /dev/null +++ b/ArmClient.Gui/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ArmGuiClient.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ArmGuiClient.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/ArmClient.Gui/Properties/Resources.resx b/ArmClient.Gui/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/ArmClient.Gui/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ArmClient.Gui/Properties/Settings.Designer.cs b/ArmClient.Gui/Properties/Settings.Designer.cs new file mode 100644 index 0000000..a55961b --- /dev/null +++ b/ArmClient.Gui/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ArmGuiClient.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/ArmClient.Gui/Properties/Settings.settings b/ArmClient.Gui/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/ArmClient.Gui/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ArmClient.Gui/Utils/HttpLoggingHandler.cs b/ArmClient.Gui/Utils/HttpLoggingHandler.cs new file mode 100644 index 0000000..30fe7d1 --- /dev/null +++ b/ArmClient.Gui/Utils/HttpLoggingHandler.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json; +using System; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace ArmGuiClient.Utils +{ + class HttpLoggingHandler : DelegatingHandler + { + private readonly bool _verbose; + + public HttpLoggingHandler(HttpMessageHandler innerHandler, bool verbose) + : base(innerHandler) + { + this._verbose = verbose; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (_verbose) + { + Logger.InfoLn("---------- Request -----------------------"); + + Logger.InfoLn("{0} {1} HTTP/{2}", request.Method, request.RequestUri.PathAndQuery, request.Version); + Logger.InfoLn("Host: {0}", request.RequestUri.Host); + + foreach (var header in request.Headers) + { + string headerVal = string.Empty; + if (String.Equals("Authorization", header.Key)) + { + headerVal = header.Value.First().Substring(0, 70) + "..."; + } + else + { + headerVal = String.Join("; ", header.Value); + } + Logger.InfoLn("{0}: {1}", header.Key, headerVal); + } + + await DumpContent(request.Content); + } + + var watch = new Stopwatch(); + watch.Start(); + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + watch.Stop(); + + int responseStatusCocde = (int)response.StatusCode; + + if (_verbose) + { + Logger.InfoLn("---------- Response ({0} ms) ------------", watch.ElapsedMilliseconds); + Logger.InfoLn("HTTP/{0} {1} {2}", response.Version, responseStatusCocde, response.StatusCode); + foreach (var header in response.Headers) + { + Logger.InfoLn("{0}: {1}", header.Key, String.Join("; ", header.Value)); + } + } + + await DumpContent(response.Content, this.isErrorRequest(responseStatusCocde)); + return response; + } + + private async Task DumpContent(HttpContent content, bool isErrorContent = false) + { + if (content == null || content.Headers.ContentType == null) + { + return; + } + var result = await content.ReadAsStringAsync(); + + Logger.InfoLn(string.Empty); + if (!string.IsNullOrWhiteSpace(result)) + { + dynamic parsedJson = JsonConvert.DeserializeObject(result); + string indentedResult = JsonConvert.SerializeObject(parsedJson, Formatting.Indented); + + Logger.WriteLn(indentedResult, isErrorContent ? Logger.ErrorBrush : Logger.InfoBrush); + } + } + + private bool isErrorRequest(int responseStatusCode) + { + return responseStatusCode >= 400 && responseStatusCode < 600; + } + } +} diff --git a/ArmClient.Gui/Utils/Logger.cs b/ArmClient.Gui/Utils/Logger.cs new file mode 100644 index 0000000..8937264 --- /dev/null +++ b/ArmClient.Gui/Utils/Logger.cs @@ -0,0 +1,49 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace ArmGuiClient.Utils +{ + internal class Logger + { + public static readonly Brush InfoBrush = Brushes.LightGreen; + public static readonly Brush WarnBrush = Brushes.Yellow; + public static readonly Brush ErrorBrush = Brushes.Red; + + private static RichTextBox _outputRTB; + private static FlowDocument _flowDoc; + + public static void Init(RichTextBox textBox) + { + _outputRTB = textBox; + _flowDoc = _outputRTB.Document; + } + + public static void WriteLn(string content, Brush background) + { + Application.Current.Dispatcher.Invoke(() => + { + var para = new Paragraph(new Run(content)); + para.Background = background; + _outputRTB.Document.Blocks.Add(para); + _outputRTB.ScrollToEnd(); + }); + } + + public static void InfoLn(string format, params object[] args) + { + WriteLn(string.Format(format, args), InfoBrush); + } + + public static void WarnLn(string format, params object[] args) + { + WriteLn(string.Format(format, args), WarnBrush); + } + + public static void ErrorLn(string format, params object[] args) + { + WriteLn(string.Format(format, args), ErrorBrush); + } + } +} diff --git a/ArmClient.Gui/config.json b/ArmClient.Gui/config.json new file mode 100644 index 0000000..398a7e8 --- /dev/null +++ b/ArmClient.Gui/config.json @@ -0,0 +1,129 @@ +{ + "TargetEnvironment": "Prod", + "ApiVersions": [ "2014-04-01", "2014-11-01" ], + "Verbose": "true", + "Editor": "%windir%\\system32\\notepad.exe", + "AutoPromptEditor": "true", + "DefaultValues": { + "resourceGroup": "tryag", + siteName: "trysite001" + }, + "Actioins": [ + { + "httpMethod": "GET", + "name": "List Resource Groups", + "template": "/subscriptions/{subscription}/resourceGroups?api-version={apiVersion}" + }, + { + "httpMethod": "GET", + "name": "Get Resource Group", + "template": "/subscriptions/{subscription}/resourceGroups/{resourceGroup}?api-version={apiVersion}", + "params": [ + { + "name": "Resource Group Name", + "placeHolder": "resourceGroup", + "required": "true" + } + ] + }, + { + "httpMethod": "PUT", + "name": "Create Resource Group", + "template": "/subscriptions/{subscription}/resourceGroups/{resourceGroup}?api-version={apiVersion}", + "params": [ + { + "name": "Resource Group Name", + "placeHolder": "resourceGroup", + "required": "true" + } + ], + "payload": { + "location": "{location}" + } + }, + { + "httpMethod": "DELETE", + "name": "Delete Resource Group", + "template": "/subscriptions/{subscription}/resourceGroups/{resourceGroup}?api-version={apiVersion}", + "params": [ + { + "name": "Resource Group Name", + "placeHolder": "resourceGroup", + "required": "true" + } + ] + }, + { + "httpMethod": "GET", + "name": "List Websites", + "template": "/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/sites?api-version={apiVersion}", + "params": [ + { + "name": "Resource Group Name", + "placeHolder": "resourceGroup", + "required": "true" + } + ] + }, + { + "httpMethod": "GET", + "name": "Get Website", + "template": "/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/sites/{siteName}?api-version={apiVersion}", + "params": [ + { + "name": "Resource Group Name", + "placeHolder": "resourceGroup", + "required": "true" + }, + { + "name": "Website Name", + "placeHolder": "siteName", + "required": "true" + } + ] + }, + { + "httpMethod": "GET", + "name": "List Site Extensions", + "template": "/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/sites/{siteName}/siteextensions?api-version={apiVersion}", + "params": [ + { + "name": "Resource Group Name", + "placeHolder": "resourceGroup", + "required": "true" + }, + { + "name": "Website Name", + "placeHolder": "siteName", + "required": "true" + } + ] + }, + { + "httpMethod": "PUT", + "name": "Install Site Extensions", + "template": "/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/sites/{siteName}/siteextensions/{extensionName}?api-version={apiVersion}", + "params": [ + { + "name": "Resource Group Name", + "placeHolder": "resourceGroup", + "required": "true" + }, + { + "name": "Website Name", + "placeHolder": "siteName", + "required": "true" + }, + { + "name": "Extension Name", + "placeHolder": "extensionName", + "required": "true" + } + ], + "payload": { + "properties": { + } + } + } + ] +} diff --git a/ArmClient.Gui/packages.config b/ArmClient.Gui/packages.config new file mode 100644 index 0000000..bc66014 --- /dev/null +++ b/ArmClient.Gui/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file