Files
certctl/install-agent.sh
T
shankar0123 bc6039a79e chore: sweep github.com/shankar0123/certctl URL refs to certctl-io/certctl
Post-transfer cosmetic + release-critical URL refresh after moving the
repo from github.com/shankar0123/certctl to github.com/certctl-io/certctl
(2026-05-03). GitHub HTTP redirects continue to forward old URLs forever,
so existing operators are not broken — but aligns the canonical
references with the new owner so:

- procurement engineers / contributors browsing the docs see the right
  URL on first read
- operators copying the agent install one-liner hit the new path
  directly without going through a redirect
- the Helm chart's default image repository points at the canonical org
  registry path
- the OnboardingWizard rendered to first-run UI users shows the new
  URL in the install snippets and doc anchor links
- the GitHub Actions release workflow pushes container images to
  ghcr.io/certctl-io/certctl-{server,agent} (was: shankar0123)
- the release-notes Markdown body in release.yml — which gets stamped
  into every future release page — references the post-transfer
  cert-identity (cosign keyless signing now uses the certctl-io
  workflow URL) and the post-transfer SLSA provenance source-uri.
  Without this, every cosign verify / slsa-verifier command on a
  v2.1.0+ release would fail because the cert-identity-regexp would
  not match the signing identity GitHub Actions OIDC issues post-
  transfer. Old releases (v2.0.67 and earlier) keep their immutable
  release-notes pointing at the shankar0123 path and remain
  verifiable via their own published instructions.

Customer impact:
- Operators on ghcr.io/shankar0123/certctl-{server,agent}:latest
  silently freeze on whatever tag was current at transfer time. They
  get no errors; they just stop receiving updates. The next release
  notes need a one-line callout (Phase 3.1 of cowork/transfer-
  certctl-to-org.md) telling them to update their image path to
  ghcr.io/certctl-io/certctl-{server,agent}.
- All other URLs (git clone, install one-liner, raw.githubusercontent
  URLs, browser links, GitHub API) continue to resolve via permanent
  HTTP redirects. The sweep is cosmetic for those.

Files swept (30 total):
  .github/workflows/release.yml — IMAGE_NAMESPACE, source-uri,
    cosign cert-identity-regexp, IMAGE= snippet (5 refs total).
  CHANGELOG.md, README.md — anchor links, badges, install one-liner,
    cosign verify snippets in operator-facing sections.
  api/openapi.yaml — info / externalDocs URLs.
  install-agent.sh — GITHUB_REPO const + systemd unit Documentation=
    field.
  deploy/ENVIRONMENTS.md, deploy/helm/{CHART_SUMMARY,INDEX,
    INSTALLATION,README}.md, deploy/helm/certctl/{Chart.yaml,
    README.md,values.yaml}, deploy/helm/examples/values-*.yaml —
    chart docs + image repository defaults across dev / prod-ha
    overrides.
  docs/{certctl-for-cert-manager-users,connector-iis,connectors,
    migrate-from-acmesh,migrate-from-certbot,quickstart,test-env,
    why-certctl}.md — operator-facing doc URLs.
  examples/{acme-nginx,acme-wildcard-dns01,multi-issuer,
    private-ca-traefik,step-ca-haproxy}/docker-compose.yml +
    examples/step-ca-haproxy/step-ca-haproxy.md — example image:
    paths and accompanying narrative.
  web/src/pages/OnboardingWizard.tsx — first-run-UI URL refs (curl
    install one-liners, agent docker image path, doc anchor links).

Files intentionally NOT swept (Choice A from cowork/transfer-certctl-
to-org.md):
  go.mod, go.sum — module declaration stays github.com/shankar0123/
    certctl. Existing imports compile because Go uses the path
    declared in go.mod, not the URL it was fetched from. Internal-
    only project; no external Go consumers; rename will land as a
    mechanical sed when one materializes.
  ~250 *.go files — every import remains github.com/shankar0123/
    certctl/internal/...
  deploy/test/f5-mock-icontrol/go.mod — separate test sub-module;
    same Choice A logic; module path stays.

