Skip to content

Commit 231a559

Browse files
authored
Merge pull request #3984 from markcowl/reserror
Add cmdlet for Resolving Hyak and AutoRest errors
2 parents f60f70d + 2a3b85f commit 231a559

File tree

18 files changed

+1011
-3
lines changed

18 files changed

+1011
-3
lines changed

src/Common/Commands.Common/Extensions/CmdletExtensions.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
using System.Collections.Generic;
1717
using System.Collections.ObjectModel;
1818
using System.IO;
19+
using System.Linq;
1920
using System.Management.Automation;
21+
using System.Management.Automation.Runspaces;
2022
using System.Reflection;
23+
using System.Text;
2124

2225
namespace Microsoft.WindowsAzure.Commands.Utilities.Common
2326
{
@@ -64,6 +67,80 @@ public static string ResolvePath(this PSCmdlet psCmdlet, string path)
6467
return fullPath;
6568
}
6669

70+
/// <summary>
71+
/// Execute the given cmdlet in powershell usign the given pipeline parameters.
72+
/// </summary>
73+
/// <typeparam name="T">The output type for the cmdlet</typeparam>
74+
/// <param name="cmdlet">The cmdlet to execute</param>
75+
/// <param name="name">The name of the cmdlet</param>
76+
/// <param name="cmdletParameters">The parameters to pass to the cmdlet on the pipeline</param>
77+
/// <returns>The output of executing the cmdlet</returns>
78+
public static List<T> ExecuteCmdletInPipeline<T>(this PSCmdlet cmdlet, string name, params object[] cmdletParameters)
79+
{
80+
List<T> output = new List<T>();
81+
using (System.Management.Automation.PowerShell powershell = System.Management.Automation.PowerShell.Create(RunspaceMode.NewRunspace))
82+
{
83+
var info = new CmdletInfo(name, cmdlet.GetType());
84+
Collection<T> result = powershell.AddCommand(info).Invoke<T>(cmdletParameters);
85+
if (powershell.Streams.Error != null && powershell.Streams.Error.Count > 0)
86+
{
87+
StringBuilder details = new StringBuilder();
88+
powershell.Streams.Error.ForEach(e => details.AppendFormat("Error: {0}\n", e.ToString()));
89+
throw new InvalidOperationException(string.Format("Errors while running cmdlet:\n {0}", details.ToString()));
90+
}
91+
92+
if (result != null && result.Count > 0)
93+
{
94+
result.ForEach(output.Add);
95+
}
96+
}
97+
98+
return output;
99+
}
100+
101+
/// <summary>
102+
/// Execute the given cmdlet in powershell with the given parameters after injecting the given exception. It is expected that the cmdlet has a runtime that can be used for receiving output
103+
/// </summary>
104+
/// <typeparam name="T"></typeparam>
105+
/// <param name="cmdlet">The cmdlet to execute</param>
106+
/// <param name="name">The name of the cmdlet</param>
107+
/// <param name="exception">The exception to inject into the error stream</param>
108+
/// <param name="cmdletParameters">The parameters to pass to the cmdlet on the pipeline</param>
109+
public static void ExecuteCmdletWithExceptionInPipeline<T>(this PSCmdlet cmdlet, string name, Exception exception, params KeyValuePair<string, object>[] cmdletParameters)
110+
{
111+
List<T> output = new List<T>();
112+
using (System.Management.Automation.PowerShell powershell = System.Management.Automation.PowerShell.Create(RunspaceMode.NewRunspace))
113+
{
114+
var info = new CmdletInfo(name, cmdlet.GetType());
115+
powershell.AddCommand("Write-Error");
116+
powershell.AddParameter("Exception", exception);
117+
powershell.Invoke();
118+
powershell.Commands.Clear();
119+
powershell.AddCommand(info);
120+
foreach (var pair in cmdletParameters)
121+
{
122+
if (pair.Value == null)
123+
{
124+
powershell.AddParameter(pair.Key);
125+
}
126+
else
127+
{
128+
powershell.AddParameter(pair.Key, pair.Value);
129+
}
130+
}
131+
Collection<T> result = powershell.Invoke<T>();
132+
powershell.Streams.Error.ForEach(cmdlet.WriteError);
133+
powershell.Streams.Debug.ForEach(d => cmdlet.WriteDebug(d.Message));
134+
powershell.Streams.Verbose.ForEach(v => cmdlet.WriteWarning(v.Message));
135+
powershell.Streams.Warning.ForEach(w => cmdlet.WriteWarning(w.Message));
136+
137+
if (result != null && result.Count > 0)
138+
{
139+
result.ForEach(r => cmdlet.WriteObject(r));
140+
}
141+
}
142+
}
143+
67144
public static List<T> ExecuteScript<T>(this PSCmdlet cmdlet, string contents)
68145
{
69146
List<T> output = new List<T>();

src/ResourceManager/Common/Commands.ScenarioTests.ResourceManager.Common/PSCmdletExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using System;
1616
using System.Management.Automation;
1717
using System.Reflection;
18+
using System.Threading;
1819

1920
namespace Microsoft.WindowsAzure.Commands.ScenarioTest
2021
{
@@ -46,6 +47,17 @@ public static void ExecuteCmdlet(this PSCmdlet cmdlet)
4647
throw e.InnerException;
4748
}
4849
}
50+
public static void ExecuteCommand(this PSCmdlet cmdlet)
51+
{
52+
try
53+
{
54+
GetProtectedMethod("ProcessRecord").Invoke(cmdlet, new object[] { });
55+
}
56+
catch (TargetInvocationException e)
57+
{
58+
throw e.InnerException;
59+
}
60+
}
4961

