update roadmap phases and bootstrap scripts across platforms
This commit is contained in:
parent
f8863710b1
commit
9628ec36c4
9 changed files with 153 additions and 64 deletions
|
|
@ -12,7 +12,7 @@ OTO Core가 발급하는 bootstrap 흐름을 Linux 단일 shell script 기준에
|
|||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
[완료]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
@ -38,22 +38,28 @@ OTO Core가 발급하는 bootstrap 흐름을 Linux 단일 shell script 기준에
|
|||
OS별 bootstrap command와 release asset 선택 기준을 OTO Core와 runner asset이 같은 의미로 해석하도록 만든다.
|
||||
|
||||
- [x] [platform-matrix] Linux, macOS, Windows의 우선 지원 OS/arch와 release asset naming 규칙을 정리한다. 검증: 문서와 테스트 fixture가 동일한 asset 이름을 사용한다.
|
||||
- [ ] [core-command] OTO Core bootstrap command 요청/응답이 대상 OS 또는 script 종류를 표현하고 올바른 bootstrap URL/command를 반환한다. 검증: `cd services/core && go test ./...`가 OS별 command 케이스를 포함해 통과한다.
|
||||
- [ ] [unix-bootstrap] Linux/macOS shell bootstrap이 OS/arch를 감지해 올바른 release asset을 내려받고 config/runtime 경로를 생성한다. 검증: `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` 또는 대응 smoke가 통과한다.
|
||||
- [ ] [windows-bootstrap] Windows PowerShell bootstrap이 release asset 다운로드, 압축 해제, config 생성, agent 실행 안내 또는 시작 흐름을 제공한다. 검증: script parsing/fixture 테스트가 Windows 경로와 quoting을 검증한다.
|
||||
- [ ] [oto-binary] bootstrap으로 설치된 `oto` binary의 실행 경로와 버전 확인 방법을 OS별로 고정한다. 검증: fixture 또는 smoke가 설치 후 `oto --version` 또는 동등한 binary 확인 절차를 검증한다.
|
||||
- [ ] [node-connect] bootstrap으로 생성된 agent 설정이 `oto agent run`에서 OTO Core runner registry에 runner/node로 등록 또는 연결되는 최소 흐름을 보장한다. 검증: `cd services/core && go test ./...`와 runner agent 연결 smoke 또는 fixture 테스트가 통과한다.
|
||||
- [x] [core-command] OTO Core bootstrap command 요청/응답이 대상 OS 또는 script 종류를 표현하고 올바른 bootstrap URL/command를 반환한다. 검증: `cd services/core && go test ./...`가 OS별 command 케이스를 포함해 통과한다.
|
||||
- [x] [unix-bootstrap] Linux/macOS shell bootstrap이 OS/arch를 감지해 올바른 release asset을 내려받고 config/runtime 경로를 생성한다. 검증: `cd apps/runner && dart test test/oto_agent_bootstrap_script_test.dart` 또는 대응 smoke가 통과한다.
|
||||
- [x] [windows-bootstrap] Windows PowerShell bootstrap이 release asset 다운로드, 압축 해제, config 생성, agent 실행 안내 또는 시작 흐름을 제공한다. 검증: script parsing/fixture 테스트가 Windows 경로와 quoting을 검증한다.
|
||||
- [x] [oto-binary] bootstrap으로 설치된 `oto` binary의 실행 경로와 버전 확인 방법을 OS별로 고정한다. 검증: fixture 또는 smoke가 설치 후 `oto --version` 또는 동등한 binary 확인 절차를 검증한다.
|
||||
- [x] [node-connect] bootstrap으로 생성된 agent 설정이 `oto agent run`에서 OTO Core runner registry에 runner/node로 등록 또는 연결되는 최소 흐름을 보장한다. 검증: `cd services/core && go test ./...`와 runner agent 연결 smoke 또는 fixture 테스트가 통과한다.
|
||||
- [x] [docs-smoke] README와 `agent-test/local/agent-smoke.md`가 OS별 bootstrap command, secret 기록 금지, release URL 검증 기준을 최신화한다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 없음
|
||||
- 상태: 승인됨
|
||||
- 요청일: 2026-06-16
|
||||
- 완료 근거:
|
||||
- 원격 runner `/Users/toki/agent-work/oto`에서 `cd services/core && go test -count=1 ./...` 통과로 OS별 bootstrap command, embedded bootstrap asset serving, runner registry/httpserver 회귀를 확인했다.
|
||||
- 원격 runner `/Users/toki/agent-work/oto`에서 `cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart` 통과로 Unix bootstrap, binary verification, generated agent config, OTO Server runner 연결 smoke를 확인했다.
|
||||
- 원격 runner `/Users/toki/agent-work/oto`에서 `cd apps/runner && dart pub get && dart analyze` 통과로 runner 정적 분석 회귀를 확인했다.
|
||||
- 코드 레벨 종료 검토 중 bootstrap config YAML 값이 따옴표/백슬래시를 포함할 때 깨질 수 있는 작은 이슈를 확인했고, shell bootstrap은 double-quoted YAML escaping, PowerShell bootstrap은 single-quoted YAML escaping으로 보강했다.
|
||||
- 로컬 checkout에서 `git diff --check`, `cd services/core && go test -count=1 ./...`, `cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_agent_bootstrap_ps1_test.dart`가 통과했다.
|
||||
- 원격 runner 임시 tree `/tmp/oto-verify-bootstrap.j89dYx`에서 로컬 HEAD와 이번 변경 파일을 기준으로 `cd services/core && go test -count=1 ./...`, `cd apps/runner && dart pub get && dart analyze && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart test/oto_agent_bootstrap_ps1_test.dart`가 통과했다.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
- [x] 사용자가 완료 결과를 확인했다
|
||||
- [x] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 코드 레벨 종료 검토에서 확인된 작은 YAML escaping 이슈를 처리했고, 원격 runner 기준 재검증이 통과해 `bootstrap-platforms` Epic과 Milestone을 완료 처리한다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -15,10 +15,10 @@
|
|||
완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
|
||||
스케치 Milestone은 아직 구현 가능한 계획이 아니므로 계획 Milestone보다 아래에 둔다.
|
||||
|
||||
- [진행중] Runner bootstrap, OTO binary, 노드 연결
|
||||
- 경로: `agent-roadmap/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md`
|
||||
- [완료] Runner bootstrap, OTO binary, 노드 연결
|
||||
- 경로: `agent-roadmap/archive/phase/control-plane-product-surface/milestones/cross-os-runner-bootstrap.md`
|
||||
- 요약: OTO Core bootstrap command와 bootstrap asset이 Linux, macOS, Windows에서 `oto` binary를 내려받고 runner/node로 Core에 연결할 수 있도록 확장한다.
|
||||
- [계획] Flutter 콘솔 스켈레톤
|
||||
- [진행중] Flutter 콘솔 스켈레톤
|
||||
- 경로: `agent-roadmap/phase/control-plane-product-surface/milestones/flutter-console-skeleton.md`
|
||||
- 요약: bootstrap과 runner/node 연결 기준을 바탕으로 `apps/client`와 `packages/flutter/oto_console`에 Control Plane 주요 화면, 상태 표시, 빈 상태를 갖춘 UI 스켈레톤을 만든다.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
[진행중]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
|
|||
|
|
@ -78,17 +78,25 @@ try {
|
|||
$null = New-Item -ItemType Directory -Path $workspaceRoot -Force
|
||||
$null = New-Item -ItemType Directory -Path $logDir -Force
|
||||
|
||||
$agentIdYaml = $agentId.Replace("'", "''")
|
||||
$agentAliasYaml = $agentAlias.Replace("'", "''")
|
||||
$enrollmentTokenYaml = $enrollmentToken.Replace("'", "''")
|
||||
$serverUrlYaml = $serverUrl.Replace("'", "''")
|
||||
$installDirYaml = ($installDir -replace '\\', '/').Replace("'", "''")
|
||||
$workspaceRootYaml = ($workspaceRoot -replace '\\', '/').Replace("'", "''")
|
||||
$logDirYaml = ($logDir -replace '\\', '/').Replace("'", "''")
|
||||
|
||||
$configContent = @"
|
||||
agent:
|
||||
id: "$agentId"
|
||||
alias: "$agentAlias"
|
||||
enrollment_token: "$enrollmentToken"
|
||||
id: '$agentIdYaml'
|
||||
alias: '$agentAliasYaml'
|
||||
enrollment_token: '$enrollmentTokenYaml'
|
||||
server:
|
||||
url: "$serverUrl"
|
||||
url: '$serverUrlYaml'
|
||||
runtime:
|
||||
install_dir: "$($installDir -replace '\\', '/')"
|
||||
workspace_root: "$($workspaceRoot -replace '\\', '/')"
|
||||
log_dir: "$($logDir -replace '\\', '/')"
|
||||
install_dir: '$installDirYaml'
|
||||
workspace_root: '$workspaceRootYaml'
|
||||
log_dir: '$logDirYaml'
|
||||
"@
|
||||
[System.IO.File]::WriteAllText($configPath, $configContent)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ usage() {
|
|||
echo " --no-background Run in the foreground"
|
||||
}
|
||||
|
||||
yaml_escape() {
|
||||
local value="$1"
|
||||
value="${value//\\/\\\\}"
|
||||
value="${value//\"/\\\"}"
|
||||
value="${value//$'\t'/\\t}"
|
||||
value="${value//$'\r'/\\r}"
|
||||
value="${value//$'\n'/\\n}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--server-url|--edge-url)
|
||||
|
|
@ -140,15 +150,15 @@ mkdir -p "$log_dir"
|
|||
|
||||
cat <<EOF > "$config_path"
|
||||
agent:
|
||||
id: "$agent_id"
|
||||
alias: "$agent_alias"
|
||||
enrollment_token: "$enrollment_token"
|
||||
id: "$(yaml_escape "$agent_id")"
|
||||
alias: "$(yaml_escape "$agent_alias")"
|
||||
enrollment_token: "$(yaml_escape "$enrollment_token")"
|
||||
server:
|
||||
url: "$server_url"
|
||||
url: "$(yaml_escape "$server_url")"
|
||||
runtime:
|
||||
install_dir: "$install_dir"
|
||||
workspace_root: "$workspace_root"
|
||||
log_dir: "$log_dir"
|
||||
install_dir: "$(yaml_escape "$install_dir")"
|
||||
workspace_root: "$(yaml_escape "$workspace_root")"
|
||||
log_dir: "$(yaml_escape "$log_dir")"
|
||||
EOF
|
||||
|
||||
chmod 600 "$config_path"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@ import 'dart:io';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
final scriptFile = File(
|
||||
'assets/script/powershell/oto_agent_bootstrap.ps1',
|
||||
);
|
||||
final scriptFile = File('assets/script/powershell/oto_agent_bootstrap.ps1');
|
||||
|
||||
group('Windows PowerShell Bootstrap Script Contract Tests', () {
|
||||
late String content;
|
||||
|
|
@ -34,13 +32,16 @@ void main() {
|
|||
expect(content, contains('--no-background'));
|
||||
});
|
||||
|
||||
test('should support Windows x64 and arm64 zip assets via dynamic arch combination', () {
|
||||
expect(content, contains("'x64'"));
|
||||
expect(content, contains("'arm64'"));
|
||||
expect(content, contains('oto-windows-'));
|
||||
expect(content, contains('.zip'));
|
||||
expect(content, contains(r'"oto-windows-$archName.zip"'));
|
||||
});
|
||||
test(
|
||||
'should support Windows x64 and arm64 zip assets via dynamic arch combination',
|
||||
() {
|
||||
expect(content, contains("'x64'"));
|
||||
expect(content, contains("'arm64'"));
|
||||
expect(content, contains('oto-windows-'));
|
||||
expect(content, contains('.zip'));
|
||||
expect(content, contains(r'"oto-windows-$archName.zip"'));
|
||||
},
|
||||
);
|
||||
|
||||
test('should detect architecture via PROCESSOR_ARCHITECTURE', () {
|
||||
expect(content, contains('PROCESSOR_ARCHITECTURE'));
|
||||
|
|
@ -72,18 +73,28 @@ void main() {
|
|||
expect(content, contains('log_dir:'));
|
||||
});
|
||||
|
||||
test('should escape YAML single-quoted config values', () {
|
||||
expect(content, contains(r'''.Replace("'", "''")'''));
|
||||
expect(content, contains(r"id: '$agentIdYaml'"));
|
||||
expect(content, contains(r"enrollment_token: '$enrollmentTokenYaml'"));
|
||||
expect(content, contains(r"serverUrlYaml = $serverUrl.Replace("));
|
||||
});
|
||||
|
||||
test('should contain agent runtime command and background behavior', () {
|
||||
expect(content, contains('agent run'));
|
||||
expect(content, contains('--config'));
|
||||
expect(content, contains('oto-agent.pid'));
|
||||
});
|
||||
|
||||
test('should use separate stdout and stderr log files for background process', () {
|
||||
expect(content, contains('RedirectStandardOutput'));
|
||||
expect(content, contains('RedirectStandardError'));
|
||||
expect(content, contains('oto-agent.out.log'));
|
||||
expect(content, contains('oto-agent.err.log'));
|
||||
});
|
||||
test(
|
||||
'should use separate stdout and stderr log files for background process',
|
||||
() {
|
||||
expect(content, contains('RedirectStandardOutput'));
|
||||
expect(content, contains('RedirectStandardError'));
|
||||
expect(content, contains('oto-agent.out.log'));
|
||||
expect(content, contains('oto-agent.err.log'));
|
||||
},
|
||||
);
|
||||
|
||||
test('should contain binary verification before background execution', () {
|
||||
expect(content, contains('Verifying OTO binary...'));
|
||||
|
|
@ -104,7 +115,8 @@ void main() {
|
|||
for (final line in lines) {
|
||||
final trimmed = line.trim();
|
||||
// Skip the config write block that intentionally contains the token key
|
||||
if (trimmed.contains('enrollment_token:') && trimmed.contains(r'"$enrollmentToken"')) {
|
||||
if (trimmed.contains('enrollment_token:') &&
|
||||
trimmed.contains(r'"$enrollmentToken"')) {
|
||||
continue;
|
||||
}
|
||||
// No Write-Host / Write-Error / Write-Output line should directly print
|
||||
|
|
@ -115,7 +127,8 @@ void main() {
|
|||
expect(
|
||||
trimmed,
|
||||
isNot(contains(r'$enrollmentToken')),
|
||||
reason: 'Output statement must not expose enrollment token: $trimmed',
|
||||
reason:
|
||||
'Output statement must not expose enrollment token: $trimmed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ class _BootstrapRunResult {
|
|||
|
||||
/// fake curl에 요청된 asset basename. curl이 호출되지 않으면 null.
|
||||
final String? requestedAsset;
|
||||
final String? configContent;
|
||||
|
||||
const _BootstrapRunResult({
|
||||
required this.exitCode,
|
||||
required this.stdout,
|
||||
required this.stderr,
|
||||
this.requestedAsset,
|
||||
this.configContent,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +43,8 @@ Future<_BootstrapRunResult> _runBootstrapWithFakeUname(
|
|||
required String unameS,
|
||||
required String unameM,
|
||||
bool provideTarGz = true,
|
||||
String serverUrl = 'https://server.example.com',
|
||||
String agentId = 'test-agent',
|
||||
String token = 'test-token-secret',
|
||||
int fakeOtoVersionExitCode = 0,
|
||||
}) async {
|
||||
|
|
@ -119,9 +123,9 @@ $copyLine
|
|||
final result = await Process.run('bash', [
|
||||
scriptFile.absolute.path,
|
||||
'--server-url',
|
||||
'https://server.example.com',
|
||||
serverUrl,
|
||||
'--agent-id',
|
||||
'test-agent',
|
||||
agentId,
|
||||
'--enrollment-token',
|
||||
token,
|
||||
'--release-base-url',
|
||||
|
|
@ -133,6 +137,10 @@ $copyLine
|
|||
final raw = (await assetLog.readAsString()).trim();
|
||||
if (raw.isNotEmpty) requestedAsset = raw.split('\n').first.trim();
|
||||
}
|
||||
final configFile = File('${tempHome.path}/.oto/agent/config.yaml');
|
||||
final configContent = await configFile.exists()
|
||||
? await configFile.readAsString()
|
||||
: null;
|
||||
|
||||
// background로 실행된 fake oto 프로세스 종료
|
||||
final pidFile = File('${tempHome.path}/.oto/agent/oto-agent.pid');
|
||||
|
|
@ -147,6 +155,7 @@ $copyLine
|
|||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString(),
|
||||
requestedAsset: requestedAsset,
|
||||
configContent: configContent,
|
||||
);
|
||||
} finally {
|
||||
await tmpDir.delete(recursive: true);
|
||||
|
|
@ -358,6 +367,31 @@ void main() {
|
|||
expect(r.stdout, isNot(contains(token)));
|
||||
expect(r.stderr, isNot(contains(token)));
|
||||
});
|
||||
|
||||
test('escapes YAML double-quoted config values', () async {
|
||||
const agentId = r'agent-"quoted-\id';
|
||||
const token = r'token-"quoted-\secret';
|
||||
final r = await _runBootstrapWithFakeUname(
|
||||
scriptFile,
|
||||
unameS: 'Linux',
|
||||
unameM: 'x86_64',
|
||||
provideTarGz: true,
|
||||
agentId: agentId,
|
||||
token: token,
|
||||
);
|
||||
|
||||
expect(r.exitCode, 0, reason: 'stderr: ${r.stderr}');
|
||||
expect(r.configContent, isNotNull);
|
||||
expect(r.configContent!, contains(r'id: "agent-\"quoted-\\id"'));
|
||||
expect(
|
||||
r.configContent!,
|
||||
contains(r'enrollment_token: "token-\"quoted-\\secret"'),
|
||||
);
|
||||
|
||||
final config = AgentConfig.fromYamlContent(r.configContent!);
|
||||
expect(config.agent.id, agentId);
|
||||
expect(config.agent.enrollmentToken, token);
|
||||
});
|
||||
});
|
||||
|
||||
group('Bootstrap Config Fixture', () {
|
||||
|
|
|
|||
|
|
@ -78,17 +78,25 @@ try {
|
|||
$null = New-Item -ItemType Directory -Path $workspaceRoot -Force
|
||||
$null = New-Item -ItemType Directory -Path $logDir -Force
|
||||
|
||||
$agentIdYaml = $agentId.Replace("'", "''")
|
||||
$agentAliasYaml = $agentAlias.Replace("'", "''")
|
||||
$enrollmentTokenYaml = $enrollmentToken.Replace("'", "''")
|
||||
$serverUrlYaml = $serverUrl.Replace("'", "''")
|
||||
$installDirYaml = ($installDir -replace '\\', '/').Replace("'", "''")
|
||||
$workspaceRootYaml = ($workspaceRoot -replace '\\', '/').Replace("'", "''")
|
||||
$logDirYaml = ($logDir -replace '\\', '/').Replace("'", "''")
|
||||
|
||||
$configContent = @"
|
||||
agent:
|
||||
id: "$agentId"
|
||||
alias: "$agentAlias"
|
||||
enrollment_token: "$enrollmentToken"
|
||||
id: '$agentIdYaml'
|
||||
alias: '$agentAliasYaml'
|
||||
enrollment_token: '$enrollmentTokenYaml'
|
||||
server:
|
||||
url: "$serverUrl"
|
||||
url: '$serverUrlYaml'
|
||||
runtime:
|
||||
install_dir: "$($installDir -replace '\\', '/')"
|
||||
workspace_root: "$($workspaceRoot -replace '\\', '/')"
|
||||
log_dir: "$($logDir -replace '\\', '/')"
|
||||
install_dir: '$installDirYaml'
|
||||
workspace_root: '$workspaceRootYaml'
|
||||
log_dir: '$logDirYaml'
|
||||
"@
|
||||
[System.IO.File]::WriteAllText($configPath, $configContent)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ usage() {
|
|||
echo " --no-background Run in the foreground"
|
||||
}
|
||||
|
||||
yaml_escape() {
|
||||
local value="$1"
|
||||
value="${value//\\/\\\\}"
|
||||
value="${value//\"/\\\"}"
|
||||
value="${value//$'\t'/\\t}"
|
||||
value="${value//$'\r'/\\r}"
|
||||
value="${value//$'\n'/\\n}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--server-url|--edge-url)
|
||||
|
|
@ -140,15 +150,15 @@ mkdir -p "$log_dir"
|
|||
|
||||
cat <<EOF > "$config_path"
|
||||
agent:
|
||||
id: "$agent_id"
|
||||
alias: "$agent_alias"
|
||||
enrollment_token: "$enrollment_token"
|
||||
id: "$(yaml_escape "$agent_id")"
|
||||
alias: "$(yaml_escape "$agent_alias")"
|
||||
enrollment_token: "$(yaml_escape "$enrollment_token")"
|
||||
server:
|
||||
url: "$server_url"
|
||||
url: "$(yaml_escape "$server_url")"
|
||||
runtime:
|
||||
install_dir: "$install_dir"
|
||||
workspace_root: "$workspace_root"
|
||||
log_dir: "$log_dir"
|
||||
install_dir: "$(yaml_escape "$install_dir")"
|
||||
workspace_root: "$(yaml_escape "$workspace_root")"
|
||||
log_dir: "$(yaml_escape "$log_dir")"
|
||||
EOF
|
||||
|
||||
chmod 600 "$config_path"
|
||||
|
|
|
|||
Loading…
Reference in a new issue