Skip to content

Commit 18a778d

Browse files
authored
Improve error reporting to the end user (#124)
1 parent f734c7f commit 18a778d

File tree

4 files changed

+454
-33
lines changed

4 files changed

+454
-33
lines changed

Sources/xcodeinstall/Secrets/SecretsStorageAWS+Soto.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ final class SecretsStorageAWSSoto: SecretsStorageAWSSDKProtocol {
223223
)
224224

225225
} catch {
226-
log.error("Unexpected error while updating secrets\n\(error)")
226+
log.debug("Unexpected error while updating secrets\n\(error)")
227227
throw wrapCredentialError(error)
228228
}
229229
}
@@ -257,7 +257,7 @@ final class SecretsStorageAWSSoto: SecretsStorageAWSSDKProtocol {
257257
throw error
258258

259259
} catch {
260-
log.error("Unexpected error while retrieving secrets\n\(error)")
260+
log.debug("Unexpected error while retrieving secrets\n\(error)")
261261
throw wrapCredentialError(error)
262262

263263
}

Sources/xcodeinstall/Secrets/SecretsStorageAWS.swift

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,6 @@ import Foundation
1717
import FoundationNetworking
1818
#endif
1919

20-
// the errors thrown by the SecretsManager class
21-
enum SecretsStorageAWSError: Error, LocalizedError {
22-
case invalidRegion(region: String)
23-
case secretDoesNotExist(secretname: String)
24-
case noCredentialProvider(profileName: String?, underlyingError: Error)
25-
26-
var errorDescription: String? {
27-
switch self {
28-
case .invalidRegion(let region):
29-
return "Invalid AWS region: '\(region)'"
30-
case .secretDoesNotExist(let secretname):
31-
return "AWS secret '\(secretname)' does not exist"
32-
case .noCredentialProvider(let profileName, let underlyingError):
33-
if let profileName {
34-
return "No AWS credentials found for profile '\(profileName)'. "
35-
+ "Verify the profile exists in ~/.aws/credentials or ~/.aws/config. "
36-
+ "Underlying error: \(underlyingError)"
37-
} else {
38-
return "No AWS credential provider found. "
39-
+ "Configure credentials via environment variables, ~/.aws/credentials, or an EC2 instance profile. "
40-
+ "Underlying error: \(underlyingError)"
41-
}
42-
}
43-
}
44-
}
45-
4620
// the names we are using to store the secrets
4721
enum AWSSecretsName: String {
4822
case appleCredentials = "xcodeinstall-apple-credentials"
@@ -164,7 +138,7 @@ class SecretsStorageAWS: SecretsHandlerProtocol {
164138
)
165139

166140
} catch {
167-
log.error("⚠️ can not save cookies file in AWS Secret Manager: \(error)")
141+
log.debug("⚠️ can not save cookies file in AWS Secret Manager: \(error)")
168142
throw error
169143
}
170144

@@ -180,7 +154,7 @@ class SecretsStorageAWS: SecretsHandlerProtocol {
180154
let result = session.cookies()
181155
return result
182156
} catch {
183-
log.error("Error when trying to load session : \(error)")
157+
log.debug("Error when trying to load session : \(error)")
184158
throw error
185159
}
186160
}
@@ -204,7 +178,7 @@ class SecretsStorageAWS: SecretsHandlerProtocol {
204178
newValue: newSessionSecret
205179
)
206180
} catch {
207-
log.error("Error when trying to save session : \(error)")
181+
log.debug("Error when trying to save session : \(error)")
208182
throw error
209183
}
210184

@@ -224,7 +198,7 @@ class SecretsStorageAWS: SecretsHandlerProtocol {
224198
return try await self.awsSDK.retrieveSecret(secretId: AWSSecretsName.appleCredentials)
225199

226200
} catch {
227-
log.error("Error when trying to load session : \(error)")
201+
log.debug("Error when trying to load credentials : \(error)")
228202
throw error
229203
}
230204
}
@@ -238,7 +212,7 @@ class SecretsStorageAWS: SecretsHandlerProtocol {
238212
)
239213

