Skip to content

Commit

Permalink
Merge pull request #4 from appswithlove/feature/gitlab
Browse files Browse the repository at this point in the history
GitLab Runner Support
  • Loading branch information
Marcocanc committed Mar 7, 2023
2 parents 2b2b13b + b07fde9 commit 35b80cc
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 1 deletion.
20 changes: 20 additions & 0 deletions Cilicon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
0EA6AF5F2992BFB2007094CD /* GitlabService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA6AF5E2992BFB2007094CD /* GitlabService.swift */; };
0EBACEC3299287AA00A041C4 /* GitlabProvisionerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBACEC2299287AA00A041C4 /* GitlabProvisionerConfig.swift */; };
0EBACEC6299287BF00A041C4 /* GitlabRunnerProvisioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBACEC5299287BF00A041C4 /* GitlabRunnerProvisioner.swift */; };
A9492E5E2922376B005616CE /* ImageCopier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9492E5D2922376B005616CE /* ImageCopier.swift */; };
A9517C492937ACB500785136 /* ProcessProvisioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9517C482937ACB500785136 /* ProcessProvisioner.swift */; };
A9517C4B2937AFB900785136 /* ProcessProvisionerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9517C4A2937AFB900785136 /* ProcessProvisionerConfig.swift */; };
Expand Down Expand Up @@ -46,6 +49,9 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
0EA6AF5E2992BFB2007094CD /* GitlabService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitlabService.swift; sourceTree = "<group>"; };
0EBACEC2299287AA00A041C4 /* GitlabProvisionerConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitlabProvisionerConfig.swift; sourceTree = "<group>"; };
0EBACEC5299287BF00A041C4 /* GitlabRunnerProvisioner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitlabRunnerProvisioner.swift; sourceTree = "<group>"; };
A9492E5D2922376B005616CE /* ImageCopier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCopier.swift; sourceTree = "<group>"; };
A9517C482937ACB500785136 /* ProcessProvisioner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessProvisioner.swift; sourceTree = "<group>"; };
A9517C4A2937AFB900785136 /* ProcessProvisionerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessProvisionerConfig.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -105,6 +111,15 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
0EBACEC4299287BF00A041C4 /* Gitlab Runner */ = {
isa = PBXGroup;
children = (
0EBACEC5299287BF00A041C4 /* GitlabRunnerProvisioner.swift */,
0EA6AF5E2992BFB2007094CD /* GitlabService.swift */,
);
path = "Gitlab Runner";
sourceTree = "<group>";
};
A9517C472937ACA800785136 /* Process */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -176,6 +191,7 @@
A9728DFB2918F9A000342A77 /* ConfigManager.swift */,
A9FDAB2229375E8100B8CA1F /* DirectoryMountConfig.swift */,
A9728DF72918F9A000342A77 /* GithubProvisionerConfig.swift */,
0EBACEC2299287AA00A041C4 /* GitlabProvisionerConfig.swift */,
A9517C4A2937AFB900785136 /* ProcessProvisionerConfig.swift */,
A9728DF82918F9A000342A77 /* HardwareConfig.swift */,
A9728DFA2918F9A000342A77 /* ProvisionerConfig.swift */,
Expand All @@ -189,6 +205,7 @@
A9728E042918F9BF00342A77 /* Provisioner.swift */,
A9517C472937ACA800785136 /* Process */,
A9728E032918F9B000342A77 /* Github Actions */,
0EBACEC4299287BF00A041C4 /* Gitlab Runner */,
);
path = Provisioner;
sourceTree = "<group>";
Expand Down Expand Up @@ -342,15 +359,18 @@
A9728E152918FA4700342A77 /* AppleEvents.swift in Sources */,
A9492E5E2922376B005616CE /* ImageCopier.swift in Sources */,
A9728DFC2918F9A000342A77 /* VMConfigurationHelper.swift in Sources */,
0EBACEC3299287AA00A041C4 /* GitlabProvisionerConfig.swift in Sources */,
A9728DE42918F7C500342A77 /* VirtualMachineView.swift in Sources */,
A9728E0A2918F9C600342A77 /* GithubActionsProvisioner.swift in Sources */,
A9728DFD2918F9A000342A77 /* GithubProvisionerConfig.swift in Sources */,
A9517C492937ACB500785136 /* ProcessProvisioner.swift in Sources */,
A9728DFF2918F9A000342A77 /* Config.swift in Sources */,
A9728DE82918F7D000342A77 /* VMManager.swift in Sources */,
0EA6AF5F2992BFB2007094CD /* GitlabService.swift in Sources */,
A9728E052918F9BF00342A77 /* Provisioner.swift in Sources */,
A9FDAB1D292E5A2E00B8CA1F /* NSSound+SystemSounds.swift in Sources */,
A9728E092918F9C600342A77 /* GithubService.swift in Sources */,
0EBACEC6299287BF00A041C4 /* GitlabRunnerProvisioner.swift in Sources */,
A9728DFE2918F9A000342A77 /* HardwareConfig.swift in Sources */,
A9728E002918F9A000342A77 /* ProvisionerConfig.swift in Sources */,
A9728E012918F9A000342A77 /* ConfigManager.swift in Sources */,
Expand Down
12 changes: 12 additions & 0 deletions Cilicon/Config/GitLabProvisionerConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

