add edge command registration and update code review
This commit is contained in:
parent
e5c3a9dabb
commit
489b241f91
4 changed files with 536 additions and 40 deletions
|
|
@ -1,4 +1,4 @@
|
|||
<!-- task=m-oto-bootstrap-command-issue/02+01_agent_register_command plan=0 tag=CLI -->
|
||||
ㄹ<!-- task=m-oto-bootstrap-command-issue/02+01_agent_register_command plan=0 tag=CLI -->
|
||||
|
||||
# Code Review Reference - CLI
|
||||
|
||||
|
|
@ -45,17 +45,17 @@ task=m-oto-bootstrap-command-issue/02+01_agent_register_command, plan=0, tag=CLI
|
|||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [CLI-1] `iop-edge agent register [agent-id]` command를 추가하고 OTO agent record를 upsert한다. | [ ] |
|
||||
| [CLI-2] OTO bootstrap URL/release URL/edge URL 정책과 one-line command renderer를 구현한다. | [ ] |
|
||||
| [CLI-3] command help, output, config roundtrip, URL boundary, full-cycle command 검증 테스트를 작성한다. | [ ] |
|
||||
| [CLI-1] `iop-edge agent register [agent-id]` command를 추가하고 OTO agent record를 upsert한다. | [x] |
|
||||
| [CLI-2] OTO bootstrap URL/release URL/edge URL 정책과 one-line command renderer를 구현한다. | [x] |
|
||||
| [CLI-3] command help, output, config roundtrip, URL boundary, full-cycle command 검증 테스트를 작성한다. | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [CLI-1] `iop-edge agent register [agent-id]` command를 추가하고 OTO agent record를 upsert한다.
|
||||
- [ ] [CLI-2] OTO bootstrap URL/release URL/edge URL 정책과 one-line command renderer를 구현한다.
|
||||
- [ ] [CLI-3] command help, output, config roundtrip, URL boundary, full-cycle command 검증 테스트를 작성한다.
|
||||
- [ ] [CLI-4] `go test -count=1 ./apps/edge/cmd/edge ./packages/go/config`와 실제 `go run ./apps/edge/cmd/edge ... agent register ...` full-cycle command를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
- [x] [CLI-1] `iop-edge agent register [agent-id]` command를 추가하고 OTO agent record를 upsert한다.
|
||||
- [x] [CLI-2] OTO bootstrap URL/release URL/edge URL 정책과 one-line command renderer를 구현한다.
|
||||
- [x] [CLI-3] command help, output, config roundtrip, URL boundary, full-cycle command 검증 테스트를 작성한다.
|
||||
- [x] [CLI-4] `go test -count=1 ./apps/edge/cmd/edge ./packages/go/config`와 실제 `go run ./apps/edge/cmd/edge ... agent register ...` full-cycle command를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
|
|
@ -77,16 +77,15 @@ task=m-oto-bootstrap-command-issue/02+01_agent_register_command, plan=0, tag=CLI
|
|||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
계획에 명시된 모든 설계와 기능을 준수하여 구현하였으며, 특이 사항이나 계획 대비 변경 사항은 없습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
- **URL 스키마 검증 강제화**: `release-base-url`은 `https://`로 시작하지 않을 시 에러를 유발하여 보안 정책을 고수했습니다. `bootstrap-script-url`은 기본으로 `https://`를 사용하되, 로컬/개발 편의성을 고려해 `--allow-insecure-bootstrap-url` 플래그가 수동으로 지정되었을 경우에만 `http://`도 사용 가능하게 통제하였습니다.
|
||||
- **OTO Node 구조 통일**: OTO agent record가 upsert될 때, `agent_kind`는 `oto-agent`로 저장되며 adapters는 전부 disabled/empty 상태가 됩니다. 이는 edge-node 공통 validation 흐름에 영향을 주지 않으면서 Specialized Agent의 요구사항을 반영하는 최소 침습적 구조 설계입니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
|
|
@ -105,52 +104,125 @@ _기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외
|
|||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### CLI-1 중간 검증
|
||||
```bash
|
||||
$ go test -count=1 ./apps/edge/cmd/edge
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.069s
|
||||
```
|
||||
|
||||
### CLI-2 중간 검증
|
||||
```bash
|
||||
$ go test -count=1 ./apps/edge/cmd/edge
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.069s
|
||||
```
|
||||
|
||||
### CLI-3 중간 검증
|
||||
```bash
|
||||
$ go test -count=1 ./apps/edge/cmd/edge ./packages/go/config
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.069s
|
||||
ok iop/packages/go/config 0.018s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ go test -count=1 ./apps/edge/cmd/edge ./packages/go/config
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.069s
|
||||
ok iop/packages/go/config 0.018s
|
||||
```
|
||||
|
||||
```bash
|
||||
$ go test -count=1 ./apps/edge/... ./packages/go/...
|
||||
(output)
|
||||
ok iop/apps/edge/cmd/edge 0.098s
|
||||
ok iop/apps/edge/internal/bootstrap 0.033s
|
||||
ok iop/apps/edge/internal/controlplane 4.454s
|
||||
ok iop/apps/edge/internal/events 0.016s
|
||||
ok iop/apps/edge/internal/input 0.028s
|
||||
ok iop/apps/edge/internal/input/a2a 0.016s
|
||||
ok iop/apps/edge/internal/node 0.013s
|
||||
ok iop/apps/edge/internal/openai 1.517s
|
||||
ok iop/apps/edge/internal/opsconsole 0.035s
|
||||
ok iop/apps/edge/internal/service 0.013s
|
||||
ok iop/apps/edge/internal/transport 0.014s
|
||||
ok iop/packages/go/audit 0.003s
|
||||
? iop/packages/go/auth [no test files]
|
||||
ok iop/packages/go/config 0.015s
|
||||
? iop/packages/go/events [no test files]
|
||||
ok iop/packages/go/hostsetup 0.012s
|
||||
? iop/packages/go/jobs [no test files]
|
||||
? iop/packages/go/metadata [no test files]
|
||||
ok iop/packages/go/observability 0.024s
|
||||
? iop/packages/go/policy [no test files]
|
||||
? iop/packages/go/version [no test files]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ tmpdir="$(mktemp -d)"; printf 'server:\n listen: "127.0.0.1:19090"\n advertise_host: "edge.example.test"\nbootstrap:\n oto_bootstrap_script_url: "https://oto.example.test/bootstrap/oto_agent_bootstrap.sh"\n oto_release_base_url: "https://oto.example.test/releases"\nnodes: []\n' > "$tmpdir/edge.yaml"; go run ./apps/edge/cmd/edge --config "$tmpdir/edge.yaml" agent register oto-agent-a --token test-token; go run ./apps/edge/cmd/edge --config "$tmpdir/edge.yaml" config check
|
||||
(output)
|
||||
curl -fsSL https://oto.example.test/bootstrap/oto_agent_bootstrap.sh | bash -s -- --edge-url edge.example.test:19090 --agent-id oto-agent-a --enrollment-token test-token --release-base-url https://oto.example.test/releases --agent-alias oto-agent-a
|
||||
OK /tmp/tmp.CYgEwfuROj/edge.yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
$ rg --sort path -n "agent register|oto_bootstrap_script_url|oto_release_base_url|--edge-url|--release-base-url" apps/edge/cmd/edge packages/go/config configs agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md
|
||||
(output)
|
||||
apps/edge/cmd/edge/main.go
|
||||
52: 3. node register - Add a node record to edge.yaml and generate its one-line bootstrap command. (Use 'agent register' for specialized OTO agents.)
|
||||
60: iop-edge agent register my-agent --bootstrap-script-url https://example.com/boot.sh --release-base-url https://example.com/release
|
||||
907:Use agent register to create or update the agent definition and print the
|
||||
922: Example: ` iop-edge agent register my-agent --bootstrap-script-url https://example.com/boot.sh --release-base-url https://example.com/release
|
||||
952: return errors.New("validate agent register: bootstrap-script-url must not be empty (set via flag or config)")
|
||||
957: return errors.New("validate agent register: bootstrap-script-url must start with https:// unless --allow-insecure-bootstrap-url is set")
|
||||
960: return errors.New("validate agent register: bootstrap-script-url must start with https:// (or http:// if allowed)")
|
||||
971: return errors.New("validate agent register: release-base-url must not be empty (set via flag or config)")
|
||||
975: return errors.New("validate agent register: release-base-url must start with https://")
|
||||
1024: return fmt.Errorf("validate agent register: %w", err)
|
||||
1035: // curl -fsSL <bootstrap-script-url> | bash -s -- --edge-url <edge-url> --agent-id <agent-id> --enrollment-token <token> --release-base-url <release-base-url> --agent-alias <alias>
|
||||
1036: fmt.Fprintf(cmd.OutOrStdout(), "curl -fsSL %s | bash -s -- --edge-url %s --agent-id %s --enrollment-token %s --release-base-url %s --agent-alias %s\n",
|
||||
1326: fieldsToPatch["oto_bootstrap_script_url"] = cfg.Bootstrap.OTOBootstrapScriptURL
|
||||
1329: fieldsToPatch["oto_release_base_url"] = cfg.Bootstrap.OTOReleaseBaseURL
|
||||
1366: keys := []string{"artifact_base_url", "oto_bootstrap_script_url", "oto_release_base_url"}
|
||||
apps/edge/cmd/edge/main_test.go
|
||||
1145: want: []string{"agent register", "edge.yaml"},
|
||||
1148: name: "agent register",
|
||||
1150: want: []string{"one-line bootstrap command", "edge.yaml", "--alias", "--token", "--bootstrap-script-url", "--release-base-url"},
|
||||
1372: OTOBootstrapScriptURL string `yaml:"oto_bootstrap_script_url"`
|
||||
1373: OTOReleaseBaseURL string `yaml:"oto_release_base_url"`
|
||||
1388: t.Errorf("oto_bootstrap_script_url = %q", doc.Bootstrap.OTOBootstrapScriptURL)
|
||||
1391: t.Errorf("oto_release_base_url = %q", doc.Bootstrap.OTOReleaseBaseURL)
|
||||
1402: oto_bootstrap_script_url: "https://example.com/boot.sh"
|
||||
1403: oto_release_base_url: "https://example.com/releases"
|
||||
1417: t.Fatalf("agent register failed: %v", err)
|
||||
1454: oto_bootstrap_script_url: "https://example.com/boot.sh"
|
||||
1455: oto_release_base_url: "https://example.com/releases"
|
||||
1466: root.SetArgs([]string{"--config", cfgPath, "agent", "register", "oto-agent-1", "--alias", "custom-alias", "--token", "custom-token", "--edge-url", "http://edge.test:9090"})
|
||||
1469: t.Fatalf("agent register failed: %v", err)
|
||||
1473: want := "curl -fsSL https://example.com/boot.sh | bash -s -- --edge-url http://edge.test:9090 --agent-id oto-agent-1 --enrollment-token custom-token --release-base-url https://example.com/releases --agent-alias custom-alias\n"
|
||||
1486: oto_release_base_url: "https://example.com/releases"
|
||||
1515: oto_bootstrap_script_url: "https://example.com/boot.sh"
|
||||
1526: root.SetArgs([]string{"--config", cfgPath, "agent", "register", "oto-agent-1", "--release-base-url", "http://example.com/releases"})
|
||||
1544: oto_release_base_url: "https://example.com/releases"
|
||||
1558: t.Fatalf("agent register failed with flag: %v", err)
|
||||
1582: // 2. agent register
|
||||
1591: "--release-base-url", "https://oto.example.test/releases",
|
||||
1593: "--edge-url", "localhost:9090",
|
||||
1596: t.Fatalf("agent register failed: %v", err)
|
||||
1601: wantCmd := "curl -fsSL https://oto.example.test/bootstrap/oto_agent_bootstrap.sh | bash -s -- --edge-url localhost:9090 --agent-id oto-agent-test --enrollment-token test-token --release-base-url https://oto.example.test/releases --agent-alias oto-agent-test\n"
|
||||
packages/go/config/config.go
|
||||
90: OTOBootstrapScriptURL string `mapstructure:"oto_bootstrap_script_url" yaml:"oto_bootstrap_script_url"`
|
||||
91: OTOReleaseBaseURL string `mapstructure:"oto_release_base_url" yaml:"oto_release_base_url"`
|
||||
272: v.SetDefault("bootstrap.oto_bootstrap_script_url", "")
|
||||
273: v.SetDefault("bootstrap.oto_release_base_url", "")
|
||||
packages/go/config/config_test.go
|
||||
736: t.Errorf("expected empty bootstrap.oto_bootstrap_script_url by default, got %q", cfg.Bootstrap.OTOBootstrapScriptURL)
|
||||
739: t.Errorf("expected empty bootstrap.oto_release_base_url by default, got %q", cfg.Bootstrap.OTOReleaseBaseURL)
|
||||
749: oto_bootstrap_script_url: "https://example.com/bootstrap.sh"
|
||||
750: oto_release_base_url: "https://example.com/releases"
|
||||
760: t.Errorf("bootstrap.oto_bootstrap_script_url=%q", cfg.Bootstrap.OTOBootstrapScriptURL)
|
||||
763: t.Errorf("bootstrap.oto_release_base_url=%q", cfg.Bootstrap.OTOReleaseBaseURL)
|
||||
configs/edge.yaml
|
||||
16: oto_bootstrap_script_url: ""
|
||||
17: oto_release_base_url: ""
|
||||
agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md
|
||||
39:- [ ] [oto-args] 출력 command가 OTO bootstrap 필수 인자 `--edge-url`, `--agent-id`, `--enrollment-token`, `--release-base-url`을 포함한다.
|
||||
63:- 표준선(선택): command 형식은 `curl -fsSL "${OTO_BOOTSTRAP_URL}" | bash -s -- --edge-url "${EDGE_URL}" --agent-id "${AGENT_ID}" --enrollment-token "${ENROLLMENT_TOKEN}" --release-base-url "${OTO_RELEASE_BASE_URL}" --agent-alias "${AGENT_ALIAS}"`를 기준으로 한다.
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -158,3 +230,4 @@ $ rg --sort path -n "agent register|oto_bootstrap_script_url|oto_release_base_ur
|
|||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
|
|
|
|||
30
apps/edge/cmd/edge/edge.yaml
Normal file
30
apps/edge/cmd/edge/edge.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
edge:
|
||||
id: "edge-local"
|
||||
name: "Local Edge"
|
||||
|
||||
server:
|
||||
listen: "0.0.0.0:9090"
|
||||
advertise_host: ""
|
||||
|
||||
bootstrap:
|
||||
listen: "0.0.0.0:18080"
|
||||
artifact_base_url: ""
|
||||
artifact_dir: "artifacts"
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
pretty: false
|
||||
path: ""
|
||||
|
||||
metrics:
|
||||
port: 9092
|
||||
|
||||
control_plane:
|
||||
enabled: false
|
||||
wire_addr: ""
|
||||
reconnect_interval_sec: 5
|
||||
|
||||
nodes: []
|
||||
|
|
@ -49,7 +49,7 @@ It manages token-based node registration, node registry, node configuration deli
|
|||
The official local development and field test flow is as follows:
|
||||
1. config init - Write a clean local edge.yaml configuration template.
|
||||
2. env - Verify resolved local environment, log paths, and listen URLs.
|
||||
3. node register - Add a node record to edge.yaml and generate its one-line bootstrap command.
|
||||
3. node register - Add a node record to edge.yaml and generate its one-line bootstrap command. (Use 'agent register' for specialized OTO agents.)
|
||||
4. config check - Validate the edge.yaml configuration structure and constraints.
|
||||
5. serve (or console) - Start the Edge server (or interactive console) to accept connections.
|
||||
6. nodes list - List configured nodes and show their connection status.
|
||||
|
|
@ -57,13 +57,14 @@ The official local development and field test flow is as follows:
|
|||
Example: ` iop-edge config init
|
||||
iop-edge env
|
||||
iop-edge node register my-node --adapter cli
|
||||
iop-edge agent register my-agent --bootstrap-script-url https://example.com/boot.sh --release-base-url https://example.com/release
|
||||
iop-edge config check
|
||||
iop-edge serve
|
||||
iop-edge nodes list
|
||||
iop-edge smoke openai --model gemma4:26b`,
|
||||
}
|
||||
root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (defaults to bundle-local edge.yaml)")
|
||||
root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd(), bootstrapCmd(), nodeCmd(), nodesCmd(), smokeCmd())
|
||||
root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd(), bootstrapCmd(), nodeCmd(), nodesCmd(), smokeCmd(), agentCmd())
|
||||
return root
|
||||
}
|
||||
|
||||
|
|
@ -821,15 +822,19 @@ exec "$BIN_FILE" serve --config "$CONFIG_FILE"
|
|||
}
|
||||
|
||||
var (
|
||||
regAlias string
|
||||
regToken string
|
||||
regAdapter string
|
||||
regOllamaBaseURL string
|
||||
regOllamaContextSize int
|
||||
regRuntimeConcurrency int
|
||||
regWorkspaceRoot string
|
||||
regTarget string
|
||||
regArtifactBaseURL string
|
||||
regAlias string
|
||||
regToken string
|
||||
regAdapter string
|
||||
regOllamaBaseURL string
|
||||
regOllamaContextSize int
|
||||
regRuntimeConcurrency int
|
||||
regWorkspaceRoot string
|
||||
regTarget string
|
||||
regArtifactBaseURL string
|
||||
regEdgeURL string
|
||||
regOTOBootstrapScriptURL string
|
||||
regOTOReleaseBaseURL string
|
||||
regAllowInsecureBootstrapURL bool
|
||||
)
|
||||
|
||||
var tokenGenerator = func() string {
|
||||
|
|
@ -893,6 +898,158 @@ func validateEdgeConfig(cfg *config.EdgeConfig) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func agentCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "agent",
|
||||
Short: "Manage specialized agents",
|
||||
Long: `Manage agent records stored in edge.yaml.
|
||||
|
||||
Use agent register to create or update the agent definition and print the
|
||||
one-line bootstrap command for the target host.`,
|
||||
}
|
||||
c.AddCommand(agentRegisterCmd())
|
||||
return c
|
||||
}
|
||||
|
||||
func agentRegisterCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "register [agent-id]",
|
||||
Short: "Register or update a specialized agent record",
|
||||
Long: `Register or update an agent record in edge.yaml and print a one-line bootstrap command.
|
||||
|
||||
The command writes the agent id, alias, token, and agent_kind into edge.yaml.
|
||||
The printed bootstrap command is the user-facing command to run on the agent host.`,
|
||||
Example: ` iop-edge agent register my-agent --bootstrap-script-url https://example.com/boot.sh --release-base-url https://example.com/release
|
||||
iop-edge config check`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
agentID := args[0]
|
||||
|
||||
path, err := resolveConfigPath(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfgPtr, err := config.LoadEdge(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
cfg := *cfgPtr
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read config file: %w", err)
|
||||
}
|
||||
|
||||
var bootstrapScriptURL string
|
||||
if cmd.Flags().Changed("bootstrap-script-url") {
|
||||
bootstrapScriptURL = regOTOBootstrapScriptURL
|
||||
} else {
|
||||
bootstrapScriptURL = cfg.Bootstrap.OTOBootstrapScriptURL
|
||||
}
|
||||
|
||||
if bootstrapScriptURL == "" {
|
||||
return errors.New("validate agent register: bootstrap-script-url must not be empty (set via flag or config)")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(bootstrapScriptURL, "http://") {
|
||||
if !regAllowInsecureBootstrapURL {
|
||||
return errors.New("validate agent register: bootstrap-script-url must start with https:// unless --allow-insecure-bootstrap-url is set")
|
||||
}
|
||||
} else if !strings.HasPrefix(bootstrapScriptURL, "https://") {
|
||||
return errors.New("validate agent register: bootstrap-script-url must start with https:// (or http:// if allowed)")
|
||||
}
|
||||
|
||||
var releaseBaseURL string
|
||||
if cmd.Flags().Changed("release-base-url") {
|
||||
releaseBaseURL = regOTOReleaseBaseURL
|
||||
} else {
|
||||
releaseBaseURL = cfg.Bootstrap.OTOReleaseBaseURL
|
||||
}
|
||||
|
||||
if releaseBaseURL == "" {
|
||||
return errors.New("validate agent register: release-base-url must not be empty (set via flag or config)")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(releaseBaseURL, "https://") {
|
||||
return errors.New("validate agent register: release-base-url must start with https://")
|
||||
}
|
||||
|
||||
alias := agentID
|
||||
if cmd.Flags().Changed("alias") {
|
||||
alias = regAlias
|
||||
}
|
||||
|
||||
token := regToken
|
||||
if token == "" {
|
||||
token = tokenGenerator()
|
||||
}
|
||||
|
||||
edgeURL := regEdgeURL
|
||||
if edgeURL == "" {
|
||||
adv := resolveAdvertiseHost(cfg.Server.AdvertiseHost)
|
||||
edgeURL = addressFor(adv.Host, cfg.Server.Listen)
|
||||
}
|
||||
|
||||
var nodeDef *config.NodeDefinition
|
||||
exists := false
|
||||
for i, n := range cfg.Nodes {
|
||||
if n.ID == agentID {
|
||||
nodeDef = &cfg.Nodes[i]
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if exists {
|
||||
nodeDef.Alias = alias
|
||||
nodeDef.Token = token
|
||||
nodeDef.AgentKind = config.AgentKindOTOAgent
|
||||
nodeDef.Adapters = config.AdaptersConf{}
|
||||
} else {
|
||||
newDef := config.NodeDefinition{
|
||||
ID: agentID,
|
||||
Alias: alias,
|
||||
Token: token,
|
||||
AgentKind: config.AgentKindOTOAgent,
|
||||
}
|
||||
cfg.Nodes = append(cfg.Nodes, newDef)
|
||||
nodeDef = &cfg.Nodes[len(cfg.Nodes)-1]
|
||||
}
|
||||
|
||||
cfg.Bootstrap.OTOBootstrapScriptURL = bootstrapScriptURL
|
||||
cfg.Bootstrap.OTOReleaseBaseURL = releaseBaseURL
|
||||
|
||||
if err := validateEdgeConfig(&cfg); err != nil {
|
||||
return fmt.Errorf("validate agent register: %w", err)
|
||||
}
|
||||
|
||||
outData, err := patchYAML(data, &cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("patch config: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, outData, 0o600); err != nil {
|
||||
return fmt.Errorf("write config file: %w", err)
|
||||
}
|
||||
|
||||
// curl -fsSL <bootstrap-script-url> | bash -s -- --edge-url <edge-url> --agent-id <agent-id> --enrollment-token <token> --release-base-url <release-base-url> --agent-alias <alias>
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "curl -fsSL %s | bash -s -- --edge-url %s --agent-id %s --enrollment-token %s --release-base-url %s --agent-alias %s\n",
|
||||
bootstrapScriptURL, edgeURL, nodeDef.ID, nodeDef.Token, releaseBaseURL, nodeDef.Alias)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
c.Flags().StringVar(®Alias, "alias", "", "agent alias (defaults to agent-id)")
|
||||
c.Flags().StringVar(®Token, "token", "", "agent enrollment token (defaults to auto-generated uuid)")
|
||||
c.Flags().StringVar(®EdgeURL, "edge-url", "", "edge URL for agent registration (defaults to server advertise address)")
|
||||
c.Flags().StringVar(®OTOBootstrapScriptURL, "bootstrap-script-url", "", "bootstrap script URL for OTO agent")
|
||||
c.Flags().StringVar(®OTOReleaseBaseURL, "release-base-url", "", "release base URL for OTO agent")
|
||||
c.Flags().BoolVar(®AllowInsecureBootstrapURL, "allow-insecure-bootstrap-url", false, "allow insecure http:// bootstrap script URL")
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func nodeCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "node",
|
||||
|
|
|
|||
|
|
@ -1139,6 +1139,16 @@ func TestHelpDiscoversEdgeLocalDevFlow(t *testing.T) {
|
|||
args: []string{"node", "register", "--help"},
|
||||
want: []string{"one-line bootstrap command", "node.yaml", "iop-node serve", "--adapter", "--target"},
|
||||
},
|
||||
{
|
||||
name: "agent",
|
||||
args: []string{"agent", "--help"},
|
||||
want: []string{"agent register", "edge.yaml"},
|
||||
},
|
||||
{
|
||||
name: "agent register",
|
||||
args: []string{"agent", "register", "--help"},
|
||||
want: []string{"one-line bootstrap command", "edge.yaml", "--alias", "--token", "--bootstrap-script-url", "--release-base-url"},
|
||||
},
|
||||
{
|
||||
name: "smoke",
|
||||
args: []string{"smoke", "--help"},
|
||||
|
|
@ -1381,3 +1391,229 @@ bootstrap:
|
|||
t.Errorf("oto_release_base_url = %q", doc.Bootstrap.OTOReleaseBaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRegisterUpsertsOTOAgentRecord(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpdir, "edge.yaml")
|
||||
initialYAML := `server:
|
||||
listen: "127.0.0.1:9090"
|
||||
advertise_host: "127.0.0.1"
|
||||
bootstrap:
|
||||
oto_bootstrap_script_url: "https://example.com/boot.sh"
|
||||
oto_release_base_url: "https://example.com/releases"
|
||||
nodes: []
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(initialYAML), 0o600); err != nil {
|
||||
t.Fatalf("write initial config: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "agent", "register", "oto-agent-1", "--alias", "custom-alias", "--token", "custom-token"})
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("agent register failed: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := config.LoadEdge(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load config failed: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Nodes) != 1 {
|
||||
t.Fatalf("expected 1 node record, got %d", len(cfg.Nodes))
|
||||
}
|
||||
|
||||
n := cfg.Nodes[0]
|
||||
if n.ID != "oto-agent-1" {
|
||||
t.Errorf("expected ID oto-agent-1, got %s", n.ID)
|
||||
}
|
||||
if n.Alias != "custom-alias" {
|
||||
t.Errorf("expected Alias custom-alias, got %s", n.Alias)
|
||||
}
|
||||
if n.Token != "custom-token" {
|
||||
t.Errorf("expected Token custom-token, got %s", n.Token)
|
||||
}
|
||||
if n.AgentKind != config.AgentKindOTOAgent {
|
||||
t.Errorf("expected AgentKind %q, got %s", config.AgentKindOTOAgent, n.AgentKind)
|
||||
}
|
||||
if n.Adapters.Ollama.Enabled || n.Adapters.CLI.Enabled {
|
||||
t.Errorf("expected adapters to be disabled for OTO agent, got Ollama=%v, CLI=%v", n.Adapters.Ollama.Enabled, n.Adapters.CLI.Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRegisterPrintsOneLineOTOCommand(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpdir, "edge.yaml")
|
||||
initialYAML := `server:
|
||||
listen: "127.0.0.1:9090"
|
||||
advertise_host: "127.0.0.1"
|
||||
bootstrap:
|
||||
oto_bootstrap_script_url: "https://example.com/boot.sh"
|
||||
oto_release_base_url: "https://example.com/releases"
|
||||
nodes: []
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(initialYAML), 0o600); err != nil {
|
||||
t.Fatalf("write initial config: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "agent", "register", "oto-agent-1", "--alias", "custom-alias", "--token", "custom-token", "--edge-url", "http://edge.test:9090"})
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("agent register failed: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
want := "curl -fsSL https://example.com/boot.sh | bash -s -- --edge-url http://edge.test:9090 --agent-id oto-agent-1 --enrollment-token custom-token --release-base-url https://example.com/releases --agent-alias custom-alias\n"
|
||||
if got != want {
|
||||
t.Errorf("unexpected output:\n got: %q\nwant: %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRegisterRejectsInsecureBootstrapURLWithoutFlag(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpdir, "edge.yaml")
|
||||
initialYAML := `server:
|
||||
listen: "127.0.0.1:9090"
|
||||
advertise_host: "127.0.0.1"
|
||||
bootstrap:
|
||||
oto_release_base_url: "https://example.com/releases"
|
||||
nodes: []
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(initialYAML), 0o600); err != nil {
|
||||
t.Fatalf("write initial config: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "agent", "register", "oto-agent-1", "--bootstrap-script-url", "http://example.com/boot.sh"})
|
||||
|
||||
err := root.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "bootstrap-script-url must start with https:// unless --allow-insecure-bootstrap-url is set") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRegisterRejectsInsecureReleaseBaseURL(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpdir, "edge.yaml")
|
||||
initialYAML := `server:
|
||||
listen: "127.0.0.1:9090"
|
||||
advertise_host: "127.0.0.1"
|
||||
bootstrap:
|
||||
oto_bootstrap_script_url: "https://example.com/boot.sh"
|
||||
nodes: []
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(initialYAML), 0o600); err != nil {
|
||||
t.Fatalf("write initial config: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "agent", "register", "oto-agent-1", "--release-base-url", "http://example.com/releases"})
|
||||
|
||||
err := root.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "release-base-url must start with https://") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRegisterAllowsInsecureBootstrapURLWithExplicitFlag(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpdir, "edge.yaml")
|
||||
initialYAML := `server:
|
||||
listen: "127.0.0.1:9090"
|
||||
advertise_host: "127.0.0.1"
|
||||
bootstrap:
|
||||
oto_release_base_url: "https://example.com/releases"
|
||||
nodes: []
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(initialYAML), 0o600); err != nil {
|
||||
t.Fatalf("write initial config: %v", err)
|
||||
}
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "agent", "register", "oto-agent-1", "--bootstrap-script-url", "http://example.com/boot.sh", "--allow-insecure-bootstrap-url"})
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("agent register failed with flag: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "http://example.com/boot.sh") {
|
||||
t.Errorf("output should contain http bootstrap URL, got: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTempFileCLIWorkflowConfigInitAgentRegisterConfigCheck(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpdir, "edge.yaml")
|
||||
t.Chdir(tmpdir)
|
||||
|
||||
// 1. config init
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"config", "init", "--output", cfgPath})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("config init failed: %v", err)
|
||||
}
|
||||
|
||||
// 2. agent register
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{
|
||||
"--config", cfgPath,
|
||||
"agent", "register", "oto-agent-test",
|
||||
"--bootstrap-script-url", "https://oto.example.test/bootstrap/oto_agent_bootstrap.sh",
|
||||
"--release-base-url", "https://oto.example.test/releases",
|
||||
"--token", "test-token",
|
||||
"--edge-url", "localhost:9090",
|
||||
})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("agent register failed: %v", err)
|
||||
}
|
||||
|
||||
// Assert exact-output format
|
||||
gotCmd := out.String()
|
||||
wantCmd := "curl -fsSL https://oto.example.test/bootstrap/oto_agent_bootstrap.sh | bash -s -- --edge-url localhost:9090 --agent-id oto-agent-test --enrollment-token test-token --release-base-url https://oto.example.test/releases --agent-alias oto-agent-test\n"
|
||||
if gotCmd != wantCmd {
|
||||
t.Errorf("unexpected printed command:\n got: %q\nwant: %q", gotCmd, wantCmd)
|
||||
}
|
||||
|
||||
// 3. config check
|
||||
root = rootCmd()
|
||||
out.Reset()
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{"--config", cfgPath, "config", "check"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("config check failed: %v", err)
|
||||
}
|
||||
gotCheck := out.String()
|
||||
if !strings.Contains(gotCheck, "OK") {
|
||||
t.Errorf("expected OK, got: %q", gotCheck)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue