Skip to content

Commit

Permalink
Add handling of enterprise self hosted runner
Browse files Browse the repository at this point in the history
  • Loading branch information
mattia committed Dec 21, 2023
1 parent 48578a9 commit 567b8a6
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ public protocol GitHubCredentialsStore: AnyObject {
var organizationName: String? { get async }
var repositoryName: String? { get async }
var ownerName: String? { get async }
var enterpriseName: String? { get async }
var appId: String? { get async }
var privateKey: Data? { get async }
func setSelfHostedURL(_ selfHostedURL: URL?) async
func setOrganizationName(_ organizationName: String?) async
func setRepository(_ repositoryName: String?, withOwner ownerName: String?) async
func setEnterpriseName(_ enterpriseName: String?) async
func setAppID(_ appID: String?) async
func setPrivateKey(_ privateKeyData: Data?) async
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public final actor GitHubCredentialsStoreKeychain: GitHubCredentialsStore {
static let organizationName = "github.credentials.organizationName"
static let repositoryName = "github.credentials.repositoryName"
static let ownerName = "github.credentials.ownerName"
static let enterpriseName = "github.credentials.enterpriseName"
static let appId = "github.credentials.appId"
}

Expand Down Expand Up @@ -37,6 +38,11 @@ public final actor GitHubCredentialsStoreKeychain: GitHubCredentialsStore {
return await keychain.password(forAccount: PasswordAccount.ownerName, belongingToService: serviceName)
}
}
public var enterpriseName: String? {
get async {
return await keychain.password(forAccount: PasswordAccount.enterpriseName, belongingToService: serviceName)
}
}
public var appId: String? {
get async {
return await keychain.password(forAccount: PasswordAccount.appId, belongingToService: serviceName)
Expand Down Expand Up @@ -84,6 +90,14 @@ public final actor GitHubCredentialsStoreKeychain: GitHubCredentialsStore {
}
}

public func setEnterpriseName(_ enterpriseName: String?) async {
if let enterpriseName {
_ = await keychain.setPassword(enterpriseName, forAccount: PasswordAccount.enterpriseName, belongingToService: serviceName)
} else {
await keychain.removePassword(forAccount: PasswordAccount.enterpriseName, belongingToService: serviceName)
}
}

public func setAppID(_ appID: String?) async {
if let appID {
_ = await keychain.setPassword(appID, forAccount: PasswordAccount.appId, belongingToService: serviceName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import Foundation
public enum GitHubRunnerScope: String, CaseIterable {
case organization
case repo
case enterpriseServer
}
15 changes: 15 additions & 0 deletions Packages/GitHub/Sources/GitHubServiceLive/GitHubServiceLive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ private enum GitHubServiceLiveError: LocalizedError {
case organizationNameUnavailable
case repositoryNameUnavailable
case repositoryOwnerNameUnavailable
case enterpriseNameUnavailable
case appIDUnavailable
case privateKeyUnavailable
case appIsNotInstalled
Expand All @@ -21,6 +22,8 @@ private enum GitHubServiceLiveError: LocalizedError {
return "The repository name is not available"
case .repositoryOwnerNameUnavailable:
return "The repository owner name is not available"
case .enterpriseNameUnavailable:
return "The enterprise name is not available"
case .appIDUnavailable:
return "The app ID is not available"
case .privateKeyUnavailable:
Expand Down Expand Up @@ -141,6 +144,11 @@ private extension GitHubRunnerScope {
}

return "/repos/\(ownerName)/\(repositoryName)/actions/runners/registration-token"
case .enterpriseServer:
guard let enterpriseName = await credentialsStore.enterpriseName else {
throw GitHubServiceLiveError.enterpriseNameUnavailable
}
return "/enterprises/\(enterpriseName)/actions/runners/registration-token"
}
}

Expand All @@ -160,6 +168,11 @@ private extension GitHubRunnerScope {
}

return "/repos/\(ownerName)/\(repositoryName)/actions/runners/downloads"
case .enterpriseServer:
guard let enterpriseName = await credentialsStore.enterpriseName else {
throw GitHubServiceLiveError.enterpriseNameUnavailable
}
return "/enterprises/\(enterpriseName)/actions/runners/downloads"
}
}

Expand All @@ -169,6 +182,8 @@ private extension GitHubRunnerScope {
return await credentialsStore.organizationName
case .repo:
return await credentialsStore.ownerName
case .enterpriseServer:
return await credentialsStore.enterpriseName
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ struct GitHubPrivateKeyPicker: View {
L10n.Settings.Github.PrivateKey.Scopes.organization
case .repo:
L10n.Settings.Github.PrivateKey.Scopes.repository
case .enterpriseServer:
"Check permissions: `manage_runners:enterprise`"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ struct GitHubSettingsView: View {
prompt: Text(L10n.Settings.Github.RepositoryName.prompt)
)
.disabled(!viewModel.isSettingsEnabled)
case .enterpriseServer:
TextField(
"Enterprise name",
text: $viewModel.enterpriseName,
prompt: Text("Acme Enterprise")
)
.disabled(!viewModel.isSettingsEnabled)
}
}
Section {
Expand Down Expand Up @@ -92,6 +99,8 @@ private extension GitHubRunnerScope {
L10n.Settings.RunnerScope.organization
case .repo:
L10n.Settings.RunnerScope.repository
case .enterpriseServer:
"Enterprise"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class GitHubSettingsViewModel: ObservableObject {
@Published var organizationName: String = ""
@Published var repositoryName: String = ""
@Published var ownerName: String = ""
@Published var enterpriseName: String = ""
@Published var appId: String = ""
@Published var privateKeyName = ""
@Published var version: GitHubServiceVersion.Kind
Expand All @@ -21,11 +22,13 @@ final class GitHubSettingsViewModel: ObservableObject {
private let credentialsStore: GitHubCredentialsStore
private var cancellables: Set<AnyCancellable> = []
private var createAppURL: URL {
var url = URL(string: "https://github.com")!
if !organizationName.isEmpty, case .organization = runnerScope {
url = url.appending(path: "/organizations/\(organizationName)")
get async {
var url = await credentialsStore.selfHostedURL ?? .gitHub
if !organizationName.isEmpty, case .organization = runnerScope {
url = url.appending(path: "/organizations/\(organizationName)")
}
return url.appending(path: "/settings/apps")
}
return url.appending(path: "/settings/apps")
}

init(settingsStore: SettingsStore, credentialsStore: GitHubCredentialsStore, isSettingsEnabled: AnyPublisher<Bool, Never>) {

Check warning on line 34 in Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsViewModel.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines (function_body_length)
Expand Down Expand Up @@ -65,27 +68,44 @@ final class GitHubSettingsViewModel: ObservableObject {
$ownerName.nilIfEmpty(),
$repositoryName.nilIfEmpty()
)
.combineLatest($enterpriseName.nilIfEmpty())
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.dropFirst()
.sink { [weak self] runnerScope, organizationName, ownerName, repositoryName in
.map { tuple, enterpriseName in
let runnerScope = tuple.0
let organizationName = tuple.1
let ownerName = tuple.2
let repositoryName = tuple.3
return (runnerScope, organizationName, ownerName, repositoryName, enterpriseName)
}
.sink { [weak self] runnerScope, organizationName, ownerName, repositoryName, enterpriseName in
self?.settingsStore.githubRunnerScope = runnerScope
switch runnerScope {
case .organization:
Task {
await self?.credentialsStore.setOrganizationName(organizationName)
await self?.credentialsStore.setRepository(nil, withOwner: nil)
await self?.credentialsStore.setEnterpriseName(nil)
}
case .repo:
Task {
await self?.credentialsStore.setOrganizationName(nil)
await self?.credentialsStore.setRepository(repositoryName, withOwner: ownerName)
}
case .enterpriseServer:
Task {
await self?.credentialsStore.setOrganizationName(nil)
await self?.credentialsStore.setRepository(nil, withOwner: nil)
await self?.credentialsStore.setEnterpriseName(enterpriseName)
}
}
}.store(in: &cancellables)
}

func openCreateApp() {
NSWorkspace.shared.open(createAppURL)
Task {
NSWorkspace.shared.open(await createAppURL)
}
}

func storePrivateKey(at fileURL: URL) async {
Expand Down Expand Up @@ -113,6 +133,7 @@ final class GitHubSettingsViewModel: ObservableObject {
organizationName = await credentialsStore.organizationName ?? ""
repositoryName = await credentialsStore.repositoryName ?? ""
ownerName = await credentialsStore.ownerName ?? ""
enterpriseName = await credentialsStore.enterpriseName ?? ""
appId = await credentialsStore.appId ?? ""
let privateKey = await credentialsStore.privateKey
privateKeyName = privateKey != nil ? settingsStore.gitHubPrivateKeyName ?? "" : ""
Expand All @@ -124,3 +145,7 @@ private extension Publisher where Output == String {
map { !$0.isEmpty ? $0 : nil }.eraseToAnyPublisher()
}
}

private extension URL {
static let gitHub = URL(string: "https://github.com/")!
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,25 @@ public struct VirtualMachineResourcesServiceEphemeral: VirtualMachineResourcesSe

private extension VirtualMachineResourcesServiceEphemeral {
private func getRunnerURL() async throws -> URL {
let baseURL = await gitHubCredentialsStore.selfHostedURL ?? .gitHub
switch runnerScope {
case .organization:
let organizationName = try await getOrganizationName()
guard let runnerURL = URL(string: "https://github.com/" + organizationName) else {
throw VirtualMachineResourcesServiceEphemeralError.invalidRunnerURL
}
return runnerURL
return baseURL.appending(path: organizationName, directoryHint: .notDirectory)
case .repo:
guard
let ownerName = await gitHubCredentialsStore.ownerName,
let repositoryName = await gitHubCredentialsStore.repositoryName,
let runnerURL = URL(string: "https://github.com/\(ownerName)/\(repositoryName)")
let repositoryName = await gitHubCredentialsStore.repositoryName
else {
throw VirtualMachineResourcesServiceEphemeralError.invalidRunnerURL
}
return runnerURL
return baseURL.appending(path: "\(ownerName)/\(repositoryName)", directoryHint: .notDirectory)
case .enterpriseServer:
guard let enterpriseName = await gitHubCredentialsStore.enterpriseName else {
throw VirtualMachineResourcesServiceEphemeralError.invalidRunnerURL
}
// SEE https://docs.github.com/en/[email protected]/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-enterprise
return baseURL.appending(path: enterpriseName, directoryHint: .notDirectory)
}
}

Expand All @@ -129,3 +132,7 @@ private extension VirtualMachineResourcesServiceEphemeral {
}
}
}

private extension URL {
static let gitHub = URL(string: "https://github.com/")!
}

0 comments on commit 567b8a6

Please sign in to comment.