Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: test

on:
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
# bats lives in test/lib as a submodule; lint itself does not need it, but
# checking out submodules keeps every job consistent and cheap.
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Install shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Lint
run: make lint

unit-smoke:
runs-on: ubuntu-latest
steps:
# REQUIRED: bats-core/support/assert are git submodules under test/lib.
# Without submodules: recursive the runner binary is absent and the suite
# cannot start.
- uses: actions/checkout@v7
with:
submodules: recursive
# ubuntu-latest ships GNU bash 5 as the default shell, which is all the
# unit + smoke tiers require. No systemd, no docker, no bats install
# (it is vendored).
- name: Unit + smoke tiers
run: make test-unit test-smoke
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ eggs/
.eggs/
lib/
lib64/
# but not the vendored bats-core test framework (submodules)
!test/lib/
parts/
sdist/
var/
Expand Down
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "test/lib/bats-core"]
path = test/lib/bats-core
url = https://github.com/bats-core/bats-core.git
[submodule "test/lib/bats-support"]
path = test/lib/bats-support
url = https://github.com/bats-core/bats-support.git
[submodule "test/lib/bats-assert"]
path = test/lib/bats-assert
url = https://github.com/bats-core/bats-assert.git
10 changes: 10 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Shared shellcheck config for main.sh and the test suite.
# Picked up automatically by `make lint`, CI, and editor integrations.

# Modern Bash idioms — keep the codebase on the current conventions:
enable=require-double-brackets # prefer [[ ]] over [ ] (SC2292)
enable=deprecate-which # prefer `command -v` over `which`

# `data` (api_call) and `SSM_BIN_NAME` (detect_ssm_service) are globals set for
# other functions / external readers, not unused assignments.
disable=SC2034
35 changes: 35 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# main.sh test harness — see test/README.md for the contract.
# bats-core and its helper libs are vendored as git submodules under test/lib/;
# nothing is installed at runtime. Run `git submodule update --init --recursive`
# after a fresh clone.

BATS := ./test/lib/bats-core/bin/bats
TIERS := test/unit test/smoke

.DEFAULT_GOAL := help

.PHONY: help test test-unit test-smoke lint

help: ## List the available targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "} {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'

test: ## Run every tier (unit + smoke), scoped to the tier dirs
$(BATS) --recursive $(TIERS)

test-unit: ## Run the unit tier (pure functions, no I/O)
$(BATS) --recursive test/unit

test-smoke: ## Run the smoke tier (subprocess CLI-contract)
$(BATS) --recursive test/smoke

