Skip to content

Commit

Permalink
Added service: Azure, ref #44
Browse files Browse the repository at this point in the history
  • Loading branch information
inket committed Jun 15, 2019
1 parent 13e533b commit 54faf1d
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 3 deletions.
120 changes: 120 additions & 0 deletions Scripts/generate_azure_services.swift
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()
20 changes: 17 additions & 3 deletions stts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@
B2CE72D6228587E000847E00 /* Acquia.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CE72D5228587E000847E00 /* Acquia.swift */; };
B2CEB9A11DFD8984005FB901 /* StartAtLogin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2CEB9A01DFD8984005FB901 /* StartAtLogin.framework */; };
B2CEB9A21DFD8987005FB901 /* StartAtLogin.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = B2CEB9A01DFD8984005FB901 /* StartAtLogin.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
B2CEE5D722B49CB50073C743 /* Azure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CEE5D622B49CB50073C743 /* Azure.swift */; };
B2CEE5D922B49CC80073C743 /* AzureStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CEE5D822B49CC80073C743 /* AzureStore.swift */; };
B2CEE5DB22B4A97B0073C743 /* AzureServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2CEE5DA22B4A97B0073C743 /* AzureServices.swift */; };
B2D032D01D6455E300D51CB8 /* TravisCI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D032CF1D6455E300D51CB8 /* TravisCI.swift */; };
B2D2B6CF20870E700011723B /* StatusPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D2B6CE20870E700011723B /* StatusPage.swift */; };
B2D468291E2F0F82000F8F6B /* Coveralls.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D468281E2F0F82000F8F6B /* Coveralls.swift */; };
Expand Down Expand Up @@ -324,6 +327,10 @@
B2CBE08E21B95A7B00F5B28F /* GitLab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitLab.swift; sourceTree = "<group>"; };
B2CE72D5228587E000847E00 /* Acquia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Acquia.swift; sourceTree = "<group>"; };
B2CEB9A01DFD8984005FB901 /* StartAtLogin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StartAtLogin.framework; path = Carthage/Build/Mac/StartAtLogin.framework; sourceTree = SOURCE_ROOT; };
B2CEE5D622B49CB50073C743 /* Azure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Azure.swift; sourceTree = "<group>"; };
B2CEE5D822B49CC80073C743 /* AzureStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureStore.swift; sourceTree = "<group>"; };
B2CEE5DA22B4A97B0073C743 /* AzureServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureServices.swift; sourceTree = "<group>"; };
B2CEE5DC22B4A9900073C743 /* generate_azure_services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = generate_azure_services.swift; sourceTree = "<group>"; };
B2CF3FBF1DF7DD2100B66491 /* stts.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = stts.entitlements; sourceTree = "<group>"; };
B2D032CF1D6455E300D51CB8 /* TravisCI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TravisCI.swift; sourceTree = "<group>"; };
B2D2B6CE20870E700011723B /* StatusPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -640,15 +647,17 @@
B2E70FD222B0650A000BCAD2 /* Scripts */ = {
isa = PBXGroup;
children = (
B2E70FD322B0657F000BCAD2 /* generate_services_plist.sh */,
B2CEE5DC22B4A9900073C743 /* generate_azure_services.swift */,
B2E70FD622B066EC000BCAD2 /* generate_google_services.swift */,
B2E70FD322B0657F000BCAD2 /* generate_services_plist.sh */,
);
path = Scripts;
sourceTree = "<group>";
};
B2E70FDF22B0844F000BCAD2 /* Generated */ = {
isa = PBXGroup;
children = (
B2CEE5DA22B4A97B0073C743 /* AzureServices.swift */,
B2E70FDC22B07FB0000BCAD2 /* FirebaseServices.swift */,
B2E70FD722B069BA000BCAD2 /* GoogleCloudPlatformServices.swift */,
);
Expand All @@ -658,9 +667,9 @@
B2FDC91F1D6706FB00F0B728 /* Services */ = {
isa = PBXGroup;
children = (
B2E70FDF22B0844F000BCAD2 /* Generated */,
B2BB19B820B8FEB200A97A87 /* Cachet */,
B2253A69209F3B9F0039F574 /* Firebase */,
B2E70FDF22B0844F000BCAD2 /* Generated */,
B2493F071E26052400556A83 /* Google Cloud Platform */,
B2DCE68E22159731001447D8 /* LambStatus */,
B22FDE4921089D4F00359EC9 /* PayPal */,
Expand Down Expand Up @@ -689,6 +698,8 @@
B2FDC9201D67071500F0B728 /* Super */ = {
isa = PBXGroup;
children = (
B2CEE5D622B49CB50073C743 /* Azure.swift */,
B2CEE5D822B49CC80073C743 /* AzureStore.swift */,
B2BB19B920B8FEFD00A97A87 /* CachetService.swift */,
B2EB51CD1E7004A3001D6F78 /* ExanaService.swift */,
B2253A6C209F3F9F0039F574 /* FirebaseService.swift */,
Expand Down Expand Up @@ -844,7 +855,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "chmod +x $SRCROOT/Scripts/generate_google_services.swift\n$SRCROOT/Scripts/generate_google_services.swift\n";
shellScript = "chmod +x $SRCROOT/Scripts/generate_google_services.swift\n$SRCROOT/Scripts/generate_google_services.swift\nchmod +x $SRCROOT/Scripts/generate_azure_services.swift\n$SRCROOT/Scripts/generate_azure_services.swift\n";
};
B28F83321E24BDC100F2070F /* Generate Services plist file */ = {
isa = PBXShellScriptBuildPhase;
Expand Down Expand Up @@ -930,6 +941,7 @@
A0C3F34B220FD630005D9998 /* Datadog.swift in Sources */,
B2B2D1031D49D5080014D780 /* AppDelegate.swift in Sources */,
B25695601E247C8C00FAE413 /* SmartyStreets.swift in Sources */,
B2CEE5D722B49CB50073C743 /* Azure.swift in Sources */,
B25695611E247C8C00FAE413 /* Squarespace.swift in Sources */,
B2894CE420EEE2780009CCA3 /* Buildkite.swift in Sources */,
B20396361EE6A060008CCA21 /* Trello.swift in Sources */,
Expand All @@ -939,7 +951,9 @@
B2B015C21DBDE69B00699211 /* StatusTableCell.swift in Sources */,
B271688A2291AEEE001E608C /* Asana.swift in Sources */,
B299C84B1DD04E850024D2E9 /* Pusher.swift in Sources */,
B2CEE5DB22B4A97B0073C743 /* AzureServices.swift in Sources */,
B2163B0020BA56EF00B14A85 /* StatusCakeService.swift in Sources */,
B2CEE5D922B49CC80073C743 /* AzureStore.swift in Sources */,
B2D032D01D6455E300D51CB8 /* TravisCI.swift in Sources */,
B235341F22888EFE00DEDC36 /* Sendinblue.swift in Sources */,
B2D468291E2F0F82000F8F6B /* Coveralls.swift in Sources */,
Expand Down
33 changes: 33 additions & 0 deletions stts/Services/Generated/AzureServices.swift
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"
}
28 changes: 28 additions & 0 deletions stts/Services/Super/Azure.swift
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)
}
}
}
95 changes: 95 additions & 0 deletions stts/Services/Super/AzureStore.swift
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
}
}

0 comments on commit 54faf1d

Please sign in to comment.