5062
public static void SetCommandRuntimeMock(this PSCmdlet cmdlet, ICommandRuntime value)
5163
{

src/ResourceManager/Profile/AzureRM.Profile.psd1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ CmdletsToExport = 'Disable-AzureRmDataCollection', 'Enable-AzureRmDataCollection
7777
'Set-AzureRmEnvironment', 'Add-AzureRmEnvironment',
7878
'Get-AzureRmSubscription', 'Add-AzureRmAccount', 'Get-AzureRmContext',
7979
'Set-AzureRmContext', 'Import-AzureRmContext', 'Save-AzureRmContext',
80-
'Get-AzureRmTenant', 'Send-Feedback'
80+
'Get-AzureRmTenant', 'Send-Feedback', 'Resolve-AzureRmError'
8181

8282
# Variables to export from this module
8383
# VariablesToExport = @()
8484

8585
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
86-
AliasesToExport = 'Login-AzureRmAccount', 'Select-AzureRmSubscription'
86+
AliasesToExport = 'Login-AzureRmAccount', 'Select-AzureRmSubscription', 'Resolve-Error'
8787

8888
# DSC resources to export from this module
8989
# DscResourcesToExport = @()

src/ResourceManager/Profile/Commands.Profile.Test/Commands.Profile.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
<Compile Include="ClientFactoryTests.cs" />
187187
<Compile Include="CommonDataCmdletTests.cs" />
188188
<Compile Include="EnvironmentCmdletTests.cs" />
189+
<Compile Include="ErrorResolutionTests.cs" />
189190
<Compile Include="MockDataStore.cs" />
190191
<Compile Include="MockSubscriptionClientFactory.cs" />
191192
<Compile Include="NullClient.cs" />
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using Hyak.Common;
16+
using Microsoft.Azure.Commands.Profile.Errors;
17+
using Microsoft.Azure.Commands.ScenarioTest;
18+
using Microsoft.WindowsAzure.Commands.Common.Test.Mocks;
19+
using Microsoft.WindowsAzure.Commands.ScenarioTest;
20+
using Microsoft.WindowsAzure.Commands.Utilities.Common;
21+
using System;
22+
using System.Collections.Generic;
23+
using System.Management.Automation;
24+
using System.Net;
25+
using System.Net.Http;
26+
using Xunit;
27+
28+
namespace Microsoft.Azure.Commands.Profile.Test
29+
{
30+
public class ErrorResolutionTests
31+
{
32+
class TestHyakException : CloudException
33+
{
34+
public TestHyakException(string message, CloudHttpRequestErrorInfo request, CloudHttpResponseErrorInfo response) : base(message)
35+
{
36+
Request = request;
37+
Response = response;
38+
}
39+
}
40+
41+
[Fact]
42+
[Trait(Category.AcceptanceType, Category.CheckIn)]
43+
public void DoesNotThrowWithNullError()
44+
{
45+
TestExecutionHelpers.SetUpSessionAndProfile();
46+
var cmdlet = new ResolveError();
47+
var output = cmdlet.ExecuteCmdletInPipeline<AzureErrorRecord>("Resolve-Error");
48+
Assert.True(output == null || output.Count == 0);
49+
output = cmdlet.ExecuteCmdletInPipeline<AzureErrorRecord>("Resolve-Error", new ErrorRecord[] { null, null });
50+
Assert.True(output == null || output.Count == 0);
51+
output = cmdlet.ExecuteCmdletInPipeline<AzureErrorRecord>("Resolve-Error", new ErrorRecord[] { null, new ErrorRecord(new Exception(null), null, ErrorCategory.AuthenticationError, null) });
52+
Assert.NotNull(output);
53+
Assert.Equal(1, output.Count);
54+
var record = output[0] as AzureExceptionRecord;
55+
Assert.NotNull(record);
56+
Assert.Equal(ErrorCategory.AuthenticationError, record.ErrorCategory.Category);
57+
}
58+
59+
[Fact]
60+
[Trait(Category.AcceptanceType, Category.CheckIn)]
61+
public void HandlesExceptionError()
62+
{
63+
var runtime = new MockCommandRuntime();
64+
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://www.contoso.com/resource?api-version-1.0"));
65+
request.Headers.Add("x-ms-request-id", "HyakRequestId");
66+
var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
67+
var hyakException = new TestHyakException("exception message", CloudHttpRequestErrorInfo.Create(request), CloudHttpResponseErrorInfo.Create(response))
68+
{
69+
Error = new Hyak.Common.CloudError { Code="HyakCode", Message="HyakError"}
70+
};
71+
72+
var autorestException = new Microsoft.Rest.Azure.CloudException("exception message")
73+
{
74+
Body = new Microsoft.Rest.Azure.CloudError { Code = "AutorestCode", Message = "Autorest message" },
75+
Request = new Rest.HttpRequestMessageWrapper(request, ""),
76+
Response = new Rest.HttpResponseMessageWrapper(response, ""),
77+
RequestId = "AutoRestRequestId"
78+
};
79+
80+
var cmdlet = new ResolveError
81+
{
82+
Error = new []
83+
{
84+
new ErrorRecord(new Exception("exception message"), "errorCode", ErrorCategory.AuthenticationError, this),
85+
new ErrorRecord(hyakException, "errorCode", ErrorCategory.ConnectionError, this),
86+
new ErrorRecord(autorestException , "errorCode", ErrorCategory.InvalidOperation, this),
87+
},
88+
CommandRuntime = runtime
89+
};
90+
91+
cmdlet.ExecuteCmdlet();
92+
Assert.NotNull(runtime.OutputPipeline);
93+
Assert.Equal(3, runtime.OutputPipeline.Count);
94+
var errorResult = runtime.OutputPipeline[0] as AzureExceptionRecord;
95+
Assert.NotNull(errorResult);
96+
Assert.Equal(ErrorCategory.AuthenticationError, errorResult.ErrorCategory.Category);
97+
Assert.NotNull(errorResult.Exception);
98+
Assert.Equal(errorResult.Exception.GetType(), typeof(Exception));
99+
Assert.Equal("exception message", errorResult.Exception.Message);
100+
var hyakResult = runtime.OutputPipeline[1] as AzureRestExceptionRecord;
101+
Assert.NotNull(hyakResult);
102+
Assert.Equal(ErrorCategory.ConnectionError, hyakResult.ErrorCategory.Category);
103+
Assert.NotNull(errorResult.Exception);
104+
Assert.Equal(hyakResult.Exception.GetType(), typeof(TestHyakException));
105+
Assert.Equal("exception message", hyakResult.Exception.Message);
106+
Assert.NotNull(hyakResult.RequestMessage);
107+
Assert.Equal(HttpMethod.Get.ToString(), hyakResult.RequestMessage.Verb);
108+
Assert.Equal(new Uri("https://www.contoso.com/resource?api-version-1.0"), hyakResult.RequestMessage.Uri);
109+
Assert.NotNull(hyakResult.ServerResponse);
110+
Assert.Equal(HttpStatusCode.BadRequest.ToString(), hyakResult.ServerResponse.ResponseStatusCode);
111+
var autorestResult = runtime.OutputPipeline[2] as AzureRestExceptionRecord;
112+
Assert.NotNull(autorestResult);
113+
Assert.Equal(ErrorCategory.InvalidOperation, autorestResult.ErrorCategory.Category);
114+
Assert.NotNull(autorestResult.Exception);
115+
Assert.Equal(autorestResult.Exception.GetType(), typeof(Microsoft.Rest.Azure.CloudException));
116+
Assert.Equal("exception message", autorestResult.Exception.Message);
117+
Assert.NotNull(autorestResult.RequestMessage);
118+
Assert.Equal(HttpMethod.Get.ToString(), autorestResult.RequestMessage.Verb);
119+
Assert.Equal(new Uri("https://www.contoso.com/resource?api-version-1.0"), autorestResult.RequestMessage.Uri);
120+
Assert.NotNull(autorestResult.ServerResponse);
121+
Assert.Equal(HttpStatusCode.BadRequest.ToString(), autorestResult.ServerResponse.ResponseStatusCode);
122+
Assert.Equal("AutoRestRequestId", autorestResult.RequestId);
123+
Assert.Contains("AutorestCode", autorestResult.ServerMessage);
124+
Assert.Contains("Autorest message", autorestResult.ServerMessage);
125+
}
126+
127+
[Fact]
128+
[Trait(Category.AcceptanceType, Category.CheckIn)]
129+
public void LastParameterFindsLastError()
130+
{
131+
TestExecutionHelpers.SetUpSessionAndProfile();
132+
var mock = new MockCommandRuntime();
133+
var cmdlet = new ResolveError { CommandRuntime = mock };
134+
var message = "RuntimeErrorMessage";
135+
var exception = new Exception(message);
136+
cmdlet.ExecuteCmdletWithExceptionInPipeline<AzureErrorRecord>("Resolve-AzureRmError", exception, new KeyValuePair<string, object>("Last", null ) );
137+
Assert.NotNull(mock.ErrorStream);
138+
Assert.Equal(1, mock.ErrorStream.Count);
139+
Assert.NotNull(mock.OutputPipeline);
140+
Assert.Equal(1, mock.OutputPipeline.Count);
141+
var record = mock.OutputPipeline[0] as AzureExceptionRecord;
142+
Assert.NotNull(record);
143+
Assert.NotNull(record.Exception);
144+
Assert.Equal(typeof(Exception), record.Exception.GetType());
145+
Assert.Equal(message, record.Message);
146+
147+
148+
}
149+
}
150+
}

src/ResourceManager/Profile/Commands.Profile/AzureRmProfileStartup.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
"Select-AzureRmSubscription" = "Set-AzureRmContext";
1818
"Save-AzureRmProfile" = "Save-AzureRmContext";
1919
"Select-AzureRmProfile" = "Import-AzureRmContext";
20+
"Resolve-Error" = "Resolve-AzureRmError";
2021
}.GetEnumerator() | Select @{Name='Name'; Expression={$_.Key}}, @{Name='Value'; Expression={$_.Value}} | New-Alias -Description "AzureAlias"