Files intentionally NOT swept (other reasons):
  README.md lines 244-245 — Scarf-pixel docker-pull commands.
    shankar0123.docker.scarf.sh/... is a Scarf-account hostname
    (per-user, not per-repo) and the pixel keeps tracking pulls
    against the operator's personal Scarf account. Migrating to a
    certctl-io Scarf account is a separate decision (create org
    Scarf account → re-create package → update README).
  deploy/test/f5-mock-icontrol/f5-mock-icontrol — checked-in
    compiled binary with shankar0123/certctl baked into Go build
    info via the sub-module path. Out of scope for a URL sweep;
    will refresh on the next `make test-integration` rebuild.

Verification:
  gofmt: clean (no .go files touched).
  go vet ./...: clean (verified at this SHA in 1.3 of the transfer
    checklist; no .go changes since).
  go build ./...: clean (same).
  go test -short on representative packages: green (same).
  Diff shape: 30 files, 74 insertions / 74 deletions, net-zero size,
    pure URL substitution.
2026-05-03 23:39:50 +00:00

631 lines
20 KiB
Bash

#!/bin/bash
# certctl Agent Install Script
# Detects OS (Linux/macOS) and architecture, downloads binary from GitHub Releases,
# installs to system path, configures service (systemd/launchd), and prompts for config.
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
GITHUB_REPO="certctl-io/certctl"
RELEASE_URL="https://github.com/${GITHUB_REPO}/releases/latest/download"
INSTALL_DIR="/usr/local/bin"
SERVICE_NAME="certctl-agent"
# Detect OS and architecture
detect_platform() {
local os="$(uname -s)"
local arch="$(uname -m)"
case "$os" in
Linux*)
OS_TYPE="linux"
;;
Darwin*)
OS_TYPE="darwin"
;;
*)
echo -e "${RED}Error: Unsupported OS: $os${NC}"
exit 1
;;
esac
case "$arch" in
x86_64)
ARCH_TYPE="amd64"
;;
aarch64|arm64)
ARCH_TYPE="arm64"
;;
*)
echo -e "${RED}Error: Unsupported architecture: $arch${NC}"
exit 1
;;
esac
}
# Print usage information
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
Install and configure the certctl agent on your system.
OPTIONS:
-h, --help Show this help message
--server-url URL Set CERTCTL_SERVER_URL (skips interactive prompt)
--api-key KEY Set CERTCTL_API_KEY (skips interactive prompt)
--agent-id ID Set CERTCTL_AGENT_ID (defaults to hostname)
--no-start Install but don't start the service
EXAMPLES:
# Interactive install (download first):
curl -sSLO https://raw.githubusercontent.com/${GITHUB_REPO}/master/install-agent.sh
chmod +x install-agent.sh
sudo ./install-agent.sh
# Non-interactive install (pipe via curl):
curl -sSL https://raw.githubusercontent.com/${GITHUB_REPO}/master/install-agent.sh \\
| sudo bash -s -- \\
--server-url https://certctl.example.com \\
--api-key YOUR_API_KEY
CONTROL-PLANE TLS TRUST:
The certctl server is HTTPS-only as of v2.2. This installer does NOT copy a CA
bundle — the generated agent.env leaves TLS trust to the system root store by
default. If the server uses a private/enterprise or self-signed CA, set
CERTCTL_SERVER_CA_BUNDLE_PATH in the generated agent.env to point at the CA
bundle, or (dev only) CERTCTL_SERVER_TLS_INSECURE_SKIP_VERIFY=true. See the
commented block in the generated agent.env for the full menu.
EOF
}
# Parse command-line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
--server-url)
SERVER_URL="${2:-}"
if [[ -z "$SERVER_URL" ]]; then
echo -e "${RED}Error: --server-url requires a value${NC}" >&2
exit 1
fi
shift 2
;;
--server-url=*)
SERVER_URL="${1#*=}"
shift
;;
--api-key)
API_KEY="${2:-}"
if [[ -z "$API_KEY" ]]; then
echo -e "${RED}Error: --api-key requires a value${NC}" >&2
exit 1
fi
shift 2
;;
--api-key=*)
API_KEY="${1#*=}"
shift
;;
--agent-id)
AGENT_ID="${2:-}"
if [[ -z "$AGENT_ID" ]]; then
echo -e "${RED}Error: --agent-id requires a value${NC}" >&2
exit 1
fi
shift 2
;;
--agent-id=*)
AGENT_ID="${1#*=}"
shift
;;
--no-start)
NO_START=true
shift
;;
*)
echo -e "${RED}Error: Unknown option: $1${NC}" >&2
usage
exit 1
;;
esac
done
}
# Ensure stdin is interactive before prompting. When the script is piped via
# curl|bash, stdin is the pipe from curl, so `read` hits EOF immediately and
# set -e aborts the script silently. Reopen stdin from the controlling terminal
# (/dev/tty) if available; otherwise print a helpful error pointing at the
# flag-based non-interactive install.
ensure_interactive_input() {
# If all required config is already provided via flags, no prompting needed.
if [[ -n "${SERVER_URL:-}" && -n "${API_KEY:-}" ]]; then
return
fi
# Already interactive — nothing to do.
if [[ -t 0 ]]; then
return
fi
# Piped stdin — try to reopen from the controlling terminal. Actually
# attempt to open /dev/tty inside a subshell: the device node may exist
# even when the process has no controlling terminal (ENXIO on open), so
# `[[ -r /dev/tty ]]` is not reliable.
if ( exec </dev/tty ) 2>/dev/null; then
exec </dev/tty
return
fi
# No terminal available — emit clear guidance and exit.
# Use printf '%b' so the ANSI color escapes in $RED/$NC are interpreted
# rather than rendered as literal backslash sequences (a heredoc would
# keep them as raw text).
{
printf '%b\n' "${RED}Error: No interactive terminal available.${NC}"
printf '\n'
printf 'The installer was piped through curl and no controlling terminal (/dev/tty)\n'
printf 'is available for prompts. Pass the required values as flags instead:\n'
printf '\n'
printf ' curl -sSL https://raw.githubusercontent.com/%s/master/install-agent.sh \\\n' "$GITHUB_REPO"
printf ' | sudo bash -s -- \\\n'
printf ' --server-url https://certctl.example.com \\\n'
printf ' --api-key YOUR_API_KEY\n'
printf '\n'
printf 'Or download the script first and run it directly:\n'
printf '\n'
printf ' curl -sSLO https://raw.githubusercontent.com/%s/master/install-agent.sh\n' "$GITHUB_REPO"
printf ' chmod +x install-agent.sh\n'
printf ' sudo ./install-agent.sh\n'
printf '\n'
} >&2
exit 1
}
# Check if running as root/sudo on Linux
check_privileges() {
if [[ "$OS_TYPE" == "linux" && "$EUID" -ne 0 ]]; then
echo -e "${RED}Error: This script must be run as root on Linux. Try: sudo $0${NC}"
exit 1
fi
}
# Download agent binary from GitHub Releases
# IMPORTANT: main() captures this function's stdout via `binary_path=$(download_binary)`,
# so every status/error message MUST go to stderr (>&2). Only the final
# `echo "$temp_file"` is allowed on stdout — that's the return value.
#
# We deliberately do NOT register an EXIT trap to clean up $temp_file: because
# of the command substitution, this function runs in a subshell, and any EXIT
# trap set here fires when the subshell exits — which is *before* install_binary
# gets a chance to cp the file. Cleanup on success is install_binary's job
# (after the cp), and cleanup on curl failure is handled inline below.
download_binary() {
local binary_name="certctl-agent-${OS_TYPE}-${ARCH_TYPE}"
local download_url="${RELEASE_URL}/${binary_name}"
echo -e "${YELLOW}Downloading certctl agent (${OS_TYPE}-${ARCH_TYPE})...${NC}" >&2
if ! command -v curl &> /dev/null; then
echo -e "${RED}Error: curl is required but not installed${NC}" >&2
exit 1
fi
local temp_file
temp_file=$(mktemp)
if ! curl -sSL -f "$download_url" -o "$temp_file" >&2; then
rm -f "$temp_file"
echo -e "${RED}Error: Failed to download binary from $download_url${NC}" >&2
echo "Make sure the latest release exists on GitHub with the binary asset for ${OS_TYPE}-${ARCH_TYPE}." >&2
exit 1
fi
chmod +x "$temp_file"
echo "$temp_file"
}
# Install binary to system path
install_binary() {
local binary_path="$1"
echo -e "${YELLOW}Installing to $INSTALL_DIR/$SERVICE_NAME...${NC}"
if [[ "$OS_TYPE" == "linux" ]]; then
cp "$binary_path" "$INSTALL_DIR/$SERVICE_NAME"
else
# macOS: use sudo if not already running as root
if [[ "$EUID" -ne 0 ]]; then
sudo cp "$binary_path" "$INSTALL_DIR/$SERVICE_NAME"
else
cp "$binary_path" "$INSTALL_DIR/$SERVICE_NAME"
fi
fi
chmod +x "$INSTALL_DIR/$SERVICE_NAME"
echo -e "${GREEN}Binary installed: $INSTALL_DIR/$SERVICE_NAME${NC}"
# Clean up the temp file created by download_binary. We can't use an EXIT
# trap inside download_binary because it runs in a subshell (command
# substitution), so the trap would fire before we got here. Doing it
# explicitly after the successful cp is the simplest correct pattern.
rm -f "$binary_path"
}
# Prompt for configuration. Any value supplied via flag is honored as-is
# and we only prompt for the missing pieces. `read || true` prevents set -e
# from aborting the script on EOF — instead the empty check below fires the
# proper "required" error message.
prompt_for_config() {
if [[ -z "${SERVER_URL:-}" ]]; then
echo ""
echo -e "${YELLOW}Enter certctl server URL (e.g., https://certctl.example.com):${NC}"
read -r SERVER_URL || true
if [[ -z "${SERVER_URL:-}" ]]; then
echo -e "${RED}Error: Server URL is required${NC}" >&2
echo "Hint: pass --server-url <URL> to run non-interactively." >&2
exit 1
fi
fi
if [[ -z "${API_KEY:-}" ]]; then
echo -e "${YELLOW}Enter certctl API key:${NC}"
read -rs API_KEY || true
echo ""
if [[ -z "${API_KEY:-}" ]]; then
echo -e "${RED}Error: API key is required${NC}" >&2
echo "Hint: pass --api-key <KEY> to run non-interactively." >&2
exit 1
fi
fi
if [[ -z "${AGENT_ID:-}" ]]; then
local default_agent_id
default_agent_id="$(hostname)"
# If stdin is still piped (no /dev/tty was available but SERVER_URL +
# API_KEY arrived via flags), skip the prompt entirely and use the
# default — no need to block on an optional value.
if [[ -t 0 ]]; then
echo -e "${YELLOW}Enter agent ID (default: $default_agent_id):${NC}"
read -r AGENT_ID || true
fi
if [[ -z "${AGENT_ID:-}" ]]; then
AGENT_ID="$default_agent_id"
fi
fi
}
# Create configuration directory and env file (Linux)
setup_linux_config() {
local config_dir="/etc/certctl"
local config_file="$config_dir/agent.env"
local key_dir="/var/lib/certctl/keys"
echo -e "${YELLOW}Creating configuration directory...${NC}"
# Create /etc/certctl with restrictive permissions
mkdir -p "$config_dir"
chmod 755 "$config_dir"
# Create key storage directory with 0700 permissions
mkdir -p "$key_dir"
chmod 700 "$key_dir"
# Write agent configuration (overwrite if exists)
cat > "$config_file" <<EOF
# certctl Agent Configuration
# Generated by install-agent.sh on $(date)
# Agent ID (unique identifier in the fleet)
CERTCTL_AGENT_ID=$AGENT_ID
# Control plane server URL (HTTPS-only as of v2.2)
CERTCTL_SERVER_URL=$SERVER_URL
# API authentication key
CERTCTL_API_KEY=$API_KEY
# Key generation mode (agent = agent-side keygen, server = server-side for demo only)
CERTCTL_KEYGEN_MODE=agent
# Key storage directory (agent-side keygen)
CERTCTL_KEY_DIR=$key_dir
# ---- Control-plane TLS trust ----
# The certctl server is HTTPS-only (v2.2+). The agent's HTTP client MUST trust the
# server's certificate chain. Pick ONE of the approaches below:
#
# 1) Public CA (Let's Encrypt, DigiCert, etc.) — no config needed; system trust store works.
# 2) Private / enterprise CA — point the agent at the CA bundle that signed the server cert:
# CERTCTL_SERVER_CA_BUNDLE_PATH=/etc/certctl/server-ca.crt
#
# 3) Self-signed server cert (Helm/compose bootstrap) — same env var, just point at the
# extracted self-signed CA bundle (e.g. from the certctl-server-tls Kubernetes secret
# via: kubectl get secret certctl-server-tls -o jsonpath='{.data.ca\.crt}' | base64 -d).
#
# 4) Dev/eval only — disable verification entirely (NEVER do this in production):
# CERTCTL_SERVER_TLS_INSECURE_SKIP_VERIFY=true
# Logging level (debug, info, warn, error)
# CERTCTL_LOG_LEVEL=info
# Discovery directories (comma-separated paths to scan for existing certs)
# CERTCTL_DISCOVERY_DIRS=/etc/letsencrypt/live,/etc/ssl/certs
# Enable deployment verification (TLS endpoint check post-deployment)
# CERTCTL_VERIFY_DEPLOYMENT=true
EOF
# Restrict permissions on env file (contains API key)
chmod 600 "$config_file"
echo -e "${GREEN}Configuration written to: $config_file${NC}"
}
# Create configuration directory and env file (macOS)
setup_macos_config() {
local config_dir="$HOME/.certctl"
local config_file="$config_dir/agent.env"
local key_dir="$config_dir/keys"
echo -e "${YELLOW}Creating configuration directory...${NC}"
# Create ~/.certctl with restrictive permissions
mkdir -p "$config_dir"
chmod 700 "$config_dir"
# Create key storage directory
mkdir -p "$key_dir"
chmod 700 "$key_dir"
# Write agent configuration (overwrite if exists)
cat > "$config_file" <<EOF
# certctl Agent Configuration
# Generated by install-agent.sh on $(date)
# Agent ID (unique identifier in the fleet)
CERTCTL_AGENT_ID=$AGENT_ID
# Control plane server URL (HTTPS-only as of v2.2)
CERTCTL_SERVER_URL=$SERVER_URL
# API authentication key
CERTCTL_API_KEY=$API_KEY
# Key generation mode (agent = agent-side keygen, server = server-side for demo only)
CERTCTL_KEYGEN_MODE=agent
# Key storage directory (agent-side keygen)
CERTCTL_KEY_DIR=$key_dir
# ---- Control-plane TLS trust ----
# The certctl server is HTTPS-only (v2.2+). The agent's HTTP client MUST trust the
# server's certificate chain. Pick ONE of the approaches below:
#
# 1) Public CA (Let's Encrypt, DigiCert, etc.) — no config needed; system trust store works.
# 2) Private / enterprise CA — point the agent at the CA bundle that signed the server cert:
# CERTCTL_SERVER_CA_BUNDLE_PATH=$HOME/.certctl/server-ca.crt
#
# 3) Self-signed server cert (Helm/compose bootstrap) — same env var, just point at the
# extracted self-signed CA bundle (e.g. from the certctl-server-tls Kubernetes secret
# via: kubectl get secret certctl-server-tls -o jsonpath='{.data.ca\.crt}' | base64 -d).
#
# 4) Dev/eval only — disable verification entirely (NEVER do this in production):
# CERTCTL_SERVER_TLS_INSECURE_SKIP_VERIFY=true
# Logging level (debug, info, warn, error)
# CERTCTL_LOG_LEVEL=info
# Discovery directories (comma-separated paths to scan for existing certs)
# CERTCTL_DISCOVERY_DIRS=/etc/letsencrypt/live,/etc/ssl/certs
# Enable deployment verification (TLS endpoint check post-deployment)
# CERTCTL_VERIFY_DEPLOYMENT=true
EOF
# Restrict permissions on env file (contains API key)
chmod 600 "$config_file"
echo -e "${GREEN}Configuration written to: $config_file${NC}"
}
# Create and enable systemd service (Linux only)
setup_systemd_service() {
local service_file="/etc/systemd/system/${SERVICE_NAME}.service"
echo -e "${YELLOW}Creating systemd service file...${NC}"
cat > "$service_file" <<'EOF'
[Unit]
Description=certctl Agent - Certificate Lifecycle Management
Documentation=https://github.com/certctl-io/certctl
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal
# Load environment from /etc/certctl/agent.env
EnvironmentFile=/etc/certctl/agent.env
# Command to start the agent
ExecStart=/usr/local/bin/certctl-agent
[Install]
WantedBy=multi-user.target
EOF
chmod 644 "$service_file"
echo -e "${GREEN}Service file created: $service_file${NC}"
# Reload systemd daemon
systemctl daemon-reload
}
# Create and enable launchd plist (macOS only)
setup_launchd_service() {
local plist_file="$HOME/Library/LaunchAgents/com.certctl.agent.plist"
local config_file="$HOME/.certctl/agent.env"
local launcher_script="$HOME/.certctl/launcher.sh"
local home_dir="$HOME"
echo -e "${YELLOW}Creating launchd service file...${NC}"
mkdir -p "$(dirname "$plist_file")"
# Create wrapper script that sources env file before executing agent
cat > "$launcher_script" <<'LAUNCHER_SCRIPT'
#!/bin/bash
set -a
source "$HOME/.certctl/agent.env"
set +a
exec /usr/local/bin/certctl-agent
LAUNCHER_SCRIPT
chmod 755 "$launcher_script"
# Create plist that references the launcher script
cat > "$plist_file" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.certctl.agent</string>
<key>ProgramArguments</key>
<array>
<string>$home_dir/.certctl/launcher.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>$home_dir</string>
</dict>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>$home_dir/.certctl/agent.log</string>
<key>StandardOutPath</key>
<string>$home_dir/.certctl/agent.log</string>
</dict>
</plist>
EOF
chmod 644 "$plist_file"
echo -e "${GREEN}Service file created: $plist_file${NC}"
echo -e "${GREEN}Launcher script created: $launcher_script${NC}"
}
# Start the agent service
start_service() {
if [[ "${NO_START:-false}" == "true" ]]; then
echo -e "${YELLOW}Service not started (--no-start flag used)${NC}"
return
fi
echo -e "${YELLOW}Starting certctl agent service...${NC}"
if [[ "$OS_TYPE" == "linux" ]]; then
systemctl enable "$SERVICE_NAME"
systemctl start "$SERVICE_NAME"
sleep 2
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo -e "${GREEN}Service started successfully${NC}"
else
echo -e "${RED}Warning: Service may not have started. Check logs with: systemctl status $SERVICE_NAME${NC}"
fi
else
# macOS: load launchd service for current user
launchctl load "$HOME/Library/LaunchAgents/com.certctl.agent.plist" 2>/dev/null || true
sleep 1
echo -e "${GREEN}Service loaded into launchd${NC}"
fi
}
# Print success message with next steps
print_summary() {
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}certctl Agent Installation Complete${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "Configuration:"
if [[ "$OS_TYPE" == "linux" ]]; then
echo " Config file: /etc/certctl/agent.env"
echo " Key storage: /var/lib/certctl/keys"
echo " Service: /etc/systemd/system/${SERVICE_NAME}.service"
echo " View logs: journalctl -u ${SERVICE_NAME} -f"
else
echo " Config file: $HOME/.certctl/agent.env"
echo " Key storage: $HOME/.certctl/keys"
echo " Service: $HOME/Library/LaunchAgents/com.certctl.agent.plist"
echo " View logs: tail -f $HOME/.certctl/agent.log"
fi
echo ""
echo "Next steps:"
echo " 1. Verify the service is running"
if [[ "$OS_TYPE" == "linux" ]]; then
echo " systemctl status ${SERVICE_NAME}"
else
echo " launchctl list | grep certctl"
fi
echo ""
echo " 2. Visit your certctl dashboard: $SERVER_URL"
echo " 3. The agent should appear in the fleet overview within 30 seconds"
echo ""
}
# Main installation flow
main() {
parse_args "$@"
detect_platform
check_privileges
echo -e "${GREEN}certctl Agent Installer${NC}"
echo "Detected platform: ${OS_TYPE}-${ARCH_TYPE}"
echo ""
ensure_interactive_input
prompt_for_config
# Download and install binary
local binary_path
binary_path=$(download_binary)
install_binary "$binary_path"
# Setup OS-specific configuration
if [[ "$OS_TYPE" == "linux" ]]; then
setup_linux_config
setup_systemd_service
else
setup_macos_config
setup_launchd_service
fi
# Start the service
start_service
# Print summary
print_summary
}
main "$@"