-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
293 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
#!/usr/bin/swift | ||
|
||
import Foundation | ||
|
||
struct AzureZone { | ||
let serviceName: String | ||
let zoneIdentifier: String | ||
|
||
var className: String { | ||
var sanitizedName = serviceName | ||
sanitizedName = sanitizedName.replacingOccurrences(of: " & ", with: "And") | ||
sanitizedName = sanitizedName.replacingOccurrences(of: "/", with: "") | ||
sanitizedName = sanitizedName.replacingOccurrences(of: ":", with: "") | ||
return sanitizedName.components(separatedBy: " ").map { $0.capitalized(firstLetterOnly: true) }.joined(separator: "") | ||
} | ||
|
||
init(identifier: String, serviceName: String) { | ||
zoneIdentifier = identifier | ||
|
||
if !serviceName.hasPrefix("Azure") { | ||
self.serviceName = "Azure \(serviceName)" | ||
} else { | ||
self.serviceName = serviceName | ||
} | ||
} | ||
|
||
var output: String { | ||
return """ | ||
class \(className): Azure { | ||
let name = "\(serviceName)" | ||
let zoneIdentifier = "\(zoneIdentifier)" | ||
} | ||
""" | ||
} | ||
} | ||
|
||
extension String { | ||
subscript(_ range: NSRange) -> String { | ||
// Why we still have to do this shit in 2019 I don't know | ||
let start = self.index(self.startIndex, offsetBy: range.lowerBound) | ||
let end = self.index(self.startIndex, offsetBy: range.upperBound) | ||
let subString = self[start..<end] | ||
return String(subString) | ||
} | ||
|
||
func capitalized(firstLetterOnly: Bool) -> String { | ||
return firstLetterOnly ? (prefix(1).capitalized + dropFirst()) : self | ||
} | ||
} | ||
|
||
func envVariable(forKey key: String) -> String { | ||
guard let variable = ProcessInfo.processInfo.environment[key] else { | ||
print("error: Environment variable '\(key)' not set") | ||
exit(1) | ||
} | ||
|
||
return variable | ||
} | ||
|
||
func discoverZones() -> [AzureZone] { | ||
var result = [AzureZone]() | ||
|
||
var dataResult: Data? | ||
|
||
let semaphore = DispatchSemaphore(value: 0) | ||
URLSession.shared.dataTask(with: URL(string: "https://status.azure.com/en-us/status")!) { data, _, _ in | ||
dataResult = data | ||
semaphore.signal() | ||
}.resume() | ||
|
||
_ = semaphore.wait(timeout: .now() + .seconds(10)) | ||
|
||
guard let data = dataResult, var body = String(data: data, encoding: .utf8) else { | ||
print("warning: Build script generate_azure_services could not retrieve list of Azure zones") | ||
exit(0) | ||
} | ||
|
||
body = body.replacingOccurrences(of: "\n", with: "") | ||
|
||
// swiftlint:disable:next force_try | ||
let regex = try! NSRegularExpression( | ||
pattern: "li role=\"presentation\".*?data-zone-name=\"(.*?)\".*?data-event-property=\"(.*?)\"", | ||
options: [.caseInsensitive, .dotMatchesLineSeparators] | ||
) | ||
|
||
let range = NSRange(location: 0, length: body.count) | ||
regex.enumerateMatches(in: body, options: [], range: range) { textCheckingResult, _, _ in | ||
guard let textCheckingResult = textCheckingResult, textCheckingResult.numberOfRanges == 3 else { return } | ||
|
||
let identifier = body[textCheckingResult.range(at: 1)] | ||
let serviceName = body[textCheckingResult.range(at: 2)] | ||
|
||
result.append(AzureZone(identifier: identifier, serviceName: serviceName)) | ||
} | ||
|
||
return result | ||
} | ||
|
||
func main() { | ||
let srcRoot = envVariable(forKey: "SRCROOT") | ||
let outputPath = "\(srcRoot)/stts/Services/Generated/AzureServices.swift" | ||
let zones = discoverZones() | ||
|
||
let header = """ | ||
// This file is generated by generate_azure_services.swift and should not be modified manually. | ||
import Foundation | ||
""" | ||
|
||
let content = zones.map { $0.output }.joined(separator: "\n\n") | ||
let footer = "" | ||
|
||
let output = [header, content, footer].joined(separator: "\n") | ||
|
||
// swiftlint:disable:next force_try | ||
try! output.write(toFile: outputPath, atomically: true, encoding: .utf8) | ||
} | ||
|
||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// This file is generated by generate_azure_services.swift and should not be modified manually. | ||
|
||
import Foundation | ||
|
||
class AzureAmericas: Azure { | ||
let name = "Azure Americas" | ||
let zoneIdentifier = "americas" | ||
} | ||
|
||
class AzureEurope: Azure { | ||
let name = "Azure Europe" | ||
let zoneIdentifier = "europe" | ||
} | ||
|
||
class AzureAsiaPacific: Azure { | ||
let name = "Azure Asia Pacific" | ||
let zoneIdentifier = "asia" | ||
} | ||
|
||
class AzureAfrica: Azure { | ||
let name = "Azure Africa" | ||
let zoneIdentifier = "africa" | ||
} | ||
|
||
class AzureGovernment: Azure { | ||
let name = "Azure Government" | ||
let zoneIdentifier = "azure-government" | ||
} | ||
|
||
class AzureMiddleEastAndAfrica: Azure { | ||
let name = "Azure Middle East and Africa" | ||
let zoneIdentifier = "middle-east-africa" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// | ||
// Azure.swift | ||
// stts | ||
// | ||
|
||
import Foundation | ||
|
||
typealias Azure = BaseAzure & RequiredServiceProperties & AzureStoreService | ||
|
||
class BaseAzure: BaseService { | ||
private static var store = AzureStore() | ||
|
||
let url = URL(string: "https://status.azure.com/en-us/status")! | ||
|
||
override func updateStatus(callback: @escaping (BaseService) -> Void) { | ||
guard let realSelf = self as? Azure else { fatalError("BaseAzure should not be used directly.") } | ||
|
||
BaseAzure.store.loadStatus { [weak realSelf] in | ||
guard let strongSelf = realSelf else { return } | ||
|
||
let (status, message) = BaseAzure.store.status(for: strongSelf) | ||
strongSelf.status = status | ||
strongSelf.message = message | ||
|
||
callback(strongSelf) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// | ||
// AzureStore.swift | ||
// stts | ||
// | ||
|
||
import Kanna | ||
|
||
protocol AzureStoreService { | ||
var name: String { get } | ||
var zoneIdentifier: String { get } | ||
} | ||
|
||
class AzureStore { | ||
private var url = URL(string: "https://status.azure.com/en-us/status")! | ||
private var statuses: [String: ServiceStatus] = [:] | ||
private var loadErrorMessage: String? | ||
private var callbacks: [() -> Void] = [] | ||
private var lastUpdateTime: TimeInterval = 0 | ||
private var currentlyReloading: Bool = false | ||
|
||
func loadStatus(_ callback: @escaping () -> Void) { | ||
callbacks.append(callback) | ||
|
||
guard !currentlyReloading else { return } | ||
|
||
// Throttling to prevent multiple requests if the first one finishes too quickly | ||
guard Date.timeIntervalSinceReferenceDate - lastUpdateTime >= 3 else { return clearCallbacks() } | ||
|
||
currentlyReloading = true | ||
|
||
URLSession.sharedWithoutCaching.dataTask(with: url) { data, _, error in | ||
defer { | ||
self.currentlyReloading = false | ||
self.clearCallbacks() | ||
} | ||
|
||
self.statuses = [:] | ||
|
||
guard let data = data else { return self._fail(error) } | ||
guard let doc = try? HTML(html: data, encoding: .utf8) else { return self._fail("Couldn't parse response") } | ||
|
||
let zones = doc.css("li.zone[role=presentation]").compactMap { $0["data-zone-name"] } | ||
zones.forEach { identifier in | ||
let table = doc.css("table.status-table.region-status-table[data-zone-name=\(identifier)]").first | ||
|
||
table.map { | ||
guard let status = self.parseZoneTable($0) else { return } | ||
self.statuses[identifier] = status | ||
} | ||
} | ||
|
||
self.lastUpdateTime = Date.timeIntervalSinceReferenceDate | ||
}.resume() | ||
} | ||
|
||
func status(for service: AzureStoreService) -> (ServiceStatus, String) { | ||
let status = statuses[service.zoneIdentifier] | ||
|
||
switch status { | ||
case .good?: return (.good, "Good") | ||
case .minor?: return (.minor, "Warning") | ||
case .major?: return (.major, "Critical") | ||
case .notice?: return (.notice, "Information") | ||
default: return (.undetermined, loadErrorMessage ?? "Unexpected error") | ||
} | ||
} | ||
|
||
private func clearCallbacks() { | ||
callbacks.forEach { $0() } | ||
callbacks = [] | ||
} | ||
|
||
private func parseZoneTable(_ table: XMLElement) -> ServiceStatus? { | ||
return table.css("use").compactMap { svgElement -> ServiceStatus? in | ||
guard let svgName = svgElement["xlink:href"] else { return nil } | ||
|
||
switch svgName { | ||
case "#svg-check": return .good | ||
case "#svg-health-warning": return .minor | ||
case "#svg-health-error": return .major | ||
case "#svg-health-information": return .notice | ||
default: return nil | ||
} | ||
}.max() | ||
} | ||
|
||
private func _fail(_ error: Error?) { | ||
_fail(ServiceStatusMessage.from(error)) | ||
} | ||
|
||
private func _fail(_ message: String) { | ||
loadErrorMessage = message | ||
lastUpdateTime = 0 | ||
} | ||
} |