src/ResourceManager/Profile/Commands.Profile/Commands.Profile.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@
131131
<Compile Include="Environment\GetAzureRMEnvironment.cs" />
132132
<Compile Include="Environment\SetAzureRMEnvironment.cs" />
133133
<Compile Include="Environment\AddAzureRMEnvironment.cs" />
134+
<Compile Include="Errors\AzureRestExceptionRecord.cs" />
135+
<Compile Include="Errors\AzureExceptionRecord.cs" />
136+
<Compile Include="Errors\AzureErrorRecord.cs" />
137+
<Compile Include="Errors\HttpRequestInfo.cs" />
138+
<Compile Include="Errors\HttpResponseInfo.cs" />
139+
<Compile Include="Errors\HttpMessageInfo.cs" />
140+
<Compile Include="Errors\ResolveError.cs" />
134141
<Compile Include="Feedback\SendFeedback.cs" />
135142
<Compile Include="Models\AzureRmProfileExtensions.cs" />
136143
<Compile Include="Models\ModelExtensions.cs" />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using System.Management.Automation;
19+
using System.Text;
20+
using System.Threading.Tasks;
21+
22+
namespace Microsoft.Azure.Commands.Profile.Errors
23+
{
24+
public class AzureErrorRecord
25+
{
26+
public AzureErrorRecord(ErrorRecord record)
27+
{
28+
InvocationInfo = record.InvocationInfo;
29+
ScriptStackTrace = record.ScriptStackTrace;
30+
ErrorCategory = record.CategoryInfo;
31+
ErrorDetails = record.ErrorDetails;
32+
}
33+
34+
public ErrorDetails ErrorDetails { get; set; }
35+
36+
public ErrorCategoryInfo ErrorCategory { get; set; }
37+
38+
public InvocationInfo InvocationInfo { get; set; }
39+
40+
public string ScriptStackTrace { get; set; }
41+
42+
}
43+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using System;
16+
using System.Management.Automation;
17+
18+
namespace Microsoft.Azure.Commands.Profile.Errors
19+
{
20+
public class AzureExceptionRecord : AzureErrorRecord
21+
{
22+
public AzureExceptionRecord(Exception exception, ErrorRecord record, bool inner = false) : base(record)
23+
{
24+
Message = exception.Message;
25+
HelpLink = exception.HelpLink;
26+
StackTrace = exception.StackTrace;
27+
Exception = exception;
28+
}
29+
30+
public bool InnerException { get; set; }
31+
32+
public Exception Exception { get; }
33+
34+
public string Message { get; set; }
35+
36+
public string StackTrace { get; set; }
37+
38+
public string HelpLink { get; set; }
39+
}
40+
}

0 commit comments

Comments
 (0)