341 lines
9.4 KiB
Go
341 lines
9.4 KiB
Go
package edgecmd
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"iop/packages/go/config"
|
|
"iop/packages/go/version"
|
|
)
|
|
|
|
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", universalBootstrapScriptURL(artifactBaseURL))
|
|
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")
|
|
same, err := sameFile(opts.NodeBinary, dstNode)
|
|
if err != nil {
|
|
return fmt.Errorf("stat iop-node: %w", err)
|
|
}
|
|
if same {
|
|
if err := os.Chmod(dstNode, 0o755); err != nil {
|
|
return fmt.Errorf("chmod iop-node: %w", err)
|
|
}
|
|
} else {
|
|
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)
|
|
}
|
|
if err := writeUniversalBootstrapScript(bootstrapDir, opts); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeUniversalBootstrapScript(bootstrapDir string, opts nodeBootstrapPackOptions) error {
|
|
universalOpts := opts
|
|
universalOpts.Target = ""
|
|
scriptPath := filepath.Join(bootstrapDir, "node.sh")
|
|
if err := os.MkdirAll(bootstrapDir, 0o755); err != nil {
|
|
return fmt.Errorf("mkdir bootstrap dir: %w", err)
|
|
}
|
|
if err := os.WriteFile(scriptPath, []byte(nodeBootstrapScript(universalOpts)), 0o755); err != nil {
|
|
return fmt.Errorf("write universal bootstrap script: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sameFile(a, b string) (bool, error) {
|
|
ai, err := os.Stat(a)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
bi, err := os.Stat(b)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return os.SameFile(ai, bi), 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) + `
|
|
DEFAULT_TARGET=` + shellQuote(opts.Target) + `
|
|
DEFAULT_ARTIFACT_BASE_URL=` + shellQuote(strings.TrimSuffix(opts.ArtifactBaseURL, "/")) + `
|
|
DEFAULT_EDGE_ADDR=` + shellQuote(opts.EdgeAddr) + `
|
|
POSITIONAL_NODE_TOKEN="${1:-}"
|
|
TARGET="$DEFAULT_TARGET"
|
|
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}"
|
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/iop-node-bootstrap.XXXXXX")"
|
|
|
|
detect_target() {
|
|
local os arch
|
|
case "$(uname -s)" in
|
|
Linux*) os="linux" ;;
|
|
Darwin*) os="darwin" ;;
|
|
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
|
*)
|
|
echo "[iop-bootstrap] unsupported OS: $(uname -s)" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) arch="amd64" ;;
|
|
arm64|aarch64) arch="arm64" ;;
|
|
*)
|
|
echo "[iop-bootstrap] unsupported architecture: $(uname -m)" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
|
|
printf '%s-%s\n' "$os" "$arch"
|
|
}
|
|
|
|
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.sh | bash -s TOKEN" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ -z "$TARGET" ]]; then
|
|
TARGET="$(detect_target)"
|
|
fi
|
|
|
|
BIN_FILE="$INSTALL_DIR/iop-node"
|
|
if [[ "$TARGET" == windows-* ]]; then
|
|
BIN_FILE="$INSTALL_DIR/iop-node.exe"
|
|
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"
|
|
`
|
|
}
|