struct GitlabProvisionerConfig: Decodable {
/// The name by which the runner can be identified
let name: String
/// The url to register the runner at. In a self-hosted environment, this is probably your main GitLab URL, e.g. https://gitlab.yourdomain.net/
let url: URL
/// The runner registration token, can be obtained in the GitLab runner UI
let registrationToken: String
/// A list of tags to apply to the runner, comma-separated
let tagList: String
}
5 changes: 5 additions & 0 deletions Cilicon/Config/ProvisionerConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation

enum ProvisionerConfig: Decodable {
case github(GithubProvisionerConfig)
case gitlab(GitlabProvisionerConfig)
case process(ProcessProvisionerConfig)
case none

Expand All @@ -17,6 +18,9 @@ enum ProvisionerConfig: Decodable {
case .github:
let config = try container.decode(GithubProvisionerConfig.self, forKey: .config)
self = .github(config)
case .gitlab:
let config = try container.decode(GitlabProvisionerConfig.self, forKey: .config)
self = .gitlab(config)
case .process:
let config = try container.decode(ProcessProvisionerConfig.self, forKey: .config)
self = .process(config)
Expand All @@ -27,6 +31,7 @@ enum ProvisionerConfig: Decodable {

enum ProvisionerType: String, Decodable {
case github
case gitlab
case process
case none
}
Expand Down
79 changes: 79 additions & 0 deletions Cilicon/Provisioner/Gitlab Runner/GitlabRunnerProvisioner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import Foundation

class GitlabRunnerProvisioner: Provisioner {
let config: Config
let runnerConfig: GitlabProvisionerConfig
let service: GitlabService
let fileManager: FileManager

private var runnerToken: String?

init(config: Config, gitlabConfig: GitlabProvisionerConfig, fileManager: FileManager = .default) {
self.config = config
self.runnerConfig = gitlabConfig
self.service = GitlabService(config: gitlabConfig)
self.fileManager = fileManager
}

func provision(bundle: VMBundle) async throws {
let registration = try await service.registerRunner()
try setRunnerEndpointURL(bundle: bundle, url: runnerConfig.url)
try setRunnerToken(bundle: bundle, token: registration.token)
self.runnerToken = registration.token
}

func deprovision(bundle: VMBundle) async throws {
if let runnerToken {
try await service.deregisterRunner(runnerToken: runnerToken)
} else {
print("Nothing to deregister, skipping...")
}
return
}

private func setRunnerEndpointURL(bundle: VMBundle, url: URL) throws {
let tokenPath = bundle.runnerEndpointURL.relativePath
guard fileManager.createFile(atPath: tokenPath, contents: url.absoluteString.data(using: .utf8)) else {
throw GitlabRunnerProvisioner.Error.couldNotCreateRunnerTokenFile(path: tokenPath)
}
}

private func setRunnerToken(bundle: VMBundle, token: String) throws {
let tokenPath = bundle.runnerTokenURL.relativePath
guard fileManager.createFile(atPath: tokenPath, contents: token.data(using: .utf8)) else {
throw GitlabRunnerProvisioner.Error.couldNotCreateRunnerTokenFile(path: tokenPath)
}
}
}

extension GitlabRunnerProvisioner {
enum Error: Swift.Error {
case couldNotCreateRunnerTokenFile(path: String)
case couldNotCreateRunnerEndpointFile(path: String)
case invalidConfiguration(reason: String)
}
}

extension GitlabRunnerProvisioner.Error: LocalizedError {
var errorDescription: String? {
switch self {
case let .couldNotCreateRunnerTokenFile(path):
return "Could not create Runner Token File at \(path)"
case let .couldNotCreateRunnerEndpointFile(path):
return "Could not create Runner Endpoint File at \(path)"
case let .invalidConfiguration(reason):
return "Configuration invalid: \(reason)"
}
}
}


fileprivate extension VMBundle {
var runnerTokenURL: URL {
resourcesURL.appending(component: "RUNNER_TOKEN")
}

var runnerEndpointURL: URL {
resourcesURL.appending(component: "RUNNER_ENDPOINT_URL")
}
}
147 changes: 147 additions & 0 deletions Cilicon/Provisioner/Gitlab Runner/GitlabService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import Foundation

class GitlabService {
private let urlSession: URLSession
let config: GitlabProvisionerConfig
let baseURL: URL

init(config: GitlabProvisionerConfig) {
self.config = config
self.baseURL = config.url
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
self.urlSession = URLSession(configuration: config)
}

private func apiURL() -> URL {
baseURL
.appendingPathComponent("api")
.appendingPathComponent("v4")
}

private func runnersURL() -> URL {
apiURL()
.appendingPathComponent("runners")
}
}

// MARK: Methods

extension GitlabService {
func registerRunner() async throws -> RunnerRegistrationResponse {
let registration = RunnerRegistration(registrationToken: config.registrationToken,
description: config.name,
tags: config.tagList.components(separatedBy: ","))
let jsonData = try encode(registration)
let (data, response) = try await postRequest(to: runnersURL(), jsonData: jsonData)

guard let httpResponse = response as? HTTPURLResponse else {
throw Error.couldNotRegisterRunner(reason: "Expected a HTTP Response, got \(response)")
}

guard httpResponse.statusCode == 201 else {
throw Error.couldNotRegisterRunner(reason: "Got response code \(httpResponse.statusCode), expected to receive 201 instead.")
}

guard let registrationResponse: RunnerRegistrationResponse = try? decode(data) else {
throw Error.couldNotRegisterRunner(reason: "Could not decode the response")
}

return registrationResponse
}

func deregisterRunner(runnerToken token: String) async throws {
let deletion = RunnerDeletion(token: token)
let jsonData = try encode(deletion)

let (_, response) = try await deleteRequest(to: runnersURL(), jsonData: jsonData)

guard let httpResponse = response as? HTTPURLResponse else {
throw Error.couldNotDeleteRunner(reason: "Expected a HTTP Response, got \(response)")
}

guard httpResponse.statusCode == 204 else {
throw Error.couldNotDeleteRunner(reason: "Got response code \(httpResponse.statusCode), expected to receive 204 instead.")
}
}
}

// MARK: Requests

private extension GitlabService {
private func postRequest(to url: URL, jsonData: Data) async throws -> (Data, URLResponse) {
return try await makeRequest(to: url, method: "POST", jsonData: jsonData)
}

private func deleteRequest(to url: URL, jsonData: Data) async throws -> (Data, URLResponse) {
return try await makeRequest(to: url, method: "DELETE", jsonData: jsonData)
}

private func makeRequest(to url: URL, method: String, jsonData: Data) async throws -> (Data, URLResponse) {
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData

return try await urlSession.data(for: request)
}
}

// MARK: Codable

private extension GitlabService {
private func encode(_ encodable: Encodable) throws -> Data {
let encoder = JSONEncoder()
return try encoder.encode(encodable)
}

private func decode<T: Decodable>(_ decodable: Data) throws -> T {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(T.self, from: decodable)
}
}

// MARK: Models

extension GitlabService {
private struct RunnerRegistration: Codable {
let registrationToken: String
let description: String
let tags: [String]

enum CodingKeys: String, CodingKey {
case registrationToken = "token"
case description
case tags = "tag_list"
}
}

public struct RunnerRegistrationResponse: Decodable {
let id: Int
let token: String
}

private struct RunnerDeletion: Codable {
let token: String
}
}

extension GitlabService {
enum Error: Swift.Error {
case couldNotRegisterRunner(reason: String)
case couldNotDeleteRunner(reason: String)
}
}

extension GitlabService.Error: LocalizedError {
var errorDescription: String? {
switch self {
case .couldNotRegisterRunner(let reason):
return "Could not register runner: \(reason)"
case .couldNotDeleteRunner(let reason):
return "Could not delete runner: \(reason)"
}
}
}

2 changes: 2 additions & 0 deletions Cilicon/VMManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class VMManager: NSObject, ObservableObject {
switch config.provisioner {
case .github(let githubConfig):
self.provisioner = GithubActionsProvisioner(config: config, ghConfig: githubConfig)
case .gitlab(let gitlabConfig):
self.provisioner = GitlabRunnerProvisioner(config: config, gitlabConfig: gitlabConfig)
case .process(let processConfig):
self.provisioner = ProcessProvisioner(path: processConfig.executablePath, arguments: processConfig.arguments)
case .none:
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Depending on the provisioner you choose, Cilicon places files required by your G

The [Github Actions Provisioner](/Cilicon/Provisioner/Github%20Actions/GithubActionsProvisioner.swift) provisions the image with the runner download URL, a registration token, the runner name and runner labels.

The [Gitlab Runner Provisioner](/Cilicon/Provisioner/Gitlab%20Runner/GitlabRunnerProvisioner.swift) provisions the image with the runner endpoint URL and a runner token.

The [Process Provisioner](Cilicon/Provisioner/Process/ProcessProvisioner.swift) runs an executable of your choice when provisioning and deprovisioning a bundle. It passes the bundle path, the action (either `provision` or `deprovision`) as well as any extra arguments of your choice to the executable.

You may also opt out of using a provisioner by setting the provisioner type to `none`. This may work fine with services like Buildkite which use non-expiring registration tokens.
Expand All @@ -47,7 +49,7 @@ Cilicon listens for a shutdown of the Guest OS and removes the used image before
</p>

## 🚀 Getting Started
Currently Cilicon offers native support for Github Actions. It also offers a "Process" provisioner (which allows running an executable for provisioning and deprovisioning) and a provisioner-less mode.
Currently Cilicon offers native support for Github Actions and Gitlab Runner on self-hosted instances. It also offers a "Process" provisioner (which allows running an executable for provisioning and deprovisioning) and a provisioner-less mode.
The host as well as the guest system must be running macOS 13 or newer and, as the name implies, Cilicon only runs on Apple Silicon.

To get started download Cilicon and Cilicon Installer from the [latest release](https://github.com/traderepublic/Cilicon/releases/latest).
Expand Down Expand Up @@ -75,6 +77,8 @@ The resulting `.bundle` file can be opened by right-clicking it in Finder and pr

Cilicon expects a valid `cilicon.yml` file to be present in the Host OS's home directory.

#### GitHub Actions

To use the Github Actions provisioner you will need to create and install a new Github App with `Self-hosted runners` `Read & Write` permissions on the organization level and provide your config with the respective information.


Expand All @@ -99,6 +103,22 @@ editorMode: false
For more information on available optional and required properties, see [Config.swift](/Cilicon/Config/Config.swift).
#### Gitlab Runner
To use the GitLab Runner provisioner, download the GitLab Runner binary `gitlab-runner-darwin-arm64` from the GitLab Runner Releases page and place it in the VM Bundle's `Resources` folder so that it can be accessed by the VM.

Configure the `cilicon.yml` file with the correct values:

``` yml
provisioner:
type: gitlab
config:
name: "my-runner"
url: "https://gitlab.yourcompany.net/"
registrationToken: "your-runner-registration-token"
tagList: "some-tags,comma-separated"
```

### 🔧 Setting up the Guest OS
Once you have created a new VM Bundle you will need to set it up. To do so, enable the `editorMode` in the `cilicon.yml` file.
This will disable bundle duplication, provisioning and automatic restarting after shutdown.
Expand Down
Loading

0 comments on commit 35b80cc

Please sign in to comment.