SHELL_SOURCES := main.sh test/mocks/bin/* test/helpers/*.bash

lint: ## shellcheck correctness + modern-idiom gate (config in .shellcheckrc)
# General correctness (warnings and up); SC2034 is disabled in .shellcheckrc.
shellcheck --severity=warning main.sh
shellcheck test/mocks/bin/*
shellcheck test/helpers/*.bash
# Idiom gate: enforce [[ ]] over [ ] (SC2292) and command substitution over
# backticks (SC2006) across every shell source, regardless of severity.
shellcheck --include=SC2292,SC2006 $(SHELL_SOURCES)
45 changes: 32 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
- [3.0 Setup](#30-setup)
- [3.1 Environment](#31-environment)
- [3.2 Registration](#32-registration)
- [3.2.1 Get credentials from StackGuardian](#331-get-credentials-from-stackguardian)
- [3.2.2 Run the script for registration](#332-run-the-script-for-registration)
- [3.3 De-registration](#34-de-registration)
- [3.2.1 Get credentials from StackGuardian](#321-get-credentials-from-stackguardian)
- [3.2.2 Run the script for registration](#322-run-the-script-for-registration)
- [3.3 De-registration](#33-de-registration)
- [3.4 Restart](#34-restart)
- [Other options](#other-options)
- [System diagnostics](#system-diagnostics)
- [Managing `cgroupsv2`](#managing-cgroupsv2)
- [Troubleshooting](#troubleshooting)
- [Development](#development)

## 1.0 Introduction

Expand All @@ -20,7 +23,7 @@ registering external (self-hosted) instances to the StackGuardian platform.
Configuration is very simple, get credentials from StackGuardian platform,
and run `main.sh` script with credentials.

Check [Setup](#setup) for more details.
Check [Setup](#30-setup) for more details.

## 2.0 How it works

Expand All @@ -41,7 +44,7 @@ Only, _task definition_ will live on _AWS ECS_.

Setup is very simple. We tried to make it as automated as possible.
All you have to do is run `main.sh` with wanted option that you want to execute:
[Registration](#registration) or [De-registration](#de-registration), and
[Registration](#32-registration) or [De-registration](#33-de-registration), and
provided credentials from _StackGuardian_ platform.

> For more details the `main.sh` script has integrated _help_ menu:
Expand Down Expand Up @@ -114,12 +117,12 @@ But, to achieve similar experience it is enough to [`deregister`](#33-de-registr
We included 2 commands for easier system diagnostics and management.
These should help you keep your system clean and debug in case of errors.

> INFO: Any of following actions keep state in a file at `/tmp/diagnostic.json`.
> INFO: Any of following actions keep state in a file at `/var/lib/sg-runner/diagnostic.json`.

With any command you can provide `--debug` flag.
With this, you will get more output while running commands.

> INFO: All logs are being kept at `/tmp/sg_runner.log`.
> INFO: All logs are being kept at `/var/log/sg_runner.log`.

### Health check

Expand Down Expand Up @@ -158,7 +161,7 @@ export CGROUPSV2_PREVIEW=true
and then

```
./main.sh cgropusv2 disable
./main.sh cgroupsv2 disable
```

> Reboot is required after such action.
Expand All @@ -174,7 +177,23 @@ To revert you can just run:
- StackGuardian uses AWS SSM to setup connection between SG control plane and runners, you can diagnose SSM client using `ssm-cli get-diagnostics --output table`

- If the registration was successful but you can't see Ping Status and IP Address for the Runner on StackGuardian Platform inside the Runner Group's -> Runner Instances tab please re-register runner using the following command:
```bash
./main.sh deregister --sg-node-token "TOKEN" --organization "ORG" --runner-group "RUNNER_GROUP" && \
./main.sh register --sg-node-token "TOKEN" --organization "ORG" --runner-group "RUNNER_GROUP"
```
```bash
./main.sh deregister --sg-node-token "TOKEN" --organization "ORG" --runner-group "RUNNER_GROUP" && \
./main.sh register --sg-node-token "TOKEN" --organization "ORG" --runner-group "RUNNER_GROUP"
```

## Development

`main.sh` has an automated test suite (unit + smoke) built with
[bats-core](https://github.com/bats-core/bats-core), vendored under `test/lib/`
as git submodules — nothing is downloaded at runtime.

```sh
git submodule update --init --recursive # once, after a fresh clone
make test # run the unit + smoke tiers
make lint # shellcheck: correctness + modern-idiom gate
```

See [`test/README.md`](test/README.md) for the test harness contract, the mock
framework, and how to add tests. Every pull request against `main` runs `lint`
and the test suite in CI.
47 changes: 31 additions & 16 deletions main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,25 @@ NO_PROXY="169.254.169.254,169.254.170.2,/var/run/docker.sock"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly LOG_FILE="/var/log/sg_runner.log"
LOG_FILE="${LOG_FILE:=/var/log/sg_runner.log}"
readonly LOG_FILE

# static
# readonly COMMANDS=("jq" "crontab")
readonly COMMANDS=("jq")
readonly CONTAINER_ORCHESTRATORS=("docker")
readonly SG_DOCKER_NETWORK="sg-net"

# Filesystem locations (overridable for testing; default to production paths)
ECS_CONFIG_DIR="${ECS_CONFIG_DIR:=/etc/ecs}"
ECS_LOG_DIR="${ECS_LOG_DIR:=/var/log/ecs}"
ECS_DATA_DIR="${ECS_DATA_DIR:=/var/lib/ecs/data}"
REGISTRATION_DIR="${REGISTRATION_DIR:=/var/log/registration}"
readonly ECS_CONFIG_DIR ECS_LOG_DIR ECS_DATA_DIR REGISTRATION_DIR

# diagnostics
readonly SG_DIAGNOSTIC_DIR="/var/lib/sg-runner"
SG_DIAGNOSTIC_DIR="${SG_DIAGNOSTIC_DIR:=/var/lib/sg-runner}"
readonly SG_DIAGNOSTIC_DIR
readonly SG_DIAGNOSTIC_FILE="${SG_DIAGNOSTIC_DIR}/diagnostic.json"
readonly SG_DIAGNOSTIC_TMP_FILE="${SG_DIAGNOSTIC_DIR}/diagnostic.json.tmp"

Expand Down Expand Up @@ -247,10 +256,10 @@ print_details() { #{{{
#}}}: print_details

save_registration_details() { #{{{
mkdir -p /var/log/registration
mkdir -p "$REGISTRATION_DIR"
print_details
print_details | sed 's/\x1B\[[0-9;]*[JKmsu]//g' \
>>/var/log/registration/"registration_details_$(date +'%Y-%m-%dT%H-%M-%S%z').txt"
>>"$REGISTRATION_DIR/registration_details_$(date +'%Y-%m-%dT%H-%M-%S%z').txt"
}
#}}}: save_registration_details

Expand Down Expand Up @@ -309,7 +318,7 @@ EOF
if [[ -z "$status_code" ]]; then
err "Unknown status code."
exit 1
elif [ "$status_code" != "200" ] && [ "$status_code" != "201" ] && [ "$status_code" != "100" ]; then
elif [[ "$status_code" != "200" && "$status_code" != "201" && "$status_code" != "100" ]]; then
return 1
else
return 0
Expand Down Expand Up @@ -613,8 +622,8 @@ iptables_ensure() { #{{{
#}}}: iptables_ensure

configure_local_data() { #{{{
mkdir -p /var/log/ecs /etc/ecs /var/lib/ecs/data /var/log/registration/
rm -rf /etc/ecs/ecs.config >/dev/null
mkdir -p "$ECS_LOG_DIR" "$ECS_CONFIG_DIR" "$ECS_DATA_DIR" "$REGISTRATION_DIR"
rm -rf "$ECS_CONFIG_DIR/ecs.config" >/dev/null

spinner_wait "Configuring local data.."

Expand All @@ -626,7 +635,7 @@ configure_local_data() { #{{{
ECS_INSTANCE_ATTRIBUTES="{\"sg_organization\": \"${ORGANIZATION_NAME}\",\"sg_runner_id\": \"${RUNNER_ID}\", \"sg_runner_group_id\": \"${RUNNER_GROUP_ID}\"}"
fi

cat >/etc/ecs/ecs.config <<EOF
cat >"$ECS_CONFIG_DIR/ecs.config" <<EOF
ECS_CLUSTER=${ECS_CLUSTER}
AWS_DEFAULT_REGION=${LOCAL_AWS_DEFAULT_REGION}
ECS_INSTANCE_ATTRIBUTES=${ECS_INSTANCE_ATTRIBUTES}
Expand All @@ -647,7 +656,7 @@ ECS_EXTERNAL=true
EOF

if [[ -n "${HTTP_PROXY}" ]]; then
cat >>/etc/ecs/ecs.config <<EOF
cat >>"$ECS_CONFIG_DIR/ecs.config" <<EOF
HTTP_PROXY=${HTTP_PROXY}
HTTPS_PROXY=${HTTP_PROXY}
NO_PROXY=${NO_PROXY}
Expand Down Expand Up @@ -868,9 +877,9 @@ register_instance() { #{{{
deregister_instance() { #{{{
local url

if [[ -e /etc/ecs/ecs.config ]]; then
if [[ -e "$ECS_CONFIG_DIR/ecs.config" ]]; then
local instance_attrs runner_group_id_cfg org_name_cfg
instance_attrs="$(grep ECS_INSTANCE_ATTRIBUTES /etc/ecs/ecs.config | cut -d "=" -f2-)"
instance_attrs="$(grep ECS_INSTANCE_ATTRIBUTES "$ECS_CONFIG_DIR/ecs.config" | cut -d "=" -f2-)"
runner_group_id_cfg="$(echo "$instance_attrs" | jq -r '.sg_runner_group_id')"
org_name_cfg="$(echo "$instance_attrs" | jq -r '.sg_organization')"

Expand Down Expand Up @@ -1046,6 +1055,8 @@ validate_runner_id() { #{{{
#}}}: validate_runner_id

is_root() { #{{{
# SG_SKIP_ROOT_CHECK bypasses the root requirement (for testing only).
[[ "${SG_SKIP_ROOT_CHECK:-}" == "true" ]] && return 0
if (($(id -u) != 0)); then
die "This script must be run as" "root"
fi
Expand Down Expand Up @@ -1172,12 +1183,12 @@ preflight() { #{{{
# Probe IMDSv2 (AWS); silently no-ops elsewhere.
local imdsv2_token attached_iam_role
imdsv2_token=$(curl --max-time 5 -fSsLkX PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 120" 2>/dev/null)
if [ -n "$imdsv2_token" ]; then
if [[ -n "$imdsv2_token" ]]; then
attached_iam_role=$(curl --max-time 10 -fSsLk --proto "https" -H "X-aws-ec2-metadata-token: $imdsv2_token" "http://169.254.169.254/latest/meta-data/iam/security-credentials/" 2>/dev/null)
else
attached_iam_role=$(curl --max-time 10 -fSsLk "http://169.254.169.254/latest/meta-data/iam/security-credentials/" 2>/dev/null)
fi
if [ -n "$attached_iam_role" ]; then
if [[ -n "$attached_iam_role" ]]; then
debug "Response:" "$attached_iam_role"
info "Found an IAM role attached to the instance" "$attached_iam_role"
fi
Expand Down Expand Up @@ -1248,7 +1259,11 @@ main() { #{{{

#}}}: Preflight + main

trap cleanup SIGINT
trap exit_help EXIT
# Only wire traps and run main when executed directly. When sourced (e.g. by a
# test harness), expose the functions without side effects.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
trap cleanup SIGINT
trap exit_help EXIT

main "$@"
main "$@"
fi
Loading