feat: edge local dev infrastructure and artifact server implementation
- Add artifact_server.go for edge node artifact serving - Refactor bootstrap/runtime.go to use new artifact server - Update Makefile for new build workflow - Remove deprecated bin/build/field-binaries.sh - Update edge README with new development workflow - Update edge-local-dev-guide.md with artifact server info - Add config and hostsetup updates for artifact serving - Update agent-ops rules and testing domain rules
This commit is contained in:
parent
329576345d
commit
5c3fef65e9
13 changed files with 499 additions and 251 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -10,9 +10,11 @@ agent-ops/rules/private/
|
|||
/temp_verif/
|
||||
/bin/control-plane
|
||||
/bin/edge
|
||||
/bin/artifacts/
|
||||
/bin/iop-control-plane
|
||||
/bin/iop-edge
|
||||
/bin/iop-node
|
||||
/bin/iop-node-*
|
||||
/bin/iop-worker
|
||||
/bin/node
|
||||
/bin/worker
|
||||
|
|
|
|||
21
Makefile
21
Makefile
|
|
@ -1,8 +1,11 @@
|
|||
.PHONY: all build build-edge build-node dist dist-edge dist-node tidy test test-e2e test-openai-ollama proto proto-dart portal-test portal-build-web clean
|
||||
.PHONY: all build build-edge build-node build-node-target pack-node-target tidy test test-e2e test-openai-ollama proto proto-dart portal-test portal-build-web clean
|
||||
|
||||
GOFLAGS ?= -trimpath
|
||||
BUILD_DIR ?= bin
|
||||
DIST_APPS ?= edge node
|
||||
NODE_TARGET ?= $(shell go env GOOS)-$(shell go env GOARCH)
|
||||
NODE_TARGET_PARTS = $(subst -, ,$(NODE_TARGET))
|
||||
NODE_GOOS = $(word 1,$(NODE_TARGET_PARTS))
|
||||
NODE_GOARCH = $(word 2,$(NODE_TARGET_PARTS))
|
||||
|
||||
all: build
|
||||
|
||||
|
|
@ -16,14 +19,12 @@ build-edge:
|
|||
build-node:
|
||||
go build $(GOFLAGS) -o $(BUILD_DIR)/iop-node ./apps/node/cmd/node
|
||||
|
||||
dist:
|
||||
APPS="$(DIST_APPS)" GOFLAGS="$(GOFLAGS)" bin/build/field-binaries.sh
|
||||
build-node-target:
|
||||
@test -n "$(NODE_GOOS)" && test -n "$(NODE_GOARCH)" || (echo "NODE_TARGET must be <goos>-<goarch>" >&2; exit 2)
|
||||
GOOS=$(NODE_GOOS) GOARCH=$(NODE_GOARCH) go build $(GOFLAGS) -o $(BUILD_DIR)/iop-node-$(NODE_TARGET) ./apps/node/cmd/node
|
||||
|
||||
dist-edge:
|
||||
$(MAKE) dist DIST_APPS=edge
|
||||
|
||||
dist-node:
|
||||
$(MAKE) dist DIST_APPS=node
|
||||
pack-node-target: build-edge build-node-target
|
||||
cd $(BUILD_DIR) && ./iop-edge bootstrap pack --target $(NODE_TARGET) --node-binary ./iop-node-$(NODE_TARGET)
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
|
@ -87,6 +88,8 @@ portal-build-web:
|
|||
|
||||
clean:
|
||||
rm -f bin/iop-edge bin/iop-node bin/iop-control-plane bin/iop-worker
|
||||
rm -f bin/iop-node-*
|
||||
rm -f bin/node bin/edge bin/control-plane bin/worker
|
||||
rm -rf bin/artifacts
|
||||
rm -rf dist
|
||||
rm -f iop.db
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ last_rule_updated_at: 2026-05-28
|
|||
- `Makefile` — 공식 test target, proto generation, 보조 smoke target을 선언하는 위치이다.
|
||||
- `bin/edge.sh` — repo 내부 edge console/server 개발 진단 helper이다.
|
||||
- `bin/node.sh` — repo 내부 node 연결 개발 진단 helper이다. field 사용자 기본 경로로 안내하지 않는다.
|
||||
- `bin/build/field-binaries.sh` — field 배포용 edge/node 바이너리 build entrypoint이다.
|
||||
- `iop-edge bootstrap pack` — Node host bootstrap용 artifact 폴더를 준비하는 공식 command이다.
|
||||
- `scripts/e2e-smoke.sh` — mock/real profile 기반 보조 edge-node smoke 검증이다.
|
||||
- `scripts/e2e-openai-ollama.sh` — OpenAI-compatible Ollama 입력 표면 보조 smoke 검증이다.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ last_rule_updated_at: 2026-05-28
|
|||
- field Node bootstrap의 사용자 명령은 `curl -fsSL <완성된-bootstrap-url> | bash -s <token>` 형태를 기준으로 한다. 다른 bootstrap/enrollment 작업에서도 같은 수준의 단일 token UX를 우선 적용하고, 예외가 필요하면 사용자에게 먼저 확인한다.
|
||||
- repo 내부 edge-node 진단의 상세 수행 절차와 기능별 체크리스트는 `agent-ops/skills/project/e2e-smoke/SKILL.md`를 따른다.
|
||||
- `make test-e2e`, `scripts/e2e-smoke.sh`, `scripts/e2e-openai-ollama.sh`는 보조 smoke 명령이다. 실행할 수 있으면 보조 확인으로 기록하되, full-cycle 실제 구동이나 field bootstrap 검증을 대체하지 않는다.
|
||||
- `bin/build/field-binaries.sh` 변경 시 최소 현재 host target build를 실행하고 산출물 위치, checksum 생성, node bootstrap script가 positional token UX를 유지하는지 확인한다.
|
||||
- `iop-edge bootstrap pack` 또는 내장 artifact server 변경 시 최소 현재 host target build를 실행하고 artifact 폴더 위치, checksum 생성, node bootstrap script가 positional token UX를 유지하는지 확인한다.
|
||||
- 풀테스트에서는 실제 외부 CLI profile 검증을 필수로 수행한다. 환경, 계정, provider, 원격 endpoint 문제로 호출할 수 없거나 실패한 profile은 누락하지 말고 profile별 실패 또는 blocker로 보고한다.
|
||||
- 작업 최종 보고에는 실행한 테스트 명령, repo 내부 edge-node 진단 수행 여부, 보조 E2E smoke 수행 여부, full-cycle 실제 구동 수행 여부를 명시한다. 수행하지 못한 필수 검증은 이유와 남은 위험을 함께 적는다.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
- `proto/iop/` — IOP 메시지 계약 원본이다.
|
||||
- `proto/gen/iop/` — protobuf 생성물이다.
|
||||
- `configs/` — 앱별 YAML 설정 예시이다.
|
||||
- `bin/` — 개발 보조 shell entrypoint와 field binary build entrypoint이다. Edge/Node 운영 UX의 공식 표면은 `iop-edge`/`iop-node` 바이너리 command로 모은다.
|
||||
- `bin/` — 개발 보조 shell entrypoint와 로컬 바이너리 산출 위치이다. Edge/Node 운영 UX와 Node bootstrap artifact 준비는 `iop-edge`/`iop-node` 바이너리 command 표면에 모은다.
|
||||
- `scripts/` — 보조 E2E smoke와 입력 표면 검증 스크립트이다.
|
||||
- `Makefile` — 빌드와 테스트 진입점을 정의한다.
|
||||
- `docs/` — 사람용 최신 가이드만 둔다. 환경별 값은 `agent-ops/rules/private/`에 두고, `rules.md`는 라우터로만 쓴다.
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ iop-edge env
|
|||
iop-edge config init
|
||||
iop-edge config print --config /etc/iop/edge.yaml
|
||||
iop-edge config check --config /etc/iop/edge.yaml
|
||||
iop-edge bootstrap pack
|
||||
iop-edge node register <node-id>
|
||||
iop-edge nodes list
|
||||
iop-edge smoke openai --model <model>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -13,6 +14,7 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ The official local development and field test flow is as follows:
|
|||
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(), nodeCmd(), nodesCmd(), smokeCmd())
|
||||
root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd(), bootstrapCmd(), nodeCmd(), nodesCmd(), smokeCmd())
|
||||
return root
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +111,16 @@ func resolveEdgeLogPath(explicit string) (string, error) {
|
|||
return filepath.Join(filepath.Dir(exe), "logs", "edge.log"), nil
|
||||
}
|
||||
|
||||
func resolveBootstrapArtifactDir(dir string) string {
|
||||
if dir == "" {
|
||||
dir = "artifacts"
|
||||
}
|
||||
if filepath.IsAbs(dir) {
|
||||
return dir
|
||||
}
|
||||
return filepath.Join(binaryDir(), dir)
|
||||
}
|
||||
|
||||
// binaryDir returns the directory of the running executable. Falls back to the
|
||||
// current working directory if os.Executable fails.
|
||||
func binaryDir() string {
|
||||
|
|
@ -140,6 +152,7 @@ func serveCmd() *cobra.Command {
|
|||
}
|
||||
}
|
||||
cfg.Logging.Path = logPath
|
||||
cfg.Bootstrap.ArtifactDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
|
||||
fx.New(bootstrap.Module(cfg)).Run()
|
||||
return nil
|
||||
},
|
||||
|
|
@ -165,6 +178,7 @@ func consoleCmd() *cobra.Command {
|
|||
}
|
||||
}
|
||||
cfg.Logging.Path = logPath
|
||||
cfg.Bootstrap.ArtifactDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
|
||||
return runConsole(context.Background(), cfg, os.Stdin, os.Stdout)
|
||||
},
|
||||
}
|
||||
|
|
@ -309,6 +323,7 @@ func envCmd() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Bootstrap.ArtifactDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
|
||||
renderEnv(cmd.OutOrStdout(), envReport{
|
||||
ConfigPath: path,
|
||||
BinaryDir: binaryDir(),
|
||||
|
|
@ -373,8 +388,12 @@ func addressFor(host, listen string) string {
|
|||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
func defaultArtifactBaseURL(host string) string {
|
||||
return "http://" + addressFor(host, "0.0.0.0:18080")
|
||||
func defaultArtifactBaseURL(host string, listen ...string) string {
|
||||
bootstrapListen := "0.0.0.0:18080"
|
||||
if len(listen) > 0 && listen[0] != "" {
|
||||
bootstrapListen = listen[0]
|
||||
}
|
||||
return "http://" + addressFor(host, bootstrapListen)
|
||||
}
|
||||
|
||||
func bootstrapScriptURL(baseURL, target string) string {
|
||||
|
|
@ -397,7 +416,7 @@ func renderEnv(w io.Writer, r envReport) {
|
|||
artifactURL := strings.TrimSuffix(r.Bootstrap.ArtifactBaseURL, "/")
|
||||
artifactSource := "configured"
|
||||
if artifactURL == "" {
|
||||
artifactURL = defaultArtifactBaseURL(r.Advertise.Host)
|
||||
artifactURL = defaultArtifactBaseURL(r.Advertise.Host, r.Bootstrap.Listen)
|
||||
artifactSource = "derived"
|
||||
}
|
||||
|
||||
|
|
@ -406,6 +425,8 @@ func renderEnv(w io.Writer, r envReport) {
|
|||
fmt.Fprintf(w, "log_path: %s\n", r.LogPath)
|
||||
fmt.Fprintf(w, "advertise_host: %s (%s)\n", r.Advertise.Host, r.Advertise.Source)
|
||||
fmt.Fprintf(w, "node_transport: %s\n", addressFor(r.Advertise.Host, r.Server.Listen))
|
||||
fmt.Fprintf(w, "artifact_dir: %s\n", r.Bootstrap.ArtifactDir)
|
||||
fmt.Fprintf(w, "artifact_listen: %s\n", r.Bootstrap.Listen)
|
||||
fmt.Fprintf(w, "artifact_base_url: %s (%s)\n", artifactURL, artifactSource)
|
||||
fmt.Fprintf(w, "node_bootstrap: %s\n", bootstrapScriptURL(artifactURL, runtime.GOOS+"-"+runtime.GOARCH))
|
||||
if r.OpenAI.Enabled {
|
||||
|
|
@ -472,6 +493,255 @@ func setupCmd() *cobra.Command {
|
|||
return c
|
||||
}
|
||||
|
||||
func bootstrapCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "bootstrap",
|
||||
Short: "Prepare node bootstrap artifacts",
|
||||
Long: `Prepare node bootstrap artifacts served by iop-edge serve.
|
||||
|
||||
The default output directory is the configured bootstrap.artifact_dir resolved
|
||||
beside the iop-edge binary. The directory is static and can be copied as-is.`,
|
||||
}
|
||||
c.AddCommand(bootstrapPackCmd())
|
||||
return c
|
||||
}
|
||||
|
||||
func bootstrapPackCmd() *cobra.Command {
|
||||
var nodeBinary string
|
||||
var outputDir string
|
||||
var target string
|
||||
var artifactBaseURL string
|
||||
var edgeAddr string
|
||||
c := &cobra.Command{
|
||||
Use: "pack",
|
||||
Short: "Pack iop-node into the configured bootstrap artifact directory",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
cfg, err := loadEdgeConfigForBootstrapPack(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adv := resolveAdvertiseHost(cfg.Server.AdvertiseHost)
|
||||
if nodeBinary == "" {
|
||||
nodeBinary = filepath.Join(binaryDir(), "iop-node")
|
||||
}
|
||||
if outputDir == "" {
|
||||
outputDir = resolveBootstrapArtifactDir(cfg.Bootstrap.ArtifactDir)
|
||||
}
|
||||
if target == "" {
|
||||
target = runtime.GOOS + "-" + runtime.GOARCH
|
||||
}
|
||||
if artifactBaseURL == "" {
|
||||
if cfg.Bootstrap.ArtifactBaseURL != "" {
|
||||
artifactBaseURL = strings.TrimSuffix(cfg.Bootstrap.ArtifactBaseURL, "/")
|
||||
} else {
|
||||
artifactBaseURL = defaultArtifactBaseURL(adv.Host, cfg.Bootstrap.Listen)
|
||||
}
|
||||
}
|
||||
if edgeAddr == "" {
|
||||
edgeAddr = addressFor(adv.Host, cfg.Server.Listen)
|
||||
}
|
||||
if err := packNodeBootstrap(nodeBootstrapPackOptions{
|
||||
NodeBinary: nodeBinary,
|
||||
OutputDir: outputDir,
|
||||
Target: target,
|
||||
Version: version.Version,
|
||||
ArtifactBaseURL: artifactBaseURL,
|
||||
EdgeAddr: edgeAddr,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "wrote bootstrap artifacts to %s\n", outputDir)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "bootstrap: %s\n", bootstrapScriptURL(artifactBaseURL, target))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.Flags().StringVar(&nodeBinary, "node-binary", "", "iop-node binary to pack (defaults to iop-node beside iop-edge)")
|
||||
c.Flags().StringVarP(&outputDir, "output", "o", "", "artifact output directory (defaults to bootstrap.artifact_dir)")
|
||||
c.Flags().StringVar(&target, "target", "", "target platform label (defaults to current GOOS-GOARCH)")
|
||||
c.Flags().StringVar(&artifactBaseURL, "artifact-base-url", "", "artifact base URL baked into the bootstrap script")
|
||||
c.Flags().StringVar(&edgeAddr, "edge-addr", "", "edge transport address baked into node.yaml")
|
||||
return c
|
||||
}
|
||||
|
||||
func loadEdgeConfigForBootstrapPack(cmd *cobra.Command) (*config.EdgeConfig, error) {
|
||||
cfg, _, err := loadEdgeConfig(cmd)
|
||||
if err == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no edge.yaml found") {
|
||||
return nil, err
|
||||
}
|
||||
return &config.EdgeConfig{
|
||||
Server: config.EdgeServerConf{
|
||||
Listen: "0.0.0.0:9090",
|
||||
},
|
||||
Bootstrap: config.EdgeBootstrapConf{
|
||||
Listen: "0.0.0.0:18080",
|
||||
ArtifactDir: "artifacts",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type nodeBootstrapPackOptions struct {
|
||||
NodeBinary string
|
||||
OutputDir string
|
||||
Target string
|
||||
Version string
|
||||
ArtifactBaseURL string
|
||||
EdgeAddr string
|
||||
}
|
||||
|
||||
func packNodeBootstrap(opts nodeBootstrapPackOptions) error {
|
||||
if opts.NodeBinary == "" {
|
||||
return errors.New("node binary path is required")
|
||||
}
|
||||
if opts.OutputDir == "" {
|
||||
return errors.New("output directory is required")
|
||||
}
|
||||
if opts.Target == "" {
|
||||
return errors.New("target is required")
|
||||
}
|
||||
if strings.ContainsAny(opts.Target, `/\`) {
|
||||
return fmt.Errorf("target %q must not contain path separators", opts.Target)
|
||||
}
|
||||
targetDir := filepath.Join(opts.OutputDir, opts.Target)
|
||||
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir target dir: %w", err)
|
||||
}
|
||||
dstNode := filepath.Join(targetDir, "iop-node")
|
||||
if err := copyFile(opts.NodeBinary, dstNode, 0o755); err != nil {
|
||||
return fmt.Errorf("copy iop-node: %w", err)
|
||||
}
|
||||
sum, err := fileSHA256(dstNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sumLine := fmt.Sprintf("%x iop-node\n", sum)
|
||||
if err := os.WriteFile(filepath.Join(targetDir, "iop-node.sha256"), []byte(sumLine), 0o644); err != nil {
|
||||
return fmt.Errorf("write iop-node.sha256: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(targetDir, "SHA256SUMS"), []byte(sumLine), 0o644); err != nil {
|
||||
return fmt.Errorf("write SHA256SUMS: %w", err)
|
||||
}
|
||||
bootstrapDir := filepath.Join(opts.OutputDir, "bootstrap")
|
||||
if err := os.MkdirAll(bootstrapDir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir bootstrap dir: %w", err)
|
||||
}
|
||||
script := nodeBootstrapScript(opts)
|
||||
scriptPath := filepath.Join(bootstrapDir, "node-"+opts.Target+".sh")
|
||||
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
||||
return fmt.Errorf("write bootstrap script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
_ = out.Close()
|
||||
return err
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(dst, mode)
|
||||
}
|
||||
|
||||
func fileSHA256(path string) ([]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open checksum file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return nil, fmt.Errorf("hash file: %w", err)
|
||||
}
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
func shellQuote(s string) string {
|
||||
return strconv.Quote(s)
|
||||
}
|
||||
|
||||
func nodeBootstrapScript(opts nodeBootstrapPackOptions) string {
|
||||
return `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION=` + shellQuote(opts.Version) + `
|
||||
TARGET=` + shellQuote(opts.Target) + `
|
||||
DEFAULT_ARTIFACT_BASE_URL=` + shellQuote(strings.TrimSuffix(opts.ArtifactBaseURL, "/")) + `
|
||||
DEFAULT_EDGE_ADDR=` + shellQuote(opts.EdgeAddr) + `
|
||||
POSITIONAL_NODE_TOKEN="${1:-}"
|
||||
ARTIFACT_BASE_URL="$DEFAULT_ARTIFACT_BASE_URL"
|
||||
EDGE_ADDR="$DEFAULT_EDGE_ADDR"
|
||||
NODE_TOKEN="$POSITIONAL_NODE_TOKEN"
|
||||
INSTALL_DIR="${IOP_HOME:-$HOME/iop-field}"
|
||||
CONFIG_FILE="${IOP_NODE_CONFIG:-$INSTALL_DIR/node.yaml}"
|
||||
BIN_FILE="$INSTALL_DIR/iop-node"
|
||||
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/iop-node-bootstrap.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
ARTIFACT_BASE_URL="${ARTIFACT_BASE_URL%/}"
|
||||
|
||||
if [[ -z "$NODE_TOKEN" ]]; then
|
||||
echo "[iop-bootstrap] missing node token" >&2
|
||||
echo "[iop-bootstrap] usage: curl -fsSL <artifact-base-url>/bootstrap/node-$TARGET.sh | bash -s TOKEN" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[iop-bootstrap] version=$VERSION target=$TARGET"
|
||||
echo "[iop-bootstrap] install_dir=$INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/iop-node" -o "$TMP_DIR/iop-node"
|
||||
curl -fsSL "$ARTIFACT_BASE_URL/$TARGET/SHA256SUMS" -o "$TMP_DIR/SHA256SUMS"
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | sha256sum -c -)
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
(cd "$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | shasum -a 256 -c -)
|
||||
else
|
||||
echo "[iop-bootstrap] warning: no sha256 checker found; skipping checksum verification" >&2
|
||||
fi
|
||||
|
||||
cp "$TMP_DIR/iop-node" "$BIN_FILE"
|
||||
chmod +x "$BIN_FILE"
|
||||
if command -v xattr >/dev/null 2>&1; then
|
||||
xattr -d com.apple.quarantine "$BIN_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat > "$CONFIG_FILE" <<YAML
|
||||
transport:
|
||||
edge_addr: "$EDGE_ADDR"
|
||||
token: "$NODE_TOKEN"
|
||||
|
||||
logging:
|
||||
level: "${IOP_LOG_LEVEL:-info}"
|
||||
pretty: true
|
||||
|
||||
metrics:
|
||||
port: ${IOP_NODE_METRICS_PORT:-9091}
|
||||
YAML
|
||||
|
||||
echo "[iop-bootstrap] config=$CONFIG_FILE"
|
||||
echo "[iop-bootstrap] starting node against $EDGE_ADDR"
|
||||
exec "$BIN_FILE" serve --config "$CONFIG_FILE"
|
||||
`
|
||||
}
|
||||
|
||||
var (
|
||||
regAlias string
|
||||
regToken string
|
||||
|
|
@ -702,7 +972,7 @@ directly in the default flow.`,
|
|||
cfg.Bootstrap.ArtifactBaseURL = regArtifactBaseURL
|
||||
}
|
||||
if artifactURL == "" {
|
||||
artifactURL = defaultArtifactBaseURL(resolveAdvertiseHost(cfg.Server.AdvertiseHost).Host)
|
||||
artifactURL = defaultArtifactBaseURL(resolveAdvertiseHost(cfg.Server.AdvertiseHost).Host, cfg.Bootstrap.Listen)
|
||||
}
|
||||
|
||||
// Validate mutated config before writing
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ import (
|
|||
func TestRootCmdIncludesOperationalCommands(t *testing.T) {
|
||||
root := rootCmd()
|
||||
want := map[string]bool{
|
||||
"serve": false,
|
||||
"console": false,
|
||||
"version": false,
|
||||
"config": false,
|
||||
"env": false,
|
||||
"setup": false,
|
||||
"serve": false,
|
||||
"console": false,
|
||||
"version": false,
|
||||
"config": false,
|
||||
"env": false,
|
||||
"setup": false,
|
||||
"bootstrap": false,
|
||||
}
|
||||
for _, c := range root.Commands() {
|
||||
if _, ok := want[c.Name()]; ok {
|
||||
|
|
@ -398,6 +399,61 @@ func TestResolveEdgeLogPathRespectsExplicitPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBootstrapPackCreatesNodeArtifacts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "edge.yaml")
|
||||
yamlStr := `server:
|
||||
listen: "127.0.0.1:39090"
|
||||
advertise_host: "edge.example.test"
|
||||
bootstrap:
|
||||
listen: "127.0.0.1:38080"
|
||||
artifact_dir: "artifacts"
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
nodeBinary := filepath.Join(dir, "iop-node")
|
||||
if err := os.WriteFile(nodeBinary, []byte("fake-node-binary"), 0o755); err != nil {
|
||||
t.Fatalf("write node binary: %v", err)
|
||||
}
|
||||
outDir := filepath.Join(dir, "out")
|
||||
|
||||
root := rootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs([]string{
|
||||
"--config", cfgPath,
|
||||
"bootstrap", "pack",
|
||||
"--node-binary", nodeBinary,
|
||||
"--output", outDir,
|
||||
"--target", "linux-arm64",
|
||||
})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute pack: %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
for _, path := range []string{
|
||||
filepath.Join(outDir, "linux-arm64", "iop-node"),
|
||||
filepath.Join(outDir, "linux-arm64", "SHA256SUMS"),
|
||||
filepath.Join(outDir, "bootstrap", "node-linux-arm64.sh"),
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected artifact %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
script, err := os.ReadFile(filepath.Join(outDir, "bootstrap", "node-linux-arm64.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("read script: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(script), `DEFAULT_ARTIFACT_BASE_URL="http://edge.example.test:38080"`) {
|
||||
t.Fatalf("script missing derived artifact URL:\n%s", script)
|
||||
}
|
||||
if !strings.Contains(string(script), `DEFAULT_EDGE_ADDR="edge.example.test:39090"`) {
|
||||
t.Fatalf("script missing derived edge addr:\n%s", script)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAdvertiseHostPrefersExplicit(t *testing.T) {
|
||||
got := resolveAdvertiseHost("explicit.example.test")
|
||||
if got.Host != "explicit.example.test" || got.Source != "explicit" {
|
||||
|
|
|
|||
72
apps/edge/internal/bootstrap/artifact_server.go
Normal file
72
apps/edge/internal/bootstrap/artifact_server.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ArtifactServer serves node bootstrap scripts and node binaries from the
|
||||
// configured artifact directory. It is intentionally static: iop-edge prepares
|
||||
// the directory, then serve exposes it without mutating the artifacts.
|
||||
type ArtifactServer struct {
|
||||
listen string
|
||||
dir string
|
||||
logger *zap.Logger
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func NewArtifactServer(listen, dir string, logger *zap.Logger) *ArtifactServer {
|
||||
return &ArtifactServer{listen: listen, dir: dir, logger: logger}
|
||||
}
|
||||
|
||||
func (s *ArtifactServer) Start(ctx context.Context) error {
|
||||
if s == nil || s.listen == "" || s.dir == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := os.Stat(s.dir); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
s.logger.Warn("bootstrap artifact directory does not exist", zap.String("dir", s.dir))
|
||||
} else {
|
||||
return fmt.Errorf("stat bootstrap artifact dir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.FileServer(http.Dir(s.dir)))
|
||||
s.server = &http.Server{
|
||||
Addr: s.listen,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
ln, err := net.Listen("tcp", s.listen)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bootstrap artifact server listen %s: %w", s.listen, err)
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = s.server.Shutdown(shutdownCtx)
|
||||
}()
|
||||
go func() {
|
||||
s.logger.Info("bootstrap artifact server listening", zap.String("addr", s.listen), zap.String("dir", s.dir))
|
||||
if err := s.server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Warn("bootstrap artifact server exited", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ArtifactServer) Stop(ctx context.Context) error {
|
||||
if s == nil || s.server == nil {
|
||||
return nil
|
||||
}
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ type Runtime struct {
|
|||
Service *edgeservice.Service
|
||||
Server *transport.Server
|
||||
Input *edgeinput.Manager
|
||||
Artifact *ArtifactServer
|
||||
|
||||
lifetimeMu sync.Mutex
|
||||
lifetimeCancel context.CancelFunc
|
||||
|
|
@ -50,6 +51,7 @@ func NewRuntime(cfg *config.EdgeConfig) (*Runtime, error) {
|
|||
bus := edgeevents.NewBus()
|
||||
svc := edgeservice.New(registry, bus)
|
||||
inputManager := edgeinput.NewManager(*cfg, svc, logger.Named("input"))
|
||||
artifactServer := NewArtifactServer(cfg.Bootstrap.Listen, cfg.Bootstrap.ArtifactDir, logger.Named("bootstrap"))
|
||||
|
||||
server, err := transport.NewServer(cfg.Server.Listen, registry, nodeStore, logger)
|
||||
if err != nil {
|
||||
|
|
@ -65,6 +67,7 @@ func NewRuntime(cfg *config.EdgeConfig) (*Runtime, error) {
|
|||
Service: svc,
|
||||
Server: server,
|
||||
Input: inputManager,
|
||||
Artifact: artifactServer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +96,12 @@ func (r *Runtime) Start(ctx context.Context) error {
|
|||
_ = r.Server.Stop()
|
||||
return err
|
||||
}
|
||||
if err := r.Artifact.Start(lifetimeCtx); err != nil {
|
||||
r.cancelLifetime()
|
||||
_ = r.Input.Stop(context.Background())
|
||||
_ = r.Server.Stop()
|
||||
return err
|
||||
}
|
||||
r.startMetrics()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -102,11 +111,15 @@ func (r *Runtime) Stop() error {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
inputErr := r.Input.Stop(ctx)
|
||||
artifactErr := r.Artifact.Stop(ctx)
|
||||
err := r.Server.Stop()
|
||||
_ = r.Logger.Sync()
|
||||
if err == nil {
|
||||
err = inputErr
|
||||
}
|
||||
if err == nil {
|
||||
err = artifactErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,213 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
DIST_DIR="${DIST_DIR:-"$ROOT_DIR/dist/field"}"
|
||||
TARGETS="${TARGETS:-}"
|
||||
APPS="${APPS:-"edge node"}"
|
||||
CGO_ENABLED="${CGO_ENABLED:-0}"
|
||||
GOFLAGS="${GOFLAGS:-"-trimpath"}"
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
cat <<'USAGE'
|
||||
Build field-deployable IOP edge/node binaries.
|
||||
|
||||
Usage:
|
||||
make dist
|
||||
make dist-edge
|
||||
make dist-node
|
||||
|
||||
This script is the build engine behind the Makefile targets.
|
||||
|
||||
Environment:
|
||||
APPS="edge node" apps to build
|
||||
TARGETS="linux/amd64 linux/arm64" GOOS/GOARCH target list
|
||||
DIST_DIR=dist/field output directory
|
||||
VERSION=<value> release version label
|
||||
COMMIT=<value> commit label
|
||||
BUILD_DATE=<value> build date label
|
||||
ARTIFACT_BASE_URL=<url> default bootstrap artifact base URL
|
||||
BOOTSTRAP_EDGE_ADDR=<host:port> default Edge address written to node.yaml
|
||||
BOOTSTRAP_NODE_TOKEN=<token> optional fallback token baked into bootstrap script
|
||||
CGO_ENABLED=0 Go CGO setting
|
||||
GOFLAGS="-trimpath" extra go build flags
|
||||
LDFLAGS="-s -w" linker flags
|
||||
USAGE
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VERSION="${VERSION:-"$(git -C "$ROOT_DIR" describe --tags --always --dirty 2>/dev/null || echo dev)"}"
|
||||
COMMIT="${COMMIT:-"$(git -C "$ROOT_DIR" rev-parse --short=12 HEAD 2>/dev/null || echo unknown)"}"
|
||||
BUILD_DATE="${BUILD_DATE:-"$(date -u +%Y%m%dT%H%M%SZ)"}"
|
||||
LDFLAGS="${LDFLAGS:-"-s -w"}"
|
||||
|
||||
if [[ -z "$TARGETS" ]]; then
|
||||
TARGETS="$(go env GOOS)/$(go env GOARCH)"
|
||||
fi
|
||||
|
||||
checksum() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$@"
|
||||
return
|
||||
fi
|
||||
shasum -a 256 "$@"
|
||||
}
|
||||
|
||||
app_enabled() {
|
||||
local wanted="$1"
|
||||
local app
|
||||
for app in $APPS; do
|
||||
if [[ "$app" == "$wanted" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
build_one() {
|
||||
local app="$1"
|
||||
local goos="$2"
|
||||
local goarch="$3"
|
||||
local output_dir="$DIST_DIR/$VERSION/$goos-$goarch"
|
||||
local output="$output_dir/iop-$app"
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
echo "building $app for $goos/$goarch -> $output"
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
GOOS="$goos" GOARCH="$goarch" CGO_ENABLED="$CGO_ENABLED" \
|
||||
go build $GOFLAGS -ldflags "$LDFLAGS" -o "$output" "./apps/$app/cmd/$app"
|
||||
)
|
||||
(
|
||||
cd "$output_dir"
|
||||
checksum "iop-$app" > "iop-$app.sha256"
|
||||
)
|
||||
}
|
||||
|
||||
write_node_bootstrap() {
|
||||
local goos="$1"
|
||||
local goarch="$2"
|
||||
local target_dir="$goos-$goarch"
|
||||
local bootstrap_dir="$DIST_DIR/$VERSION/bootstrap"
|
||||
local script="$bootstrap_dir/node-$target_dir.sh"
|
||||
local default_base_url="${ARTIFACT_BASE_URL:-"http://toki-labs.com:18080"}"
|
||||
local default_edge_addr="${BOOTSTRAP_EDGE_ADDR:-"toki-labs.com:19090"}"
|
||||
local default_node_token="${BOOTSTRAP_NODE_TOKEN:-}"
|
||||
|
||||
mkdir -p "$bootstrap_dir"
|
||||
cat > "$script" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="$VERSION"
|
||||
TARGET="$target_dir"
|
||||
DEFAULT_ARTIFACT_BASE_URL="$default_base_url"
|
||||
DEFAULT_EDGE_ADDR="$default_edge_addr"
|
||||
DEFAULT_NODE_TOKEN="$default_node_token"
|
||||
POSITIONAL_NODE_TOKEN="\${1:-}"
|
||||
ARTIFACT_BASE_URL="\$DEFAULT_ARTIFACT_BASE_URL"
|
||||
EDGE_ADDR="\$DEFAULT_EDGE_ADDR"
|
||||
NODE_TOKEN="\${POSITIONAL_NODE_TOKEN:-\$DEFAULT_NODE_TOKEN}"
|
||||
INSTALL_DIR="\${IOP_HOME:-\$HOME/iop-field}"
|
||||
CONFIG_FILE="\${IOP_NODE_CONFIG:-\$INSTALL_DIR/node.yaml}"
|
||||
BIN_FILE="\$INSTALL_DIR/iop-node"
|
||||
TMP_DIR="\$(mktemp -d "\${TMPDIR:-/tmp}/iop-node-bootstrap.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "\$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
ARTIFACT_BASE_URL="\${ARTIFACT_BASE_URL%/}"
|
||||
|
||||
if [[ -z "\$NODE_TOKEN" ]]; then
|
||||
echo "[iop-bootstrap] missing node token" >&2
|
||||
echo "[iop-bootstrap] usage: curl -fsSL <artifact-base-url>/bootstrap/node-\$TARGET.sh | bash -s TOKEN" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[iop-bootstrap] version=\$VERSION target=\$TARGET"
|
||||
echo "[iop-bootstrap] install_dir=\$INSTALL_DIR"
|
||||
mkdir -p "\$INSTALL_DIR"
|
||||
|
||||
curl -fsSL "\$ARTIFACT_BASE_URL/\$TARGET/iop-node" -o "\$TMP_DIR/iop-node"
|
||||
curl -fsSL "\$ARTIFACT_BASE_URL/\$TARGET/SHA256SUMS" -o "\$TMP_DIR/SHA256SUMS"
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
(cd "\$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | sha256sum -c -)
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
(cd "\$TMP_DIR" && grep -E '[ *]iop-node$' SHA256SUMS | shasum -a 256 -c -)
|
||||
else
|
||||
echo "[iop-bootstrap] warning: no sha256 checker found; skipping checksum verification" >&2
|
||||
fi
|
||||
|
||||
cp "\$TMP_DIR/iop-node" "\$BIN_FILE"
|
||||
chmod +x "\$BIN_FILE"
|
||||
if command -v xattr >/dev/null 2>&1; then
|
||||
xattr -d com.apple.quarantine "\$BIN_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat > "\$CONFIG_FILE" <<YAML
|
||||
transport:
|
||||
edge_addr: "\$EDGE_ADDR"
|
||||
token: "\$NODE_TOKEN"
|
||||
|
||||
logging:
|
||||
level: "\${IOP_LOG_LEVEL:-info}"
|
||||
pretty: true
|
||||
|
||||
metrics:
|
||||
port: \${IOP_NODE_METRICS_PORT:-9091}
|
||||
YAML
|
||||
|
||||
echo "[iop-bootstrap] config=\$CONFIG_FILE"
|
||||
echo "[iop-bootstrap] starting node against \$EDGE_ADDR"
|
||||
exec "\$BIN_FILE" serve --config "\$CONFIG_FILE"
|
||||
EOF
|
||||
chmod +x "$script"
|
||||
}
|
||||
|
||||
for target in $TARGETS; do
|
||||
IFS=/ read -r goos goarch extra <<< "$target"
|
||||
if [[ -z "${goos:-}" || -z "${goarch:-}" || -n "${extra:-}" ]]; then
|
||||
echo "invalid target '$target'; expected GOOS/GOARCH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for app in $APPS; do
|
||||
case "$app" in
|
||||
edge|node)
|
||||
build_one "$app" "$goos" "$goarch"
|
||||
;;
|
||||
*)
|
||||
echo "unsupported app '$app'; expected edge or node" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
output_dir="$DIST_DIR/$VERSION/$goos-$goarch"
|
||||
present_apps=()
|
||||
for app in edge node; do
|
||||
if [[ -f "$output_dir/iop-$app" ]]; then
|
||||
present_apps+=("$app")
|
||||
fi
|
||||
done
|
||||
{
|
||||
echo "version=$VERSION"
|
||||
echo "commit=$COMMIT"
|
||||
echo "build_date=$BUILD_DATE"
|
||||
echo "target=$goos/$goarch"
|
||||
echo "apps=${present_apps[*]:-}"
|
||||
echo "updated_apps=$APPS"
|
||||
} > "$output_dir/manifest.env"
|
||||
(
|
||||
cd "$output_dir"
|
||||
checksum iop-* | grep -v '\.sha256$' > SHA256SUMS
|
||||
)
|
||||
if app_enabled node; then
|
||||
write_node_bootstrap "$goos" "$goarch"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "field binaries written to $DIST_DIR/$VERSION"
|
||||
|
|
@ -33,12 +33,6 @@ bin/iop-edge
|
|||
bin/iop-node
|
||||
```
|
||||
|
||||
checksum, manifest, bootstrap script까지 포함한 field bundle이 필요할 때만 별도로 만든다.
|
||||
|
||||
```bash
|
||||
make dist
|
||||
```
|
||||
|
||||
## 2. Bundle 디렉터리 준비
|
||||
|
||||
하나의 디렉터리에 `iop-edge`와 `iop-node` 바이너리가 함께 있다고 가정한다.
|
||||
|
|
@ -90,6 +84,7 @@ cd /opt/iop-dev
|
|||
config path
|
||||
binary dir
|
||||
log dir
|
||||
artifact dir
|
||||
advertise host
|
||||
Node transport address
|
||||
artifact/bootstrap URL
|
||||
|
|
@ -107,7 +102,32 @@ connected nodes
|
|||
<bundle-dir>/logs/
|
||||
```
|
||||
|
||||
## 5. Node 등록과 bootstrap 출력
|
||||
## 5. Bootstrap artifact 준비
|
||||
|
||||
Node host에서 bootstrap 명령까지 테스트하려면 `iop-edge serve`가 제공할 artifact 폴더를 준비한다.
|
||||
|
||||
```bash
|
||||
./iop-edge bootstrap pack
|
||||
```
|
||||
|
||||
기본 산출물:
|
||||
|
||||
```text
|
||||
<bundle-dir>/artifacts/
|
||||
bootstrap/node-<os>-<arch>.sh
|
||||
<os>-<arch>/iop-node
|
||||
<os>-<arch>/SHA256SUMS
|
||||
```
|
||||
|
||||
Mac Node host용 artifact가 필요하면 repo root에서 대상 platform을 지정해 pack한다.
|
||||
|
||||
```bash
|
||||
make pack-node-target NODE_TARGET=darwin-arm64
|
||||
```
|
||||
|
||||
Intel Mac이면 `NODE_TARGET=darwin-amd64`를 사용한다.
|
||||
|
||||
## 6. Node 등록과 bootstrap 출력
|
||||
|
||||
Node 정의는 `edge.yaml`을 직접 편집하지 않고 `iop-edge node register`로 만든다.
|
||||
|
||||
|
|
@ -121,6 +141,7 @@ Ollama Node로 등록할 때:
|
|||
|
||||
```bash
|
||||
./iop-edge node register node-silicon-ollama \
|
||||
--target darwin-arm64 \
|
||||
--adapter ollama \
|
||||
--ollama-base-url http://127.0.0.1:11434
|
||||
```
|
||||
|
|
@ -136,7 +157,7 @@ curl -fsSL <bootstrap-url> | bash -s <generated-token>
|
|||
사용자는 이 한 줄만 Node host에서 실행한다.
|
||||
전달받은 명령에 `IOP_ARTIFACT_BASE_URL=`, `IOP_EDGE_ADDR=`, `IOP_NODE_TOKEN=` 같은 named parameter가 있으면 기본 경로가 아니다.
|
||||
|
||||
## 6. Config 검증
|
||||
## 7. Config 검증
|
||||
|
||||
```bash
|
||||
./iop-edge config check
|
||||
|
|
@ -144,7 +165,7 @@ curl -fsSL <bootstrap-url> | bash -s <generated-token>
|
|||
|
||||
이 단계에서 `nodes[]` token/id/alias 충돌, adapter/runtime 설정 오류가 잡혀야 한다.
|
||||
|
||||
## 7. Edge 실행
|
||||
## 8. Edge 실행
|
||||
|
||||
개발 중 foreground 실행:
|
||||
|
||||
|
|
@ -152,6 +173,9 @@ curl -fsSL <bootstrap-url> | bash -s <generated-token>
|
|||
./iop-edge serve
|
||||
```
|
||||
|
||||
`serve`는 Edge transport와 함께 bootstrap artifact 서버도 시작한다.
|
||||
기본 artifact 경로는 `<bundle-dir>/artifacts/`이고 기본 bootstrap URL 포트는 `18080`이다.
|
||||
|
||||
서비스로 올릴 때:
|
||||
|
||||
```bash
|
||||
|
|
@ -164,7 +188,7 @@ sudo ./iop-edge setup --enable --start
|
|||
tail -f logs/edge.log
|
||||
```
|
||||
|
||||
## 8. Node host에서 bootstrap 실행
|
||||
## 9. Node host에서 bootstrap 실행
|
||||
|
||||
`iop-edge node register ...`가 출력한 한 줄을 Node host에서 그대로 실행한다.
|
||||
|
||||
|
|
@ -175,21 +199,33 @@ curl -fsSL <bootstrap-url> | bash -s <generated-token>
|
|||
Node host 사용자는 `node.yaml`을 만들거나 편집하지 않는다.
|
||||
Node 프로세스 실행 방식은 bootstrap 내부 구현 세부다.
|
||||
|
||||
## 9. 연결 확인
|
||||
## 10. 연결 확인
|
||||
|
||||
Edge host에서 확인한다.
|
||||
Edge host 로그에서 Node 등록 이벤트를 확인한다.
|
||||
|
||||
```bash
|
||||
tail -f logs/edge.log
|
||||
```
|
||||
|
||||
기대 로그:
|
||||
|
||||
```text
|
||||
node registered
|
||||
```
|
||||
|
||||
설정에 등록된 Node 목록은 아래 명령으로 확인한다.
|
||||
|
||||
```bash
|
||||
./iop-edge nodes list
|
||||
```
|
||||
|
||||
OpenAI-compatible smoke:
|
||||
OpenAI-compatible smoke는 OpenAI 입력 표면과 Ollama model 설정이 완료된 뒤 실행한다.
|
||||
|
||||
```bash
|
||||
./iop-edge smoke openai --model gemma4:26b
|
||||
```
|
||||
|
||||
## 10. Cline 접속 정보 확인
|
||||
## 11. Cline 접속 정보 확인
|
||||
|
||||
```bash
|
||||
./iop-edge env
|
||||
|
|
@ -205,7 +241,7 @@ API Key: ASCII 값
|
|||
|
||||
API Key에는 한글이나 non-ASCII 문자를 넣지 않는다.
|
||||
|
||||
## 11. 문제 발생 시 공유할 것
|
||||
## 12. 문제 발생 시 공유할 것
|
||||
|
||||
```bash
|
||||
./iop-edge env
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ type EdgeServerConf struct {
|
|||
|
||||
type EdgeBootstrapConf struct {
|
||||
ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"`
|
||||
Listen string `mapstructure:"listen" yaml:"listen"`
|
||||
ArtifactDir string `mapstructure:"artifact_dir" yaml:"artifact_dir"`
|
||||
}
|
||||
|
||||
type EdgeOpenAIConf struct {
|
||||
|
|
@ -222,6 +224,8 @@ func setDefaults(v *viper.Viper) {
|
|||
|
||||
func setEdgeDefaults(v *viper.Viper) {
|
||||
v.SetDefault("server.listen", "0.0.0.0:9090")
|
||||
v.SetDefault("bootstrap.listen", "0.0.0.0:18080")
|
||||
v.SetDefault("bootstrap.artifact_dir", "artifacts")
|
||||
v.SetDefault("openai.enabled", false)
|
||||
v.SetDefault("openai.listen", "0.0.0.0:8080")
|
||||
v.SetDefault("openai.adapter", "ollama")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ server:
|
|||
advertise_host: ""
|
||||
|
||||
bootstrap:
|
||||
listen: "0.0.0.0:18080"
|
||||
artifact_base_url: ""
|
||||
artifact_dir: "artifacts"
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
|
|
@ -43,7 +45,9 @@ server:
|
|||
advertise_host: ""
|
||||
|
||||
bootstrap:
|
||||
listen: "0.0.0.0:18080"
|
||||
artifact_base_url: ""
|
||||
artifact_dir: "artifacts"
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
|
|
|
|||
Loading…
Reference in a new issue