pax_global_header 0000666 0000000 0000000 00000000064 15177357701 0014527 g ustar 00root root 0000000 0000000 52 comment=731bc47166e220cbe9972caad32b21d7ab173223
ansible-receptor-0f6ae46/ 0000775 0000000 0000000 00000000000 15177357701 0015402 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/.dockerignore 0000664 0000000 0000000 00000000363 15177357701 0020060 0 ustar 00root root 0000000 0000000 .idea
receptor
receptor.exe
receptor.app
recepcert
net
receptorctl-test-venv/
.container-flag*
.VERSION
kubectl
/receptorctl/AUTHORS
/receptorctl/ChangeLog
/receptor-python-worker/ChangeLog
/receptor-python-worker/AUTHORS
.vagrant/
Dockerfile
ansible-receptor-0f6ae46/.github/ 0000775 0000000 0000000 00000000000 15177357701 0016742 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/.github/dependabot.yml 0000664 0000000 0000000 00000001501 15177357701 0021567 0 ustar 00root root 0000000 0000000 ---
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
- "go"
ignore:
- dependency-name: "golang"
versions: ["1.24", "1.25"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
- "github-actions"
- package-ecosystem: "pip"
directory: "/receptor-python-worker"
groups:
dependencies:
patterns:
- "*"
schedule:
interval: "daily"
labels:
- "dependencies"
- "pip"
- package-ecosystem: "pip"
directory: "/receptorctl"
groups:
dependencies:
patterns:
- "*"
schedule:
interval: "daily"
labels:
- "dependencies"
- "pip"
ansible-receptor-0f6ae46/.github/issue_labeler.yml 0000664 0000000 0000000 00000000037 15177357701 0022303 0 ustar 00root root 0000000 0000000 ---
needs_triage:
- '.*'
...
ansible-receptor-0f6ae46/.github/workflows/ 0000775 0000000 0000000 00000000000 15177357701 0020777 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/.github/workflows/artifact-k8s-logs.sh 0000775 0000000 0000000 00000000507 15177357701 0024602 0 ustar 00root root 0000000 0000000 #!/bin/bash
PODS_DIR=/tmp/receptor-testing/K8sPods
mkdir "$PODS_DIR"
PODS="$(kubectl get pods --template '{{range.items}}{{.metadata.name}}{{"\n"}}{{end}}')"
for pod in $PODS ; do
mkdir "$PODS_DIR/$pod"
kubectl get pod "$pod" --output=json > "$PODS_DIR/$pod/pod"
kubectl logs "$pod" > "$PODS_DIR/$pod/logs"
done
ansible-receptor-0f6ae46/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000005367 15177357701 0024625 0 ustar 00root root 0000000 0000000 ---
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on: # yamllint disable-line rule:truthy
push:
branches: ["devel", release_*]
pull_request:
# The branches below must be a subset of the branches above
branches: ["devel"]
schedule:
- cron: '18 2 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['go', 'python']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
ansible-receptor-0f6ae46/.github/workflows/coverage_reporting.yml 0000664 0000000 0000000 00000005707 15177357701 0025417 0 ustar 00root root 0000000 0000000 ---
name: Codecov
on: # yamllint disable-line rule:truthy
pull_request: # yamllint disable-line rule:empty-values
push:
branches: [devel]
env:
DESIRED_GO_VERSION: '1.24'
DESIRED_PYTHON_VERSION: '3.12'
jobs:
go_test_coverage:
name: go test coverage
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.DESIRED_GO_VERSION }}
- name: build and install receptor
run: |
make build-all
sudo cp ./receptor /usr/local/bin/receptor
- name: Download kind binary
run: curl --proto '=https' --tlsv1.2 -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 && chmod +x ./kind
- name: Create k8s cluster
run: ./kind create cluster --wait 60s
- name: Wait for nodes to be ready
run: kubectl wait --for=condition=Ready nodes --all --timeout=60s
- name: Interact with the cluster
run: kubectl get nodes
- name: Run receptor tests with coverage
run: make coverage
- name: Set up nox
uses: wntrblm/nox@2026.04.10
with:
python-versions: ${{ env.DESIRED_PYTHON_VERSION }}
- name: Provision nox environment for coverage
run: nox --install-only --session coverage
working-directory: ./receptorctl
- name: Run `receptorctl` nox coverage session
run: nox --no-install --session coverage
working-directory: ./receptorctl
- name: SonarCube Static Scans (on push)
uses: SonarSource/sonarqube-scan-action@v7
if: github.event_name == 'push' && github.repository == 'ansible/receptor'
env:
SONAR_TOKEN: ${{ secrets[format('{0}', vars.SONAR_TOKEN_SECRET_NAME)] }}
with:
args: >
-Dsonar.go.coverage.reportPaths=coverage.txt
-Dsonar.python.coverage.reportPaths=receptorctl/receptorctl_coverage.xml
- name: Upload Code Coverage Report from Receptor Unit Tests
uses: actions/upload-artifact@v7
with:
name: receptor-coverage-report
path: coverage.txt
- name: Save off PR Number
run: echo "PR ${{ github.event.number }}" > pr_number.txt
- name: Upload PR Number
uses: actions/upload-artifact@v7
with:
name: pr_number
path: pr_number.txt
- name: get k8s logs
if: ${{ failure() }}
run: .github/workflows/artifact-k8s-logs.sh
- name: Archive receptor binary
uses: actions/upload-artifact@v7
with:
name: receptor
path: /usr/local/bin/receptor
- name: Upload Code Coverage Report from Receptorctl Unit Tests
uses: actions/upload-artifact@v7
with:
name: receptorctl-coverage-report
path: receptorctl/receptorctl_coverage.xml
ansible-receptor-0f6ae46/.github/workflows/dependency_review.yml 0000664 0000000 0000000 00000000526 15177357701 0025224 0 ustar 00root root 0000000 0000000 ---
name: 'Dependency Review'
on: [pull_request] # yamllint disable-line rule:truthy
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v6
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
...
ansible-receptor-0f6ae46/.github/workflows/devel_image.yml 0000664 0000000 0000000 00000003041 15177357701 0023761 0 ustar 00root root 0000000 0000000 ---
name: Publish devel image
on: # yamllint disable-line rule:truthy
push:
branches: [devel]
jobs:
release:
runs-on: ubuntu-latest
name: Push devel image
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
# setup qemu and buildx
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Install build dependencies
run: |
pip install build
# we will first build the image for x86 and load it on the host for testing
- name: Build Image
run: |
export CONTAINERCMD="docker buildx"
export EXTRA_OPTS="--platform linux/amd64 --load"
make container REPO=quay.io/${{ github.repository }} TAG=devel
- name: Test Image
run: docker run --rm quay.io/${{ github.repository }}:devel receptor --version
- name: Login To Quay
uses: docker/login-action@v4
with:
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_TOKEN }}
registry: quay.io/${{ github.repository }}
# Since x86 image is built in previous step
# buildx will use cached image, hence overall time will not be affected
- name: Build Multiarch Image & Push To Quay
run: |
export CONTAINERCMD="docker buildx"
export EXTRA_OPTS="--platform linux/amd64,linux/ppc64le,linux/arm64 --push"
make container REPO=quay.io/${{ github.repository }} TAG=devel
ansible-receptor-0f6ae46/.github/workflows/devel_whl.yml 0000664 0000000 0000000 00000001514 15177357701 0023474 0 ustar 00root root 0000000 0000000 ---
name: Publish nightly wheel
on: # yamllint disable-line rule:truthy
push:
branches: [devel]
jobs:
sdist:
runs-on: ubuntu-latest
name: Build wheel
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install build dependencies
run: |
pip install build
- name: Build wheel
run: |
make clean receptorctl_wheel
- name: Upload wheel
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
pip install boto3
ansible -i localhost, -c local all -m aws_s3 \
-a "bucket=receptor-nightlies object=receptorctl/receptorctl-0.0.0-py3-none-any.whl src=$(ls receptorctl/dist/*.whl | head -n 1) mode=put"
ansible-receptor-0f6ae46/.github/workflows/promote.yml 0000664 0000000 0000000 00000006736 15177357701 0023223 0 ustar 00root root 0000000 0000000 ---
name: Promote Release
on: # yamllint disable-line rule:truthy
release:
types: [published]
jobs:
promote:
runs-on: ubuntu-latest
env:
TAG: ${{github.event.release.tag_name}}
steps:
- name: Checkout Receptor
uses: actions/checkout@v6
- name: Log in to GHCR
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io
- name: Log in to Quay
uses: docker/login-action@v4
with:
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_TOKEN }}
registry: quay.io
- name: Copy Image to Quay
uses: akhilerm/tag-push-action@v2.2.0
with:
src: ghcr.io/${{ github.repository }}:${{env.TAG}}
dst: |
quay.io/${{ github.repository }}:${{env.TAG}}
quay.io/${{ github.repository }}:latest
- name: Check if floating tag is needed
run: |
if [[ $TAG == *"dev"* ]];
then
echo "FLOATING_TAG=$(echo $TAG | sed 's/[0-9]\+$//')" >> $GITHUB_ENV
else
echo "FLOATING_TAG=$TAG" >> $GITHUB_ENV
fi
- name: Push floating tag to Quay
uses: akhilerm/tag-push-action@v2.2.0
with:
src: ghcr.io/${{ github.repository }}:${{env.TAG}}
dst: quay.io/${{ github.repository }}:${{env.FLOATING_TAG}}
- name: Install python
uses: actions/setup-python@v6
- name: Install dependencies
run: |
python3 -m pip install twine build
- name: Set official pypi info
run: echo pypi_repo=pypi >> $GITHUB_ENV
if: ${{ github.repository_owner == 'ansible' }}
- name: Set unofficial pypi info
run: echo pypi_repo=testpypi >> $GITHUB_ENV
if: ${{ github.repository_owner != 'ansible' }}
- name: Set receptor pypi version
run: echo RECEPTORCTL_PYPI_VERSION=$(curl --silent https://pypi.org/pypi/receptorctl/json | jq --raw-output '"v" + .info.version') >> $GITHUB_ENV
- name: Build receptorctl and upload to pypi
if: ${{ env.RECEPTORCTL_PYPI_VERSION != env.TAG }}
env:
PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
make receptorctl_wheel receptorctl_sdist VERSION=$TAG
twine upload \
-r ${{ env.pypi_repo }} \
-u "$PYPI_USERNAME" \
-p "$PYPI_PASSWORD" \
receptorctl/dist/*
publish:
runs-on: ubuntu-latest
permissions:
contents: write
env:
VERSION: ${{github.event.release.tag_name}}
steps:
- name: Checkout Receptor
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Build packages
run: |
make build-package GOOS=linux GOARCH=amd64 BINNAME=receptor
make build-package GOOS=linux GOARCH=arm64 BINNAME=receptor
make build-package GOOS=darwin GOARCH=amd64 BINNAME=receptor
make build-package GOOS=darwin GOARCH=arm64 BINNAME=receptor
make build-package GOOS=windows GOARCH=amd64 BINNAME=receptor.exe
make build-package GOOS=windows GOARCH=arm64 BINNAME=receptor.exe
- name: Publish packages
uses: softprops/action-gh-release@v3
with:
files: |-
dist/checksums.txt
dist/*.tar.gz
ansible-receptor-0f6ae46/.github/workflows/pull_request.yml 0000664 0000000 0000000 00000011144 15177357701 0024247 0 ustar 00root root 0000000 0000000 ---
name: CI
on: # yamllint disable-line rule:truthy
pull_request: # yamllint disable-line rule:empty-values
env:
DESIRED_GO_VERSION: '1.24'
DESIRED_GOLANGCI_LINT_VERSION: 'v1.64'
DESIRED_PYTHON_VERSION: '3.12'
jobs:
lint-receptor:
name: lint-receptor
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: ${{ env.DESIRED_GO_VERSION }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: ${{ env.DESIRED_GOLANGCI_LINT_VERSION }}
receptor:
name: receptor (Go ${{ matrix.go-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.24"]
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: build and install receptor
run: |
make build-all
sudo cp ./receptor /usr/local/bin/receptor
- name: Download kind binary
run: curl --proto '=https' --tlsv1.2 -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 && chmod +x ./kind
- name: Create k8s cluster
run: ./kind create cluster --wait 60s
- name: Wait for nodes to be ready
run: kubectl wait --for=condition=Ready nodes --all --timeout=60s
- name: Interact with the cluster
run: kubectl get nodes
- name: Run receptor tests
run: make test
- name: get k8s logs
if: ${{ failure() }}
run: .github/workflows/artifact-k8s-logs.sh
- name: remove sockets before archiving logs
if: ${{ failure() }}
run: find /tmp/receptor-testing -name controlsock -delete
- name: Artifact receptor data for ${{ matrix.go-version }}
uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: test-logs-${{ matrix.go-version }}
path: /tmp/receptor-testing
- name: Archive receptor binary for ${{ matrix.go-version }}
uses: actions/upload-artifact@v7
with:
name: receptor-${{ matrix.go-version }}
path: /usr/local/bin/receptor
receptorctl:
name: Run receptorctl tests${{ '' }} # Nest jobs under the same sidebar category
needs: receptor
strategy:
fail-fast: false
matrix:
python-version:
# NOTE: The highest and the lowest versions come
# NOTE: first as their statuses are most likely to
# NOTE: signal problems early:
- 3.12
- 3.11
- "3.10"
uses: ./.github/workflows/reusable-nox.yml
with:
python-version: ${{ matrix.python-version }}
session: tests-${{ matrix.python-version }}
download-receptor: true
go-version: '1.24'
lint-receptorctl:
name: Lint receptorctl${{ '' }} # Nest jobs under the same sidebar category
strategy:
fail-fast: false
matrix:
session:
- check_style
- check_format
uses: ./.github/workflows/reusable-nox.yml
with:
python-version: '3.12'
session: ${{ matrix.session }}
container:
name: container
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.DESIRED_PYTHON_VERSION }}
- name: Install python dependencies
run: pip install build
- name: Build container
run: make container REPO=receptor LATEST=yes
- name: Write out basic config
run: |
cat << EOF > test.cfg
---
- local-only:
- control-service:
service: control
filename: /tmp/receptor.sock
- work-command:
worktype: cat
command: cat
EOF
- name: Run receptor (and wait a few seconds for it to boot)
run: |
podman run --name receptor -d -v $PWD/test.cfg:/etc/receptor/receptor.conf:Z localhost/receptor
sleep 3
podman logs receptor
- name: Submit work and assert the output we expect
run: |
output=$(podman exec -i receptor receptorctl work submit cat -l 'hello world' -f)
echo $output
if [[ "$output" != "hello world" ]]; then
echo "Output did not contain expected value"
exit 1
fi
ansible-receptor-0f6ae46/.github/workflows/reusable-nox.yml 0000664 0000000 0000000 00000003433 15177357701 0024131 0 ustar 00root root 0000000 0000000 ---
name: Receptorctl nox sessions
on: # yamllint disable-line rule:truthy
workflow_call:
inputs:
python-version:
type: string
description: The Python version to use.
required: true
session:
type: string
description: The nox session to run.
required: true
download-receptor:
type: boolean
description: Whether to perform go binary download.
required: false
default: false
go-version:
type: string
description: The Go version to use.
required: false
env:
FORCE_COLOR: 1
NOXSESSION: ${{ inputs.session }}
jobs:
nox:
runs-on: ubuntu-latest
name: >- # can't use `env` in this context:
Run `receptorctl` ${{ inputs.session }} session
steps:
- name: Download the `receptor` binary
if: fromJSON(inputs.download-receptor)
uses: actions/download-artifact@v8
with:
name: receptor-${{ inputs.go-version }}
path: /usr/local/bin/
- name: Set executable bit on the `receptor` binary
if: fromJSON(inputs.download-receptor)
run: sudo chmod a+x /usr/local/bin/receptor
- name: Set up nox
uses: wntrblm/nox@2026.04.10
with:
python-versions: ${{ inputs.python-version }}
- name: Check out the source code from Git
uses: actions/checkout@v6
with:
fetch-depth: 0 # Needed for the automation in Nox to find the last tag
sparse-checkout: receptorctl
- name: Provision nox environment for ${{ env.NOXSESSION }}
run: nox --install-only
working-directory: ./receptorctl
- name: Run `receptorctl` nox ${{ env.NOXSESSION }} session
run: nox --no-install
working-directory: ./receptorctl
ansible-receptor-0f6ae46/.github/workflows/sonar_checks.yml 0000664 0000000 0000000 00000005506 15177357701 0024172 0 ustar 00root root 0000000 0000000 name: SonarQube Static Scans (PR)
on:
workflow_run:
workflows:
- Codecov
types:
- completed
jobs:
sonarcloud:
permissions:
contents: read
actions: read
pull-requests: read
name: SonarQube Static Scans (PR)
runs-on: ubuntu-latest
env:
go_version: '1.24'
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Fetch receptor coverage report
uses: actions/download-artifact@v8
with:
name: receptor-coverage-report
path: .
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Fetch receptorctl coverage report
uses: actions/download-artifact@v8
with:
name: receptorctl-coverage-report
path: .
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Fetch PR Number
uses: actions/download-artifact@v8
with:
name: pr_number
path: .
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Extract PR Number
run: |
cat pr_number.txt
echo $(head -n1 pr_number.txt | awk '{print $2}')
echo "PR_NUMBER=$(head -n1 pr_number.txt | awk '{print $2}')" >> $GITHUB_ENV
- name: Get Additional PR Information
uses: octokit/request-action@v2.x
id: pr_info
with:
route: GET /repos/{repo}/pulls/{number}
repo: ${{ github.event.repository.full_name }}
number: ${{ env.PR_NUMBER }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set Additional PR Information
run: |
echo "PR_BASE=${{ fromJson(steps.pr_info.outputs.data).base.ref }}" >> $GITHUB_ENV
echo "PR_HEAD=${{ fromJson(steps.pr_info.outputs.data).head.ref }}" >> $GITHUB_ENV
- name: Checkout Code for PR
run: |
gh pr checkout ${{ env.PR_NUMBER }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: SonarQube Scan for receptor
uses: SonarSource/sonarqube-scan-action@v7
env:
SONAR_TOKEN: ${{ secrets[format('{0}', vars.SONAR_TOKEN_SECRET_NAME)] }}
with:
args: >
-Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }}
-Dsonar.pullrequest.key=${{ env.PR_NUMBER }}
-Dsonar.pullrequest.branch=${{ env.PR_HEAD }}
-Dsonar.pullrequest.base=${{ env.PR_BASE }}
-Dsonar.go.coverage.reportPaths=coverage.txt
-Dsonar.python.coverage.reportPaths=receptorctl_coverage.xml
ansible-receptor-0f6ae46/.github/workflows/stage.yml 0000664 0000000 0000000 00000006454 15177357701 0022636 0 ustar 00root root 0000000 0000000 ---
name: Stage Release
on: # yamllint disable-line rule:truthy
workflow_dispatch:
inputs:
version:
description: 'Version to release. (x.y.z) Will create a tag / draft release.'
required: true
default: ''
ref:
description: 'The ref to tag. Can only be the 40 character SHA.'
required: true
default: ''
confirm:
description: 'Are you sure? Set this to yes.'
required: true
default: 'no'
name:
description: 'Name of the person in single quotes who will create a tag / draft release.'
required: true
default: ''
type: string
email:
description: 'Email of the person who will create a tag / draft release.'
required: true
default: ''
env:
DESIRED_PYTHON_VERSION: '3.12'
jobs:
stage:
runs-on: ubuntu-latest
permissions:
packages: write
contents: write
env:
REF: ${{ github.event.inputs.ref }}
steps:
- name: Verify inputs
run: |
set -e
if [[ ${{ github.event.inputs.confirm }} != "yes" ]]; then
>&2 echo "Confirm must be 'yes'"
exit 1
fi
if [[ ${{ github.event.inputs.version }} == "" ]]; then
>&2 echo "Set version to continue."
exit 1
fi
exit 0
- name: Checkout receptor
uses: actions/checkout@v6
with:
ref: ${{ env.REF }}
- name: Install python
uses: actions/setup-python@v6
with:
python-version: ${{ env.DESIRED_PYTHON_VERSION }}
- name: Install dependencies
run: |
python3 -m pip install build
# setup qemu and buildx
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to registry
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io
# Image repository names must be lowercase, so get lowercase repo owner for ghcr.io
# See https://github.com/orgs/community/discussions/27086.
- name: Lowercase github owner
run: |
echo "OWNER=$(echo $GITHUB_REPOSITORY_OWNER | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Build container image
run: |
make container CONTAINERCMD="docker buildx" EXTRA_OPTS="--platform linux/amd64,linux/ppc64le,linux/arm64 --push" REPO=ghcr.io/${{ env.OWNER }}/receptor VERSION=v${{ github.event.inputs.version }} LATEST=yes
- name: Get current time
uses: josStorer/get-current-time@v2
id: current-time
- name: Create draft release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ansible-playbook tools/ansible/stage.yml \
-e version="${{ github.event.inputs.version }}" \
-e repo="${{ env.OWNER }}/receptor" \
-e github_token="$GITHUB_TOKEN" \
-e target_commitish="${{ github.event.inputs.ref }}" \
-e tagger_name="${{ github.event.inputs.name }}" \
-e tagger_email="${{ github.event.inputs.email }}" \
-e time="${{ steps.current-time.outputs.time }}" \
-v
ansible-receptor-0f6ae46/.github/workflows/test-reporting.yml 0000664 0000000 0000000 00000005453 15177357701 0024517 0 ustar 00root root 0000000 0000000 ---
name: Generate junit test report
on: # yamllint disable-line rule:truthy
pull_request: # yamllint disable-line rule:empty-values
push:
branches: [devel]
env:
DESIRED_GO_VERSION: '1.24'
jobs:
generate_junit_test_report:
name: go test coverage
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.DESIRED_GO_VERSION }}
- name: build and install receptor
run: |
make build-all
sudo cp ./receptor /usr/local/bin/receptor
- name: Download kind binary
run: curl --proto '=https' --tlsv1.2 -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 && chmod +x ./kind
- name: Create k8s cluster
run: ./kind create cluster --wait 60s
- name: Wait for nodes to be ready
run: kubectl wait --for=condition=Ready nodes --all --timeout=60s
- name: Interact with the cluster
run: kubectl get nodes
- name: Install go junit reporting
run: go install github.com/jstemmer/go-junit-report/v2@latest
- name: Run receptor tests
run: go test -v 2>&1 $(go list ./... | grep -vE '/tests/|mock_|example') | go-junit-report > report.xml
- name: Upload test results to dashboard
if: >-
!cancelled()
&& github.event_name == 'push'
&& github.repository == 'ansible/receptor'
&& github.ref_name == github.event.repository.default_branch
env:
PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_USER: ${{ vars.PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_USER }}
PDE_ORG_RESULTS_UPLOAD_PASSWORD: ${{ secrets.PDE_ORG_RESULTS_UPLOAD_PASSWORD }}
run: >-
curl -v --user "$PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_USER:$PDE_ORG_RESULTS_UPLOAD_PASSWORD"
--form "xunit_xml=@report.xml"
--form "component_name=receptor"
--form "git_commit_sha=${{ github.sha }}"
--form "git_repository_url=https://github.com/${{ github.repository }}"
"${{ vars.PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_URL }}/api/results/upload/"
- name: get k8s logs
if: ${{ failure() }}
run: .github/workflows/artifact-k8s-logs.sh
- name: remove sockets before archiving logs
if: ${{ failure() }}
run: find /tmp/receptor-testing -name controlsock -delete
- name: Artifact receptor data
uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: test-logs
path: /tmp/receptor-testing
- name: Archive receptor binary
uses: actions/upload-artifact@v7
with:
name: receptor
path: /usr/local/bin/receptor
ansible-receptor-0f6ae46/.github/workflows/triage_new.yml 0000664 0000000 0000000 00000000735 15177357701 0023653 0 ustar 00root root 0000000 0000000 ---
name: Triage
on: # yamllint disable-line rule:truthy
issues:
types:
- opened
jobs:
triage:
runs-on: ubuntu-latest
name: Label
steps:
- name: Label issues
uses: github/issue-labeler@v3.4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
not-before: 2021-12-07T07:00:00Z
configuration-path: .github/issue_labeler.yml
enable-versioned-regex: 0
if: github.event_name == 'issues'
ansible-receptor-0f6ae46/.gitignore 0000664 0000000 0000000 00000001045 15177357701 0017372 0 ustar 00root root 0000000 0000000 .DS_Store
.idea
.vscode
kind
receptor
receptor.exe
receptor.app
recepcert
/net
.container-flag*
.VERSION
.python-version
kubectl
/receptorctl/.nox
/receptorctl/.VERSION
/receptorctl/AUTHORS
/receptorctl/ChangeLog
/receptor-python-worker/.VERSION
/receptor-python-worker/ChangeLog
/receptor-python-worker/AUTHORS
/receptorctl/venv/
receptorctl-test-venv/
.vagrant/
/docs/build
/dist
/test-configs
coverage.*
venv
/vendor
pkg/services/.lock
pkg/workceptor/status
pkg/workceptor/status.lock
**/__debug_*
.history/
.claude/
CLAUDE*
claude_*
.cursorrules ansible-receptor-0f6ae46/.gitleaks.toml 0000664 0000000 0000000 00000000221 15177357701 0020153 0 ustar 00root root 0000000 0000000 [allowlist]
description = "Global Allowlist"
paths = [
'''pkg/certificates/ca_test.go''',
'''pkg/netceptor/tlsconfig_test.go''',
]
ansible-receptor-0f6ae46/.golangci.yml 0000664 0000000 0000000 00000007414 15177357701 0017774 0 ustar 00root root 0000000 0000000 ---
run:
timeout: 10m
linters:
disable-all: true
enable:
- asciicheck
- bodyclose
- depguard
- dogsled
- durationcheck
- gci
- gocritic
- godot
- gofmt
- gofumpt
- goheader
- goimports
- gomodguard
- gosec
- gosimple
- govet
- importas
- ineffassign
- makezero
- misspell
- nakedret
- nilerr
- nlreturn
- noctx
- nolintlint
- prealloc
- predeclared
- rowserrcheck
- sqlclosecheck
- staticcheck
- stylecheck
- tparallel
- typecheck
- unconvert
- unused
- wastedassign
- whitespace
linters-settings:
depguard:
rules:
main:
files:
- "$all"
- "!$test"
- "!**/functional/**/*.go"
- "!tests/goroutines/*.go"
allow:
- "$gostd"
- "github.com/ansible/receptor/internal/version"
- "github.com/ansible/receptor/cmd"
- "github.com/ansible/receptor/pkg"
- "github.com/creack/pty"
- "github.com/fsnotify/fsnotify"
- "github.com/ghjm/cmdline"
- "github.com/golang-jwt/jwt/v4"
- "github.com/google/shlex"
- "github.com/gorilla/websocket"
- "github.com/jupp0r/go-priority-queue"
- "github.com/minio/highwayhash"
- "github.com/pbnjay/memory"
- "github.com/quic-go/quic-go"
- "github.com/rogpeppe/go-internal/lockedfile"
- "github.com/songgao/water"
- "github.com/vishvananda/netlink"
- "github.com/spf13/viper"
- "github.com/spf13/cobra"
- "gopkg.in/yaml.v2"
- "k8s.io/api/core"
- "k8s.io/apimachinery/pkg"
- "k8s.io/client-go"
- "github.com/grafana/pyroscope-go"
- "github.com/sirupsen/logrus"
- "golang.org/x/net/ipv4"
- "golang.org/x/net/ipv6"
- "go.uber.org/goleak"
tests:
files:
- "$test"
- "**/functional/**/*.go"
- "**/goroutines/**/*.go"
allow:
- "$gostd"
- "github.com/ansible/receptor/pkg"
- "github.com/ansible/receptor/tests/utils"
- "github.com/fortytw2/leaktest"
- "github.com/fsnotify/fsnotify"
- "github.com/gorilla/websocket"
- "github.com/prep/socketpair"
- "github.com/google/go-cmp/cmp"
- "github.com/ghjm/cmdline"
- "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/errors"
- "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/fields"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/apimachinery/pkg/selection"
- "k8s.io/apimachinery/pkg/watch"
- "k8s.io/client-go/kubernetes"
- "k8s.io/client-go/rest"
- "k8s.io/client-go/tools/remotecommand"
- "k8s.io/client-go/tools/clientcmd"
- "k8s.io/client-go/tools/clientcmd/api"
- "github.com/quic-go/quic-go"
- "github.com/quic-go/quic-go/logging"
- "github.com/AaronH88/quic-go"
- "github.com/stretchr/testify/assert"
- "go.uber.org/mock/gomock"
- "gopkg.in/yaml.v2"
- "golang.org/x/sys/unix"
- "go.uber.org/goleak"
issues:
# Dont commit the following line.
# It will make CI pass without telling you about errors.
# fix: true
exclude:
- "lostcancel" # TODO: Context is not canceled on multiple occasions. Needs more detailed work to be fixed.
- "SA2002|thelper|testinggoroutine" # TODO: Test interface used outside of its routine, tests need to be rewritten.
- "G306" # TODO: Restrict perms of touched files.
- "G402|G404" # TODO: Make TLS more secure.
- "G204" # gosec is throwing a fit, ignore.
...
ansible-receptor-0f6ae46/.readthedocs.yaml 0000664 0000000 0000000 00000002035 15177357701 0020631 0 ustar 00root root 0000000 0000000 ---
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-lts-latest
tools:
golang: "1.24"
python: "3.12"
# You can also specify other tool versions:
# nodejs: "20"
# rust: "1.70"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/source/requirements.txt
...
ansible-receptor-0f6ae46/CONTRIBUTING.md 0000664 0000000 0000000 00000012306 15177357701 0017635 0 ustar 00root root 0000000 0000000 # Receptor
Hi there! We're excited to have you as a contributor.
Have questions about this document or anything not covered here? Create a topic using the [AAP tag on the Ansible Forum](https://forum.ansible.com/tag/aap).
## Table of contents
- [Receptor](#receptor)
- [Table of contents](#table-of-contents)
- [Things to know prior to submitting code](#things-to-know-prior-to-submitting-code)
- [Setting up your development environment](#setting-up-your-development-environment)
- [Fork and clone the Receptor repo](#fork-and-clone-the-receptor-repo)
- [Development Requirements](#development-requirements)
- [Build and Run the Development Environment](#build-and-run-the-development-environment)
- [Building Receptor](#building-receptor)
- [Building container images](#building-container-images)
- [Running tests](#running-tests)
- [What should I work on?](#what-should-i-work-on)
- [Submitting Pull Requests](#submitting-pull-requests)
- [Reporting Issues](#reporting-issues)
- [Getting Help](#getting-help)
## Things to know prior to submitting code
- All code submissions are done through pull requests against the `devel` branch.
- You must use `git commit --signoff` for any commit to be merged, and agree that usage of --signoff constitutes agreement with the terms of [DCO 1.1](./DCO_1_1.md).
- Take care to make sure no merge commits are in the submission, and use `git rebase` vs `git merge` for this reason.
- If collaborating with someone else on the same branch, consider using `--force-with-lease` instead of `--force`. This will prevent you from accidentally overwriting commits pushed by someone else. For more information, see [git push docs](https://git-scm.com/docs/git-push#git-push---force-with-leaseltrefnamegt).
- If submitting a large code change, it's a good idea to create a [forum topic tagged with 'aap'](https://forum.ansible.com/tag/aap), and talk about what you would like to do or add first. This not only helps everyone know what's going on, it also helps save time and effort, if the community decides some changes are needed.
- We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions, or need assistance, please reach out to our community team at [codeofconduct@ansible.com](mailto:codeofconduct@ansible.com)
## Setting up your development environment
Our team uses [VS Code](https://code.visualstudio.com/) with the [Golang extension](https://marketplace.visualstudio.com/items?itemName=golang.Go) installed for our development environments. The instrustions below will show how to set up an environment using this tool set.
### Fork and clone the Receptor repo
If you have not done so already, you'll need to fork the Receptor repo on GitHub. For more on how to do this, see [Fork a Repo](https://help.github.com/articles/fork-a-repo/).
### Development Requirements
- [Git](https://git-scm.com/book/en/v2)
- [Golang](https://go.dev/doc/install)
- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/)
- [make](https://www.gnu.org/software/make/manual/make.html)
### Build and Run the Development Environment
#### Building Receptor
`make build-all`
#### Building container images
```bash
python -m venv .venv
source .venv/bin/activate
pip install build
make container
```
#### Running tests
`make test`
## What should I work on?
We have a ["good first issue" label](https://github.com/ansible/receptor/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) we put on some issues that might be a good starting point for new contributors.
Fixing bugs and updating the documentation are always appreciated, so reviewing the backlog of issues is always a good place to start.
## Submitting Pull Requests
Fixes and Features for Receptor will go through the Github pull request process. Submit your pull request (PR) against the `devel` branch.
Here are a few things you can do to help the visibility of your change, and increase the likelihood that it will be accepted:
- No issues when running linters/code checkers
- No issues from unit tests
- Write tests for new functionality, update/add tests for bug fixes
- Make the smallest change possible
- Write good commit messages. See [How to write a Git commit message](https://chris.beams.io/posts/git-commit/).
We like to keep our commit history clean, and will require resubmission of pull requests that contain merge commits. Use `git pull --rebase`, rather than
`git pull`, and `git rebase`, rather than `git merge`.
Sometimes it might take us a while to fully review your PR. We try to keep the `devel` branch in good working order, and so we review requests carefully. Please be patient.
When your PR is initially submitted the checks will not be run until a maintainer allows them to be. Once a maintainer has done a quick review of your work the PR will have the linter and unit tests run against them via GitHub Actions, and the status reported in the PR.
## Reporting Issues
We welcome your feedback, and encourage you to file an issue when you run into a problem.
## Getting Help
If you require additional assistance, please submit your question to the [Ansible Forum](https://forum.ansible.com/tag/aap).
ansible-receptor-0f6ae46/LICENSE.md 0000664 0000000 0000000 00000022130 15177357701 0017004 0 ustar 00root root 0000000 0000000 Apache License
==============
_Version 2.0, January 2004_
_<>_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
ansible-receptor-0f6ae46/Makefile 0000664 0000000 0000000 00000017102 15177357701 0017043 0 ustar 00root root 0000000 0000000 # If the current git commit has been tagged, use as the version. (e.g. v1.4.5)
# Otherwise the version is +git (e.g. 1.4.5+f031d2)
OFFICIAL_VERSION := $(shell if VER=`git describe --exact-match --tags 2>/dev/null`; then echo $$VER; else echo ""; fi)
ifneq ($(OFFICIAL_VERSION),)
VERSION := $(OFFICIAL_VERSION)
else ifneq ($(shell git tag --list),)
VERSION := $(shell git describe --tags | cut -d - -f -1)+git$(shell git rev-parse --short HEAD)
else ifeq ($(VERSION),)
VERSION := $(error No tags found in git repository)
# else VERSION was passed as a command-line argument to make
endif
# When building Receptor, tags can be used to remove undesired
# features. This is primarily used for deploying Receptor in a
# security sensitive role, where it is desired to have no possibility
# of a service being accidentally enabled. Features are controlled
# using the TAGS environment variable, which is a comma delimeted
# list of zero or more of the following:
#
# no_controlsvc: Disable the control service
#
# no_backends: Disable all backends (except external via the API)
# no_tcp_backend: Disable the TCP backend
# no_udp_backend: Disable the UDP backend
# no_websocket_backend: Disable the websocket backent
#
# no_services: Disable all services
# no_proxies: Disable the TCP, UDP and Unix proxy services
# no_ip_router: Disable the IP router service
#
# no_tls_config: Disable the ability to configure TLS server/client configs
#
# no_workceptor: Disable the unit-of-work subsystem (be network only)
#
# no_cert_auth: Disable commands related to CA and certificate generation
TAGS ?=
ifeq ($(TAGS),)
TAGPARAM=
else
TAGPARAM=--tags $(TAGS)
endif
DEBUG ?=
ifeq ($(DEBUG),1)
DEBUGFLAGS=-gcflags=all="-N -l"
else
DEBUGFLAGS=
endif
GO ?= go
receptor: $(shell find pkg -type f -name '*.go') ./cmd/receptor-cl/receptor.go
CGO_ENABLED=0 GOFLAGS="-buildvcs=false" $(GO) build \
-o receptor \
$(DEBUGFLAGS) \
-ldflags "-X 'github.com/ansible/receptor/internal/version.Version=$(VERSION)'" \
$(TAGPARAM) \
./cmd/receptor-cl
clean:
@rm -fv .container-flag*
@rm -fv .VERSION
@rm -fv receptorctl/.VERSION
@rm -fv receptor-python-worker/.VERSION
@rm -rfv dist/
@rm -fv $(KUBECTL_BINARY)
@rm -fv packaging/container/receptor
@rm -rfv packaging/container/RPMS/
@rm -fv packaging/container/*.whl
@rm -fv receptor receptor.exe receptor.app net
@rm -fv receptorctl/dist/*
@rm -fv receptor-python-worker/dist/*
@rm -rfv receptorctl-test-venv/
ARCH ?= amd64
OS=linux
KUBECTL_BINARY=./kubectl
STABLE_KUBECTL_VERSION=$(shell curl --silent https://storage.googleapis.com/kubernetes-release/release/stable.txt)
kubectl:
if [ "$(wildcard $(KUBECTL_BINARY))" != "" ]; \
then \
FOUND_KUBECTL_VERSION=$$(./kubectl version --client=true | head --lines=1 | cut --delimiter=' ' --field=3); \
else \
FOUND_KUBECTL_VERSION=; \
fi
if [ "${FOUND_KUBECTL_VERSION}" != "$(STABLE_KUBECTL_VERSION)" ]; \
then \
curl \
--location \
--output $(KUBECTL_BINARY) \
https://storage.googleapis.com/kubernetes-release/release/$(STABLE_KUBECTL_VERSION)/bin/$(OS)/$(ARCH)/kubectl; \
chmod 0700 $(KUBECTL_BINARY); \
fi
GOLANGCI_LINT_VERSION ?= v1.60.3
GOLANGCI_LINT_BINARY := $(shell go env GOPATH)/bin/golangci-lint
lint: $(GOLANGCI_LINT_BINARY)
@$(GOLANGCI_LINT_BINARY) run cmd/... pkg/... example/...
$(GOLANGCI_LINT_BINARY):
@echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)..."
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(shell go env GOPATH)/bin $(GOLANGCI_LINT_VERSION)
receptorctl-lint: receptor
@cd receptorctl && nox -s lint
format:
@find cmd/ pkg/ -type f -name '*.go' -exec $(GO) fmt {} \;
fmt: format
generate:
${GO} generate ./...
generate-clean:
@echo "Removing existing mocks"
@find . -type d -name 'mock*' -prune -exec rm -rf {} +
pre-commit:
@pre-commit run --all-files
build-all:
@echo "Running Go builds..." && \
GOOS=windows $(GO) build \
-o receptor.exe \
./cmd/receptor-cl && \
GOOS=darwin $(GO) build \
-o receptor.app \
./cmd/receptor-cl && \
$(GO) build \
example/*.go && \
$(GO) build \
-o receptor \
-ldflags "-X 'github.com/ansible/receptor/internal/version.Version=$(VERSION)'" \
./cmd/receptor-cl
BINNAME='receptor'
CHECKSUM_PROGRAM='sha256sum'
GOARCH=$(ARCH)
GOOS=$(OS)
DIST := receptor_$(shell echo '$(VERSION)' | sed 's/^v//')_$(GOOS)_$(GOARCH)
build-package:
@echo "Building and packaging binary for $(GOOS)/$(GOARCH) as dist/$(DIST).tar.gz" && \
mkdir -p dist/$(DIST) && \
GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 $(GO) build \
-o dist/$(DIST)/$(BINNAME) \
$(DEBUGFLAGS) \
-ldflags "-X 'github.com/ansible/receptor/internal/version.Version=$(VERSION)'" \
$(TAGPARAM) \
./cmd/receptor-cl && \
tar -C dist/$(DIST) -zcf dist/$(DIST).tar.gz $(BINNAME) && \
cd dist/ && \
$(CHECKSUM_PROGRAM) $(DIST).tar.gz >> checksums.txt
RUNTEST ?=
ifeq ($(RUNTEST),)
TESTCMD =
else
TESTCMD = -run $(RUNTEST)
endif
BLOCKLIST='/tests/|mock_|example'
COVERAGE_FILE='coverage.txt'
coverage: build-all
PATH="${PWD}:${PATH}" \
$(GO) test $$($(GO) list ./... | grep -vE $(BLOCKLIST)) \
$(TESTCMD) \
-count=1 \
-cover \
-covermode=atomic \
-coverprofile=$(COVERAGE_FILE) \
-race \
-timeout 5m
test: receptor
PATH="${PWD}:${PATH}" \
$(GO) test $$($(GO) list ./...) \
$(TESTCMD) \
-count=1 \
-race \
-timeout 5m
receptorctl-test: receptor
@cd receptorctl && nox -s tests
testloop: receptor
@i=1; while echo "------ $$i" && \
make test; do \
i=$$((i+1)); done
kubetest: kubectl
./kubectl get nodes
version:
@echo $(VERSION) > .VERSION
@echo ".VERSION created for $(VERSION)"
RECEPTORCTL_WHEEL = receptorctl/dist/receptorctl-$(VERSION:v%=%)-py3-none-any.whl
$(RECEPTORCTL_WHEEL): $(shell find receptorctl/receptorctl -type f -name '*.py')
@cd receptorctl && SETUPTOOLS_SCM_PRETEND_VERSION_FOR_RECEPTORCTL=$(VERSION) python3 -m build --wheel
receptorctl_wheel: $(RECEPTORCTL_WHEEL)
RECEPTORCTL_SDIST = receptorctl/dist/receptorctl-$(VERSION:v%=%).tar.gz
$(RECEPTORCTL_SDIST): $(shell find receptorctl/receptorctl -type f -name '*.py')
@cd receptorctl && SETUPTOOLS_SCM_PRETEND_VERSION_FOR_RECEPTORCTL=$(VERSION) python3 -m build --sdist
receptorctl_sdist: $(RECEPTORCTL_SDIST)
RECEPTOR_PYTHON_WORKER_WHEEL = receptor-python-worker/dist/receptor_python_worker-$(VERSION:v%=%)-py3-none-any.whl
$(RECEPTOR_PYTHON_WORKER_WHEEL): $(shell find receptor-python-worker/receptor_python_worker -type f -name '*.py')
@cd receptor-python-worker && SETUPTOOLS_SCM_PRETEND_VERSION_FOR_RECEPTOR_PYTHON_WORKER=$(VERSION) python3 -m build --wheel
# Container command can be docker or podman
CONTAINERCMD ?= podman
# Repo without tag
REPO := quay.io/ansible/receptor
# TAG is VERSION with a '-' instead of a '+', to avoid invalid image reference error.
TAG := $(subst +,-,$(VERSION))
# Set this to tag image as :latest in addition to :$(VERSION)
LATEST :=
EXTRA_OPTS ?=
space := $(subst ,, )
CONTAINER_FLAG_FILE = .container-flag-$(VERSION)$(subst $(space),,$(subst /,,$(EXTRA_OPTS)))
container: $(CONTAINER_FLAG_FILE)
$(CONTAINER_FLAG_FILE): $(RECEPTORCTL_WHEEL) $(RECEPTOR_PYTHON_WORKER_WHEEL)
# Developer Note: only committed files are included with git archive.
@git archive --format tar.gz HEAD > packaging/container/source.tar.gz
@cp $(RECEPTORCTL_WHEEL) packaging/container
@cp $(RECEPTOR_PYTHON_WORKER_WHEEL) packaging/container
$(CONTAINERCMD) build $(EXTRA_OPTS) packaging/container --build-arg VERSION=$(VERSION:v%=%) -t $(REPO):$(TAG) $(if $(LATEST),-t $(REPO):latest,)
touch $@
.PHONY: lint format fmt pre-commit build-all test clean testloop container version receptorctl-tests kubetest
ansible-receptor-0f6ae46/README.md 0000664 0000000 0000000 00000006700 15177357701 0016664 0 ustar 00root root 0000000 0000000 # Receptor
[](https://sonarcloud.io/summary/new_code?id=ansible_receptor)
Receptor is an overlay network intended to ease the distribution of work across a large and dispersed collection of workers. Receptor nodes establish peer-to-peer connections with each other via existing networks. Once connected, the Receptor mesh provides datagram (UDP-like) and stream (TCP-like) capabilities to applications, as well as robust unit-of-work handling with resiliency against transient network failures.
See the readthedocs page for Receptor at:
## Terminology and Concepts
* _Receptor_: The Receptor application taken as a whole, that typically runs as a daemon.
* _Receptorctl_: A user-facing command line used to interact with Receptor, typically over a Unix domain socket.
* _Netceptor_: The networking part of Receptor. Usable as a Go library.
* _Workceptor_: The unit-of-work handling of Receptor, which makes use of Netceptor. Also usable as a Go library.
* _Node_: A single running instance of Receptor.
* _Node ID_: An arbitrary string identifying a single node, analogous to an IP address.
* _Service_: An up-to-8-character string identifying an endpoint on a Receptor node that can receive messages. Analogous to a port number in TCP or UDP.
* _Backend_: A type of connection that Receptor nodes can pass traffic over. Current backends include TCP, UDP and websockets.
* _Control Service_: A built-in service that usually runs under the name `control`. Used to report status and to launch and monitor work.
## How to Get It
The easiest way to check out Receptor is to run it as a container. Images are kept on the Quay registry. To use this, run:
```bash
[docker|podman] pull quay.io/ansible/receptor
[docker|podman] run -d -v /path/to/receptor.conf:/etc/receptor/receptor.conf:Z receptor
```
## Use as a Go library
This code can be imported and used from Go programs. The main libraries are:
* _Netceptor_:
* _Workceptor_:
See the `example/` directory for examples of using these libraries from Go.
## Use as a command-line tool
The `receptor` command runs a Receptor node with access to all included backends and services. See `receptor --help` for details.
The command line is organized into entities which take parameters, like: `receptor --entity1 param1=value1 param2=value1 --entity2 param1=value2 param2=value2`. In this case we are configuring two things, `entity1` and `entity2`, each of which takes two parameters. Distinct entities are marked with a double dash, and bare parameters attach to the immediately preceding entity.
Receptor can also take its configuration from a file in YAML format. The allowed directives are the same as on the command line, with a top-level list of entities and each entity receiving zero or more parameters as a dict. The above command in YAML format would look like this:
```bash
---
- entity1:
param1: value1
param2: value1
- entity2:
param1: value2
param2: value2
```
## Python Receptor and the 0.6 versions
As of June 25th, this repo is the Go implementation of Receptor. If you are looking for the older Python version of Receptor, including any 0.6.x version, it is now located at .
ansible-receptor-0f6ae46/cmd/ 0000775 0000000 0000000 00000000000 15177357701 0016145 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/cmd/config.go 0000664 0000000 0000000 00000016755 15177357701 0017757 0 ustar 00root root 0000000 0000000 package cmd
import (
"fmt"
"os"
"reflect"
"github.com/ansible/receptor/pkg/backends"
"github.com/ansible/receptor/pkg/certificates"
"github.com/ansible/receptor/pkg/controlsvc"
"github.com/ansible/receptor/pkg/logger"
"github.com/ansible/receptor/pkg/netceptor"
"github.com/ansible/receptor/pkg/services"
"github.com/ansible/receptor/pkg/types"
"github.com/ansible/receptor/pkg/workceptor"
"github.com/ghjm/cmdline"
"github.com/spf13/viper"
)
type Initer interface {
Init() error
}
type Preparer interface {
Prepare() error
}
type Runer interface {
Run() error
}
type Reloader interface {
Reload() error
}
type ReceptorConfig struct {
// Used pointer structs to apply defaults to config
Node *types.NodeCfg
Trace logger.TraceCfg
LogLevel *logger.LoglevelCfg `mapstructure:"log-level"`
ControlServices []*controlsvc.CmdlineConfigUnix `mapstructure:"control-services"`
TLSClients []netceptor.TLSClientConfig `mapstructure:"tls-clients"`
TLSServer []netceptor.TLSServerConfig `mapstructure:"tls-servers"`
WorkCommands []workceptor.CommandWorkerCfg `mapstructure:"work-commands"`
WorkKubernetes []*workceptor.KubeWorkerCfg `mapstructure:"work-kubernetes"`
WorkSigning workceptor.SigningKeyPrivateCfg `mapstructure:"work-signing"`
WorkVerification workceptor.VerifyingKeyPublicCfg `mapstructure:"work-verification"`
IPRouters []services.IPRouterCfg
TCPClients []services.TCPProxyOutboundCfg `mapstructure:"tcp-clients"`
TCPServers []services.TCPProxyInboundCfg `mapstructure:"tcp-servers"`
UDPClients []services.TCPProxyInboundCfg `mapstructure:"udp-clients"`
UDPServers []services.TCPProxyInboundCfg `mapstructure:"udp-servers"`
UnixSocketClients []services.UnixProxyOutboundCfg `mapstructure:"unix-socket-clients"`
UnixSocketServers []services.UnixProxyInboundCfg `mapstructure:"unix-socket-servers"`
}
type CertificatesConfig struct {
InitCA certificates.InitCAConfig `mapstructure:"cert-init"`
MakeReq []certificates.MakeReqConfig `mapstructure:"cert-makereqs"`
SignReq []certificates.SignReqConfig `mapstructure:"cert-signreqs"`
}
type BackendConfig struct {
TCPListeners []*backends.TCPListenerCfg `mapstructure:"tcp-listeners"`
UDPListeners []*backends.UDPListenerCfg `mapstructure:"udp-listeners"`
WSListeners []*backends.WebsocketListenerCfg `mapstructure:"ws-listeners"`
TCPPeers []*backends.TCPDialerCfg `mapstructure:"tcp-peers"`
UDPPeers []*backends.UDPDialerCfg `mapstructure:"udp-peers"`
WSPeers []*backends.WebsocketDialerCfg `mapstructure:"ws-peers"`
LocalOnly backends.NullBackendCfg `mapstructure:"local-only"`
}
func PrintPhaseErrorMessage(configName string, phase string, err error) {
fmt.Printf("ERROR: %s for %s on %s phase\n", err, configName, phase)
}
func ParseReceptorConfig() (*ReceptorConfig, error) {
var receptorConfig ReceptorConfig
err := viper.Unmarshal(&receptorConfig)
if err != nil {
return nil, err
}
return &receptorConfig, nil
}
func ParseCertificatesConfig() (*CertificatesConfig, error) {
var certifcatesConfig CertificatesConfig
err := viper.Unmarshal(&certifcatesConfig)
if err != nil {
return nil, err
}
return &certifcatesConfig, nil
}
func ParseBackendConfig() (*BackendConfig, error) {
var backendConfig BackendConfig
err := viper.Unmarshal(&backendConfig)
if err != nil {
return nil, err
}
return &backendConfig, nil
}
func isConfigEmpty(v reflect.Value) bool {
isEmpty := true
for i := 0; i < v.NumField(); i++ {
if reflect.Value.IsZero(v.Field(i)) {
continue
}
isEmpty = false
}
return isEmpty
}
func RunConfigV2(v reflect.Value) {
phases := []string{"Init", "Prepare", "Run"}
for _, phase := range phases {
for i := 0; i < v.NumField(); i++ {
if reflect.Value.IsZero(v.Field(i)) {
continue
}
switch v.Field(i).Kind() {
case reflect.Slice:
for j := 0; j < v.Field(i).Len(); j++ {
RunPhases(phase, v.Field(i).Index(j))
}
default:
RunPhases(phase, v.Field(i))
}
}
}
}
// RunPhases runs the appropriate function (Init, Prepare, Run) on a command.
func RunPhases(phase string, v reflect.Value) {
cmd := v.Interface()
var err error
if phase == "Init" {
switch c := cmd.(type) {
case Initer:
err = c.Init()
if err != nil {
PrintPhaseErrorMessage(v.Type().Name(), phase, err)
}
default:
}
}
if phase == "Prepare" {
switch c := cmd.(type) {
case Preparer:
err = c.Prepare()
if err != nil {
PrintPhaseErrorMessage(v.Type().Name(), phase, err)
}
default:
}
}
if phase == "Run" {
switch c := cmd.(type) {
case Runer:
err = c.Run()
if err != nil {
PrintPhaseErrorMessage(v.Type().Name(), phase, err)
}
default:
}
}
}
// ReloadServices iterates through key/values calling reload on applicable services.
func ReloadServices(v reflect.Value) {
for i := 0; i < v.NumField(); i++ {
// if the services is not initialised, skip
if reflect.Value.IsZero(v.Field(i)) {
continue
}
var err error
switch v.Field(i).Kind() {
case reflect.Slice:
// iterate over all the type fields
for j := 0; j < v.Field(i).Len(); j++ {
serviceItem := v.Field(i).Index(j).Interface()
switch c := serviceItem.(type) {
// check to see if the selected type field satisfies reload
// call reload on cfg object
case Reloader:
err = c.Reload()
if err != nil {
PrintPhaseErrorMessage(v.Type().Name(), "reload", err)
}
// if cfg object does not satisfy, do nothing
default:
}
}
// runs for non slice fields
default:
switch c := v.Field(i).Interface().(type) {
case Reloader:
err = c.Reload()
if err != nil {
PrintPhaseErrorMessage(v.Type().Name(), "reload", err)
}
default:
}
}
}
}
func RunConfigV1() {
cl := cmdline.NewCmdline()
cl.AddConfigType("node", "Specifies the node configuration of this instance", types.NodeCfg{}, cmdline.Required, cmdline.Singleton)
cl.AddConfigType("local-only", "Runs a self-contained node with no backend", backends.NullBackendCfg{}, cmdline.Singleton)
cl.AddConfigType("pyroscope-client", "Profile Receptor using Pyroscope, client ", types.ReceptorPyroscopeCfg{}, cmdline.Singleton)
// Add registered config types from imported modules
for _, appName := range []string{
"receptor-version",
"receptor-logging",
"receptor-tls",
"receptor-certificates",
"receptor-control-service",
"receptor-command-service",
"receptor-ip-router",
"receptor-proxies",
"receptor-backends",
"receptor-workers",
} {
cl.AddRegisteredConfigTypes(appName)
}
osArgs := os.Args[1:]
err := cl.ParseAndRun(osArgs, []string{"Init", "Prepare", "Run"}, cmdline.ShowHelpIfNoArgs)
if err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
if cl.WhatRan() != "" {
// We ran an exclusive command, so we aren't starting any back-ends
os.Exit(0)
}
configPath := ""
for i, arg := range osArgs {
if arg == "--config" || arg == "-c" {
if len(osArgs) > i+1 {
configPath = osArgs[i+1]
}
break
}
}
// only allow reloading if a configuration file was provided. If ReloadCL is
// not set, then the control service reload command will fail
if configPath != "" {
// create closure with the passed in args to be ran during a reload
reloadParseAndRun := func(toRun []string) error {
return cl.ParseAndRun(osArgs, toRun)
}
err = controlsvc.InitReload(configPath, reloadParseAndRun)
if err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
}
ansible-receptor-0f6ae46/cmd/config_test.go 0000664 0000000 0000000 00000016317 15177357701 0021010 0 ustar 00root root 0000000 0000000 package cmd
import (
"reflect"
"testing"
"github.com/ansible/receptor/pkg/backends"
"github.com/ansible/receptor/pkg/controlsvc"
"github.com/ansible/receptor/pkg/workceptor"
)
func TestIsConfigEmpty(t *testing.T) {
// Test with an empty struct
type emptyStruct struct {
Field1 string
Field2 int
Field3 bool
}
empty := emptyStruct{}
if !isConfigEmpty(reflect.ValueOf(empty)) {
t.Error("Expected empty struct to be identified as empty")
}
// Test with a non-empty struct
nonEmpty := emptyStruct{Field1: "value"}
if isConfigEmpty(reflect.ValueOf(nonEmpty)) {
t.Error("Expected non-empty struct to be identified as non-empty")
}
}
func TestSetBackendConfigDefaults(t *testing.T) {
// Create a BackendConfig with no defaults set
config := &BackendConfig{
TCPListeners: []*backends.TCPListenerCfg{
{
BindAddr: "",
Cost: 0,
},
},
UDPListeners: []*backends.UDPListenerCfg{
{
BindAddr: "",
Cost: 0,
},
},
WSListeners: []*backends.WebsocketListenerCfg{
{
BindAddr: "",
Cost: 0,
Path: "",
},
},
TCPPeers: []*backends.TCPDialerCfg{
{
Cost: 0,
Redial: false,
},
},
UDPPeers: []*backends.UDPDialerCfg{
{
Cost: 0,
Redial: false,
},
},
WSPeers: []*backends.WebsocketDialerCfg{
{
Cost: 0,
Redial: false,
},
},
}
// Apply defaults
SetBackendConfigDefaults(config)
// Check TCP Listener defaults
if config.TCPListeners[0].BindAddr != "0.0.0.0" {
t.Errorf("Expected TCP Listener BindAddr to be '0.0.0.0', got '%s'", config.TCPListeners[0].BindAddr)
}
if config.TCPListeners[0].Cost != 1.0 {
t.Errorf("Expected TCP Listener Cost to be 1.0, got %f", config.TCPListeners[0].Cost)
}
// Check UDP Listener defaults
if config.UDPListeners[0].BindAddr != "0.0.0.0" {
t.Errorf("Expected UDP Listener BindAddr to be '0.0.0.0', got '%s'", config.UDPListeners[0].BindAddr)
}
if config.UDPListeners[0].Cost != 1.0 {
t.Errorf("Expected UDP Listener Cost to be 1.0, got %f", config.UDPListeners[0].Cost)
}
// Check WS Listener defaults
if config.WSListeners[0].BindAddr != "0.0.0.0" {
t.Errorf("Expected WS Listener BindAddr to be '0.0.0.0', got '%s'", config.WSListeners[0].BindAddr)
}
if config.WSListeners[0].Cost != 1.0 {
t.Errorf("Expected WS Listener Cost to be 1.0, got %f", config.WSListeners[0].Cost)
}
// Check TCP Peer defaults
if config.TCPPeers[0].Cost != 1.0 {
t.Errorf("Expected TCP Peer Cost to be 1.0, got %f", config.TCPPeers[0].Cost)
}
if !config.TCPPeers[0].Redial {
t.Error("Expected TCP Peer Redial to be true")
}
// Check UDP Peer defaults
if config.UDPPeers[0].Cost != 1.0 {
t.Errorf("Expected UDP Peer Cost to be 1.0, got %f", config.UDPPeers[0].Cost)
}
if !config.UDPPeers[0].Redial {
t.Error("Expected UDP Peer Redial to be true")
}
// Check WS Peer defaults
if config.WSPeers[0].Cost != 1.0 {
t.Errorf("Expected WS Peer Cost to be 1.0, got %f", config.WSPeers[0].Cost)
}
if !config.WSPeers[0].Redial {
t.Error("Expected WS Peer Redial to be true")
}
}
func TestSetReceptorConfigDefaults(t *testing.T) {
// Create a ReceptorConfig with no defaults set
config := &ReceptorConfig{
Node: nil,
LogLevel: nil,
ControlServices: []*controlsvc.CmdlineConfigUnix{
{
Service: "",
Permissions: 0,
},
},
WorkKubernetes: []*workceptor.KubeWorkerCfg{
{
AuthMethod: "",
StreamMethod: "",
},
},
}
// Apply defaults
SetReceptorConfigDefaults(config)
// Check Node defaults
if config.Node == nil {
t.Error("Expected Node to be initialized")
} else if config.Node.DataDir != "/tmp/receptor" {
t.Errorf("Expected Node DataDir to be '/tmp/receptor', got '%s'", config.Node.DataDir)
}
// Check ControlService defaults
if config.ControlServices[0].Service != "control" {
t.Errorf("Expected ControlService Service to be 'control', got '%s'", config.ControlServices[0].Service)
}
if config.ControlServices[0].Permissions != 0o600 {
t.Errorf("Expected ControlService Permissions to be 0o600, got %o", config.ControlServices[0].Permissions)
}
// Check WorkKubernetes defaults
if config.WorkKubernetes[0].AuthMethod != "incluster" {
t.Errorf("Expected WorkKubernetes AuthMethod to be 'incluster', got '%s'", config.WorkKubernetes[0].AuthMethod)
}
if config.WorkKubernetes[0].StreamMethod != "logger" {
t.Errorf("Expected WorkKubernetes StreamMethod to be 'logger', got '%s'", config.WorkKubernetes[0].StreamMethod)
}
}
// mockCommand is a struct that implements the Initer, Preparer, and Runer interfaces for testing.
type mockCommand struct {
initCalled bool
prepareCalled bool
runCalled bool
initError error
prepareError error
runError error
}
// Init implements the Initer interface.
func (m *mockCommand) Init() error {
m.initCalled = true
return m.initError
}
// Prepare implements the Preparer interface.
func (m *mockCommand) Prepare() error {
m.prepareCalled = true
return m.prepareError
}
// Run implements the Runer interface.
func (m *mockCommand) Run() error {
m.runCalled = true
return m.runError
}
func TestRunPhases(t *testing.T) {
// Test Init phase
mock := &mockCommand{}
RunPhases("Init", reflect.ValueOf(mock))
if !mock.initCalled {
t.Error("Expected Init to be called")
}
if mock.prepareCalled || mock.runCalled {
t.Error("Expected only Init to be called")
}
// Test Prepare phase
mock = &mockCommand{}
RunPhases("Prepare", reflect.ValueOf(mock))
if !mock.prepareCalled {
t.Error("Expected Prepare to be called")
}
if mock.initCalled || mock.runCalled {
t.Error("Expected only Prepare to be called")
}
// Test Run phase
mock = &mockCommand{}
RunPhases("Run", reflect.ValueOf(mock))
if !mock.runCalled {
t.Error("Expected Run to be called")
}
if mock.initCalled || mock.prepareCalled {
t.Error("Expected only Run to be called")
}
// Test with an invalid phase
mock = &mockCommand{}
RunPhases("InvalidPhase", reflect.ValueOf(mock))
if mock.initCalled || mock.prepareCalled || mock.runCalled {
t.Error("Expected no methods to be called for invalid phase")
}
}
func TestParseReceptorConfig(t *testing.T) {
// Skip this test for now as it requires more complex setup
t.Skip("Skipping TestParseReceptorConfig as it requires more complex setup")
}
func TestParseBackendConfig(t *testing.T) {
// Skip this test for now as it requires more complex setup
t.Skip("Skipping TestParseBackendConfig as it requires more complex setup")
}
func TestParseCertificatesConfig(t *testing.T) {
// Skip this test for now as it requires more complex setup
t.Skip("Skipping TestParseCertificatesConfig as it requires more complex setup")
}
// testConfig is a struct with a slice of mockCommand for testing RunConfigV2.
type testConfig struct {
Commands []*mockCommand
Empty string
}
func TestRunConfigV2(t *testing.T) {
// Create a test config with some commands
config := testConfig{
Commands: []*mockCommand{
{},
{},
},
}
// Run the config
RunConfigV2(reflect.ValueOf(config))
// Check that all commands were called for all phases
for _, cmd := range config.Commands {
if !cmd.initCalled {
t.Error("Expected Init to be called")
}
if !cmd.prepareCalled {
t.Error("Expected Prepare to be called")
}
if !cmd.runCalled {
t.Error("Expected Run to be called")
}
}
}
ansible-receptor-0f6ae46/cmd/defaults.go 0000664 0000000 0000000 00000005150 15177357701 0020304 0 ustar 00root root 0000000 0000000 package cmd
import "github.com/ansible/receptor/pkg/types"
const DefaultBindAddr = "0.0.0.0"
func SetTCPListenerDefaults(config *BackendConfig) {
for _, listener := range config.TCPListeners {
if listener.Cost == 0 {
listener.Cost = 1.0
}
if listener.BindAddr == "" {
listener.BindAddr = DefaultBindAddr
}
}
}
func SetUDPListenerDefaults(config *BackendConfig) {
for _, listener := range config.UDPListeners {
if listener.Cost == 0 {
listener.Cost = 1.0
}
if listener.BindAddr == "" {
listener.BindAddr = DefaultBindAddr
}
}
}
func SetWSListenerDefaults(config *BackendConfig) {
for _, listener := range config.WSListeners {
if listener.Cost == 0 {
listener.Cost = 1.0
}
if listener.BindAddr == "" {
listener.BindAddr = DefaultBindAddr
}
if listener.Path == "" {
listener.Path = "/"
}
}
}
func SetUDPPeerDefaults(config *BackendConfig) {
for _, peer := range config.UDPPeers {
if peer.Cost == 0 {
peer.Cost = 1.0
}
if !peer.Redial {
peer.Redial = true
}
}
}
func SetTCPPeerDefaults(config *BackendConfig) {
for _, peer := range config.TCPPeers {
if peer.Cost == 0 {
peer.Cost = 1.0
}
if !peer.Redial {
peer.Redial = true
}
}
}
func SetWSPeerDefaults(config *BackendConfig) {
for _, peer := range config.WSPeers {
if peer.Cost == 0 {
peer.Cost = 1.0
}
if !peer.Redial {
peer.Redial = true
}
}
}
func SetCmdlineUnixDefaults(config *ReceptorConfig) {
for _, service := range config.ControlServices {
if service.Permissions == 0 {
service.Permissions = 0o600
}
if service.Service == "" {
service.Service = "control"
}
}
}
func SetLogLevelDefaults(config *ReceptorConfig) {
if config.LogLevel == nil {
return
}
if config.LogLevel.Level == "" {
config.LogLevel.Level = "error"
}
}
func SetNodeDefaults(config *ReceptorConfig) {
if config.Node == nil {
config.Node = &types.NodeCfg{}
}
if config.Node.DataDir == "" {
config.Node.DataDir = "/tmp/receptor"
}
}
func SetKubeWorkerDefaults(config *ReceptorConfig) {
for _, worker := range config.WorkKubernetes {
if worker.AuthMethod == "" {
worker.AuthMethod = "incluster"
}
if worker.StreamMethod == "" {
worker.StreamMethod = "logger"
}
}
}
func SetReceptorConfigDefaults(config *ReceptorConfig) {
SetCmdlineUnixDefaults(config)
SetLogLevelDefaults(config)
SetNodeDefaults(config)
SetKubeWorkerDefaults(config)
}
func SetBackendConfigDefaults(config *BackendConfig) {
SetTCPListenerDefaults(config)
SetUDPListenerDefaults(config)
SetWSListenerDefaults(config)
SetTCPPeerDefaults(config)
SetUDPPeerDefaults(config)
SetWSPeerDefaults(config)
}
ansible-receptor-0f6ae46/cmd/defaults_test.go 0000664 0000000 0000000 00000014076 15177357701 0021352 0 ustar 00root root 0000000 0000000 package cmd
import (
"testing"
"github.com/ansible/receptor/pkg/backends"
"github.com/ansible/receptor/pkg/controlsvc"
"github.com/ansible/receptor/pkg/logger"
"github.com/ansible/receptor/pkg/types"
"github.com/ansible/receptor/pkg/workceptor"
)
func TestSetTCPListenerDefaults(t *testing.T) {
config := &BackendConfig{
TCPListeners: []*backends.TCPListenerCfg{
{
BindAddr: "",
Cost: 0,
},
},
}
SetTCPListenerDefaults(config)
if config.TCPListeners[0].BindAddr != "0.0.0.0" {
t.Errorf("Expected BindAddr to be '0.0.0.0', got '%s'", config.TCPListeners[0].BindAddr)
}
if config.TCPListeners[0].Cost != 1.0 {
t.Errorf("Expected Cost to be 1.0, got %f", config.TCPListeners[0].Cost)
}
}
func TestSetUDPListenerDefaults(t *testing.T) {
config := &BackendConfig{
UDPListeners: []*backends.UDPListenerCfg{
{
BindAddr: "",
Cost: 0,
},
},
}
SetUDPListenerDefaults(config)
if config.UDPListeners[0].BindAddr != "0.0.0.0" {
t.Errorf("Expected BindAddr to be '0.0.0.0', got '%s'", config.UDPListeners[0].BindAddr)
}
if config.UDPListeners[0].Cost != 1.0 {
t.Errorf("Expected Cost to be 1.0, got %f", config.UDPListeners[0].Cost)
}
}
func TestSetWSListenerDefaults(t *testing.T) {
config := &BackendConfig{
WSListeners: []*backends.WebsocketListenerCfg{
{
BindAddr: "",
Cost: 0,
Path: "",
},
},
}
SetWSListenerDefaults(config)
if config.WSListeners[0].BindAddr != "0.0.0.0" {
t.Errorf("Expected BindAddr to be '0.0.0.0', got '%s'", config.WSListeners[0].BindAddr)
}
if config.WSListeners[0].Cost != 1.0 {
t.Errorf("Expected Cost to be 1.0, got %f", config.WSListeners[0].Cost)
}
if config.WSListeners[0].Path != "/" {
t.Errorf("Expected Path to be '/', got '%s'", config.WSListeners[0].Path)
}
}
func TestSetUDPPeerDefaults(t *testing.T) {
config := &BackendConfig{
UDPPeers: []*backends.UDPDialerCfg{
{
Cost: 0,
Redial: false,
},
},
}
SetUDPPeerDefaults(config)
if config.UDPPeers[0].Cost != 1.0 {
t.Errorf("Expected Cost to be 1.0, got %f", config.UDPPeers[0].Cost)
}
if !config.UDPPeers[0].Redial {
t.Error("Expected Redial to be true")
}
}
func TestSetTCPPeerDefaults(t *testing.T) {
config := &BackendConfig{
TCPPeers: []*backends.TCPDialerCfg{
{
Cost: 0,
Redial: false,
},
},
}
SetTCPPeerDefaults(config)
if config.TCPPeers[0].Cost != 1.0 {
t.Errorf("Expected Cost to be 1.0, got %f", config.TCPPeers[0].Cost)
}
if !config.TCPPeers[0].Redial {
t.Error("Expected Redial to be true")
}
}
func TestSetWSPeerDefaults(t *testing.T) {
config := &BackendConfig{
WSPeers: []*backends.WebsocketDialerCfg{
{
Cost: 0,
Redial: false,
},
},
}
SetWSPeerDefaults(config)
if config.WSPeers[0].Cost != 1.0 {
t.Errorf("Expected Cost to be 1.0, got %f", config.WSPeers[0].Cost)
}
if !config.WSPeers[0].Redial {
t.Error("Expected Redial to be true")
}
}
func TestSetCmdlineUnixDefaults(t *testing.T) {
config := &ReceptorConfig{
ControlServices: []*controlsvc.CmdlineConfigUnix{
{
Service: "",
Permissions: 0,
},
},
}
SetCmdlineUnixDefaults(config)
if config.ControlServices[0].Service != "control" {
t.Errorf("Expected Service to be 'control', got '%s'", config.ControlServices[0].Service)
}
if config.ControlServices[0].Permissions != 0o600 {
t.Errorf("Expected Permissions to be 0o600, got %o", config.ControlServices[0].Permissions)
}
}
func TestSetLogLevelDefaults(t *testing.T) {
// Test with nil LogLevel
config := &ReceptorConfig{
LogLevel: nil,
}
SetLogLevelDefaults(config)
if config.LogLevel != nil {
t.Error("Expected LogLevel to remain nil")
}
// Test with empty LogLevel
config = &ReceptorConfig{
LogLevel: &logger.LoglevelCfg{
Level: "",
},
}
SetLogLevelDefaults(config)
if config.LogLevel.Level != "error" {
t.Errorf("Expected Level to be 'error', got '%s'", config.LogLevel.Level)
}
// Test with non-empty LogLevel
config = &ReceptorConfig{
LogLevel: &logger.LoglevelCfg{
Level: "debug",
},
}
SetLogLevelDefaults(config)
if config.LogLevel.Level != "debug" {
t.Errorf("Expected Level to remain 'debug', got '%s'", config.LogLevel.Level)
}
}
func TestSetNodeDefaults(t *testing.T) {
// Test with nil Node
config := &ReceptorConfig{
Node: nil,
}
SetNodeDefaults(config)
if config.Node == nil {
t.Error("Expected Node to be initialized")
} else if config.Node.DataDir != "/tmp/receptor" {
t.Errorf("Expected DataDir to be '/tmp/receptor', got '%s'", config.Node.DataDir)
}
// Test with empty DataDir
config = &ReceptorConfig{
Node: &types.NodeCfg{
DataDir: "",
},
}
SetNodeDefaults(config)
if config.Node.DataDir != "/tmp/receptor" {
t.Errorf("Expected DataDir to be '/tmp/receptor', got '%s'", config.Node.DataDir)
}
// Test with non-empty DataDir
config = &ReceptorConfig{
Node: &types.NodeCfg{
DataDir: "/custom/path",
},
}
SetNodeDefaults(config)
if config.Node.DataDir != "/custom/path" {
t.Errorf("Expected DataDir to remain '/custom/path', got '%s'", config.Node.DataDir)
}
}
func TestSetKubeWorkerDefaults(t *testing.T) {
config := &ReceptorConfig{
WorkKubernetes: []*workceptor.KubeWorkerCfg{
{
AuthMethod: "",
StreamMethod: "",
},
},
}
SetKubeWorkerDefaults(config)
if config.WorkKubernetes[0].AuthMethod != "incluster" {
t.Errorf("Expected AuthMethod to be 'incluster', got '%s'", config.WorkKubernetes[0].AuthMethod)
}
if config.WorkKubernetes[0].StreamMethod != "logger" {
t.Errorf("Expected StreamMethod to be 'logger', got '%s'", config.WorkKubernetes[0].StreamMethod)
}
// Test with non-empty values
config = &ReceptorConfig{
WorkKubernetes: []*workceptor.KubeWorkerCfg{
{
AuthMethod: "custom",
StreamMethod: "custom",
},
},
}
SetKubeWorkerDefaults(config)
if config.WorkKubernetes[0].AuthMethod != "custom" {
t.Errorf("Expected AuthMethod to remain 'custom', got '%s'", config.WorkKubernetes[0].AuthMethod)
}
if config.WorkKubernetes[0].StreamMethod != "custom" {
t.Errorf("Expected StreamMethod to remain 'custom', got '%s'", config.WorkKubernetes[0].StreamMethod)
}
}
ansible-receptor-0f6ae46/cmd/receptor-cl/ 0000775 0000000 0000000 00000000000 15177357701 0020364 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/cmd/receptor-cl/receptor.go 0000664 0000000 0000000 00000001561 15177357701 0022541 0 ustar 00root root 0000000 0000000 package main
import (
"os"
"github.com/ansible/receptor/cmd"
"github.com/ansible/receptor/pkg/logger"
"github.com/ansible/receptor/pkg/netceptor"
)
func main() {
logger := logger.NewReceptorLogger("")
var isV2 bool
newArgs := []string{}
for _, arg := range os.Args {
if arg == "--config-v2" {
isV2 = true
continue
}
newArgs = append(newArgs, arg)
}
os.Args = newArgs
if isV2 {
logger.Info("Running v2 cli/config")
cmd.Execute()
} else {
cmd.RunConfigV1()
}
for _, arg := range os.Args {
if arg == "--help" || arg == "-h" {
os.Exit(0)
}
}
if netceptor.MainInstance.BackendCount() == 0 {
logger.Warning("Nothing to do - no backends are running.\n")
logger.Warning("Run %s --help for command line instructions.\n", os.Args[0])
os.Exit(1)
}
logger.Info("Initialization complete\n")
<-netceptor.MainInstance.NetceptorDone()
}
ansible-receptor-0f6ae46/cmd/root.go 0000664 0000000 0000000 00000010076 15177357701 0017463 0 ustar 00root root 0000000 0000000 package cmd
import (
"fmt"
"os"
"reflect"
receptorVersion "github.com/ansible/receptor/internal/version"
"github.com/ansible/receptor/pkg/logger"
"github.com/ansible/receptor/pkg/netceptor"
"github.com/fsnotify/fsnotify"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
version bool
backendConfig *BackendConfig
)
const errMsgUnableToDecode = "unable to decode into struct, %v"
// rootCmd represents the base command when called without any subcommands.
var rootCmd = &cobra.Command{
Use: "receptor",
Short: "Run a receptor instance.",
Long: `
Receptor is an overlay network intended to ease the distribution of work across a large and dispersed collection of workers.
Receptor nodes establish peer-to-peer connections with each other via existing networks.
Once connected, the receptor mesh provides datagram (UDP-like) and stream (TCP-like) capabilities to applications, as well as robust unit-of-work handling with resiliency against transient network failures.`,
Run: handleRootCommand,
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.Flags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/receptor.yaml)")
rootCmd.Flags().BoolVar(&version, "version", false, "Show the Receptor version")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
l := logger.NewReceptorLogger("")
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName("receptor")
}
viper.AutomaticEnv()
viper.OnConfigChange(func(e fsnotify.Event) {
l.Info("Config file changed: %s\n", e.Name)
var newConfig *BackendConfig
viper.Unmarshal(&newConfig)
// used because OnConfigChange runs twice for some reason
// allows to skip empty first config
isEmpty := isConfigEmpty(reflect.ValueOf(*newConfig))
if isEmpty {
return
}
SetBackendConfigDefaults(newConfig)
isEqual := reflect.DeepEqual(*backendConfig, *newConfig)
if !isEqual {
// fmt.Println("reloading backends")
// this will do a reload of all reloadable services
// TODO: Optimize to only reload services that have config change
// NOTE: Make sure to account for two things
// if current config had two services then new config has zero cancel those backends
// if services has two items in a slice and one of them has changed iterate and reload on changed service
netceptor.MainInstance.CancelBackends()
l.Info("Reloading backends")
ReloadServices(reflect.ValueOf(*newConfig))
backendConfig = newConfig
return
}
l.Info("No reloadable backends were found.")
})
// TODO: use env to turn off watch config
viper.WatchConfig()
err := viper.ReadInConfig()
if err == nil {
fmt.Fprintln(os.Stdout, "Using config file:", viper.ConfigFileUsed())
}
}
func handleRootCommand(_ *cobra.Command, _ []string) {
if version {
fmt.Println(receptorVersion.Version)
os.Exit(0)
}
if cfgFile == "" && viper.ConfigFileUsed() == "" {
fmt.Fprintln(os.Stderr, "Could not locate config file (default is $HOME/receptor.yaml)")
os.Exit(1)
}
receptorConfig, err := ParseReceptorConfig()
if err != nil {
fmt.Printf(errMsgUnableToDecode, err)
os.Exit(1)
}
certifcatesConfig, err := ParseCertificatesConfig()
if err != nil {
fmt.Printf(errMsgUnableToDecode, err)
os.Exit(1)
}
backendConfig, err = ParseBackendConfig()
if err != nil {
fmt.Printf(errMsgUnableToDecode, err)
os.Exit(1)
}
isEmptyReceptorConfig := isConfigEmpty(reflect.ValueOf(*receptorConfig))
isEmptyReloadableServicesConfig := isConfigEmpty(reflect.ValueOf(*backendConfig))
RunConfigV2(reflect.ValueOf(*certifcatesConfig))
if isEmptyReceptorConfig && isEmptyReloadableServicesConfig {
fmt.Println("empty receptor config, skipping...")
os.Exit(0)
}
SetReceptorConfigDefaults(receptorConfig)
SetBackendConfigDefaults(backendConfig)
RunConfigV2(reflect.ValueOf(*receptorConfig))
RunConfigV2(reflect.ValueOf(*backendConfig))
}
ansible-receptor-0f6ae46/cmd/root_test.go 0000664 0000000 0000000 00000002464 15177357701 0020524 0 ustar 00root root 0000000 0000000 package cmd
import (
"os"
"testing"
)
func TestInitConfig(t *testing.T) {
// Save the original cfgFile value
originalCfgFile := cfgFile
defer func() {
cfgFile = originalCfgFile
}()
// Test with a specific config file
tmpfile, err := os.CreateTemp("", "receptor-config-*.yaml")
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tmpfile.Name())
configContent := `
node:
id: test-node
data-dir: /tmp/test-receptor
`
if _, err := tmpfile.Write([]byte(configContent)); err != nil {
t.Fatalf("Failed to write to temp file: %v", err)
}
if err := tmpfile.Close(); err != nil {
t.Fatalf("Failed to close temp file: %v", err)
}
// Set the config file
cfgFile = tmpfile.Name()
// Call initConfig
initConfig()
// Test with no config file (should use default)
cfgFile = ""
initConfig()
}
func TestExecute(t *testing.T) {
// Skip this test for now as it requires cobra import
t.Skip("Skipping TestExecute as it requires cobra import")
}
func TestHandleRootCommand(t *testing.T) {
// Skip this test for now as it calls os.Exit
t.Skip("Skipping TestHandleRootCommand as it calls os.Exit")
}
func TestReloadServices(t *testing.T) {
// Skip this test for now as it requires more complex setup
t.Skip("Skipping TestReloadServices as it requires more complex setup")
}
ansible-receptor-0f6ae46/docs/ 0000775 0000000 0000000 00000000000 15177357701 0016332 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/docs/Makefile 0000664 0000000 0000000 00000001771 15177357701 0020000 0 ustar 00root root 0000000 0000000 # Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@echo -e " doc8 doc8 linter"
@echo -e " lint All documentation linters (including linkcheck)"
@echo -e " rstcheck rstcheck linter"
.PHONY: doc8 help lint Makefile rstcheck server
doc8:
doc8 --ignore D001 .
# Documentation linters
lint: doc8 linkcheck rstcheck
rstcheck:
-rstcheck --recursive --warn-unknown-settings .
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
# Includes linkcheck
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
server:
python3 -m http.server
ansible-receptor-0f6ae46/docs/diagrams/ 0000775 0000000 0000000 00000000000 15177357701 0020121 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/docs/diagrams/AddListenerBackend.md 0000664 0000000 0000000 00000007137 15177357701 0024121 0 ustar 00root root 0000000 0000000 ```mermaid
sequenceDiagram
participant Application
participant Netceptor
participant Netceptor closures
participant Netceptor connectInfo ReadChan
participant Netceptor connectInfo WriteChan
Application->>+Netceptor: New
Netceptor-->>-Application: netceptor instance n1
Application->>+Backend tcp.go: NewTCPListener
Backend tcp.go-->>-Application: backendsTCPListener b1
Application->>+Netceptor: AddBackend(b1)
Netceptor->>Netceptor: backendCancel(context)
Netceptor->>+Backend tcp.go:(Backend Interface) b1.Start(context, waitgroup)
Backend tcp.go->>+Backend utils.go:listenerSession(context, waitgroup, logger, ListenFunc, AcceptFunc, CancelFunc)
Backend utils.go->>+Backend tcp.go:ListenFunc()
Backend tcp.go-->>-Backend utils.go:error
Backend utils.go->>Backend utils.go:create chan BackendSession
Backend utils.go-)+Backend sessChan Closure:go closure
loop Every time AcceptFunc is called
Backend sessChan Closure->>Backend sessChan Closure:AcceptFunc
end
Backend utils.go-->>-Backend tcp.go:chan (Backend Interface) BackendsSession
Backend tcp.go-->>-Netceptor: chan (Backend Interface) BackendsSession
Netceptor->>+Netceptor closures: go closure
Netceptor closures->>+Netceptor closures: go closure
Netceptor closures->>Netceptor closures: runProtocol(context, backend session, backendInfo)
Netceptor closures->>+Netceptor connectInfo ReadChan: make (chan []byte)
Netceptor closures->>+Netceptor connectInfo WriteChan: make (chan []byte)
Netceptor closures-)Netceptor closures: protoReader(backend session)
loop
Backend sessChan Closure->>Netceptor closures: sess.Recv(1 * time.Second)
Netceptor closures->>Netceptor connectInfo ReadChan: ci.ReadChan <- buf
end
loop
Netceptor connectInfo WriteChan->>Netceptor closures: message, more = <-ci.WriteChan
Netceptor closures->>Backend sessChan Closure: sess.Send(message)
end
Netceptor closures-)Netceptor closures: protoWriter(backend session)
Netceptor closures-)+Netceptor closures: s.sendInitialConnectMessage(ci, initDoneChan)
Netceptor closures-)Netceptor closures: s.translateStructToNetwork(MsgTypeRoute, s.makeRoutingUpdate(0))
Netceptor closures-)-Netceptor closures: context.Done()
Netceptor connectInfo ReadChan->>Netceptor closures: data = <-ci.ReadChan
alt established
alt MsgTypeData
Netceptor closures->>Netceptor closures: translateDataToMessage(data []byte)
else MsgTypeRoute
Netceptor closures->>Netceptor closures: s.handleRoutingUpdate(ri, remoteNodeID)
else MsgTypeServiceAdvertisement
Netceptor closures->>Netceptor closures: s.handleServiceAdvertisement(data, remoteNodeID)
else MsgTypeReject
Netceptor closures->>Netceptor closures: return error
end
else !established
alt msgType == MsgTypeRoute
Netceptor closures->>Netceptor closures: add connection
else msgType == MsgTypeReject
Netceptor closures->>Netceptor closures: return error
end
end
Netceptor closures->>Netceptor connectInfo WriteChan: ci.WriteChan <- ConnectMessage
Netceptor closures->>-Netceptor closures: wg Done()
Backend sessChan Closure->>Backend sessChan Closure:CancelFunc
Backend sessChan Closure-->>-Application:context.Done()
Netceptor->>Netceptor closures: wg Done()
Netceptor closures-->>-Netceptor:
Netceptor connectInfo ReadChan-->>-Netceptor closures: context.Done()
Netceptor connectInfo WriteChan-->>-Netceptor closures: context.Done()
```
ansible-receptor-0f6ae46/docs/diagrams/kubernetes_workflow.md 0000664 0000000 0000000 00000151623 15177357701 0024554 0 ustar 00root root 0000000 0000000 # Kubernetes Worker Workflow
This document provides comprehensive diagrams documenting the Kubernetes worker implementation in Receptor. The Kubernetes worker executes work units by creating and managing Kubernetes pods, with two different streaming methods for communication.
## Table of Contents
- [Kubernetes Worker Workflow](#kubernetes-worker-workflow)
- [Table of Contents](#table-of-contents)
- [Purpose](#purpose)
- [Core Capabilities](#core-capabilities)
- [Use Cases](#use-cases)
- [Overview](#overview)
- [Architecture Components](#architecture-components)
- [Core Structures](#core-structures)
- [Key Interfaces](#key-interfaces)
- [Workflow Diagrams](#workflow-diagrams)
- [Diagram 1: Overall Kubernetes Worker Flow](#diagram-1-overall-kubernetes-worker-flow)
- [Diagram 2: Authentication Flow](#diagram-2-authentication-flow)
- [Diagram 3: Pod Creation and Lifecycle](#diagram-3-pod-creation-and-lifecycle)
- [Diagram 4: Logger Streaming Method (Recommended)](#diagram-4-logger-streaming-method-recommended)
- [Diagram 5: Logger Reconnection Logic](#diagram-5-logger-reconnection-logic)
- [Diagram 6: TCP Streaming Method (Legacy)](#diagram-6-tcp-streaming-method-legacy)
- [Diagram 7: Error Handling and Retry Logic](#diagram-7-error-handling-and-retry-logic)
- [Key Features](#key-features)
- [Resilience Mechanisms](#resilience-mechanisms)
- [Configuration Flexibility](#configuration-flexibility)
- [Configuration Options](#configuration-options)
- [Environment Variables](#environment-variables)
- [Worker Configuration](#worker-configuration)
- [Work States](#work-states)
- [Error Scenarios and Handling](#error-scenarios-and-handling)
- [Kubernetes API Errors](#kubernetes-api-errors)
- [Kube API Timeout](#kube-api-timeout)
- [Kube API Connection Refused](#kube-api-connection-refused)
- [Kube API Domain Name Cannot Be Resolved](#kube-api-domain-name-cannot-be-resolved)
- [Kube API Returns Malformed Payload](#kube-api-returns-malformed-payload)
- [Kube API TLS Error](#kube-api-tls-error)
- [Kube API Is Too Old](#kube-api-is-too-old)
- [Kube API Is Too New](#kube-api-is-too-new)
- [Kube API Authentication Error](#kube-api-authentication-error)
- [Pod Lifecycle Errors](#pod-lifecycle-errors)
- [Pod Cannot Be Scheduled](#pod-cannot-be-scheduled)
- [Pod Is Killed](#pod-is-killed)
- [Container Execution Errors](#container-execution-errors)
- [Container Executing Work Is Killed](#container-executing-work-is-killed)
- [Other Containers in Pod Are Killed](#other-containers-in-pod-are-killed)
- [Invalid Input Handling](#invalid-input-handling)
- [Invalid PodTemplate Provided](#invalid-podtemplate-provided)
- [Long-Running Job Scenarios](#long-running-job-scenarios)
- [Job Running for 40+ Hours](#job-running-for-40-hours)
- [Context Cancellation](#context-cancellation)
- [Log and Disk I/O Errors](#log-and-disk-io-errors)
- [Log Returned by Kube API Is Too Long](#log-returned-by-kube-api-is-too-long)
- [Cannot Write Logs to Disk](#cannot-write-logs-to-disk)
- [Known Unknowns](#known-unknowns)
- [Related Documentation](#related-documentation)
## Purpose
The Kubernetes worker (`kubernetes.go`) enables Receptor to execute work units by creating and managing Kubernetes pods. This component provides a way to run containerized workloads in Kubernetes clusters.
### Core Capabilities
The Kubernetes worker provides:
1. **Creating Kubernetes pods** on demand with specified container images and commands
2. **Streaming input data** to the pod via stdin
3. **Capturing output** (stdout/stderr) from the pod via Kubernetes log streaming or TCP
4. **Managing pod lifecycle** from creation through completion, handling both success and failure scenarios
### Use Cases
The Kubernetes worker can execute any containerized workload, making it suitable for running custom scripts, batch jobs, data processing tasks, or any containerized service that accepts stdin and produces stdout/stderr.
One notable use case is **Ansible Automation Platform (AAP)**, which uses this Kubernetes worker to execute Ansible playbooks in Kubernetes clusters. AAP's Controller service submits work units to Receptor, which then uses this Kubernetes worker to execute them in pods, providing scalability, isolation, and resource management capabilities.
For more details on how work is submitted to Receptor, see:
- [Receptor Work Submit Flow](./receptor_work_submit_flow.md)
## Overview
The Kubernetes worker (`KubeUnit`) implements the `WorkUnit` interface to execute work by:
1. Creating Kubernetes pods with specified container images/commands
2. Streaming stdin data to the pod
3. Streaming stdout/stderr logs from the pod
4. Monitoring pod lifecycle and handling completion/failures
Two streaming methods are supported:
- **Logger Method**: Uses Kubernetes log streaming API for stdout/stderr (recommended for K8s >= 1.23.14)
- **Stdin streaming**: Uses SPDY protocol via Kubernetes attach API (similar to `kubectl attach`)
- **Stdout streaming**: Uses Kubernetes logs API with automatic reconnection support
- **TCP Method**: Pod connects back via TCP (legacy, simpler but less robust)
**What is SPDY?** SPDY is a deprecated HTTP/2 precursor protocol that Kubernetes uses for streaming operations like `kubectl exec` and `kubectl attach`. The SPDY executor creates a multiplexed connection to the Kubernetes API server that allows bidirectional streaming to/from containers. Receptor uses the Kubernetes client-go library's `remotecommand.NewSPDYExecutor()` to create these connections for streaming stdin to pods.
## Architecture Components
### Core Structures
- **KubeUnit**: Main work unit implementation for Kubernetes
- **KubeExtraData**: Stores pod configuration (image, command, namespace, etc.)
- **KubeAPIWrapper**: Wrapper around Kubernetes client-go for testability
- **KubeWorkerCfg**: Configuration object for command-line setup
### Key Interfaces
- **KubeAPIer**: Interface for Kubernetes API operations (allows mocking for tests)
- **WorkUnit**: Base interface that KubeUnit implements
## Workflow Diagrams
### Diagram 1: Overall Kubernetes Worker Flow
This diagram shows the complete flow from work submission to completion using the logger streaming method.
```mermaid
sequenceDiagram
participant Client
participant Workceptor as Work Service
(Workceptor)
participant KubeUnit as KubeUnit
participant KubeAPI as Kubernetes API
participant Pod
participant Logger as Log Stream
participant StdoutFile as stdout file
Note over Client, StdoutFile: Work submission with Kubernetes worker
Client->>Workceptor: Submit work (worktype: kubernetes)
Workceptor->>KubeUnit: Create KubeUnit instance
Workceptor->>KubeUnit: Start()
Note over KubeUnit: UpdateBasicStatus(WorkStatePending,
"Connecting to Kubernetes")
KubeUnit->>KubeUnit: connectToKube()
alt AuthMethod: kubeconfig
KubeUnit->>KubeUnit: connectUsingKubeconfig()
Note over KubeUnit: Load kubeconfig file
or use default
else AuthMethod: incluster
KubeUnit->>KubeUnit: connectUsingIncluster()
Note over KubeUnit: Use in-cluster config
else AuthMethod: runtime
KubeUnit->>KubeUnit: connectUsingKubeconfig()
Note over KubeUnit: Use runtime-provided config
end
KubeUnit->>KubeAPI: Create clientset
KubeAPI-->>KubeUnit: Kubernetes clientset
KubeUnit->>KubeUnit: RunWorkUsingLogger()
alt Pod exists (resume)
KubeUnit->>KubeAPI: Get pod by name
KubeAPI-->>KubeUnit: Existing pod
Note over KubeUnit: skipStdin = true
else New pod
KubeUnit->>KubeUnit: CreatePod()
KubeUnit->>KubeAPI: Create pod manifest
KubeAPI-->>KubeUnit: Pod created
Note over KubeUnit: Wait for pod Ready state
KubeUnit->>KubeAPI: Watch pod events
KubeAPI-->>KubeUnit: Pod status updates
alt Pod Ready
KubeAPI-->>KubeUnit: Pod Running + Ready
else Pod Failed
KubeAPI-->>KubeUnit: Pod Failed
KubeUnit->>KubeUnit: Mark as Failed
else Pod Completed
KubeAPI-->>KubeUnit: Pod Succeeded
KubeUnit->>KubeUnit: Handle completion
end
Note over KubeUnit: skipStdin = false
end
alt !skipStdin (new pod)
KubeUnit->>KubeAPI: Create SPDY executor (attach)
KubeAPI-->>KubeUnit: Executor ready
KubeUnit->>KubeUnit: Wait for container Running state
loop Retry until Running
KubeUnit->>KubeAPI: Get pod status
KubeAPI-->>KubeUnit: Container state
alt Container Waiting
Note over KubeUnit: Retry with Fibonacci backoff
else Container Terminated
KubeUnit->>KubeUnit: Mark as Failed
end
end
par Stream stdin
KubeUnit->>KubeAPI: Stream stdin via SPDY
KubeAPI->>Pod: Forward stdin data
Pod-->>KubeAPI: EOF received
KubeAPI-->>KubeUnit: Stream complete
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateRunning)
and Stream stdout
KubeUnit->>KubeAPI: Get log stream
KubeAPI->>Logger: Open log stream (with timestamps)
Logger->>KubeUnit: Stream log lines
loop Process log lines
Logger->>KubeUnit: Read log line
KubeUnit->>KubeUnit: ProcessLogLine()
(strip timestamp,
detect duplicates)
KubeUnit->>StdoutFile: Write processed line
end
alt EOF received
KubeUnit->>KubeAPI: Get pod status
alt Container Terminated
Logger->>KubeUnit: EOF (expected)
KubeUnit->>KubeUnit: Check exit code and termination reason
alt Exit code 0
KubeUnit->>KubeUnit: Mark as Succeeded
else Exit code != 0 AND reason is "Completed"/"Error"
KubeUnit->>KubeUnit: Mark as Succeeded
(normal completion with error)
else Exit code != 0 AND reason is "OOMKilled"/"Evicted"/etc
KubeUnit->>KubeUnit: Mark as Failed
(execution interrupted)
end
else Container Running
Logger->>KubeUnit: EOF (unexpected - may be 4hr timeout)
KubeUnit->>KubeUnit: Reconnect logic
Note over KubeUnit: KubeLoggingWithReconnect()
end
end
end
else skipStdin (resume)
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateRunning)
KubeUnit->>KubeAPI: Get log stream (resume from sinceTime)
loop Stream logs
KubeUnit->>KubeAPI: Read log lines
KubeAPI->>KubeUnit: Log lines with timestamps
KubeUnit->>KubeUnit: ProcessLogLine()
(skip duplicates)
KubeUnit->>StdoutFile: Write new lines
end
end
KubeUnit->>Workceptor: WorkStateSucceeded or WorkStateFailed
Workceptor-->>Client: Work completion status
```
### Diagram 2: Authentication Flow
This diagram details the three authentication methods supported by the Kubernetes worker.
```mermaid
flowchart TD
Start([KubeUnit.Start]) --> Connect[connectToKube]
Connect --> AuthMethod{Check authMethod}
AuthMethod -->|kubeconfig| Kubeconfig[connectUsingKubeconfig]
AuthMethod -->|incluster| InCluster[connectUsingIncluster]
AuthMethod -->|runtime| Runtime[connectUsingKubeconfig
with runtime config]
Kubeconfig --> CheckConfig{KubeConfig
provided?}
CheckConfig -->|No| DefaultConfig[Load default kubeconfig
~/.kube/config]
CheckConfig -->|Yes| LoadConfig[Read KubeConfig
from file/param]
DefaultConfig --> ParseDefault[Parse config
get namespace]
LoadConfig --> ParseCustom[Parse config bytes
get namespace]
ParseDefault --> BuildDefault[BuildConfigFromFlags
masterURL, kubeconfig]
ParseCustom --> BuildCustom[ClientConfigFromBytes
then ClientConfig]
InCluster --> ClusterConfig[rest.InClusterConfig
Read from:
/var/run/secrets/kubernetes.io/serviceaccount]
BuildDefault --> CheckNamespace{Namespace
provided?}
BuildCustom --> CheckNamespace
ClusterConfig --> CheckNamespace
CheckNamespace -->|No| GetNamespace[Get namespace from
kubeconfig context]
CheckNamespace -->|Yes| SetNamespace[Use provided namespace]
GetNamespace --> SetConfigVars
SetNamespace --> SetConfigVars[Set config QPS/Burst
Set rate limiter]
SetConfigVars --> CreateClientset[kubernetes.NewForConfig
Create clientset]
CreateClientset --> Done([Authentication Complete])
Runtime --> RuntimeCheck{Runtime config
provided?}
RuntimeCheck -->|No| RuntimeError[Error: secret_kube_config
must be provided]
RuntimeCheck -->|Yes| LoadRuntime[Load config from params]
LoadRuntime --> BuildCustom
```
### Diagram 3: Pod Creation and Lifecycle
This diagram shows how pods are created and how the system waits for them to become ready.
```mermaid
sequenceDiagram
participant KubeUnit
participant KubeAPI as Kubernetes API
participant Pod
participant Watch as Watch Interface
Note over KubeUnit, Watch: Pod creation and readiness check
KubeUnit->>KubeUnit: CreatePod()
alt Custom Pod Spec (KubePod provided)
KubeUnit->>KubeUnit: Decode YAML/JSON pod spec
KubeUnit->>KubeUnit: Validate "worker" container exists
KubeUnit->>KubeUnit: Set Stdin=true, StdinOnce=true
KubeUnit->>KubeUnit: Set RestartPolicy=Never
KubeUnit->>KubeUnit: Set GenerateName from namePrefix
else Simple Pod Spec
KubeUnit->>KubeUnit: Create PodSpec with:
- Image from config
- Command from config
- Args from params
- Container name: "worker"
- Stdin=true, StdinOnce=true
- RestartPolicy=Never
end
KubeUnit->>KubeUnit: Add environment variables
(if provided)
KubeUnit->>KubeAPI: Create pod
KubeAPI->>Pod: Schedule pod
KubeAPI-->>KubeUnit: Pod created (with generated name)
KubeUnit->>KubeUnit: UpdateFullStatus(WorkStatePending,
"Pod created")
KubeUnit->>KubeUnit: Store pod.Name in ExtraData
Note over KubeUnit, Watch: Wait for pod to be running and ready
KubeUnit->>KubeAPI: Create ListWatch with fieldSelector
KubeAPI-->>KubeUnit: ListWatch interface
alt Timeout configured
KubeUnit->>KubeUnit: Create context with
podPendingTimeout
else No timeout
KubeUnit->>KubeUnit: Use parent context
end
KubeUnit->>KubeUnit: Sleep 2 seconds
KubeUnit->>Watch: UntilWithSync(condition: podRunningAndReady)
Watch->>Pod: Watch pod events
loop Watch events
Pod->>Watch: Pod event (Added/Modified)
Watch->>KubeUnit: Event received
KubeUnit->>KubeUnit: podRunningAndReady() check
alt Pod Phase: Running or Pending
KubeUnit->>KubeUnit: Check PodReady condition
alt PodReady == True
Watch-->>KubeUnit: Pod ready event
else ContainersReady == False
KubeUnit->>KubeUnit: Check container status
alt ImagePullBackOff
KubeUnit->>KubeUnit: Retry check (max 3 times)
alt Retries exhausted
Watch-->>KubeUnit: Error: ErrImagePullBackOff
end
end
end
else Pod Phase: Failed
Watch-->>KubeUnit: Error: ErrPodFailed
else Pod Phase: Succeeded
Watch-->>KubeUnit: Error: ErrPodCompleted
else Pod Deleted (during startup)
Watch-->>KubeUnit: Error: NotFound
end
end
alt Pod Ready
KubeUnit->>KubeUnit: Store pod object
KubeUnit-->>KubeUnit: CreatePod() returns nil
else Error
KubeUnit->>KubeUnit: Handle error
alt ErrPodCompleted
KubeUnit->>KubeUnit: Check exit code
alt Exit code != 0
KubeUnit-->>KubeUnit: Return container failure error
else Exit code == 0
KubeUnit-->>KubeUnit: Return ErrPodCompleted
end
else Other error
KubeUnit->>KubeAPI: Try to get pod logs (fallback)
KubeUnit-->>KubeUnit: Return error with details
end
end
```
### Diagram 4: Logger Streaming Method (Recommended)
This diagram shows the logger streaming method flow, including stdin streaming and log retrieval with reconnection support.
```mermaid
sequenceDiagram
participant KubeUnit
participant KubeAPI as Kubernetes API
participant Pod
participant LogStream as Log Stream
participant StdoutFile as stdout file
participant StdinFile as stdin file
Note over KubeUnit, StdinFile: Logger streaming method workflow
KubeUnit->>KubeUnit: RunWorkUsingLogger()
alt Resume existing pod
KubeUnit->>KubeAPI: Get pod by name
KubeAPI-->>KubeUnit: Existing pod
Note over KubeUnit: skipStdin = true
(stdin already sent in initial run,
only need to reconnect to stdout logs)
else Create new pod
KubeUnit->>KubeAPI: CreatePod()
KubeAPI-->>KubeUnit: Pod created
Note over KubeUnit: skipStdin = false
(must send stdin to new pod)
end
Note over KubeUnit: streamWait.Add(2) - always expects 2 completions
alt !skipStdin (new pod)
Note over KubeUnit: Will launch 2 goroutines (stdin + stdout)
Note over KubeUnit: Wait for container Running state
loop Check container state
KubeUnit->>KubeAPI: Get pod status
KubeAPI-->>KubeUnit: Container status
alt Container Waiting
KubeUnit->>KubeUnit: Sleep with Fibonacci backoff
KubeUnit->>KubeAPI: Retry get pod
else Container Terminated
KubeUnit->>KubeUnit: Mark as Failed
else Container Running
Note over KubeUnit: Break loop
end
end
KubeUnit->>KubeAPI: Create SPDY executor
(SubResource attach)
KubeAPI-->>KubeUnit: Executor ready
par Stream stdin to pod (goroutine 1)
KubeUnit->>StdinFile: Open stdin file
StdinFile-->>KubeUnit: File reader
KubeUnit->>KubeAPI: StreamWithContext(executor,
StreamOptions{Stdin: reader})
loop Stream stdin data
StdinFile->>KubeUnit: Read chunk
KubeUnit->>KubeAPI: Send chunk via SPDY
KubeAPI->>Pod: Forward to container stdin
end
StdinFile->>KubeUnit: EOF
KubeUnit->>KubeAPI: Close stdin stream
KubeAPI->>Pod: Send EOF
Pod-->>KubeAPI: Stdin closed
KubeAPI-->>KubeUnit: Stream complete
alt Stdin stream success
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateRunning)
else Stdin stream error
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateFailed)
end
and Stream stdout from pod (goroutine 2)
KubeUnit->>StdoutFile: Open stdout file
StdoutFile-->>KubeUnit: File writer
alt Reconnect supported (K8s >= 1.23.14)
KubeUnit->>KubeUnit: KubeLoggingWithReconnect()
Note over KubeUnit: See Diagram 5 for details
else No reconnect (K8s < 1.23.14)
KubeUnit->>KubeUnit: kubeLoggingNoReconnect()
KubeUnit->>KubeAPI: GetLogs(Follow=true,
Timestamps=false)
KubeAPI->>LogStream: Open log stream
LogStream-->>KubeUnit: Stream handle
KubeUnit->>LogStream: io.Copy(stdout, stream)
loop Read log stream
LogStream->>KubeUnit: Log line
KubeUnit->>StdoutFile: Write line
end
alt Stream error
LogStream-->>KubeUnit: Error (EOF or other)
Note over KubeUnit: Log stream terminated
(may be due to rotation or 4hr timeout)
end
end
end
else skipStdin (resume)
Note over KubeUnit: Will launch 1 goroutine (stdout only)
streamWait.Done() called immediately
(no stdin goroutine needed)
KubeUnit->>KubeUnit: streamWait.Done()
(count stdin as "complete")
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateRunning)
Note over KubeUnit: Launch stdout goroutine
KubeUnit->>KubeAPI: GetLogs(Follow=true,
Timestamps=true,
SinceTime=lastTimestamp)
loop Stream logs from sinceTime
KubeUnit->>KubeAPI: Read log lines
KubeAPI->>LogStream: Stream lines with timestamps
LogStream->>KubeUnit: Log line
KubeUnit->>KubeUnit: ProcessLogLine()
(skip duplicates)
KubeUnit->>StdoutFile: Write new lines only
end
end
Note over KubeUnit: streamWait.Wait() - blocks until 2 completions
(New pod: 2 goroutines | Resume: 1 goroutine + 1 immediate Done())
alt Both streams successful
KubeUnit->>KubeUnit: UpdateFullStatus(WorkStateSucceeded)
else Error occurred
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateFailed)
end
```
### Diagram 5: Logger Reconnection Logic
This diagram details the sophisticated reconnection logic used in `KubeLoggingWithReconnect()` to handle log stream disconnections.
```mermaid
flowchart TD
Start([KubeLoggingWithReconnect]) --> MainLoop[Main reconnection loop]
MainLoop --> CheckStdin{stdinErr
!= nil?}
CheckStdin -->|Yes| Exit1([Exit - stdin failed])
CheckStdin -->|No| GetPod[Get pod with Fibonacci
retry backoff]
GetPod --> PodError{Pod get
error?}
PodError -->|Yes| RetryPod{Retries
remaining?}
RetryPod -->|Yes| SleepPod[Sleep with
Fibonacci backoff]
SleepPod --> GetPod
RetryPod -->|No| Exit2([Exit - pod error])
PodError -->|No| ResetSuccess[Reset successfulWrite flag]
ResetSuccess --> GetLogs[Get log stream
with timestamps
SinceTime sinceTime]
GetLogs --> LogError{Log stream
error?}
LogError -->|Yes| Exit3([Exit - log stream error])
LogError -->|No| ReadLoop[Read loop: process log lines]
ReadLoop --> ReadLine[ReadString '\n']
ReadLine --> ReadError{Read
error?}
ReadError -->|No| ProcessLine[ProcessLogLine:
1. Parse timestamp
2. Strip timestamp
3. Check for duplicates
4. Update sinceTime]
ProcessLine --> CheckSkip{shouldSkip
= true?}
CheckSkip -->|Yes| ReadLoop
CheckSkip -->|No| WriteStdout[Write to stdout file]
WriteStdout --> SetSuccess[Set successfulWrite = true
Reset retry counter]
SetSuccess --> ReadLoop
ReadError -->|Yes| CheckCancel{Context
Canceled?}
CheckCancel -->|Yes| CheckState{State !=
Succeeded/Failed?}
CheckState -->|Yes| Exit4([Exit - mark as Failed])
CheckState -->|No| Exit5([Exit - already complete])
CheckCancel -->|No| CheckEOF{Error ==
EOF?}
CheckEOF -->|No| RetryRead{Retries
remaining?}
RetryRead -->|Yes| SleepRead[Sleep with
Fibonacci backoff]
SleepRead --> MainLoop
RetryRead -->|No| Exit6([Exit - non-EOF error])
CheckEOF -->|Yes| GetPodAfterEOF[Get pod status
after EOF]
GetPodAfterEOF --> PodErrEOF{Pod get
error?}
PodErrEOF -->|Yes| MainLoop
PodErrEOF -->|No| CheckContainer{Found worker
container?}
CheckContainer -->|No| Exit7([Exit - container not found])
CheckContainer -->|Yes| CheckState2{Container
state?}
CheckState2 -->|Running| SleepEOF[Sleep with Fibonacci backoff
Possible 4hr timeout or
transition to terminated
No retry limit - continues until
terminated or context canceled]
SleepEOF --> MainLoop
CheckState2 -->|Terminated| CheckExitCode{Exit
code?}
CheckExitCode -->|0| LogSuccess[Mark as Succeeded
Log: Completed successfully]
CheckExitCode -->|!= 0| CheckReason{Terminated
reason?}
CheckReason -->|Completed/Error| LogErrorComplete[Mark as Succeeded
Log: Completed with error
exit code
Normal completion]
CheckReason -->|OOMKilled/Evicted/etc| LogInterrupted[Mark as Failed
Set stdoutErr
Log: Execution interrupted]
LogSuccess --> CheckLastLine{Last line
has data?}
LogErrorComplete --> CheckLastLine
LogInterrupted --> Exit9([Exit - interrupted])
CheckLastLine -->|Yes| ProcessLast[ProcessLogLine for last line]
ProcessLast --> WriteLast[Write last line to stdout]
WriteLast --> Exit10([Exit - normal completion])
CheckLastLine -->|No| Exit10
CheckState2 -->|Unknown| LogUnknown[Debug log: Will continue
Misleading: actually fails immediately
Mark as Failed + set stdoutErr]
LogUnknown --> Exit11([Exit - job failed])
```
### Diagram 6: TCP Streaming Method (Legacy)
This diagram shows the TCP streaming method where the pod connects back to the host.
```mermaid
sequenceDiagram
participant KubeUnit
participant TCPListener as TCP Listener
participant KubeAPI as Kubernetes API
participant Pod
participant TCPConn as TCP Connection
participant StdinFile as stdin file
participant StdoutFile as stdout file
Note over KubeUnit, StdoutFile: TCP streaming method workflow
KubeUnit->>KubeUnit: runWorkUsingTCP()
Note over KubeUnit: Step 1: Create TCP listener
KubeUnit->>KubeUnit: getDefaultInterface()
Find first non-loopback interface
KubeUnit->>TCPListener: Listen on interface IP:0
(auto-assign port)
TCPListener-->>KubeUnit: Listener ready
KubeUnit->>KubeUnit: Split address to get host and port
par Accept connection (async)
TCPListener->>TCPListener: Accept() - wait for connection
and Create pod with env vars
KubeUnit->>KubeUnit: CreatePod() with env:
RECEPTOR_HOST={host}
RECEPTOR_PORT={port}
KubeUnit->>KubeAPI: Create pod manifest
KubeAPI->>Pod: Schedule and start pod
Pod->>Pod: Read RECEPTOR_HOST and
RECEPTOR_PORT env vars
Pod->>TCPConn: Dial TCP connection
to {host}:{port}
TCPConn->>TCPListener: Connection established
TCPListener-->>TCPConn: Connection accepted
end
TCPListener->>KubeUnit: Connection received
KubeUnit->>TCPListener: Close listener (only accept one)
TCPListener->>TCPConn: TCP connection object
Note over KubeUnit: Step 2: Stream stdin to pod
KubeUnit->>StdinFile: Open stdin file
StdinFile-->>KubeUnit: File reader
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStatePending,
"Sending stdin to pod")
par Write stdin
loop Stream stdin data
StdinFile->>KubeUnit: Read chunk
KubeUnit->>TCPConn: Write chunk
TCPConn->>Pod: Forward data
end
StdinFile->>KubeUnit: EOF
KubeUnit->>TCPConn: CloseWrite() - send FIN
TCPConn->>Pod: EOF signal
and Monitor stdin completion
KubeUnit->>KubeUnit: Wait for stdin.Done()
alt EOF success
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateRunning)
else stdin error
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateFailed)
end
end
Note over KubeUnit: Step 3: Read stdout from pod
KubeUnit->>StdoutFile: Open stdout file
StdoutFile-->>KubeUnit: File writer
loop Read stdout data
TCPConn->>KubeUnit: Read chunk
KubeUnit->>StdoutFile: Write chunk
end
TCPConn->>KubeUnit: EOF (connection closed)
alt Context not canceled
KubeUnit->>KubeUnit: UpdateBasicStatus(WorkStateSucceeded,
"Finished")
end
Note over KubeUnit: TCP method limitations:
- No automatic reconnection
- Simpler but less robust
- Requires pod to connect back
```
### Diagram 7: Error Handling and Retry Logic
This diagram shows the comprehensive error handling and retry mechanisms throughout the Kubernetes worker.
```mermaid
flowchart TD
Start([Error Handling Overview]) --> ErrorTypes[Error Categories]
ErrorTypes --> PodErrors[Pod Lifecycle Errors]
ErrorTypes --> StreamErrors[Stream Errors]
ErrorTypes --> AuthErrors[Authentication Errors]
ErrorTypes --> TimeoutErrors[Timeout Errors]
PodErrors --> PodFailed[ErrPodFailed:
Pod Phase = Failed]
PodErrors --> PodCompleted[ErrPodCompleted:
Pod Phase = Succeeded]
PodErrors --> ImagePullBack[ErrImagePullBackOff:
Container waiting - ImagePullBackOff]
PodErrors --> NotFound[Pod NotFound:
Pod deleted during startup
or doesn't exist]
StreamErrors --> SPDYCreationError[SPDY Executor Creation Error:
Cannot create executor]
StreamErrors --> StdinStreamError[Stdin Streaming Error:
Error streaming to pod]
StreamErrors --> StdoutError[Stdout Stream Error:
Log stream EOF/timeout]
StreamErrors --> NonEOFError[Non-EOF Error:
Unexpected stream error]
AuthErrors --> ConfigError[Config Parse Error:
Invalid kubeconfig]
AuthErrors --> ClusterError[InCluster Error:
Not running in cluster]
TimeoutErrors --> PodPendingTimeout[Pod Pending Timeout:
Pod didn't become ready]
PodFailed --> HandlePodFailed[Handle: Return error with details]
PodCompleted --> HandlePodCompleted[Handle:
1. Check container exit code
2. If exit != 0, return error
3. If exit == 0, return ErrPodCompleted]
ImagePullBack --> HandleImagePull[Handle:
1. Retry check 3 times
2. If still failing, return ErrImagePullBackOff]
NotFound --> HandleNotFound[Handle: Return error with details]
SPDYCreationError --> FailSPDY[Handle:
Mark work as Failed immediately
No retries]
StdinStreamError --> RetryStdin{Retries
remaining?}
RetryStdin -->|Yes| RetryStdinAction[Retry StreamWithContext
200ms delay between retries
Max: GetKubeRetryCount times]
RetryStdinAction --> RetryStdin
RetryStdin -->|No| FailStdin[Mark work as Failed
Signal stdout to stop]
StdoutError --> CheckContext{Context
Canceled?}
CheckContext -->|Yes| CheckState{State !=
Succeeded/Failed?}
CheckState -->|Yes| FailContext[Mark as Failed]
CheckState -->|No| ExitContext[Already complete - exit]
CheckContext -->|No| CheckEOF{Error ==
EOF?}
CheckEOF -->|Yes| CheckPodState[Get pod state]
CheckPodState --> PodRunning{Container
Running?}
PodRunning -->|Yes| ReconnectLogs[Reconnect log stream
with Fibonacci backoff
Continue indefinitely
No retry limit - continues until
container terminates or context canceled]
PodRunning -->|No| PodTerminated{Container
Terminated?}
PodTerminated -->|Yes| CheckExit{Exit
code == 0?}
CheckExit -->|Yes| Success[Mark as Succeeded]
CheckExit -->|No| CheckReason{Terminated
reason?}
CheckReason -->|Completed/Error| SuccessNormal[Mark as Succeeded
Normal completion with error]
CheckReason -->|OOMKilled/etc| FailInterrupted[Mark as Failed
Interrupted execution]
CheckEOF -->|No| RetryNonEOF{Retries
remaining?}
RetryNonEOF -->|Yes| RetryLogRead[Retry read with Fibonacci backoff]
RetryLogRead --> ReconnectLogs
RetryNonEOF -->|No| FailNonEOF[Mark as Failed]
NonEOFError --> RetryNonEOF
ConfigError --> FailAuth[Mark as Failed:
Cannot authenticate]
ClusterError --> FailAuth
PodPendingTimeout --> FailTimeout[Mark as Failed:
Pod didn't become ready]
```
## Key Features
### Resilience Mechanisms
1. **Automatic Reconnection**: Logger method automatically reconnects on stream disconnection
2. **Retry Logic**: Fibonacci backoff for transient errors, with no retry limit for EOF with Running state
3. **Duplicate Detection**: Timestamp-based log line deduplication during reconnections
4. **Timeout Handling**: Configurable timeouts for pod pending state
5. **Graceful Degradation**: Falls back to no-reconnect method for older Kubernetes versions
6. **Long-Running Job Support**: Continues attempting reconnection indefinitely when EOF occurs with Running containers (handles 4-hour log stream timeouts)
### Configuration Flexibility
1. **Multiple Auth Methods**: kubeconfig, incluster, or runtime
2. **Runtime Overrides**: Allow dynamic image/command/params/pod specification
3. **Stream Method Selection**: Choose logger or TCP method
4. **Rate Limiting**: Configurable QPS and burst for API calls
5. **Timeout Configuration**: Environment variables for timeouts and retry counts
## Configuration Options
### Environment Variables
- `RECEPTOR_KUBE_TIMEOUT_START`: Base timeout for retries (default: 1s, max: 1m)
- `RECEPTOR_KUBE_RETRY_COUNT`: Number of retries (default: 5, max: 100)
- `RECEPTOR_KUBE_SUPPORT_RECONNECT`: Enable/disable/auto reconnect (default: enabled)
- `RECEPTOR_KUBE_CLIENTSET_QPS`: API rate limit QPS (default: 100)
- `RECEPTOR_KUBE_CLIENTSET_BURST`: API rate limit burst (default: 10x QPS)
- `RECEPTOR_KUBE_CLIENTSET_RATE_LIMITER`: Rate limiter type (never/always/tokenbucket)
### Worker Configuration
```yaml
- work-kubernetes:
workType: k8s-worker
namespace: default
image: my-image:latest
command: /bin/sh
params: -c
authMethod: incluster
streamMethod: logger
allowRuntimeCommand: false
allowRuntimeParams: true
deletePodOnRestart: true
```
## Work States
1. **WorkStatePending (0)**: Initial state, connecting to Kubernetes or creating pod
2. **WorkStateRunning (1)**: Pod running, streaming data
3. **WorkStateSucceeded (2)**: Work completed successfully. Determined by:
- Exit code 0, OR
- Exit code != 0 but termination reason is "Completed" or "Error" (indicates normal program completion with error)
4. **WorkStateFailed (3)**: Work failed. Determined by:
- Exit code != 0 AND termination reason indicates interruption (OOMKilled, Evicted, etc.), OR
- Other errors occurred during execution (stream errors, pod failures, etc.)
5. **WorkStateCanceled (4)**: Work was canceled, pod deleted
## Error Scenarios and Handling
This section documents how the Kubernetes worker handles various error conditions. Understanding these scenarios is critical for debugging production issues and ensuring reliable job execution.
### Kubernetes API Errors
#### Kube API Timeout
**What happens:**
- Timeouts occur during API calls (Get, Create, Watch, GetLogs)
- The underlying `client-go` library uses the context timeout if provided
- If `podPendingTimeout` is configured, pod readiness checks will timeout
- Watch operations timeout when the context is canceled
**Current handling:**
- ✅ **Pod creation**: Uses `context.WithTimeout()` if `podPendingTimeout` is set. Returns error if timeout exceeded
- ✅ **Pod readiness wait**: `UntilWithSync()` respects context timeout. Returns timeout error
- ⚠️ **API calls without explicit timeout**: Relies on context cancellation or underlying HTTP client timeouts
- ⚠️ **Retry logic**: Retries use Fibonacci backoff but may continue indefinitely if context isn't canceled
**Impact:** Jobs will fail with timeout error. Pod may remain in pending state.
#### Kube API Connection Refused
**What happens:**
- Cannot connect to Kubernetes API server (server down, network issues, firewall)
**Current handling:**
- ❌ **No explicit retry**: Connection errors during `connectToKube()` are returned immediately
- ❌ **No retry in CreatePod()**: TODO comment mentions adding retry logic but not implemented
- ✅ **Retry in log stream**: `kubeLoggingConnectionHandler()` retries up to `GetKubeRetryCount()` times with simple delay (not Fibonacci)
- ✅ **Retry in Get pod**: When resuming, retries 5 times with 200ms delay
- ✅ **Retry in log reconnection**: Main loop retries getting pod with Fibonacci backoff
**Impact:** Initial connection failure causes immediate job failure. However, transient connection issues during execution are retried.
#### Kube API Domain Name Cannot Be Resolved
**What happens:**
- DNS resolution fails for Kubernetes API server hostname
**Current handling:**
- ❌ **No explicit handling**: Treated same as connection refused
- ⚠️ **Error propagation**: Error from `BuildConfigFromFlags()` or `InClusterConfig()` bubbles up to `connectToKube()` which returns error immediately
**Impact:** Job fails at startup with DNS resolution error.
#### Kube API Returns Malformed Payload
**What happens:**
- API server returns invalid JSON/YAML or unexpected response structure
**Current handling:**
- ⚠️ **Partial handling**: Pod YAML/JSON decoding errors are caught in `CreatePod()` and returned
- ❌ **Watch/list responses**: No explicit validation of malformed API responses
- ❌ **Log stream responses**: No validation of log stream format
- ⚠️ **Error propagation**: Depends on `client-go` library to handle malformed responses
**Impact:** Likely to cause panics or unexpected behavior. Partial protection for pod spec decoding.
#### Kube API TLS Error
**What happens:**
- TLS handshake fails, certificate validation errors, certificate expired
**Current handling:**
- ❌ **No explicit handling**: TLS errors from `client-go` are propagated as-is
- ⚠️ **Error location**: TLS errors occur during `NewForConfig()` or API calls
- ⚠️ **Certificate validation**: Handled by `client-go` based on `rest.Config` TLS settings
**Impact:** Job fails with TLS error. No retry logic for TLS errors.
#### Kube API Is Too Old
**What happens:**
- Kubernetes version doesn't support required features (e.g., log stream timestamps, certain API versions)
**Current handling:**
- ✅ **Version detection**: `ShouldUseReconnect()` checks server version via `Discovery().ServerVersion()`
- ✅ **Graceful degradation**: Falls back to no-reconnect logging method for older versions
- ✅ **Compatibility check**: `IsCompatibleK8S()` validates version >= 1.23.14 for reconnect support
- ⚠️ **Feature detection**: Only checks for reconnect support. Other version-dependent features not explicitly checked.
**Impact:** Automatically falls back to legacy method. Should work on older versions.
#### Kube API Is Too New
**What happens:**
- Kubernetes version introduces breaking changes or new required fields
**Current handling:**
- ⚠️ **Limited handling**: `client-go` version compatibility should handle most cases
- ❌ **No explicit new version detection**: Assumes `client-go` handles newer API versions
- ⚠️ **API version negotiation**: Handled automatically by `client-go`
**Impact:** May work if `client-go` supports it, or may fail with API errors.
#### Kube API Authentication Error
**What happens:**
- Invalid kubeconfig, expired tokens, insufficient permissions, wrong namespace
**Current handling:**
- ✅ **kubeconfig errors**: Caught in `connectUsingKubeconfig()`, errors returned immediately
- ✅ **in-cluster errors**: `InClusterConfig()` errors caught
- ❌ **No retry**: Authentication errors are not retried (correctly, as they won't resolve)
- ⚠️ **Permission errors**: API calls return `apierrors.IsForbidden()` which propagates through error handling
- ⚠️ **Token expiration**: No token refresh logic; tokens from kubeconfig expected to be valid
**Impact:** Job fails immediately with authentication error. No automatic token refresh.
### Pod Lifecycle Errors
#### Pod Cannot Be Scheduled
**What happens:**
- No nodes available, resource constraints, node selectors/affinity rules prevent scheduling
**Current handling:**
- ✅ **Watch detects**: `podRunningAndReady()` watches for pod phase changes
- ⚠️ **Timeout handling**: If `podPendingTimeout` is set, pending pods timeout
- ⚠️ **No explicit unscheduled detection**: Doesn't specifically check `pod.Status.Conditions` for `PodScheduled: False`
- ⚠️ **Error message**: Returns generic error from `UntilWithSync()` timeout
**Impact:** Job fails with timeout if pod never schedules. Error message may not clearly indicate scheduling issue.
#### Pod Is Killed
**What happens:**
- Pod evicted due to node pressure, node shutdown, manual pod deletion via kubectl
**Current handling:**
- ✅ **Watch detects deletion during startup**: `podRunningAndReady()` watches for pod events and returns `NotFound` error if `watch.Deleted` event received while waiting for pod to become ready
- ✅ **Watch detects pod phase failures**: Returns `ErrPodFailed` if pod enters `PodFailed` phase, `ErrPodCompleted` if pod enters `PodSucceeded` phase before ready
- ⚠️ **Deletion during execution handled indirectly**: When pod is deleted during job execution:
1. Log stream closes (EOF received)
2. Subsequent `Get()` calls to retrieve pod status return errors (likely `NotFound`)
3. After retries are exhausted (default 5 retries at kubernetes.go:384-406), the job fails with error "Error getting pod X/Y. Error: pods 'X' not found"
4. No explicit `IsNotFound()` check to distinguish pod deletion from other API errors
- ⚠️ **No explicit eviction detection**: Does not check pod conditions or events for eviction-specific signals (e.g., `pod.Status.Reason == "Evicted"`)
- ⚠️ **Generic error handling**: Pod-level failures (eviction, node shutdown) are detected through watch phase changes or API Get() errors, not through specific eviction events
**Impact:** Pod deletion/eviction is detected but reported as generic API errors ("Error getting pod"). Error messages may not clearly indicate whether the pod was deleted, evicted, or experienced another failure. Handling is indirect, relies on watch events (during startup) or API errors (during execution) rather than explicit pod condition checks.
### Container Execution Errors
#### Container Executing Work Is Killed
**What happens:**
- Container running the work is killed or terminates abnormally:
- **OOMKilled**: Container exceeded memory limit (container-level event, not pod-level)
- **SIGKILL/SIGTERM**: Container killed by runtime or scheduler
- **Container runtime issues**: containerd/CRI-O failures
- **Image issues**: Container crashes on startup
**Current handling:**
- ✅ **Container state monitoring**: When EOF received on log stream, `KubeLoggingWithReconnect()` gets fresh pod status and examines container state
- ✅ **Terminated state detection**: Checks `containerState.Terminated` to determine if container has stopped
- ✅ **Exit code inspection**: Reads `containerState.Terminated.ExitCode` to determine exit status
- ✅ **Reason classification**: Checks `containerState.Terminated.Reason` field and uses a whitelist approach:
- **Whitelist reasons** `["Completed", "Error"]`: Container ran to normal completion (even if it exited with error code)
- **All other reasons** (OOMKilled, Evicted, etc.): Execution was interrupted abnormally
- ✅ **Work state determination logic**:
- Exit code 0 → WorkStateSucceeded
- Exit code != 0 + reason in whitelist (`"Completed"` or `"Error"`) → WorkStateSucceeded (normal program completion with error)
- Exit code != 0 + reason NOT in whitelist (e.g., `"OOMKilled"`, `"Evicted"`) → WorkStateFailed and sets `stdoutErr`
- ✅ **Error marking**: Sets `stdoutErr` only if execution interrupted (not for normal error completions)
- ✅ **Last line capture**: Attempts to write last line from log stream before container termination
- ✅ **Detailed logging**: Logs exit code, termination reason, and termination message for all non-zero exits
**Impact:** Container-level terminations are properly detected and classified. Work state determined by both exit code AND termination reason. This correctly distinguishes between:
- Programs that exit with non-zero status intentionally (marked as succeeded if reason is "Completed"/"Error")
- Containers killed by OOM, eviction, or other interruptions (marked as failed)
**Note:** OOMKilled is a **container-level event** that appears in `containerState.Terminated.Reason`, distinct from pod-level eviction events.
#### Other Containers in Pod Are Killed
**What happens:**
- Sidecar containers or init containers fail or are killed
- **Note**: Multi-container pods are only possible when using custom pod specs (Pod parameter). In normal mode (Image/Command/Params), the pod is created with a single container named "worker", so sidecars and init containers are not possible.
**Current handling:**
- ⚠️ **Limited detection**: Only checks `WorkerContainerName` container
- ⚠️ **No sidecar monitoring**: Doesn't check status of other containers in `pod.Status.ContainerStatuses`
- ⚠️ **No init container monitoring**: Doesn't check `pod.Status.InitContainerStatuses`
- ⚠️ **Pod phase impact**: If sidecar/init failures cause pod to fail, pod phase change is detected via watch
- ⚠️ **Init container failures**: May prevent pod from reaching Ready state, detected as timeout during `podRunningAndReady()` watch
**Impact:**
- If worker container unaffected by sidecar failure, job continues normally
- If pod fails due to sidecar/init failure, detected indirectly via pod phase (PodFailed) or timeout waiting for Ready state
- No visibility into which sidecar/init container caused the failure - only that the pod failed
- Custom pod specs allow multiple containers and init containers, but normal mode creates single-container pods only
### Invalid Input Handling
#### Invalid PodTemplate Provided
**What happens:**
- Invalid YAML/JSON, missing required fields, invalid container names, incompatible spec provided in pod template
**Current handling:**
- ✅ **YAML/JSON decoding**: Errors caught in `CreatePod()` when decoding `ked.KubePod`
- ✅ **Worker container validation**: Checks that container named "worker" exists
- ✅ **Required fields**: Kubernetes API validates pod spec during `Create()` call
- ⚠️ **Partial validation**: Only validates worker container exists, not other aspects
- ❌ **No pre-validation**: Invalid specs discovered only when creating pod
**Impact:** Job fails at pod creation with validation error. Error message includes decoding or validation details.
### Long-Running Job Scenarios
#### Job Running for 40+ Hours
**What happens:**
- Very long-running jobs that exceed normal timeouts and log stream limits
**Current handling:**
- ✅ **4-hour log stream timeout**: Kubernetes API closes log streams after 4 hours
- ✅ **Automatic reconnection**: `KubeLoggingWithReconnect()` detects EOF and reconnects with `sinceTime` to avoid duplicates
- ✅ **Timestamp-based deduplication**: `ProcessLogLine()` uses timestamps to skip duplicate lines
- ✅ **Context cancellation handling**: Checks `context.Canceled` during log reading
- ✅ **EOF with Running state**: When EOF is detected but container is still Running, the system continues attempting to reconnect indefinitely (no retry limit) using Fibonacci backoff. This handles both cases: 4-hour log stream timeouts and rapid state transitions to terminated.
- ✅ **Fibonacci backoff**: Uses `GetNextFibonacciValues()` for retry delay calculations (capped at 400 to prevent excessive delays, max sleep duration of 5 minutes)
- ⚠️ **No job timeout**: No maximum job duration enforced by Receptor itself
- ⚠️ **Context cancellation**: Depends on external context cancellation (e.g., from the work submission client)
**Impact:** Jobs can run indefinitely if context not canceled. Log streams automatically reconnect every 4 hours. When EOF occurs with a Running container, the system continues attempting reconnection indefinitely rather than failing, which improves handling of long-running jobs and 4-hour timeout scenarios.
#### Context Cancellation
**What happens:**
- Context is canceled, triggering cleanup of the work unit
- **Important distinction**:
- **Pod startup failures** (before execution begins): `Cancel()` IS called automatically
- **Errors during execution** (after pod running): Errors do NOT cancel context - they just mark job as failed and set error details
- **Context cancellation during execution**: Only from explicit user action or Receptor shutdown, NOT from execution errors
**Cancellation triggers:**
- ✅ **User cancels work**: Via `receptorctl work cancel `
- ✅ **User releases work**: Via `receptorctl work release ` (calls `Cancel()`)
- ✅ **Pod startup failure**: When `CreatePod()` fails (e.g., ErrPodFailed, ErrImagePullBackOff) - automatic cleanup
- ⚠️ **Receptor shutdown**: SIGTERM/SIGINT to the Receptor process (if signal handler cancels contexts)
- ❌ **NOT triggered by execution errors**: Container failures, OOMKilled, log stream errors, etc. do NOT cancel context - they mark job as failed but don't trigger `Cancel()`
**Current handling:**
- ✅ **Context check in log reading**: Detects `context.Canceled` and marks job as failed if not already in terminal state
- ✅ **Context propagation**: Uses `kw.GetContext()` throughout for cancellation propagation
- ✅ **Cancel() method**: Deletes pod via Kubernetes API and marks as `WorkStateCanceled`
- ⚠️ **No graceful shutdown**: No attempt to wait for current operation to complete or allow pod to finish
- ⚠️ **Immediate pod deletion**: Pod is deleted immediately, may interrupt running job and lose partial output
**Impact:**
- Job marked as `WorkStateCanceled`
- Pod deleted from Kubernetes cluster
- Partial output may be lost if job was mid-execution
- For user-initiated cancellation, this is intentional behavior
- For pod startup failures, this is automatic cleanup to remove failed pods
### Log and Disk I/O Errors
#### Log Returned by Kube API Is Too Long
**What happens:**
- Very large log outputs that exceed memory or streaming buffer limits
**Current handling:**
- ✅ **Streaming approach**: Uses streaming reads (`bufio.NewReader`) not loading all logs into memory
- ✅ **Line-by-line processing**: Processes one line at a time
- ⚠️ **No size limits**: No explicit maximum log size limits
- ⚠️ **Disk space**: Depends on available disk space for stdout file
- ⚠️ **Memory**: Should be safe due to streaming, but very long individual lines may cause issues
**AAP Controller capacity checks:**
- ✅ **Memory**: AAP controller checks memory capacity before starting jobs via `mem_capacity` algorithm
- Jobs stay in "pending" state if insufficient memory capacity available
- Reserves ~100MB per fork + 2GB for system services
- ❌ **Disk space**: AAP controller does NOT check disk space before starting jobs
- No pre-flight validation of available disk space
- Jobs can start successfully then fail mid-execution when disk fills
- Work units write stdout to disk without size limits until disk is full
**Impact:** Large logs should work due to streaming, but **disk space exhaustion is a real operational risk**. Jobs will fail with "no space left on device" errors if disk fills during execution. Monitor disk usage on execution nodes, especially `/var/lib/awx` and `/tmp`.
#### Cannot Write Logs to Disk
**What happens:**
- Disk full, permission errors, filesystem errors when writing stdout file
**Current handling:**
- ✅ **Error detection**: Checks `stdout.Write()` errors
- ✅ **Error propagation**: Sets `stdoutErr` and logs error
- ✅ **Job failure**: Marks job as failed when write error occurs
- ⚠️ **No retry**: Write errors are not retried (assumed to be persistent)
- ⚠️ **Partial writes**: If write fails mid-stream, partial data may be in file
**Impact:** Job fails immediately on write error. Partial logs may be present in stdout file.
### Known Unknowns
These scenarios either have unclear handling or require further investigation:
1. **API Server Network Partitions**: What happens if network partitions between Receptor and API server during job execution?
**What happens:**
- Network connectivity is lost between Receptor and the Kubernetes API server (routing issues, network maintenance, infrastructure failures)
- API calls fail with connection refused or timeout errors
- Receptor cannot observe pod state changes during the partition
**Current handling:**
- ✅ **Retry logic**: `KubeLoggingWithReconnect()` retries getting the pod with Fibonacci backoff (default 5 retries, max 100)
- ⚠️ **Limited retries**: With default settings (5 retries, 1s base timeout), total retry time is approximately 12-15 seconds. Partitions longer than this cause job failure
- ⚠️ **Eventual consistency**: If the partition occurs while the pod transitions from Running to Terminated, Receptor may miss the state change and fail the job even if the pod completed successfully
- ⚠️ **No infinite retry**: Retries are finite (default 5, max 100), so long partitions will cause job failure even if the pod is still running or completes successfully
**Impact:** Network partitions longer than the retry window (default ~15 seconds) will cause job failure, potentially even if the pod completes successfully during the partition. The retry logic helps with transient issues but cannot handle extended partitions.
2. **Pod Template Mutations**: What if pod spec is mutated after creation by admission controllers or webhooks? (Unknown: Original spec used, mutations not tracked)
## Related Documentation
- [Receptor Work Submit Flow](receptor_work_submit_flow.md) - General work submission flow
- [Add Listener Backend](AddListenerBackend.md) - Backend connection handling
ansible-receptor-0f6ae46/docs/diagrams/receptor_work_submit_flow.md 0000664 0000000 0000000 00000055326 15177357701 0025755 0 ustar 00root root 0000000 0000000 # Receptor Work Submit Flow Analysis
Based on the command: `receptorctl --socket /tmp/control.sock work submit --node execution cat -l hello -f`
This document provides a technical analysis of how Receptor executes work submissions. Receptor is a distributed mesh networking system used by Ansible Automation Platform (AAP) to execute tasks across multiple nodes. When you submit work (like running a command) to a remote execution node, this flow shows how it passes through 4 architectural layers (Python CLI → Control Service → Work Service → Command Worker) from submission to execution and results retrieval.
## Table of Contents
- [Command Breakdown](#command-breakdown)
- [Flow Diagrams](#flow-diagrams)
- [Diagram 1: Work Submission Flow](#diagram-1-work-submission-flow)
- [Diagram 2: Work Results Retrieval Flow](#diagram-2-work-results-retrieval-flow)
- [Key Components](#key-components)
- [Work States](#work-states)
- [Configuration](#configuration)
- [Developer Debugging Walkthrough](#developer-debugging-walkthrough)
- [Prerequisites](#prerequisites)
- [Breakpoint Locations](#breakpoint-locations)
- [Diagram 1: Work Submission Flow Breakpoints](#diagram-1-work-submission-flow-breakpoints)
- [Diagram 2: Work Results Retrieval Flow Breakpoints](#diagram-2-work-results-retrieval-flow-breakpoints)
- [Debugging Steps with VSCode](#debugging-steps-with-vscode)
- [Key Variables to Watch](#key-variables-to-watch)
- [Log Analysis](#log-analysis)
## Command Breakdown
- `--socket /tmp/control.sock`: Connect to receptor control service via Unix socket
- `work submit`: Submit a new unit of work
- `--node execution`: Target the "execution" node for work execution
- `cat`: Work type (configured as a work-command that runs the `cat` command)
- `-l hello`: Literal payload "hello"
- `-f`: Follow the job and display results
## Flow Diagrams
### Diagram 1: Work Submission Flow
This diagram shows the complete flow of submitting work and executing it on the target node.
```mermaid
%%{init: {'theme':'base', 'themeVariables': { 'actorBkg':'#000000', 'actorBorder':'#4a9eff', 'actorTextColor':'#ffffff', 'actorLineColor':'#4a9eff', 'signalColor':'#ffffff', 'signalTextColor':'#ffffff', 'labelBoxBkgColor':'#000000', 'labelBoxBorderColor':'#4a9eff', 'labelTextColor':'#ffffff', 'loopTextColor':'#ffffff', 'noteBkgColor':'#000000', 'noteTextColor':'#ffffff', 'noteBorderColor':'#ff6b35', 'activationBkgColor':'#1a1a1a', 'activationBorderColor':'#4a9eff', 'sequenceNumberColor':'#000000'}}}%%
sequenceDiagram
participant User
participant ReceptorCtl as receptorctl (Python)
participant CtrlSocket as Unix Socket
/tmp/control.sock
participant CtrlSvc as Control Service
(Go - controlsvc)
participant WorkSvc as Work Service
(Go - workceptor)
participant WorkUnit as Work Unit
(command worker)
participant CatProcess as cat command
subprocess
Note over User, CatProcess: Command: receptorctl --socket /tmp/control.sock work submit --node execution cat -l hello -f
User->>ReceptorCtl: Execute command
Note over ReceptorCtl: Parse CLI arguments
cli.py:submit()
ReceptorCtl->>ReceptorCtl: Create ReceptorControl object
ReceptorCtl->>ReceptorCtl: Parse parameters:
- node: "execution"
- worktype: "cat"
- payload_literal: "hello"
- follow: true
ReceptorCtl->>CtrlSocket: Connect to Unix socket
CtrlSocket-->>ReceptorCtl: Connection established
ReceptorCtl->>CtrlSocket: Handshake
CtrlSocket-->>ReceptorCtl: "Receptor Control, node execution"
Note over ReceptorCtl: Build work submit JSON:
{"command": "work", "subcommand": "submit",
"node": "execution", "worktype": "cat"}
ReceptorCtl->>CtrlSocket: Send JSON command + newline
CtrlSocket->>CtrlSvc: Forward command
Note over CtrlSvc: controlsvc.go:RunControlSession()
Parse JSON command
CtrlSvc->>CtrlSvc: Route to "work" command handler
CtrlSvc->>WorkSvc: workceptorCommand.ControlFunc()
Note over WorkSvc: controlsvc.go:ControlFunc()
subcommand: "submit"
WorkSvc->>WorkSvc: Parse work parameters:
- workNode: "execution"
- workType: "cat"
alt Local Node (execution == current node)
WorkSvc->>WorkSvc: AllocateUnit("cat", "", {})
WorkSvc->>WorkUnit: Create new commandUnit
Note over WorkUnit: command.go:commandUnit
- command: "cat"
- baseParams: ""
- allowRuntimeParams: false
else Remote Node
WorkSvc->>WorkSvc: AllocateRemoteUnit()
Note over WorkSvc: Would create remote worker
(not applicable for this example)
end
WorkSvc-->>CtrlSvc: Work unit ID + "Send stdin data and EOF"
CtrlSvc-->>CtrlSocket: "Work unit created with ID {uuid}. Send stdin data and EOF.\n"
CtrlSocket-->>ReceptorCtl: Response message
ReceptorCtl->>CtrlSocket: Send payload: "hello\n"
CtrlSocket->>CtrlSvc: Forward payload data
CtrlSvc->>WorkUnit: Write to stdin file
ReceptorCtl->>CtrlSocket: Send EOF (close write side)
CtrlSocket->>CtrlSvc: EOF signal
CtrlSvc->>WorkUnit: Close stdin file
WorkUnit->>WorkUnit: UpdateBasicStatus(WorkStatePending, "Starting Worker")
WorkUnit->>WorkUnit: Start() - command.go:Start()
Note over WorkUnit: Create receptor subprocess:
receptor --node id=worker --log-level {level}
--command-runner command=cat params=""
unitdir={workdir}
WorkUnit->>CatProcess: Start subprocess
CatProcess->>CatProcess: commandRunner() - command.go:commandRunner()
CatProcess->>CatProcess: exec.Command("cat")
CatProcess->>CatProcess: cmd.Stdin = unitdir/stdin
CatProcess->>CatProcess: cmd.Stdout = unitdir/stdout
CatProcess->>CatProcess: cmd.Start()
loop Status Monitoring
CatProcess->>CatProcess: Update status every 250ms:
WorkStateRunning, "Running: PID {pid}"
end
CatProcess->>CatProcess: Read from stdin: "hello"
CatProcess->>CatProcess: Write to stdout: "hello"
CatProcess->>CatProcess: cmd.Wait() - EOF reached
CatProcess->>CatProcess: UpdateBasicStatus(WorkStateSucceeded)
CatProcess->>CatProcess: Exit with code 0
WorkUnit->>WorkUnit: MonitorLocalStatus() detects completion
WorkUnit->>WorkUnit: Status = WorkStateSucceeded
Note over ReceptorCtl, WorkUnit: Work execution complete.
Websocket connection remains open for results retrieval.
```
### Diagram 2: Work Results Retrieval Flow
This diagram shows how results are retrieved when the `-f` (follow) flag is used. The websocket connection from the submission phase remains open and is reused.
```mermaid
%%{init: {'theme':'base', 'themeVariables': { 'actorBkg':'#000000', 'actorBorder':'#4a9eff', 'actorTextColor':'#ffffff', 'actorLineColor':'#4a9eff', 'signalColor':'#ffffff', 'signalTextColor':'#ffffff', 'labelBoxBkgColor':'#000000', 'labelBoxBorderColor':'#4a9eff', 'labelTextColor':'#ffffff', 'loopTextColor':'#ffffff', 'noteBkgColor':'#000000', 'noteTextColor':'#ffffff', 'noteBorderColor':'#ff6b35', 'activationBkgColor':'#1a1a1a', 'activationBorderColor':'#4a9eff', 'sequenceNumberColor':'#000000'}}}%%
sequenceDiagram
participant User
participant ReceptorCtl as receptorctl (Python)
participant CtrlSocket as Unix Socket
/tmp/control.sock
participant CtrlSvc as Control Service
(Go - controlsvc)
participant WorkSvc as Work Service
(Go - workceptor)
participant StdoutFile as stdout file
(in work unit dir)
Note over ReceptorCtl, WorkSvc: Work unit has completed execution.
Websocket connection still open from submission phase.
-f (follow) flag was specified.
Note over ReceptorCtl: Automatic results retrieval
triggered by -f flag
ReceptorCtl->>CtrlSocket: Send: {"command": "work", "subcommand": "results",
"unitid": "{unit_id}"}
CtrlSocket->>CtrlSvc: Forward results request
CtrlSvc->>WorkSvc: workceptorCommand.ControlFunc()
subcommand: "results"
Note over WorkSvc: workceptor.go:GetResults()
Stream stdout file contents
WorkSvc->>StdoutFile: Open unitdir/stdout for reading
StdoutFile-->>WorkSvc: File handle
loop Stream file contents
WorkSvc->>StdoutFile: Read chunks from stdout file
StdoutFile-->>WorkSvc: File data chunk
WorkSvc-->>CtrlSvc: Stream chunk
CtrlSvc-->>CtrlSocket: Forward chunk
CtrlSocket-->>ReceptorCtl: Websocket data
ReceptorCtl->>User: Display output ("hello")
end
WorkSvc-->>CtrlSvc: End of file reached
CtrlSvc-->>CtrlSocket: Close stream
CtrlSocket-->>ReceptorCtl: Stream complete
Note over ReceptorCtl: Results retrieved.
Now check final status.
ReceptorCtl->>CtrlSocket: Send: {"command": "work", "subcommand": "status",
"unitid": "{unit_id}"}
CtrlSocket->>CtrlSvc: Forward status request
CtrlSvc->>WorkSvc: workceptorCommand.ControlFunc()
subcommand: "status"
WorkSvc->>WorkSvc: Read status file from unitdir
WorkSvc-->>CtrlSvc: {"State": 1, "Detail": "exit status 0"}
CtrlSvc-->>CtrlSocket: JSON status response
CtrlSocket-->>ReceptorCtl: Status data
Note over ReceptorCtl: State = 1 (WorkStateSucceeded)
Exit code = 0
ReceptorCtl->>CtrlSocket: Close websocket connection
ReceptorCtl->>User: Command completed successfully (exit 0)
```
## Key Components
### 1. ReceptorCtl (Python)
- **File**: `receptorctl/receptorctl/cli.py`, `receptorctl/receptorctl/socket_interface.py`
- **Function**: Command-line interface and socket communication
- **Key Classes**: `ReceptorControl`, CLI command handlers
### 2. Control Service (Go)
- **File**: `pkg/controlsvc/controlsvc.go`
- **Function**: Protocol handler for control socket connections
- **Key Functions**: `RunControlSession()`, command routing
### 3. Work Service (Go)
- **File**: `pkg/workceptor/controlsvc.go`, `pkg/workceptor/workceptor.go`
- **Function**: Work unit management and execution
- **Key Functions**: `ControlFunc()`, `AllocateUnit()`, `Start()`
### 4. Command Worker (Go)
- **File**: `pkg/workceptor/command.go`
- **Function**: Executes shell commands as work units
- **Key Functions**: `Start()`, `commandRunner()`
## Work States
1. **WorkStatePending (0)**: Initial state, waiting to start
2. **WorkStateRunning (1)**: Currently executing
3. **WorkStateSucceeded (2)**: Completed successfully
4. **WorkStateFailed (3)**: Failed with error
## Configuration
The `cat` work type is configured via YAML:
```yaml
- work-command:
workType: cat
command: cat
```
This registers a command worker that executes the `cat` shell command when work of type "cat" is submitted.
## Developer Debugging Walkthrough
This section provides specific breakpoint locations and debugging steps to follow the code execution through the codebase.
### Prerequisites
- Set up your development environment with Go and Python debuggers
- Build receptor with debug symbols: `make build-dev` or `go build -gcflags="all=-N -l"`
- **Important**: Install receptorctl in editable/development mode so Python breakpoints work:
```bash
cd receptorctl
pip install -e .
```
This creates a link to your source code instead of copying it, allowing the debugger to hit breakpoints in your workspace files.
### Breakpoint Locations
The breakpoints are organized by diagram to help you debug each flow independently.
#### Diagram 1: Work Submission Flow Breakpoints
##### 1. ReceptorCtl Entry Point
**File**: `receptorctl/receptorctl/cli.py`
**Function**: `submit()`
```python
def submit(
ctx,
worktype,
node,
payload,
# ... other params
):
```
**What to observe**: CLI argument parsing, parameter validation
#### 2. Socket Connection Setup
**File**: `receptorctl/receptorctl/socket_interface.py`
**Function**: `connect()`
```python
def connect(self):
if self._socket is not None:
return
```
**What to observe**: Unix socket connection establishment
#### 3. Work Submission Request
**File**: `receptorctl/receptorctl/socket_interface.py`
**Function**: `submit_work()`
```python
def submit_work(
self,
worktype,
payload,
node=None,
# ... other params
):
```
**What to observe**: JSON command construction, payload handling
#### 4. Control Service Session Handler
**File**: `pkg/controlsvc/controlsvc.go`
**Function**: `RunControlSession()`
```go
func (s *Server) RunControlSession(conn net.Conn) {
s.nc.GetLogger().Debug("Client connected to control service %s\n", conn.RemoteAddr().String())
```
**What to observe**: Socket connection handling, command parsing
#### 5. JSON Command Processing
**File**: `pkg/controlsvc/controlsvc.go`
**Function**: `RunControlSession()` (command parsing section)
```go
if cmdBytes[0] == '{' {
err := json.Unmarshal(cmdBytes, &jsonData)
```
**What to observe**: JSON unmarshaling, command extraction
#### 6. Work Command Routing
**File**: `pkg/controlsvc/controlsvc.go`
**Function**: `RunControlSession()` (command lookup section)
```go
s.controlFuncLock.RLock()
var ct ControlCommandType
for f := range s.controlTypes {
```
**What to observe**: Command type lookup, routing to work handler
#### 7. Work Command Handler Entry
**File**: `pkg/workceptor/controlsvc.go`
**Function**: `ControlFunc()`
```go
func (c *workceptorCommand) ControlFunc(ctx context.Context, nc controlsvc.NetceptorForControlCommand, cfo controlsvc.ControlFuncOperations) (map[string]interface{}, error) {
```
**What to observe**: Work command parameter extraction
#### 8. Work Submit Case Handler
**File**: `pkg/workceptor/controlsvc.go`
**Function**: `ControlFunc()` (submit case)
```go
case "submit":
workNode, err := strFromMap(c.params, "node")
```
**What to observe**: Parameter extraction, node determination
#### 9. Local Work Unit Allocation
**File**: `pkg/workceptor/controlsvc.go`
**Function**: `ControlFunc()` (AllocateUnit call)
```go
worker, err = c.w.AllocateUnit(workType, workUnitID, workParams)
```
**What to observe**: Work unit creation decision (local vs remote)
#### 10. Work Unit Allocation Implementation
**File**: `pkg/workceptor/workceptor.go`
**Function**: `AllocateUnit()`
```go
func (w *Workceptor) AllocateUnit(workType string, workUnitID string, workParams map[string]string) (WorkUnit, error) {
```
**What to observe**: Work type lookup, worker factory invocation
#### 11. Command Worker Creation
**File**: `pkg/workceptor/command.go`
**Function**: `NewWorker()` (in CommandWorkerCfg)
```go
func (cfg CommandWorkerCfg) NewWorker(bwu BaseWorkUnitForWorkUnit, w *Workceptor, unitID string, workType string) WorkUnit {
```
**What to observe**: Command worker instantiation, parameter setup
#### 12. Stdin Data Handling
**File**: `pkg/workceptor/controlsvc.go`
**Function**: `ControlFunc()` (stdin handling)
```go
stdin, err := os.OpenFile(path.Join(worker.UnitDir(), "stdin"), os.O_CREATE+os.O_WRONLY, 0o600)
```
**What to observe**: Stdin file creation, data writing
#### 13. Work Unit Start
**File**: `pkg/workceptor/command.go`
**Function**: `Start()`
```go
func (cw *commandUnit) Start() error {
level := cw.GetWorkceptor().nc.GetLogger().GetLogLevel()
```
**What to observe**: Command runner subprocess creation
#### 14. Command Runner Subprocess
**File**: `pkg/workceptor/command.go`
**Function**: `runCommand()`
```go
func (cw *commandUnit) runCommand(cmd *exec.Cmd) error {
cmdSetDetach(cmd)
```
**What to observe**: Subprocess execution setup
#### 15. Command Runner Main Function
**File**: `pkg/workceptor/command.go`
**Function**: `commandRunner()`
```go
func commandRunner(command string, params string, unitdir string) error {
status := StatusFileData{}
```
**What to observe**: Actual command execution, status updates
#### 16. Command Execution
**File**: `pkg/workceptor/command.go`
**Function**: `commandRunner()` (exec.Command section)
```go
var cmd *exec.Cmd
if params == "" {
cmd = exec.Command(command)
```
**What to observe**: `cat` command execution
#### Diagram 2: Work Results Retrieval Flow Breakpoints
##### 1. Results Command Handler
**File**: `pkg/workceptor/controlsvc.go`
**Function**: `ControlFunc()` (results case)
```go
case "results":
unitID, err := strFromMap(c.params, "unitid")
```
**What to observe**: Results command parameter extraction
##### 2. Results Streaming
**File**: `pkg/workceptor/workceptor.go`
**Function**: `GetResults()`
```go
func (w *Workceptor) GetResults(ctx context.Context, unitID string, startPos int64) (chan []byte, error) {
```
**What to observe**: Stdout file streaming, chunk reading
##### 3. Status Command Handler
**File**: `pkg/workceptor/controlsvc.go`
**Function**: `ControlFunc()` (status case)
```go
case "status":
unitID, err := strFromMap(c.params, "unitid")
```
**What to observe**: Status file reading, response formatting
### Debugging Steps with VSCode
#### 1. Setup launch.json Configuration
Create or update `.vscode/launch.json` with the following configurations:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Receptor Control Node",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/receptor-cl/receptor.go",
"args": [
"--config",
"${workspaceFolder}/test-configs/control.yml"
],
"env": {},
"showLog": true
},
{
"name": "Debug Receptor Execution Node",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/receptor-cl/receptor.go",
"args": [
"--config",
"${workspaceFolder}/test-configs/execution.yml"
],
"env": {},
"showLog": true
},
{
"name": "Debug ReceptorCtl",
"type": "debugpy",
"request": "launch",
"module": "receptorctl.cli",
"cwd": "${workspaceFolder}/receptorctl",
"args": [
"--socket",
"/tmp/control.sock",
"work",
"submit",
"--node",
"execution",
"cat",
"-l",
"hello",
"-f"
],
"console": "integratedTerminal",
"justMyCode": false,
"env": {
"PYTHONWARNINGS": "ignore::RuntimeWarning"
}
}
],
"compounds": [
{
"name": "Debug Control + Execution Nodes",
"configurations": [
"Debug Receptor Control Node",
"Debug Receptor Execution Node"
],
"stopAll": true
}
]
}
```
#### 2. Start Debugging
**Important**: Before debugging, ensure receptorctl is installed in editable mode (see Prerequisites above). This is required for Python breakpoints to work.
1. **Launch Both Nodes Together**:
- Open VSCode Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
- Select "Debug: Select and Start Debugging"
- Choose "Debug Control + Execution Nodes" compound configuration
- Both receptor nodes will start with debugger attached
- Wait for nodes to be ready (watch for "control service listening" in debug console)
2. **Set Breakpoints for Diagram 1 (Work Submission)**:
**Python breakpoints:**
- `receptorctl/receptorctl/cli.py` - `submit()` function
- `receptorctl/receptorctl/socket_interface.py` - `submit_work()` function
**Go breakpoints:**
- `pkg/controlsvc/controlsvc.go` - `RunControlSession()`
- `pkg/workceptor/controlsvc.go` - `ControlFunc()` (submit case)
- `pkg/workceptor/workceptor.go` - `AllocateUnit()`
- `pkg/workceptor/command.go` - `Start()`
- `pkg/workceptor/command.go` - `commandRunner()`
3. **Set Breakpoints for Diagram 2 (Results Retrieval)**:
**Python breakpoints:**
- `receptorctl/receptorctl/socket_interface.py` - Results retrieval code (after work submission)
**Go breakpoints:**
- `pkg/workceptor/controlsvc.go` - `ControlFunc()` (results case)
- `pkg/workceptor/workceptor.go` - `GetResults()`
- `pkg/workceptor/controlsvc.go` - `ControlFunc()` (status case)
4. **Debug ReceptorCtl Client**:
- After receptor nodes are running and ready, start the Python debugger
- Select "Debug ReceptorCtl" configuration from the debug dropdown
- The command will execute and hit breakpoints in both flows sequentially
5. **Step Through Execution**:
- Use VSCode debug controls (Continue, Step Over, Step Into, Step Out)
- Watch the call stack across both Go processes
- Inspect variables in the Debug sidebar
- Observe the data flow:
- **First flow**: Submission → Execution → Completion
- **Second flow**: Results retrieval → Status check → Exit
### Key Variables to Watch
- **In receptorctl**: `worktype`, `node`, `payload_data`, `commandMap`
- **In control service**: `cmdBytes`, `jsonData`, `cmd`, `params`
- **In work service**: `workNode`, `workType`, `workParams`, `worker`
- **In command worker**: `cw.command`, `cw.baseParams`, `cmd`
- **In command runner**: `command`, `params`, `unitdir`, `status`
### Log Analysis
Enable debug logging to see the full flow:
Look for these log patterns:
```bash
# "Client connected to control service"
# "Work unit created with ID"
# "Running: PID"
# "Streaming results for work unit"
This walkthrough allows developers to trace the complete execution path from CLI input to command execution and result output.
### Related Documentation
- [AWX to Receptor Integration Flow](awx_receptor_integration.md) - Complete walkthrough of how AWX uses Receptor for job execution
- [AWX Job Execution Walkthrough](https://gist.github.com/fosterseth/f0966ac6e214099ce28be5b154fd8f5b) - Detailed AWX-side flow from API to ansible-playbook
### Official Receptor Documentation
- [Receptor GitHub Repository](https://github.com/ansible/receptor) - Source code and development information
- [Receptor Documentation](https://receptor.readthedocs.io/) - Official documentation for Receptor
- [Receptor User Guide](https://receptor.readthedocs.io/en/latest/user_guide/) - Usage and configuration guides
### Ansible Automation Platform Documentation
- [Ansible Automation Platform - Receptor Overview](https://docs.ansible.com/automation-controller/latest/html/administration/receptor.html) - Receptor in Automation Controller context
- [Ansible Automation Platform - Mesh Topology](https://docs.ansible.com/automation-controller/latest/html/administration/topology.html) - Understanding mesh networking with Receptor
- [Ansible Automation Platform Installation Guide](https://docs.ansible.com/automation-controller/latest/html/installerguide/index.html) - Installation and setup
### Technical Resources
- [Receptor Work System](https://receptor.readthedocs.io/en/latest/user_guide/workceptor.html) - Detailed work submission documentation
- [Receptor Control Service](https://receptor.readthedocs.io/en/latest/user_guide/controlsvc.html) - Control service API reference
- [ReceptorCtl Command Reference](https://receptor.readthedocs.io/en/latest/receptorctl/) - Command-line tool documentation
### Community and Support
- [Ansible Community Forum](https://forum.ansible.com/) - Community discussions and support
- [Receptor Issues](https://github.com/ansible/receptor/issues) - Bug reports and feature requests
- [Ansible AWX Project](https://github.com/ansible/awx) - Related project that uses Receptor
ansible-receptor-0f6ae46/docs/make.bat 0000664 0000000 0000000 00000001437 15177357701 0017744 0 ustar 00root root 0000000 0000000 @ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
ansible-receptor-0f6ae46/docs/requirements.txt 0000664 0000000 0000000 00000000067 15177357701 0021621 0 ustar 00root root 0000000 0000000 doc8
pbr
rstcheck >= 6
six
sphinx
sphinx_ansible_theme
ansible-receptor-0f6ae46/docs/source/ 0000775 0000000 0000000 00000000000 15177357701 0017632 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/docs/source/conf.py 0000664 0000000 0000000 00000013201 15177357701 0021126 0 ustar 00root root 0000000 0000000 # Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
AUTHOR_NAME = "Red Hat Ansible"
project = "receptor"
copyright = AUTHOR_NAME
author = AUTHOR_NAME
# The full version, including alpha/beta/rc tags
# release = '0.0.0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autosectionlabel",
]
autosectionlabel_prefix_document = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["Thumbs.db", ".DS_Store"]
pygments_style = "ansible"
language = "en"
master_doc = "index"
source_suffix = ".rst"
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_ansible_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
sidebar_collapse = False
# -- Options for HTML output -------------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "receptorrdoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
latex_documents = [
(master_doc, "receptor.tex", "receptor Documentation", AUTHOR_NAME, "manual"),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
("receptorctl/receptorctl_index", "receptorctl", "receptor client", [author], 1),
(
"receptorctl/receptorctl_connect",
"receptorctl-connect",
"Establishes a connection between local client and a Receptor node.",
[author],
1,
),
("receptorctl/receptorctl_ping", "receptorctl-ping", "Tests the network reachability of Receptor nodes.", [author], 1),
("receptorctl/receptorctl_reload", "receptorctl-reload", "Reloads the Receptor configuration for the connected node.", [author], 1),
("receptorctl/receptorctl_status", "receptorctl-status", "Displays the status of the Receptor network.", [author], 1),
(
"receptorctl/receptorctl_traceroute",
"receptorctl-traceroute",
"Displays the network route that packets follow to Receptor nodes.",
[author],
1,
),
(
"receptorctl/receptorctl_version",
"receptorctl-version",
"Displays version information for receptorctl and\
the Receptor node to which it is connected.",
[author],
1,
),
("receptorctl/receptorctl_work_cancel", "receptorctl-work-cancel", "Terminates one or more units of work.", [author], 1),
("receptorctl/receptorctl_work_list", "receptorctl-work-list", "Displays known units of work.", [author], 1),
("receptorctl/receptorctl_work_release", "receptorctl-work-release", "Deletes one or more units of work.", [author], 1),
("receptorctl/receptorctl_work_results", "receptorctl-work-results", "Gets results for units of work.", [author], 1),
("receptorctl/receptorctl_work_submit", "receptorctl-work-submit", "Requests a Receptor node to run a unit of work.", [author], 1),
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"receptor",
"receptor Documentation",
author,
"receptor",
"Overlay network to establish a persistent mesh.",
"Miscellaneous",
),
]
# -- Options for QtHelp output -------------------------------------------
# -- Options for linkcheck builder ---------------------------------------
linkcheck_report_timeouts_as_broken = False
linkcheck_timeout = 30
# -- Options for xml builder ---------------------------------------------
xml_pretty = True
# -- Options for C domain ------------------------------------------------
# -- Options for C++ domain ----------------------------------------------
# -- Options for Python domain -------------------------------------------
# -- Options for Javascript domain ---------------------------------------
ansible-receptor-0f6ae46/docs/source/contributing.rst 0000664 0000000 0000000 00000004460 15177357701 0023077 0 ustar 00root root 0000000 0000000 ******************
Contributor guide
******************
Receptor is an open source project that lives at https://github.com/ansible/receptor
.. contents::
:local:
===============
Code of conduct
===============
All project contributors must abide by the `Ansible Code of Conduct `_.
============
Contributing
============
Receptor welcomes community contributions! See the :ref:`dev_guide` for information about receptor development.
-------------
Pull requests
-------------
Contributions to Receptor go through the Github pull request process.
An initial checklist for your change to increase the likelihood of acceptance:
- No issues when running linters/code checkers
- No issues from unit/functional tests
- Write descriptive and meaningful commit messages. See `How to write a Git commit message `_ and `Learn to write good commit message and description `_.
===============
Release process
===============
Before starting the release process verify that `make test` and `go test tests/goroutines/simple_config.go` tests pass.
Maintainers have the ability to run the `Stage Release`_ workflow. Running this workflow will:
- Build and push the container image to ghcr.io. This serves as a staging environment where the image can be tested.
- Create a draft release at ``_
After the draft release has been created, edit it and populate the description. Once you are done, click "Publish release".
After the release is published, the `Promote Release `_ workflow will run automatically. This workflow will:
- Publish ``receptorctl`` to `PyPI `_.
- Pull the container image from ghcr.io, re-tag, and push to `Quay.io `_.
- Build binaries for various OSes/platforms, and attach them to the `release `_.
.. note::
If you need to re-run `Stage Release`_ more than once, delete the tag beforehand to prevent the workflow from failing.
.. _Stage Release: https://github.com/ansible/receptor/actions/workflows/stage.yml
ansible-receptor-0f6ae46/docs/source/developer_guide.rst 0000664 0000000 0000000 00000032217 15177357701 0023533 0 ustar 00root root 0000000 0000000 .. _dev_guide:
===============
Developer guide
===============
Receptor is an open source project that lives at `ansible/receptor repository `
.. contents::
:local:
See the :ref:`contributing:contributing` for more general details.
---------
Debugging
---------
^^^^^^^^^^^
Unix Socket
^^^^^^^^^^^
If you don't want to use receptorctl to control nodes, `socat ` can be used to interact with unix sockets directly.
Example:
.. code-block:: bash
echo -e '{"command": "work", "subcommand": "submit", "node": "execution", "worktype": "cat", }\n"Hi"' | socat - UNIX-CONNECT:/tmp/control.sock
-------
Linters
-------
All code must pass a suite of Go linters.
There is a pre-commit yaml file in the Receptor repository that points to the linter suite.
It is strongly recommmended to install the pre-commit yaml so that the linters run locally on each commit.
.. code-block:: bash
cd $HOME
go get github.com/golangci/golangci-lint/cmd/golangci-lint
pip install pre-commit
cd receptor
pre-commit install
See `Pre commit `_ and `Golangci-lint `_ for more details on installing and using these tools.
-------
Testing
-------
^^^^^^^^^^^
Development
^^^^^^^^^^^
Write unit tests for new features or functionality.
Add/Update tests for bug fixes.
^^^^^^^
Mocking
^^^^^^^
We are using mockgen to generate mocks for our unit tests. The mocks are living inside of a package under the real implementation, prefixed by ``mock_``. An example is the package mock_workceptor under pkg/workceptor.
We use go generate to generate our mocks. To generate all the mocks in receptor:
.. code-block:: bash
make generate
To add a mock to go generate you will need to add a line to the `generate.go` file in the root of the repository.
In order to genenerate a mock for a particular file, run:
.. code-block:: bash
mockgen -source=pkg/filename.go -destination=pkg/mock_pkg/mock_filename.go
For example, to create/update mocks for Workceptor, we can run:
.. code-block:: bash
mockgen -source=pkg/workceptor/workceptor.go -destination=pkg/workceptor/mock_workceptor/workceptor.go
After validating the mockgen command generates the mocks correctly you can add this command to the `generate.go` file.
To remove a mock you will need to remove the associated line in the `generate.go` file.
Then the following commands to generate a fresh set of mocks:
.. code-block:: bash
make generate-clean
make generate
^^^^^^^^^^
Kubernetes
^^^^^^^^^^
Some of the tests require access to a Kubernetes cluster; these tests will load in the kubeconfig file located at ``$HOME/.kube/config``. One simple way to make these tests work is to start minikube locally before running ``make test``. See https://minikube.sigs.k8s.io/docs/start/ for more information about minikube.
To skip tests that depend on Kubernetes, set environment variable ``export SKIP_KUBE=1``.
^^^^^^^^^
Execution
^^^^^^^^^
Pull requests must pass a suite of unit and integration tests before being merged into ``devel``.
``make test`` will run the full test suite locally.
-----------
Source code
-----------
The following sections help orient developers to the Receptor code base and provide a starting point for understanding how Receptor works.
^^^^^^^^^^^^^^^^^^^^^
Parsing receptor.conf
^^^^^^^^^^^^^^^^^^^^^
Let's see how items in the config file are mapped to Golang internals.
As an example, in tcp.go
.. code-block:: go
cmdline.RegisterConfigTypeForApp("receptor-backends",
"tcp-peer", "Make an outbound backend connection to a TCP peer", TCPDialerCfg{}, cmdline.Section(backendSection))
"tcp-peer" is a top-level key (action item) in receptor.conf
.. code-block:: yaml
tcp-peers:
- address: localhost:2222
``RegisterConfigTypeForApp`` tells the cmdline parser that "tcp-peer" is mapped to the ``TCPDialerCfg{}`` structure.
``main()`` in ``receptor.go`` is the entry point for a running Receptor process.
In ``receptor.go`` (modified for clarity):
.. code-block:: go
cl.ParseAndRun("receptor.conf", []string{"Init", "Prepare", "Run"})
A Receptor config file has many action items, such as ```node``, ``work-command``, and ``tcp-peer``. ``ParseAndRun`` is how each of these items are instantiated when Receptor starts.
Specifically, ParseAndRun will run the Init, Prepare, and Run methods associated with each action item.
Here is the Prepare method for ``TCPDialerCfg``. By the time this code executes, the cfg structure has already been populated with the data provided in the config file.
.. code-block:: go
// Prepare verifies the parameters are correct.
func (cfg TCPDialerCfg) Prepare() error {
if cfg.Cost <= 0.0 {
return fmt.Errorf("connection cost must be positive")
}
return nil
}
This simply does a check to make sure the provided Cost is valid.
The Run method for the ``TCPDialerCfg`` object:
.. code-block:: go
// Run runs the action.
func (cfg TCPDialerCfg) Run() error {
logger.Debug("Running TCP peer connection %s\n", cfg.Address)
host, _, err := net.SplitHostPort(cfg.Address)
if err != nil {
return err
}
tlscfg, err := netceptor.MainInstance.GetClientTLSConfig(cfg.TLS, host, "dns")
if err != nil {
return err
}
b, err := NewTCPDialer(cfg.Address, cfg.Redial, tlscfg)
if err != nil {
logger.Error("Error creating peer %s: %s\n", cfg.Address, err)
return err
}
err = netceptor.MainInstance.AddBackend(b, cfg.Cost, nil)
if err != nil {
return err
}
return nil
}
This gets a new TCP dialer object and passes it to the netceptor ``AddBackend`` method, so that it can be processed further.
``AddBackend`` will start proper Go routines that periodically dial the address defined in the TCP dialer structure, which will lead to a proper TCP connection to another Receptor node.
In general, when studying how the start up process works in Receptor, take a look at the ``Init``, ``Prepare``, and ``Run`` methods throughout the code, as these are the entry points to running those specific components of Receptor.
^^^^
Ping
^^^^
Studying how pings work in Receptor will provide a useful glimpse into the internal workings of netceptor -- the main component of Receptor that handles connections and data traffic over the mesh.
``receptorctl --socket /tmp/foo.sock ping bar``
The control-service on `foo` will receive this command and subsequently call the following,
**ping.go::ping**
.. code-block:: go
func ping(nc *netceptor.Netceptor, target string, hopsToLive byte) (time.Duration, string, error) {
pc, err := nc.ListenPacket("")
``target`` is the target node, "bar" in this case.
``nc.ListenPacket("")`` starts a new ephemeral service and returns a ``PacketConn`` object. This is a datagram connection that has a WriteTo() and ReadFrom() method for sending and receiving data to other nodes on the mesh.
**packetconn.go::ListenPacket**
.. code-block:: go
pc := &PacketConn{
s: s,
localService: service,
recvChan: make(chan *messageData),
advertise: false,
adTags: nil,
connType: ConnTypeDatagram,
hopsToLive: s.maxForwardingHops,
}
s.listenerRegistry[service] = pc
return pc, nil
``s`` is the main netceptor object, and a reference to the PacketConn object is stored in netceptor's ``listenerRegistry`` map.
**ping.go::ping**
.. code-block:: go
_, err = pc.WriteTo([]byte{}, nc.NewAddr(target, "ping"))
Sends an empty message to the address "bar:ping" on the mesh. Recall that nodes are analogous to DNS names, and services are like port numbers.
``WriteTo`` calls ``sendMessageWithHopsToLive``
**netceptor.go::sendMessageWithHopsToLive**
.. code-block:: go
md := &messageData{
FromNode: s.nodeID,
FromService: fromService,
ToNode: toNode,
ToService: toService,
HopsToLive: hopsToLive,
Data: data,
}
return s.handleMessageData(md)
Here the message is constructed with essential information such as the source node and service, and the destination node and service. The Data field contains the actual message, which is empty in this case.
``handleMessageData`` calls ``forwardMessage`` with the ``md`` object.
**netceptor.go::forwardMessage**
.. code-block:: go
nextHop, ok := s.routingTable[md.ToNode]
The current node might not be directly connected to the target node, and thus netceptor needs to determine what is the next hop to pass the data to. ``s.routingTable`` is a map where the key is a destination ("bar"), and the value is the next hop along the path to that node. In a simple two-node setup with `foo` and `bar`, ``s.routingTable["bar"] == "bar"``.
**netceptor.go::forwardMessage**
.. code-block:: go
c, ok := s.connections[nextHop]
c.WriteChan <- message
``c`` here is a ``ConnInfo`` object, which interacts with the various backend connections (UDP, TCP, websockets).
``WriteChan`` is a golang channel. Channels allows communication between separate threads (Go routines) running in the application. When `foo` and `bar` had first started, they established a backend connection. Each node runs the netceptor runProtocol go routine, which in turn starts a protoWriter go routine.
**netceptor.go::protoWriter**
.. code-block:: go
case message, more := <-ci.WriteChan:
err := sess.Send(message)
So before the "ping" command was issued, this protoWriter Go routine was already running and waiting to read messages from WriteChan.
``sess`` is a BackendSession object. BackendSession is an abstraction over the various available backends. If `foo` and `bar` are connected via TCP, then ``sess.Send(message)`` will pass along data to the already established TCP session.
**tcp.go::Send**
.. code-block:: go
func (ns *TCPSession) Send(data []byte) error {
buf := ns.framer.SendData(data)
n, err := ns.conn.Write(buf)
``ns.conn`` is net.Conn object, which is part of the Golang standard library.
At this point the message has left the node via a backend connection, where it will be received by `bar`.
Let's review the code from `bar`'s perspective and how it handles the incoming message that is targeting its "ping" service.
On the receiving side, the data will first be read here
**tcp.go::Recv**
.. code-block:: go
n, err := ns.conn.Read(buf)
ns.framer.RecvData(buf[:n])
Recv was called in protoReader Go routine, similar to the protoWriter when the message sent from `foo`.
Note that ``ns.conn.Read(buf)`` might not contain the full message, so the data is buffered until the ``messageReady()`` returns true. The size of the message is tagged in the message itself, so when Recv has received N bytes, and the message is N bytes, Recv will return.
**netceptor.go::protoReader**
.. code-block:: go
buf, err := sess.Recv(1 * time.Second)
ci.ReadChan <- buf
The data is passed to a ReadChan channel.
**netceptor.go::runProtocol**
.. code-block:: go
case data := <-ci.ReadChan:
message, err := s.translateDataToMessage(data)
err = s.handleMessageData(message)
The data is read from the channel, and deserialized into an actual message format in ``translateDataToMessage``.
**netceptor.go::handleMessageData**
.. code-block:: go
if md.ToNode == s.nodeID {
handled, err := s.dispatchReservedService(md)
This checks whether the destination node indicated in the message is the current node. If so, the message can be dispatched to the service.
"ping" is a reserved service in the netceptor instance.
.. code-block:: go
s.reservedServices = map[string]func(*messageData) error{
"ping": s.handlePing,
}
**netceptor.go::handlePing**
.. code-block:: go
func (s *Netceptor) handlePing(md *messageData) error {
return s.sendMessage("ping", md.FromNode, md.FromService, []byte{})
}
This is the ping reply handler. It sends an empty message to the FromNode (`foo`).
The FromService here is not "ping", but rather the ephemeral service that was created from ``ListenPacket("")`` in ping.go on `foo`.
With ``trace`` enabled in the Receptor configuration, the following log statements show the reply from ``bar``,
.. code-block:: bash
TRACE --- Received data length 0 from foo:h73opPEh to bar:ping via foo
TRACE --- Sending data length 0 from bar:ping to foo:h73opPEh
So the ephemeral service on `foo` is called h73opPEh (randomly generated string).
From here, the message from `bar` will passed along in a very similar fashion as the original ping message sent from `foo`.
Back on node `foo`, the message is received receive the message where it is finally handled in ping.go
**ping.go::ping**
.. code-block:: go
_, addr, err := pc.ReadFrom(buf)
.. code-block:: go
case replyChan <- fromNode:
.. code-block:: go
case remote := <-replyChan:
return time.Since(startTime), remote, nil
The data is read from the PacketConn object, written to a channel, where it is read later by the ping() function, and ping() returns with the roundtrip delay, ``time.Since(startTime)``.
ansible-receptor-0f6ae46/docs/source/getting_started_guide/ 0000775 0000000 0000000 00000000000 15177357701 0024176 5 ustar 00root root 0000000 0000000 ansible-receptor-0f6ae46/docs/source/getting_started_guide/creating_a_basic_network.rst 0000664 0000000 0000000 00000003304 15177357701 0031736 0 ustar 00root root 0000000 0000000
.. _creating_a_basic_network:
###############################
Creating a basic 3-node network
###############################
In this section, we will create a three-node network.
The three nodes are: foo, bar, and baz.
`foo -> bar <- baz`
foo and baz are directly connected to bar with TCP connections.
foo can reach baz by sending network packets through bar.
***********************
Receptor configurations
***********************
1. Create three configuration files, one for each node.
``foo.yml``
.. code-block:: yaml
- node:
id: foo
- control-service:
service: control
filename: /tmp/foo.sock
- tcp-peer:
address: localhost:2222
- log-level:
level: debug
...
``bar.yml``
.. code-block:: yaml
---
- node:
id: bar
- control-service:
service: control
filename: /tmp/bar.sock
- tcp-listener:
port: 2222
- log-level:
level: debug
...
``baz.yml``
.. code-block:: yaml
---
- node:
id: baz
- control-service:
service: control
filename: /tmp/baz.sock
- tcp-peer:
address: localhost:2222
- log-level:
level: debug
- work-command:
workType: echo
command: bash
params: "-c \"while read -r line; do echo $line; sleep 1; done\""
allowruntimeparams: true
...
2. Run the services in separate terminals.
.. code-block:: bash
./receptor --config foo.yml
.. code-block:: bash
./receptor --config bar.yml
.. code-block:: bash
./receptor --config baz.yml
.. seealso::
:ref:`configuring_receptor_with_a_config_file`
Configuring Receptor with a configuration file
:ref:`connecting_nodes`
Detail on connecting receptor nodes
ansible-receptor-0f6ae46/docs/source/getting_started_guide/index.rst 0000664 0000000 0000000 00000001472 15177357701 0026043 0 ustar 00root root 0000000 0000000 #############################
Getting started with Receptor
#############################
Receptor is an overlay network that distributes work across large and dispersed collections of worker nodes.
Receptor nodes establish peer-to-peer connections through existing networks.
Once connected, the Receptor mesh provides datagram (UDP-like) and stream (TCP-like) capabilities to applications, as well as robust unit-of-work handling with resiliency against transient network failures.
.. image:: mesh.png
.. toctree::
:maxdepth: 1
:caption: Contents:
introduction
installing_receptor
creating_a_basic_network
trying_sample_commands
.. seealso::
:ref:`interacting_with_nodes`
Further examples of working with nodes
:ref:`connecting_nodes`
Detail on connecting receptor nodes
ansible-receptor-0f6ae46/docs/source/getting_started_guide/installing_receptor.rst 0000664 0000000 0000000 00000000666 15177357701 0031007 0 ustar 00root root 0000000 0000000
.. _installing_receptor:
###################
Installing Receptor
###################
1. `Download receptor `_
2. Install receptor (per installation guide below)
3. Install receptorctl
.. code-block:: bash
pip install receptorctl
.. seealso::
:ref:`installing`
Detailed installation instructions
:ref:`using_receptor_containers`
Using receptor in containers
ansible-receptor-0f6ae46/docs/source/getting_started_guide/introduction.rst 0000664 0000000 0000000 00000000741 15177357701 0027453 0 ustar 00root root 0000000 0000000 ########################
Introduction to receptor
########################
Receptor is an overlay network.
It eases the work distribution across a large and dispersed collection
of workers
Receptor nodes establish peer-to-peer connections with each other through
existing networks
Once connected, the receptor mesh provides:
* Datagram (UDP-like) and stream (TCP-like) capabilities to applications
* Robust unit-of-work handling
* Resiliency against transient network failures
ansible-receptor-0f6ae46/docs/source/getting_started_guide/mesh.png 0000664 0000000 0000000 00000056350 15177357701 0025651 0 ustar 00root root 0000000 0000000 PNG
IHDR Ea oiCCPicc (u;KA?%D"jbB"BPKM|5%le7A`c!X6
(?W#a$2{?̹̜GXײVk
W7Ifs3Qz?z$-aCp%PF**NWSLC8Z
fV;7[\/s ::F$`7O^<
JHo@ԢtMJMOrvŶ?]9)8*W%/w⺮rzHNT
ϡcZͪ#D7neoֶgI pHYs ~ IDATx^Sֆ
ĮHQTT
Q/"
^^"Bbǎ(4EE*w?$'9)z<''{;Y{ZS/b$" " " " " "P@Kl-" " " " " MQpĪ@D@D@D@D@D@@ H(8bU " " " " " Cs@D@D@D@D@Dx*9 " " " " "PpR<
XJ`" uL@Gz%:vhgۛE @X)Ѷ" " " B`.0|}
@Qn#@;wYkW_}eXc2&G@>7-ƍ]'m1(WR-A."PڬNu䁀<@T" " " " " /@"PnzO?c=֬Jfȑ'4?Ymn5h AI&|̚5zr!^zaoE-2vy%~3/G9uQfu-[R?(+6IԶCɯ-Zv_vޱy(K.]L^}ՔiIkƦ]r%p
B76Uh
"o .[l>]p'*=)Ze(?~B{x
iӦ}ݗ*?]W;jҟhV
>-\0lذ*OU`*F)|M|VQ6lsuYE3eVƌSV:#"%LȪ\()c눀W<`e
>}zлwpwI'%E}2/_曁}*]PqO
L]?/ا\7k-,H[o w؞G}4l'|aAѱNBPgK}~C{S`-gws\}vGǵȳZk}_]> 'Xc[o
lcko}2VX-ě7oܝ~igHM}8Ү6m;?C߾}Cv"nR$ ţGMmW