240214
} catch {
241-
log.error("Error when trying to save credentials : \(error)")
215+
log.debug("Error when trying to save credentials : \(error)")
242216
throw error
243217
}
244218

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//
2+
// SecretsStorageAWSError.swift
3+
// xcodeinstall
4+
//
5+
// Created by Kiro AI
6+
//
7+
8+
#if canImport(FoundationEssentials)
9+
import FoundationEssentials
10+
#else
11+
import Foundation
12+
#endif
13+
14+
/// Errors thrown by AWS Secrets Manager operations
15+
enum SecretsStorageAWSError: Error, LocalizedError {
16+
case invalidRegion(region: String)
17+
case secretDoesNotExist(secretname: String)
18+
case noCredentialProvider(profileName: String?, underlyingError: Error)
19+
20+
var errorDescription: String? {
21+
switch self {
22+
case .invalidRegion(let region):
23+
return "Invalid AWS region: '\(region)'"
24+
case .secretDoesNotExist(let secretname):
25+
return "AWS secret '\(secretname)' does not exist"
26+
case .noCredentialProvider(let profileName, let underlyingError):
27+
return buildCredentialErrorMessage(profileName: profileName, underlyingError: underlyingError)
28+
}
29+
}
30+
31+
/// Builds a context-aware error message based on the profile configuration
32+
private func buildCredentialErrorMessage(profileName: String?, underlyingError: Error) -> String {
33+
let underlyingMessage = "\(underlyingError)"
34+
35+
// Check if this looks like an expired/invalid credential vs missing configuration
36+
let isExpiredCredential = underlyingMessage.contains("expired")
37+
|| underlyingMessage.contains("invalid")
38+
|| underlyingMessage.contains("UnrecognizedClientException")
39+
|| underlyingMessage.contains("InvalidClientTokenId")
40+
41+
if let profileName = profileName {
42+
// Try to detect the profile type
43+
let profileType = detectProfileType(profileName: profileName)
44+
45+
var message = "Your AWS session has expired or credentials are invalid for profile '\(profileName)'. "
46+
47+
switch profileType {
48+
case .sso:
49+
message += "Please reauthenticate using:\n aws sso login --profile \(profileName)"
50+
case .login:
51+
message += "Please reauthenticate using:\n aws login --profile \(profileName)"
52+
case .staticCredentials:
53+
message += "Please verify your credentials in ~/.aws/credentials or reauthenticate using:\n aws configure --profile \(profileName)"
54+
case .unknown:
55+
message += "Please reauthenticate using one of:\n"
56+
message += " aws sso login --profile \(profileName) (if using IAM Identity Center)\n"
57+
message += " aws login --profile \(profileName) (if using console credentials)\n"
58+
message += " aws configure --profile \(profileName) (to update static credentials)"
59+
}
60+
61+
if !isExpiredCredential {
62+
message += "\n\nNote: If the profile doesn't exist, verify it's configured in ~/.aws/config or ~/.aws/credentials"
63+
}
64+
65+
return message
66+
} else {
67+
// No profile specified
68+
var message = "Your AWS session has expired or no credentials are configured. "
69+
message += "Please reauthenticate using one of:\n"
70+
message += " aws login (for console credentials)\n"
71+
message += " aws sso login (for IAM Identity Center)\n"
72+
message += " aws configure (for static credentials)\n"
73+
message += " export AWS_ACCESS_KEY_ID=... (for environment variables)"
74+
75+
return message
76+
}
77+
}
78+
79+
/// Detects the type of AWS profile by reading the config file
80+
private func detectProfileType(profileName: String) -> ProfileType {
81+
let configPath = FileManager.default.homeDirectoryForCurrentUser
82+
.appendingPathComponent(".aws")
83+
.appendingPathComponent("config")
84+
85+
guard let configContent = try? String(contentsOf: configPath, encoding: .utf8) else {
86+
return .unknown
87+
}
88+
89+
// Look for the profile section
90+
// Note: The default profile can use either [default] or [profile default]
91+
let profileSections = profileName == "default"
92+
? ["[default]", "[profile default]"]
93+
: ["[profile \(profileName)]"]
94+
95+
var profileRange: Range<String.Index>?
96+
97+
for section in profileSections {
98+
if let range = configContent.range(of: section) {
99+
profileRange = range
100+
break
101+
}
102+
}
103+
104+
guard let range = profileRange else {
105+
return .unknown
106+
}
107+
108+
// Extract the profile section content (until next section or end)
109+
let startIndex = range.upperBound
110+
let remainingContent = configContent[startIndex...]
111+
112+
let sectionContent: String
113+
if let nextSectionRange = remainingContent.range(of: "\n[") {
114+
sectionContent = String(remainingContent[..<nextSectionRange.lowerBound])
115+
} else {
116+
sectionContent = String(remainingContent)
117+
}
118+
119+
// Check for SSO configuration
120+
if sectionContent.contains("sso_start_url") ||
121+
sectionContent.contains("sso_session") ||
122+
sectionContent.contains("sso_account_id") {
123+
return .sso
124+
}
125+
126+
// Check for login configuration (console credentials)
127+
// The aws login command creates a login_session entry
128+
if sectionContent.contains("login_session") {
129+
return .login
130+
}
131+
132+
// Check if credentials file has static credentials for this profile
133+
let credentialsPath = FileManager.default.homeDirectoryForCurrentUser
134+
.appendingPathComponent(".aws")
135+
.appendingPathComponent("credentials")
136+
137+
if let credentialsContent = try? String(contentsOf: credentialsPath, encoding: .utf8),
138+
credentialsContent.contains("[\(profileName)]") {
139+
return .staticCredentials
140+
}
141+
142+
// If profile exists in config but we can't determine the type
143+
return .unknown
144+
}
145+
146+
enum ProfileType {
147+
case sso
148+
case login
149+
case staticCredentials
150+
case unknown
151+
}
152+
}

0 commit comments

Comments
 (0)