Skip to content

Commit

Permalink
tweak exec process launching
Browse files Browse the repository at this point in the history
  • Loading branch information
ewilken committed Jan 30, 2024
1 parent 789669a commit fac0e80
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 35 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI

on:
push:
branches: ['main']
pull_request:
branches: ['main']

jobs:
build:
name: Build and analyze using xcodebuild
runs-on: macos-14

steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode version 15
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15'
- name: Build
run: |
xcodebuild clean build analyze -project 'Nautik Helper.xcodeproj' -scheme 'Nautik Helper' | xcpretty && exit ${PIPESTATUS[0]}
95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Release

on:
release:
types: [published]

jobs:
build:
name: Build app bundle and upload it to the release
runs-on: macos-14

steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode version 15
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15'
# https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development#creating-secrets-for-your-certificate-and-provisioning-profile
# https://defn.io/2023/09/22/distributing-mac-apps-with-github-actions
- name: Install the Apple certificate and provisioning profile
env:
# exported from Xcode (Developer ID Application Certificate)
# base64 -i ID_CERTIFICATE.p12 > ID_CERTIFICATE_BASE64
ID_CERTIFICATE_BASE64: ${{ secrets.ID_CERTIFICATE_BASE64 }}
# openssl rand -hex 32 > ID_CERTIFICATE_PASSWORD
ID_CERTIFICATE_PASSWORD: ${{ secrets.ID_CERTIFICATE_PASSWORD }}
# exported from Xcode (Apple Development Certificate)
# base64 -i BUILD_CERTIFICATE.p12 > BUILD_CERTIFICATE_BASE64
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
# openssl rand -hex 32 > BUILD_CERTIFICATE_PASSWORD
BUILD_CERTIFICATE_PASSWORD: ${{ secrets.BUILD_CERTIFICATE_PASSWORD }}
# openssl rand -hex 32 > KEYCHAIN_PASSWORD
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
ID_CERTIFICATE_PATH=$RUNNER_TEMP/id_certificate.p12
BUILD_CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificates from secrets
echo -n "$ID_CERTIFICATE_BASE64" | base64 --decode -o $ID_CERTIFICATE_PATH
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $BUILD_CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $ID_CERTIFICATE_PATH -P "$ID_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security import $BUILD_CERTIFICATE_PATH -P "$BUILD_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Build app
run: |
mkdir -p dist
xcodebuild \
archive \
-project 'Nautik Helper.xcodeproj'/ \
-scheme 'Nautik Helper' \
-configuration Release \
-destination 'generic/platform=macOS' \
-archivePath 'dist/Nautik Helper.xcarchive' \
-allowProvisioningUpdates
xcodebuild \
-exportArchive \
-archivePath 'dist/Nautik Helper.xcarchive' \
-exportOptionsPlist 'Nautik Helper/ExportOptions.plist' \
-exportPath dist/ \
-allowProvisioningUpdates
ditto -c -k --keepParent 'dist/Nautik Helper.app' dist/helper-${{ github.ref }}.zip
xcrun notarytool submit \
--wait \
--key /Users/elias/Downloads/AuthKey_6F3C73566R.p8 \
--key-id 6F3C73566R \
--issuer bc562387-e432-4ba9-90bc-bae79d1a299e \
dist/helper-1.0.0.zip
xcrun stapler staple 'dist/Nautik Helper.app'
# https://stackoverflow.com/questions/60608887/what-is-the-most-efficient-way-to-notarize-and-staple-a-zip-containing-a-app
rm dist/helper-1.0.0.zip
ditto -c -k --keepParent 'dist/Nautik Helper.app' dist/helper-${{ github.ref }}.zip
- name: Upload app bundle to release
uses: svenstaro/upload-release-action@v2
with:
file: dist/helper-${{ github.ref }}.zip
asset_name: helper-${{ github.ref }}.zip
tag: ${{ github.ref }}
overwrite: true
4 changes: 2 additions & 2 deletions Nautik Helper.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = io.nautik.Nautik.Helper;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down Expand Up @@ -370,7 +370,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.3;
PRODUCT_BUNDLE_IDENTIFIER = io.nautik.Nautik.Helper;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
74 changes: 41 additions & 33 deletions Nautik Helper/StoredCluster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,34 @@ class StoredCluster: Codable, @unchecked Sendable {
var keychain: Keychain.KeychainType
var position: Double
var name: String

var cluster: Cluster
var authInfo: AuthInfo

var defaultNamespace: String

var error: String?

var kubeConfigDeviceID: UUID
var kubeConfigDeviceUser: String
var kubeConfigPath: URL
var kubeConfigContextName: String
var credentialsExpireAt: Date?
var lastEvaluation: Date

init(
id: UUID,
keychain: Keychain.KeychainType,
position: Double,
name: String,

cluster: Cluster,
authInfo: AuthInfo,

defaultNamespace: String,

error: String? = nil,

kubeConfigDeviceID: UUID,
kubeConfigDeviceUser: String,
kubeConfigPath: URL,
Expand All @@ -46,34 +46,34 @@ class StoredCluster: Codable, @unchecked Sendable {
self.keychain = keychain
self.position = position
self.name = name

self.cluster = cluster
self.authInfo = authInfo

self.defaultNamespace = defaultNamespace

self.error = error

self.kubeConfigDeviceID = kubeConfigDeviceID
self.kubeConfigDeviceUser = kubeConfigDeviceUser
self.kubeConfigPath = kubeConfigPath
self.kubeConfigContextName = kubeConfigContextName
self.credentialsExpireAt = credentialsExpireAt
self.lastEvaluation = lastEvaluation
}

static let decoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()

func evaluateAuth() async throws {
if let caFile = cluster.certificateAuthority {
let caData = try Data(contentsOf: URL(fileURLWithPath: caFile))
cluster.certificateAuthorityData = caData
}

if let tokenFile = authInfo.tokenFile {
let token = try String(contentsOf: URL(fileURLWithPath: tokenFile), encoding: .utf8)
authInfo.token = token
Expand All @@ -87,7 +87,7 @@ class StoredCluster: Codable, @unchecked Sendable {
let clientKeyData = try Data(contentsOf: URL(fileURLWithPath: clientKeyFile))
authInfo.clientKeyData = clientKeyData
}

if let impersonate = authInfo.impersonate {
// TODO
}
Expand All @@ -97,7 +97,7 @@ class StoredCluster: Codable, @unchecked Sendable {
if let impersonateUserExtra = authInfo.impersonateUserExtra {
// TODO
}

if let authProvider = authInfo.authProvider {
// TODO
}
Expand All @@ -107,16 +107,16 @@ class StoredCluster: Codable, @unchecked Sendable {
guard let stdout = try executeCommand(command: exec.command, arguments: exec.args) else {
throw "Executing \(exec.command) yielded no stdout."
}

do {
let credential = try Self.decoder.decode(ExecCredential.self, from: Data(stdout.utf8))

await MainActor.run { [weak self] in
self?.authInfo.token = credential.status.token

self?.authInfo.clientCertificateData = credential.status.clientCertificateData.map { $0.data(using: .utf8).map { Data(base64Encoded: $0) } ?? nil } ?? nil
self?.authInfo.clientKeyData = credential.status.clientKeyData.map { $0.data(using: .utf8).map { Data(base64Encoded: $0) } ?? nil } ?? nil

self?.credentialsExpireAt = credential.status.expirationTimestamp
}
} catch {
Expand All @@ -130,7 +130,7 @@ class StoredCluster: Codable, @unchecked Sendable {
} else {
credentialsExpireAt = nil
}

error = nil
lastEvaluation = Date.now
}
Expand All @@ -142,7 +142,7 @@ struct ExecCredential: Codable, @unchecked Sendable {
let kind: String
let spec: Spec
let status: Status

struct Spec: Codable {
let cluster: Cluster?
let interactive: Bool?
Expand All @@ -160,18 +160,18 @@ func executeCommand(command: String, arguments: [String]? = nil) throws -> Strin
guard let shell = try runProcess(command: "/usr/bin/env", arguments: ["/bin/sh", "-cl", "echo $SHELL"]) else {
throw "Couldn't evaluate the user's SHELL."
}

guard let cmdPath = try runProcess(command: "/usr/bin/env", arguments: [shell, "-cl\(shell.contains("zsh") ? "i" : "")", "which \(command)"]) else {
throw "Executable \(command) not found in the user's PATH."
}
if cmdPath == "\(command) not found" {
throw "Executable \(command) not found in the user's PATH."
}

let stdout = try runProcess(command: "/usr/bin/env", arguments: [shell, "-cl", "\(cmdPath) \(arguments.map { $0.joined(separator: " ") } ?? "")"])
let stdout = try runProcess(command: "/usr/bin/env", arguments: [shell, "-cl\(shell.contains("zsh") ? "i" : "")", "\(cmdPath) \(arguments.map { $0.joined(separator: " ") } ?? "")"])

return stdout

func runProcess(command: String, arguments: [String]?, path: String? = nil) throws -> String? {
let task = Process()

Expand All @@ -185,21 +185,29 @@ func executeCommand(command: String, arguments: [String]? = nil) throws -> Strin
}
}

task.launchPath = command
task.executableURL = URL(fileURLWithPath: command)
if let arguments {
task.arguments = arguments
}

let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe

task.launch()
task.waitUntilExit()
try task.run()
//task.waitUntilExit()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()

let output = String(decoding: outputData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
let error = String(decoding: errorData, as: UTF8.self)

if !error.isEmpty {
throw error
}

return string
return output
}
}

0 comments on commit fac0e80

Please sign in to comment.