├── .dockerignore ├── .github ├── codecov.yml ├── dependabot.yml ├── issue_labeler.yml └── workflows │ ├── artifact-k8s-logs.sh │ ├── build_binary_from_ref.yml │ ├── codeql-analysis.yml │ ├── coverage_reporting.yml │ ├── dependency_review.yml │ ├── devel_image.yml │ ├── devel_whl.yml │ ├── promote.yml │ ├── pull_request.yml │ ├── reusable-nox.yml │ ├── scorecard.yml │ ├── stage.yml │ ├── test-reporting.yml │ └── triage_new.yml ├── .gitignore ├── .gitleaks.toml ├── .golangci.yml ├── .readthedocs.yaml ├── .sonarcloud.properties ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── cmd ├── config.go ├── config_test.go ├── defaults.go ├── defaults_test.go ├── receptor-cl │ └── receptor.go ├── root.go └── root_test.go ├── docs ├── Makefile ├── diagrams │ └── AddListenerBackend.md ├── make.bat ├── requirements.txt └── source │ ├── conf.py │ ├── contributing.rst │ ├── developer_guide.rst │ ├── getting_started_guide │ ├── creating_a_basic_network.rst │ ├── index.rst │ ├── installing_receptor.rst │ ├── introduction.rst │ ├── mesh.png │ └── trying_sample_commands.rst │ ├── index.rst │ ├── installation.rst │ ├── porting_guide │ ├── PORTING_2.rst │ ├── index.rst │ └── receptor_porting_index.rst │ ├── receptorctl │ ├── index.rst │ ├── receptorctl_connect.rst │ ├── receptorctl_index.rst │ ├── receptorctl_ping.rst │ ├── receptorctl_reload.rst │ ├── receptorctl_status.rst │ ├── receptorctl_traceroute.rst │ ├── receptorctl_version.rst │ ├── receptorctl_work_cancel.rst │ ├── receptorctl_work_list.rst │ ├── receptorctl_work_release.rst │ ├── receptorctl_work_results.rst │ └── receptorctl_work_submit.rst │ ├── requirements.txt │ ├── roadmap │ ├── ROADMAP_2.rst │ ├── index.rst │ └── receptor_roadmap_index.rst │ ├── upgrade │ ├── UPGRADE_2.rst │ ├── index.rst │ └── receptor_upgrade_index.rst │ └── user_guide │ ├── basic_usage.rst │ ├── configuration_options.rst │ ├── connecting_nodes.rst │ ├── edge.png │ ├── edge_networks.rst │ ├── firewall.rst │ ├── index.rst │ ├── interacting_with_nodes.rst │ ├── k8s.rst │ ├── mesh.png │ ├── remote.png │ ├── tls.rst │ └── workceptor.rst ├── example └── net.go ├── generate.go ├── go.mod ├── go.sum ├── internal └── version │ ├── version.go │ └── version_test.go ├── packaging ├── container │ ├── .gitignore │ ├── Dockerfile │ └── receptor.conf └── tc-image │ └── Dockerfile ├── pkg ├── backends │ ├── cmdline.go │ ├── mock_backends │ │ └── websockets.go │ ├── null.go │ ├── null_test.go │ ├── tcp.go │ ├── tcp_test.go │ ├── udp.go │ ├── udp_test.go │ ├── utils.go │ ├── websocket_interop_test.go │ ├── websockets.go │ └── websockets_test.go ├── certificates │ ├── ca.go │ ├── ca_test.go │ ├── cli.go │ ├── cli_test.go │ ├── cmdline.go │ ├── mock_certificates │ │ ├── oser.go │ │ └── rsaer.go │ ├── oser.go │ └── rsaer.go ├── controlsvc │ ├── connect.go │ ├── connect_test.go │ ├── controlsvc.go │ ├── controlsvc_stub.go │ ├── controlsvc_test.go │ ├── interfaces.go │ ├── mock_controlsvc │ │ ├── controlsvc.go │ │ └── interfaces.go │ ├── ping.go │ ├── ping_test.go │ ├── reload.go │ ├── reload_test.go │ ├── reload_test_yml │ │ ├── add_cfg.yml │ │ ├── drop_cfg.yml │ │ ├── init.yml │ │ ├── modify_cfg.yml │ │ ├── successful_reload.yml │ │ └── syntax_error.yml │ ├── status.go │ ├── status_test.go │ ├── traceroute.go │ └── traceroute_test.go ├── framer │ ├── framer.go │ ├── framer_test.go │ └── mock_framer │ │ └── framer.go ├── logger │ ├── logger.go │ └── logger_test.go ├── netceptor │ ├── addr.go │ ├── addr_test.go │ ├── conn.go │ ├── conn_test.go │ ├── external_backend.go │ ├── firewall_rules.go │ ├── firewall_rules_test.go │ ├── mock_netceptor │ │ ├── conn.go │ │ ├── external_backend.go │ │ ├── netceptor.go │ │ ├── packetconn.go │ │ └── ping.go │ ├── netceptor.go │ ├── netceptor_test.go │ ├── packetconn.go │ ├── packetconn_test.go │ ├── ping.go │ ├── tlsconfig.go │ └── tlsconfig_test.go ├── randstr │ ├── randstr.go │ └── randstr_test.go ├── services │ ├── cmdline.go │ ├── command.go │ ├── command_test.go │ ├── interfaces │ │ ├── mock_interfaces │ │ │ └── net_interfaces.go │ │ └── net_interfaces.go │ ├── ip_router.go │ ├── ip_router_cfg.go │ ├── mock_services │ │ ├── command.go │ │ ├── tcp_proxy.go │ │ └── udp_proxy.go │ ├── tcp_proxy.go │ ├── tcp_proxy_test.go │ ├── udp_proxy.go │ ├── udp_proxy_test.go │ ├── unix_proxy.go │ └── unix_proxy_test.go ├── tickrunner │ ├── tickrunner.go │ └── tickrunner_test.go ├── types │ ├── main.go │ └── main_test.go ├── utils │ ├── bridge.go │ ├── broker.go │ ├── broker_test.go │ ├── common.go │ ├── common_test.go │ ├── error_kind.go │ ├── error_kind_test.go │ ├── flock.go │ ├── flock_test.go │ ├── flock_windows.go │ ├── incremental_duration.go │ ├── incremental_duration_test.go │ ├── job_context.go │ ├── job_context_test.go │ ├── mock_utils │ │ └── net.go │ ├── net.go │ ├── other_name.go │ ├── other_name_test.go │ ├── readstring_context.go │ ├── sysinfo.go │ ├── sysinfo_test.go │ ├── unixsock.go │ ├── unixsock_errors.go │ ├── unixsock_test.go │ └── unixsock_windows.go └── workceptor │ ├── cmdline.go │ ├── command.go │ ├── command_detach_unixlike.go │ ├── command_detach_windows.go │ ├── command_test.go │ ├── controlsvc.go │ ├── controlsvc_test.go │ ├── interfaces.go │ ├── json_test.go │ ├── kubernetes.go │ ├── kubernetes_test.go │ ├── lock_test.go │ ├── mock_workceptor │ ├── command.go │ ├── interfaces.go │ ├── kubernetes.go │ ├── stdio_utils.go │ ├── workceptor.go │ └── workunitbase.go │ ├── python.go │ ├── python_test.go │ ├── remote_work.go │ ├── remote_work_test.go │ ├── stdio_utils.go │ ├── stdio_utils_test.go │ ├── workceptor.go │ ├── workceptor_stub.go │ ├── workceptor_test.go │ ├── workunitbase.go │ └── workunitbase_test.go ├── receptor-python-worker ├── .gitignore ├── README.md ├── pyproject.toml └── receptor_python_worker │ ├── __init__.py │ ├── __main__.py │ ├── plugin_utils.py │ └── work.py ├── receptorctl ├── .gitignore ├── MANIFEST.in ├── README.md ├── noxfile.py ├── pyproject.toml ├── receptorctl │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ └── socket_interface.py ├── requirements │ └── requirements.txt ├── setup.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── lib.py │ ├── mesh-definitions │ ├── access_control │ │ ├── node1.yaml │ │ ├── node2.yaml │ │ └── node3.yaml │ └── mesh1 │ │ ├── node1.yaml │ │ ├── node2.yaml │ │ └── node3.yaml │ ├── test_cli.py │ ├── test_connection.py │ ├── test_mesh.py │ └── test_workunit.py ├── setup.cfg ├── tests ├── .gitignore ├── Makefile ├── README.md ├── environments │ ├── container-builder │ │ ├── Containerfile │ │ ├── README.md │ │ └── build-artifacts.sh │ └── container-dev │ │ ├── Containerfile │ │ └── receptor.conf ├── functional │ ├── README.md │ ├── cli │ │ └── cli_test.go │ └── mesh │ │ ├── conn_test.go │ │ ├── factories.go │ │ ├── firewall_test.go │ │ ├── fixtures.go │ │ ├── lib.go │ │ ├── mesh_test.go │ │ ├── receptorcontrol.go │ │ ├── testdata │ │ └── echo-pod.yml │ │ ├── tls_test.go │ │ ├── work_test.go │ │ └── work_utils.go ├── goroutines │ └── simple_config.go ├── receptor-tester.sh └── utils │ ├── common.go │ └── logs.go └── tools ├── README.md ├── ansible └── stage.yml └── examples └── simple-network ├── .gitignore ├── README.md ├── build ├── receptor │ └── Dockerfile └── receptorctl │ └── Dockerfile ├── configs ├── arceus.yaml └── celebi.yaml ├── ctl.sh ├── docker-compose.yml ├── simple-network-diagram.drawio.png └── socks └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | receptor 3 | receptor.exe 4 | receptor.app 5 | recepcert 6 | net 7 | receptorctl-test-venv/ 8 | .container-flag* 9 | .VERSION 10 | kubectl 11 | /receptorctl/AUTHORS 12 | /receptorctl/ChangeLog 13 | /receptor-python-worker/ChangeLog 14 | /receptor-python-worker/AUTHORS 15 | .vagrant/ 16 | Dockerfile 17 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: auto 7 | threshold: 5% 8 | patch: 9 | default: 10 | target: auto 11 | threshold: 5% 12 | comment: 13 | layout: "header, diff, files, components" # PR comment layout 14 | behavior: default 15 | require_changes: false 16 | require_base: false 17 | require_head: true 18 | after_n_builds: 1 19 | codecov: 20 | branch: devel 21 | notify: 22 | wait_for_ci: false 23 | after_n_builds: 1 24 | component_management: 25 | default_rules: # default rules that will be inherited by all components 26 | statuses: # Status definitions 27 | - type: project # Default status = project 28 | target: auto 29 | threshold: 5% 30 | branches: 31 | - "!devel" 32 | individual_components: # Actual component breakdown 33 | - component_id: go # Required unique identifier for component (Should not be changed) 34 | name: Go # Display name (can be changed) 35 | paths: 36 | - cmd/** 37 | - internal/** 38 | - pkg/** 39 | - component_id: receptorctl 40 | name: Receptorctl 41 | paths: 42 | - receptorctl/** 43 | ignore: 44 | - "**/mock_*" 45 | - "**/*_test.go" 46 | - "receptorctl/tests" 47 | - "tests" 48 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | labels: 9 | - "dependencies" 10 | - "go" 11 | ignore: 12 | - dependency-name: "golang" 13 | versions: ["1.23", "1.24"] 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | labels: 19 | - "dependencies" 20 | - "github-actions" 21 | - package-ecosystem: "pip" 22 | directory: "/receptor-python-worker" 23 | groups: 24 | dependencies: 25 | patterns: 26 | - "*" 27 | schedule: 28 | interval: "daily" 29 | labels: 30 | - "dependencies" 31 | - "pip" 32 | - package-ecosystem: "pip" 33 | directory: "/receptorctl" 34 | groups: 35 | dependencies: 36 | patterns: 37 | - "*" 38 | schedule: 39 | interval: "daily" 40 | labels: 41 | - "dependencies" 42 | - "pip" 43 | -------------------------------------------------------------------------------- /.github/issue_labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | needs_triage: 3 | - '.*' 4 | ... 5 | -------------------------------------------------------------------------------- /.github/workflows/artifact-k8s-logs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PODS_DIR=/tmp/receptor-testing/K8sPods 4 | 5 | mkdir "$PODS_DIR" 6 | PODS="$(kubectl get pods --template '{{range.items}}{{.metadata.name}}{{"\n"}}{{end}}')" 7 | 8 | for pod in $PODS ; do 9 | mkdir "$PODS_DIR/$pod" 10 | kubectl get pod "$pod" --output=json > "$PODS_DIR/$pod/pod" 11 | kubectl logs "$pod" > "$PODS_DIR/$pod/logs" 12 | done 13 | -------------------------------------------------------------------------------- /.github/workflows/build_binary_from_ref.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Build binary from arbitratry repo / ref" 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | inputs: 6 | repository: 7 | description: 'The receptor repository to build from.' 8 | required: true 9 | default: 'ansible/receptor' 10 | ref: 11 | description: 'The ref to build. Can be a branch or any other valid ref.' 12 | required: true 13 | default: 'devel' 14 | jobs: 15 | build: 16 | name: build 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | repository: ${{ github.event.inputs.repository }} 24 | ref: ${{ github.event.inputs.ref }} 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: "1.22" 30 | 31 | - name: build-all target 32 | run: make receptor 33 | 34 | - name: Upload binary 35 | env: 36 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 37 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 38 | run: | 39 | pip install boto3 40 | ansible -i localhost, -c local all -m aws_s3 \ 41 | -a "bucket=receptor-nightlies object=refs/${{ github.event.inputs.ref }}/receptor src=./receptor mode=put" 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # For most projects, this workflow file will not need changing; you simply need 3 | # to commit it to your repository. 4 | # 5 | # You may wish to alter this file to override the set of languages analyzed, 6 | # or to provide custom queries or build logic. 7 | # 8 | # ******** NOTE ******** 9 | # We have attempted to detect the languages in your repository. Please check 10 | # the `language` matrix defined below to confirm you have the correct set of 11 | # supported CodeQL languages. 12 | # 13 | name: "CodeQL" 14 | 15 | on: # yamllint disable-line rule:truthy 16 | push: 17 | branches: ["devel", release_*] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: ["devel"] 21 | schedule: 22 | - cron: '18 2 * * 5' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: ['go', 'python'] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 38 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | 53 | # 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 54 | # queries: security-extended,security-and-quality 55 | 56 | 57 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 58 | # If this step fails, then you should remove it and run the build manually (see below) 59 | - name: Autobuild 60 | uses: github/codeql-action/autobuild@v3 61 | 62 | # Command-line programs to run using the OS shell. 63 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 64 | 65 | # If the Autobuild fails above, remove it and uncomment the following three lines. 66 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 67 | 68 | # - run: | 69 | # echo "Run, Build Application using script" 70 | # ./location_of_script_within_repo/buildscript.sh 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v3 74 | -------------------------------------------------------------------------------- /.github/workflows/coverage_reporting.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codecov 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request: # yamllint disable-line rule:empty-values 6 | push: 7 | branches: [devel] 8 | 9 | env: 10 | DESIRED_GO_VERSION: '1.22' 11 | 12 | jobs: 13 | go_test_coverage: 14 | name: go test coverage 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ env.DESIRED_GO_VERSION }} 28 | 29 | - name: build and install receptor 30 | run: | 31 | make build-all 32 | sudo cp ./receptor /usr/local/bin/receptor 33 | 34 | - name: Download kind binary 35 | run: curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 && chmod +x ./kind 36 | 37 | - name: Create k8s cluster 38 | run: ./kind create cluster 39 | 40 | - name: Interact with the cluster 41 | run: kubectl get nodes 42 | 43 | - name: Run receptor tests with coverage 44 | run: make coverage 45 | 46 | - name: Upload coverage reports to Codecov 47 | uses: codecov/codecov-action@v5 48 | with: 49 | fail_ci_if_error: true 50 | verbose: true 51 | env: 52 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 53 | 54 | - name: get k8s logs 55 | if: ${{ failure() }} 56 | run: .github/workflows/artifact-k8s-logs.sh 57 | 58 | - name: remove sockets before archiving logs 59 | if: ${{ failure() }} 60 | run: find /tmp/receptor-testing -name controlsock -delete 61 | 62 | - name: Artifact receptor data 63 | uses: actions/upload-artifact@v4.4.3 64 | if: ${{ failure() }} 65 | with: 66 | name: test-logs 67 | path: /tmp/receptor-testing 68 | 69 | - name: Archive receptor binary 70 | uses: actions/upload-artifact@v4.4.3 71 | with: 72 | name: receptor 73 | path: /usr/local/bin/receptor 74 | -------------------------------------------------------------------------------- /.github/workflows/dependency_review.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Dependency Review' 3 | on: [pull_request] # yamllint disable-line rule:truthy 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | dependency-review: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 'Checkout Repository' 13 | uses: actions/checkout@v4 14 | 15 | - name: 'Dependency Review' 16 | uses: actions/dependency-review-action@v4 17 | ... 18 | -------------------------------------------------------------------------------- /.github/workflows/devel_image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Publish devel image 4 | 5 | on: # yamllint disable-line rule:truthy 6 | push: 7 | branches: [devel] 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | name: Push devel image 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | # setup qemu and buildx 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | 25 | - name: Install build dependencies 26 | run: | 27 | pip install build 28 | 29 | # we will first build the image for x86 and load it on the host for testing 30 | - name: Build Image 31 | run: | 32 | export CONTAINERCMD="docker buildx" 33 | export EXTRA_OPTS="--platform linux/amd64 --load" 34 | make container REPO=quay.io/${{ github.repository }} TAG=devel 35 | 36 | - name: Test Image 37 | run: docker run --rm quay.io/${{ github.repository }}:devel receptor --version 38 | 39 | - name: Login To Quay 40 | uses: docker/login-action@v3 41 | with: 42 | username: ${{ secrets.QUAY_USERNAME }} 43 | password: ${{ secrets.QUAY_TOKEN }} 44 | registry: quay.io/${{ github.repository }} 45 | 46 | # Since x86 image is built in previous step 47 | # buildx will use cached image, hence overall time will not be affected 48 | - name: Build Multiarch Image & Push To Quay 49 | run: | 50 | export CONTAINERCMD="docker buildx" 51 | export EXTRA_OPTS="--platform linux/amd64,linux/ppc64le,linux/arm64 --push" 52 | make container REPO=quay.io/${{ github.repository }} TAG=devel 53 | -------------------------------------------------------------------------------- /.github/workflows/devel_whl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Publish nightly wheel 4 | 5 | on: # yamllint disable-line rule:truthy 6 | push: 7 | branches: [devel] 8 | 9 | jobs: 10 | sdist: 11 | runs-on: ubuntu-latest 12 | name: Build wheel 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Install build dependencies 19 | run: | 20 | pip install build 21 | 22 | - name: Build wheel 23 | run: | 24 | make clean receptorctl_wheel 25 | 26 | - name: Upload wheel 27 | env: 28 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 29 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 30 | run: | 31 | pip install boto3 32 | ansible -i localhost, -c local all -m aws_s3 \ 33 | -a "bucket=receptor-nightlies object=receptorctl/receptorctl-0.0.0-py3-none-any.whl src=$(ls receptorctl/dist/*.whl | head -n 1) mode=put" 34 | -------------------------------------------------------------------------------- /.github/workflows/reusable-nox.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Receptorctl nox sessions 3 | 4 | on: # yamllint disable-line rule:truthy 5 | workflow_call: 6 | inputs: 7 | python-version: 8 | type: string 9 | description: The Python version to use. 10 | required: true 11 | session: 12 | type: string 13 | description: The nox session to run. 14 | required: true 15 | download-receptor: 16 | type: boolean 17 | description: Whether to perform go binary download. 18 | required: false 19 | default: false 20 | go-version: 21 | type: string 22 | description: The Go version to use. 23 | required: false 24 | env: 25 | FORCE_COLOR: 1 26 | NOXSESSION: ${{ inputs.session }} 27 | 28 | jobs: 29 | nox: 30 | runs-on: ubuntu-latest 31 | 32 | name: >- # can't use `env` in this context: 33 | Run `receptorctl` ${{ inputs.session }} session 34 | 35 | steps: 36 | - name: Download the `receptor` binary 37 | if: fromJSON(inputs.download-receptor) 38 | uses: actions/download-artifact@v4 39 | with: 40 | name: receptor-${{ inputs.go-version }} 41 | path: /usr/local/bin/ 42 | 43 | - name: Set executable bit on the `receptor` binary 44 | if: fromJSON(inputs.download-receptor) 45 | run: sudo chmod a+x /usr/local/bin/receptor 46 | 47 | - name: Set up nox 48 | uses: wntrblm/nox@2025.02.09 49 | with: 50 | python-versions: ${{ inputs.python-version }} 51 | 52 | - name: Check out the source code from Git 53 | uses: actions/checkout@v4 54 | with: 55 | fetch-depth: 0 # Needed for the automation in Nox to find the last tag 56 | sparse-checkout: receptorctl 57 | 58 | - name: Provision nox environment for ${{ env.NOXSESSION }} 59 | run: nox --install-only 60 | working-directory: ./receptorctl 61 | 62 | - name: Run `receptorctl` nox ${{ env.NOXSESSION }} session 63 | run: nox --no-install 64 | working-directory: ./receptorctl 65 | -------------------------------------------------------------------------------- /.github/workflows/test-reporting.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generate junit test report 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request: # yamllint disable-line rule:empty-values 6 | push: 7 | branches: [devel] 8 | 9 | env: 10 | DESIRED_GO_VERSION: '1.22' 11 | 12 | jobs: 13 | generate_junit_test_report: 14 | name: go test coverage 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ env.DESIRED_GO_VERSION }} 28 | 29 | - name: build and install receptor 30 | run: | 31 | make build-all 32 | sudo cp ./receptor /usr/local/bin/receptor 33 | 34 | - name: Download kind binary 35 | run: curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 && chmod +x ./kind 36 | 37 | - name: Create k8s cluster 38 | run: ./kind create cluster 39 | 40 | - name: Interact with the cluster 41 | run: kubectl get nodes 42 | 43 | - name: Install go junit reporting 44 | run: go install github.com/jstemmer/go-junit-report/v2@latest 45 | 46 | - name: Run receptor tests 47 | run: go test -v 2>&1 $(go list ./... | grep -vE '/tests/|mock_|example') | go-junit-report > report.xml 48 | 49 | - name: Upload test results to dashboard 50 | if: >- 51 | !cancelled() 52 | && github.event_name == 'push' 53 | && github.repository == 'ansible/receptor' 54 | && github.ref_name == github.event.repository.default_branch 55 | run: >- 56 | curl -v --user "${{ vars.PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_USER }}:${{ secrets.PDE_ORG_RESULTS_UPLOAD_PASSWORD }}" 57 | --form "xunit_xml=@report.xml" 58 | --form "component_name=receptor" 59 | --form "git_commit_sha=${{ github.sha }}" 60 | --form "git_repository_url=https://github.com/${{ github.repository }}" 61 | "${{ vars.PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_URL }}/api/results/upload/" 62 | 63 | - name: get k8s logs 64 | if: ${{ failure() }} 65 | run: .github/workflows/artifact-k8s-logs.sh 66 | 67 | - name: remove sockets before archiving logs 68 | if: ${{ failure() }} 69 | run: find /tmp/receptor-testing -name controlsock -delete 70 | 71 | - name: Artifact receptor data 72 | uses: actions/upload-artifact@v4 73 | if: ${{ failure() }} 74 | with: 75 | name: test-logs 76 | path: /tmp/receptor-testing 77 | 78 | - name: Archive receptor binary 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: receptor 82 | path: /usr/local/bin/receptor 83 | -------------------------------------------------------------------------------- /.github/workflows/triage_new.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Triage 3 | 4 | on: # yamllint disable-line rule:truthy 5 | issues: 6 | types: 7 | - opened 8 | 9 | jobs: 10 | triage: 11 | runs-on: ubuntu-latest 12 | name: Label 13 | 14 | steps: 15 | - name: Label issues 16 | uses: github/issue-labeler@v3.4 17 | with: 18 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 19 | not-before: 2021-12-07T07:00:00Z 20 | configuration-path: .github/issue_labeler.yml 21 | enable-versioned-regex: 0 22 | if: github.event_name == 'issues' 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | kind 5 | receptor 6 | receptor.exe 7 | receptor.app 8 | recepcert 9 | /net 10 | .container-flag* 11 | .VERSION 12 | .python-version 13 | kubectl 14 | /receptorctl/.nox 15 | /receptorctl/.VERSION 16 | /receptorctl/AUTHORS 17 | /receptorctl/ChangeLog 18 | /receptor-python-worker/.VERSION 19 | /receptor-python-worker/ChangeLog 20 | /receptor-python-worker/AUTHORS 21 | /receptorctl/venv/ 22 | receptorctl-test-venv/ 23 | .vagrant/ 24 | /docs/build 25 | /dist 26 | /test-configs 27 | coverage.txt 28 | venv 29 | /vendor 30 | pkg/services/.lock 31 | pkg/workceptor/status 32 | pkg/workceptor/status.lock -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | [allowlist] 2 | description = "Global Allowlist" 3 | paths = [ 4 | '''pkg/certificates/ca_test.go''', 5 | '''pkg/netceptor/tlsconfig_test.go''', 6 | ] 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Read the Docs configuration file for Sphinx projects 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-lts-latest 11 | tools: 12 | golang: "1.22" 13 | python: "3.12" 14 | # You can also specify other tool versions: 15 | # nodejs: "20" 16 | # rust: "1.70" 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 22 | # builder: "dirhtml" 23 | # Fail on all warnings to avoid broken references 24 | fail_on_warning: true 25 | 26 | # Optionally build your docs in additional formats such as PDF and ePub 27 | # formats: 28 | # - pdf 29 | # - epub 30 | 31 | # Optional but recommended, declare the Python requirements required 32 | # to build your documentation 33 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 34 | python: 35 | install: 36 | - requirements: docs/source/requirements.txt 37 | ... 38 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.python.version=3.8, 3.9, 3.11, 3.12 2 | sonar.sources=. 3 | sonar.test.inclusions=**/*_test.go, receptorctl/tests/ 4 | sonar.tests=. 5 | -------------------------------------------------------------------------------- /cmd/receptor-cl/receptor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/ansible/receptor/cmd" 7 | "github.com/ansible/receptor/pkg/logger" 8 | "github.com/ansible/receptor/pkg/netceptor" 9 | ) 10 | 11 | func main() { 12 | logger := logger.NewReceptorLogger("") 13 | var isV2 bool 14 | newArgs := []string{} 15 | for _, arg := range os.Args { 16 | if arg == "--config-v2" { 17 | isV2 = true 18 | 19 | continue 20 | } 21 | newArgs = append(newArgs, arg) 22 | } 23 | 24 | os.Args = newArgs 25 | 26 | if isV2 { 27 | logger.Info("Running v2 cli/config") 28 | cmd.Execute() 29 | } else { 30 | cmd.RunConfigV1() 31 | } 32 | 33 | for _, arg := range os.Args { 34 | if arg == "--help" || arg == "-h" { 35 | os.Exit(0) 36 | } 37 | } 38 | 39 | if netceptor.MainInstance.BackendCount() == 0 { 40 | logger.Warning("Nothing to do - no backends are running.\n") 41 | logger.Warning("Run %s --help for command line instructions.\n", os.Args[0]) 42 | os.Exit(1) 43 | } 44 | 45 | logger.Info("Initialization complete\n") 46 | 47 | <-netceptor.MainInstance.NetceptorDone() 48 | } 49 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestInitConfig(t *testing.T) { 9 | // Save the original cfgFile value 10 | originalCfgFile := cfgFile 11 | defer func() { 12 | cfgFile = originalCfgFile 13 | }() 14 | 15 | // Test with a specific config file 16 | tmpfile, err := os.CreateTemp("", "receptor-config-*.yaml") 17 | if err != nil { 18 | t.Fatalf("Failed to create temp file: %v", err) 19 | } 20 | defer os.Remove(tmpfile.Name()) 21 | 22 | configContent := ` 23 | node: 24 | id: test-node 25 | data-dir: /tmp/test-receptor 26 | ` 27 | if _, err := tmpfile.Write([]byte(configContent)); err != nil { 28 | t.Fatalf("Failed to write to temp file: %v", err) 29 | } 30 | if err := tmpfile.Close(); err != nil { 31 | t.Fatalf("Failed to close temp file: %v", err) 32 | } 33 | 34 | // Set the config file 35 | cfgFile = tmpfile.Name() 36 | 37 | // Call initConfig 38 | initConfig() 39 | 40 | // Test with no config file (should use default) 41 | cfgFile = "" 42 | initConfig() 43 | } 44 | 45 | func TestExecute(t *testing.T) { 46 | // Skip this test for now as it requires cobra import 47 | t.Skip("Skipping TestExecute as it requires cobra import") 48 | } 49 | 50 | func TestHandleRootCommand(t *testing.T) { 51 | // Skip this test for now as it calls os.Exit 52 | t.Skip("Skipping TestHandleRootCommand as it calls os.Exit") 53 | } 54 | 55 | func TestReloadServices(t *testing.T) { 56 | // Skip this test for now as it requires more complex setup 57 | t.Skip("Skipping TestReloadServices as it requires more complex setup") 58 | } 59 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | @echo -e " doc8 doc8 linter" 15 | @echo -e " lint All documentation linters (including linkcheck)" 16 | @echo -e " rstcheck rstcheck linter" 17 | 18 | .PHONY: doc8 help lint Makefile rstcheck server 19 | 20 | doc8: 21 | doc8 --ignore D001 . 22 | 23 | # Documentation linters 24 | lint: doc8 linkcheck rstcheck 25 | 26 | rstcheck: 27 | -rstcheck --recursive --warn-unknown-settings . 28 | 29 | # Catch-all target: route all unknown targets to Sphinx using the new 30 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 31 | # Includes linkcheck 32 | %: Makefile 33 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 34 | 35 | 36 | server: 37 | python3 -m http.server 38 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | doc8 2 | pbr 3 | rstcheck >= 6 4 | six 5 | sphinx 6 | sphinx_ansible_theme 7 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | ****************** 2 | Contributor guide 3 | ****************** 4 | 5 | Receptor is an open source project that lives at https://github.com/ansible/receptor 6 | 7 | .. contents:: 8 | :local: 9 | 10 | =============== 11 | Code of conduct 12 | =============== 13 | 14 | All project contributors must abide by the `Ansible Code of Conduct `_. 15 | 16 | ============ 17 | Contributing 18 | ============ 19 | 20 | Receptor welcomes community contributions! See the :ref:`dev_guide` for information about receptor development. 21 | 22 | ------------- 23 | Pull requests 24 | ------------- 25 | Contributions to Receptor go through the Github pull request process. 26 | 27 | An initial checklist for your change to increase the likelihood of acceptance: 28 | - No issues when running linters/code checkers 29 | - No issues from unit/functional tests 30 | - Write descriptive and meaningful commit messages. See `How to write a Git commit message `_ and `Learn to write good commit message and description `_. 31 | 32 | =============== 33 | Release process 34 | =============== 35 | 36 | Before starting the release process verify that `make test` and `go test tests/goroutines/simple_config.go` tests pass. 37 | 38 | Maintainers have the ability to run the `Stage Release`_ workflow. Running this workflow will: 39 | 40 | - Build and push the container image to ghcr.io. This serves as a staging environment where the image can be tested. 41 | - Create a draft release at ``_ 42 | 43 | After the draft release has been created, edit it and populate the description. Once you are done, click "Publish release". 44 | 45 | After the release is published, the `Promote Release `_ workflow will run automatically. This workflow will: 46 | 47 | - Publish ``receptorctl`` to `PyPI `_. 48 | - Pull the container image from ghcr.io, re-tag, and push to `Quay.io `_. 49 | - Build binaries for various OSes/platforms, and attach them to the `release `_. 50 | 51 | .. note:: 52 | If you need to re-run `Stage Release`_ more than once, delete the tag beforehand to prevent the workflow from failing. 53 | 54 | .. _Stage Release: https://github.com/ansible/receptor/actions/workflows/stage.yml 55 | 56 | -------------------------------------------------------------------------------- /docs/source/getting_started_guide/creating_a_basic_network.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _creating_a_basic_network: 3 | 4 | ############################### 5 | Creating a basic 3-node network 6 | ############################### 7 | 8 | In this section, we will create a three-node network. 9 | The three nodes are: foo, bar, and baz. 10 | 11 | `foo -> bar <- baz` 12 | 13 | foo and baz are directly connected to bar with TCP connections. 14 | 15 | foo can reach baz by sending network packets through bar. 16 | 17 | *********************** 18 | Receptor configurations 19 | *********************** 20 | 21 | 1. Create three configuration files, one for each node. 22 | 23 | ``foo.yml`` 24 | 25 | .. code-block:: yaml 26 | 27 | - node: 28 | id: foo 29 | 30 | - control-service: 31 | service: control 32 | filename: /tmp/foo.sock 33 | 34 | - tcp-peer: 35 | address: localhost:2222 36 | 37 | - log-level: 38 | level: debug 39 | 40 | ... 41 | 42 | ``bar.yml`` 43 | 44 | .. code-block:: yaml 45 | 46 | --- 47 | - node: 48 | id: bar 49 | 50 | - control-service: 51 | service: control 52 | filename: /tmp/bar.sock 53 | 54 | - tcp-listener: 55 | port: 2222 56 | 57 | - log-level: 58 | level: debug 59 | 60 | ... 61 | 62 | ``baz.yml`` 63 | 64 | .. code-block:: yaml 65 | 66 | --- 67 | - node: 68 | id: baz 69 | 70 | - control-service: 71 | service: control 72 | filename: /tmp/baz.sock 73 | 74 | - tcp-peer: 75 | address: localhost:2222 76 | 77 | - log-level: 78 | level: debug 79 | 80 | - work-command: 81 | workType: echo 82 | command: bash 83 | params: "-c \"while read -r line; do echo $line; sleep 1; done\"" 84 | allowruntimeparams: true 85 | 86 | ... 87 | 88 | 2. Run the services in separate terminals. 89 | 90 | .. code-block:: bash 91 | 92 | ./receptor --config foo.yml 93 | 94 | .. code-block:: bash 95 | 96 | ./receptor --config bar.yml 97 | 98 | .. code-block:: bash 99 | 100 | ./receptor --config baz.yml 101 | 102 | .. seealso:: 103 | 104 | :ref:`configuring_receptor_with_a_config_file` 105 | Configuring Receptor with a configuration file 106 | :ref:`connecting_nodes` 107 | Detail on connecting receptor nodes 108 | -------------------------------------------------------------------------------- /docs/source/getting_started_guide/index.rst: -------------------------------------------------------------------------------- 1 | ############################# 2 | Getting started with Receptor 3 | ############################# 4 | 5 | Receptor is an overlay network that distributes work across large and dispersed collections of worker nodes. 6 | Receptor nodes establish peer-to-peer connections through existing networks. 7 | 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. 8 | 9 | .. image:: mesh.png 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | :caption: Contents: 14 | 15 | introduction 16 | installing_receptor 17 | creating_a_basic_network 18 | trying_sample_commands 19 | 20 | .. seealso:: 21 | 22 | :ref:`interacting_with_nodes` 23 | Further examples of working with nodes 24 | :ref:`connecting_nodes` 25 | Detail on connecting receptor nodes 26 | -------------------------------------------------------------------------------- /docs/source/getting_started_guide/installing_receptor.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _installing_receptor: 3 | 4 | ################### 5 | Installing Receptor 6 | ################### 7 | 8 | 1. `Download receptor `_ 9 | 2. Install receptor (per installation guide below) 10 | 3. Install receptorctl 11 | 12 | .. code-block:: bash 13 | 14 | pip install receptorctl 15 | 16 | .. seealso:: 17 | 18 | :ref:`installing` 19 | Detailed installation instructions 20 | :ref:`using_receptor_containers` 21 | Using receptor in containers 22 | -------------------------------------------------------------------------------- /docs/source/getting_started_guide/introduction.rst: -------------------------------------------------------------------------------- 1 | ######################## 2 | Introduction to receptor 3 | ######################## 4 | 5 | Receptor is an overlay network. 6 | It eases the work distribution across a large and dispersed collection 7 | of workers 8 | 9 | Receptor nodes establish peer-to-peer connections with each other through 10 | existing networks 11 | 12 | Once connected, the receptor mesh provides: 13 | 14 | * Datagram (UDP-like) and stream (TCP-like) capabilities to applications 15 | * Robust unit-of-work handling 16 | * Resiliency against transient network failures 17 | -------------------------------------------------------------------------------- /docs/source/getting_started_guide/mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/receptor/175087548e366d375f0dc8d97b8c1c5217393dd5/docs/source/getting_started_guide/mesh.png -------------------------------------------------------------------------------- /docs/source/getting_started_guide/trying_sample_commands.rst: -------------------------------------------------------------------------------- 1 | ################### 2 | Try Sample Commands 3 | ################### 4 | 5 | .. note:: 6 | You must complete the prior steps of network setup and Receptor installation for these commands to work. 7 | 8 | 1. Show network status 9 | 10 | .. code-block:: bash 11 | 12 | receptorctl --socket /tmp/foo.sock status 13 | 14 | 2. Ping node baz from node foo 15 | 16 | .. code-block:: bash 17 | 18 | receptorctl --socket /tmp/foo.sock ping baz 19 | 20 | 3. Submit work from foo to baz and stream results back to foo 21 | 22 | .. code-block:: bash 23 | 24 | seq 10 | receptorctl --socket /tmp/foo.sock work submit --node baz echo --payload - -f 25 | 26 | 4. List work units 27 | 28 | .. code-block:: bash 29 | 30 | receptorctl --socket /tmp/foo.sock work list --node foo 31 | 32 | 5. Get work unit id using jq 33 | 34 | .. code-block:: bash 35 | 36 | receptorctl --socket /tmp/foo.sock work list --node foo | jq --raw-output '.|keys|first' 37 | 38 | 6. Re-stream the work results from work unit 39 | 40 | .. code-block:: bash 41 | 42 | receptorctl --socket /tmp/foo.sock work results work_unit_id 43 | 44 | Congratulations, Receptor is now ready to use! 45 | 46 | .. seealso:: 47 | 48 | :ref:`control_service_commands` 49 | Control service commands 50 | :ref:`creating_a_basic_network` 51 | Creating a Basic Network 52 | :ref:`installing_receptor` 53 | Installing Receptor 54 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. receptor documentation master file, created by 2 | sphinx-quickstart on Sat May 1 19:39:15 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ############################### 7 | Ansible Receptor documentation 8 | ############################### 9 | 10 | 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. 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | installation 16 | getting_started_guide/index 17 | user_guide/index 18 | developer_guide 19 | receptorctl/index 20 | porting_guide/index 21 | roadmap/index 22 | upgrade/index 23 | contributing 24 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installing: 2 | 3 | ******************* 4 | Installation guide 5 | ******************* 6 | 7 | Download and extract precompiled binary for your OS and platform from `the releases page on GitHub `_ 8 | 9 | Alternatively, you can compile Receptor from source code (Golang 1.22+ required) 10 | 11 | .. code-block:: bash 12 | 13 | make receptor 14 | 15 | Test the installation with the following commands: 16 | 17 | .. code-block:: bash 18 | 19 | receptor --help 20 | receptor --version 21 | 22 | The preferred way to interact with Receptor nodes is to use the ``receptorctl`` command line tool 23 | 24 | .. code-block:: bash 25 | 26 | pip install receptorctl 27 | 28 | ``receptorctl`` will be used in various places throughout this documentation. 29 | -------------------------------------------------------------------------------- /docs/source/porting_guide/PORTING_2.rst: -------------------------------------------------------------------------------- 1 | -------------------------- 2 | Receptor 2.x Porting Guide 3 | -------------------------- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | We suggest you read this page along with the Receptor 2 Changelog to understand what updates you may need to make 9 | 10 | ^^^^^^^^^^^^^^^^^ 11 | Software Versions 12 | ^^^^^^^^^^^^^^^^^ 13 | 14 | "" 15 | Go 16 | "" 17 | TBD 18 | 19 | """""" 20 | Python 21 | """""" 22 | TBD 23 | 24 | ^^^^^^^^ 25 | Features 26 | ^^^^^^^^ 27 | 28 | """""""""" 29 | Deprecated 30 | """""""""" 31 | TBD 32 | 33 | """"""" 34 | Removed 35 | """"""" 36 | TBD 37 | 38 | ^^^^^^^^^^^^ 39 | Command Line 40 | ^^^^^^^^^^^^ 41 | TBD 42 | 43 | ^^^^^^^^^^^^^ 44 | Configuration 45 | ^^^^^^^^^^^^^ 46 | TBD 47 | 48 | ^^^^^^^^^^^^^^^ 49 | Control Service 50 | ^^^^^^^^^^^^^^^ 51 | TBD 52 | 53 | ^^^^^^^^^ 54 | Netceptor 55 | ^^^^^^^^^ 56 | TBD 57 | 58 | ^^^^^^^^^^ 59 | Workceptor 60 | ^^^^^^^^^^ 61 | TBD 62 | -------------------------------------------------------------------------------- /docs/source/porting_guide/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 1 3 | :glob: 4 | 5 | receptor_porting_index 6 | -------------------------------------------------------------------------------- /docs/source/porting_guide/receptor_porting_index.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Receptor Porting Guides 3 | ======================= 4 | 5 | This section lists porting guides that can help in updating your environment from one version of Receptor to the next. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :glob: 10 | :caption: Receptor Porting Guides 11 | 12 | PORTING_2 13 | -------------------------------------------------------------------------------- /docs/source/receptorctl/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 1 3 | :glob: 4 | 5 | receptorctl_index 6 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_connect.rst: -------------------------------------------------------------------------------- 1 | ------- 2 | connect 3 | ------- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl connect`` establishes a connection between local client and a Receptor node. 9 | 10 | Command syntax: ``receptorctl --socket= connect `` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | 26 | ``remote_node`` is the identifier of a Receptor node. 27 | 28 | ``remote_control_service`` is the service name of a Receptor node. 29 | 30 | .. seealso:: 31 | 32 | :ref:`connect_to_csv` 33 | Connect to any Receptor control service running on the mesh. 34 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_index.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Receptor client commands 3 | ======================== 4 | 5 | The Receptor client, ``receptorctl``, provides a command line interface for interacting with and managing Receptor nodes. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :glob: 10 | :caption: Receptorctl commands 11 | 12 | receptorctl_connect 13 | receptorctl_ping 14 | receptorctl_reload 15 | receptorctl_status 16 | receptorctl_traceroute 17 | receptorctl_version 18 | receptorctl_work_cancel 19 | receptorctl_work_list 20 | receptorctl_work_release 21 | receptorctl_work_results 22 | receptorctl_work_submit 23 | 24 | .. attention: 25 | Receptor has commands that are intended to provide internal functionality. These commands are not supported by ``receptorctl``: 26 | - ``work force-release``. 27 | - ``work status``. 28 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_ping.rst: -------------------------------------------------------------------------------- 1 | ---- 2 | ping 3 | ---- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl ping`` tests the network reachability of Receptor nodes. 9 | 10 | Command syntax: ``receptorctl --socket= [--count ] [--delay ] ping `` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | 26 | ``count`` specifies the number of pings to send. The value must be a positive integer. The default is ``4``. 27 | 28 | ``delay`` specifies the time, in seconds, to wait between pings. The value must be a positive float. The default is ``1.0``. 29 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_reload.rst: -------------------------------------------------------------------------------- 1 | ------ 2 | reload 3 | ------ 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl reload`` reloads the Receptor configuration for the connected node. 9 | 10 | Command syntax: ``receptorctl --socket= reload`` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_traceroute.rst: -------------------------------------------------------------------------------- 1 | ---------- 2 | traceroute 3 | ---------- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl traceroute`` Displays the network route that packets follow to Receptor nodes. 9 | 10 | Command syntax: ``receptorctl --socket= traceroute `` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_version.rst: -------------------------------------------------------------------------------- 1 | ------- 2 | version 3 | ------- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl version`` displays version information for receptorctl and the Receptor node to which it is connected. 9 | 10 | Command syntax: ``receptorctl --socket= version`` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_work_cancel.rst: -------------------------------------------------------------------------------- 1 | ----------- 2 | work cancel 3 | ----------- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl work cancel`` terminates one or more units of work. 9 | 10 | Command syntax: ``receptorctl --socket= work cancel <> [...]`` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | 26 | ``Unit ID`` is a unique identifier for a work unit (job). When running the ``work cancel`` command, you should specify the ``Unit ID`` for the Receptor node to which you are connected. 27 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_work_list.rst: -------------------------------------------------------------------------------- 1 | --------- 2 | work list 3 | --------- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl work list`` displays known units of work 9 | 10 | Command syntax: ``receptorctl --socket= work list`` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | 26 | The output is divided into work unit sections listed below. 27 | Field values might be listed separately. 28 | Columns are the actual JSON node values. 29 | 30 | ^^^^^^^^^^^^^^^^^ 31 | Work unit section 32 | ^^^^^^^^^^^^^^^^^ 33 | 34 | .. list-table:: Work unit section 35 | :header-rows: 1 36 | :widths: auto 37 | 38 | * - Column 39 | - Description 40 | * - ``."Work unit string"`` 41 | - Random eight character work unit (job) string. 42 | * - ``."Work unit string"."Detail"`` 43 | - Work unit output. 44 | * - ``."Work unit string"."ExtraData"`` 45 | - Additional information added for specific worktypes. 46 | * - ``.""Work unit string"."State"`` 47 | - Current state for the work unit (int). 48 | * - ``.""Work unit string"."StateName"`` 49 | - Human-readable current state for the work unit. 50 | * - ``.""Work unit string"."StdoutSize"`` 51 | - Size of the work unit output (bytes). 52 | * - ``.""Work unit string"."WorkType"`` 53 | - Execution request type for the work unit. 54 | 55 | ^^^^^^^^^^^^^^^^ 56 | Work unit states 57 | ^^^^^^^^^^^^^^^^ 58 | 59 | .. list-table:: Work unit states 60 | :header-rows: 1 61 | :widths: auto 62 | 63 | * - State 64 | - StateName 65 | - Description 66 | * - ``0`` 67 | - ``Pending`` 68 | - Work unit has not started. 69 | * - ``1`` 70 | - ``Running`` 71 | - Work unit is currently executing. 72 | * - ``2`` 73 | - ``Succeeded`` 74 | - Work unit completed without error. 75 | * - ``3`` 76 | - ``Failed`` 77 | - Work unit encountered an error or unexpected condition and did not complete. 78 | * - ``4`` 79 | - ``Canceled`` 80 | - Work unit was terminated externally. 81 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_work_release.rst: -------------------------------------------------------------------------------- 1 | ------------ 2 | work release 3 | ------------ 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl work release`` deletes one or more units of work. 9 | 10 | Command syntax: ``receptorctl --socket= work release [<>] <> [...]`` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | 26 | ``Unit ID`` is a unique identifier for a work unit (job). When running the ``work release`` command, you should specify the ``Unit ID`` for the Receptor node to which you are connected. 27 | 28 | ``--all`` deletes all work units known by the Receptor node to which you are connected. 29 | ``--force`` deletes work units locally on the Receptor node to which you are connected and takes effect even if the remote Receptor node is unreachable. 30 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_work_results.rst: -------------------------------------------------------------------------------- 1 | ------------ 2 | work results 3 | ------------ 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl work results`` gets results for successfully completed, failed, stopped, or currently running, units of work. 9 | 10 | Command syntax: ``receptorctl --socket= work results [<>] <> [...]`` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | 26 | ``Unit ID`` is a unique identifier for a work unit (job). When running the ``work results`` command, you should specify the ``Unit ID`` for the Receptor node to which you are connected. 27 | -------------------------------------------------------------------------------- /docs/source/receptorctl/receptorctl_work_submit.rst: -------------------------------------------------------------------------------- 1 | ----------- 2 | work submit 3 | ----------- 4 | 5 | .. contents:: 6 | :local: 7 | 8 | ``receptorctl work submit`` requests a Receptor node to run a unit of work. 9 | 10 | Command syntax: ``receptorctl --socket= work submit [<>] <> [<>]`` 11 | 12 | ``socket_path`` is the control socket address for the Receptor connection. 13 | The default is ``unix:`` for a Unix socket. 14 | Use ``tcp://`` for a TCP socket. 15 | The corresponding environment variable is ``RECEPTORCTL_SOCKET``. 16 | 17 | .. code-block:: text 18 | 19 | ss --listening --processes --unix 'src = unix:' 20 | Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process 21 | u_str LISTEN 0 4096 /tmp/local.sock 38130170 * 0 users:(("receptor",pid=3226769,fd=7)) 22 | 23 | ``ps -fp $(pidof receptor)`` 24 | ``lsof -p `` 25 | 26 | ``WorkType`` specifies an execution request type for the work unit. Use the ``receptorctl status`` command to find available work types for Receptor nodes. 27 | 28 | ``Runtime Parameters`` are parameters passed by Receptor to the work command. 29 | 30 | ^^^^^^^^^^^^^^^^^^^ 31 | Work submit options 32 | ^^^^^^^^^^^^^^^^^^^ 33 | 34 | You can use the following options with the ``work submit`` command: 35 | 36 | .. list-table:: 37 | :header-rows: 1 38 | :widths: auto 39 | 40 | * - Option 41 | - Description 42 | * - ``-a``, ``--param <>=<>`` 43 | - Adds a Receptor parameter in key=value format. 44 | * - ``-f``, ``--follow`` 45 | - Keeps Receptorctl to remain attached to the job and displays the job results. 46 | * - ``-l``, ``--payload-literal <>`` 47 | - Uses the value of ``<>`` as the literal unit of work data. 48 | * - ``-n``, ``--no-payload`` 49 | - Sends an empty payload. 50 | * - ``--node <>`` 51 | - Specifies the Receptor node on which the work runs. The default is the local node. 52 | * - ``-p``, ``--payload <>`` 53 | - Specifies the file that contains data for the unit of work. Specify ``-`` for standard input (stdin). 54 | * - ``--rm`` 55 | - Releases the work unit after completion. 56 | * - ``--signwork`` 57 | - Digitally signs remote work submissions to standard output (stdout). 58 | * - ``--tls-client <>`` 59 | - Specifies the TLS client that submits work to a remote node. 60 | * - ``--ttl <>`` 61 | - Specifies the time to live (TTL) for remote work requests in ``##h##m##s`` format; for example ``1h20m30s`` or ``30m10s``. Use the ``receptorctl work list`` command to display units of work on Receptor nodes and determine appropriate TTL values. 62 | -------------------------------------------------------------------------------- /docs/source/requirements.txt: -------------------------------------------------------------------------------- 1 | pbr 2 | sphinx 3 | sphinx-ansible-theme 4 | six 5 | -------------------------------------------------------------------------------- /docs/source/roadmap/ROADMAP_2.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Receptor 2 3 | ========== 4 | **Receptor Freeze: TBD** 5 | 6 | **Target: TBD** 7 | 8 | .. contents:: 9 | :local: 10 | 11 | --------------- 12 | Release Manager 13 | --------------- 14 | Aaron Hetherington (IRC/GitHub: @AaronH88) 15 | 16 | ------------- 17 | Configuration 18 | ------------- 19 | 20 | --------- 21 | Netceptor 22 | --------- 23 | 24 | ---------- 25 | Workceptor 26 | ---------- 27 | -------------------------------------------------------------------------------- /docs/source/roadmap/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 1 3 | :glob: 4 | 5 | receptor_roadmap_index 6 | -------------------------------------------------------------------------------- /docs/source/roadmap/receptor_roadmap_index.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Receptor Roadmaps 3 | ================= 4 | 5 | The ``Receptor`` team develops a roadmap for each major Receptor release. 6 | The latest roadmap shows current work; older roadmaps provide a history of the 7 | project. We don't publish roadmaps for minor or subminor versions. So 2.0 and 8 | 3.0 have roadmaps, but 2.1.0 and 2.10.1 do not. 9 | 10 | We incorporate team and community feedback in each roadmap, and aim for further 11 | transparency and better inclusion of both community desires and submissions. 12 | 13 | Each roadmap offers a *best guess*, based on the ``Receptor`` team's experience 14 | and on requests and feedback from the community, of what will be included in a 15 | given release. However, some items on the roadmap may be dropped due to time 16 | constraints, lack of community maintainers, and so on. 17 | 18 | Each roadmap is published both as an idea of what is upcoming in ``Receptor``, 19 | and as a medium for seeking further feedback from the community. 20 | 21 | You can submit feedback on the current roadmap by: 22 | - Creating issue on GitHub in `ansible/receptor repository `_ 23 | 24 | Go to `Ansible forum `_ to join the community discussions. 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | :glob: 29 | :caption: Receptor Roadmaps 30 | 31 | ROADMAP_2 32 | -------------------------------------------------------------------------------- /docs/source/upgrade/UPGRADE_2.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Receptor 2 3 | ========== 4 | 5 | To upgrade an existing Receptor installation to the latest version, follow the instructions in :ref:`installing` 6 | 7 | 8 | .. contents:: 9 | :local: 10 | 11 | -------------------------------------------------------------------------------- /docs/source/upgrade/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 1 3 | :glob: 4 | 5 | receptor_upgrade_index 6 | -------------------------------------------------------------------------------- /docs/source/upgrade/receptor_upgrade_index.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Receptor Upgrades 3 | ================= 4 | 5 | You can submit feedback on the Receptor upgrade guides can be submitted by: 6 | - Creating issue on github in `ansible/receptor repository `_ 7 | 8 | Go to `Ansible forum `_ to join the community discussions. 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | :glob: 13 | :caption: Receptor Upgrade Guides 14 | 15 | UPGRADE_2 16 | -------------------------------------------------------------------------------- /docs/source/user_guide/basic_usage.rst: -------------------------------------------------------------------------------- 1 | Using Receptor 2 | =============== 3 | 4 | . contents:: 5 | 6 | :local: 7 | 8 | Using the Receptor CLI 9 | ---------------------- 10 | 11 | .. list-table:: Persistent Flags 12 | :header-rows: 1 13 | :widths: auto 14 | 15 | * - Action 16 | - Description 17 | * - ``--config `` 18 | - Loads configuration options from a YAML file. 19 | * - ``--version`` 20 | - Display the Receptor version. 21 | * - ``--help`` 22 | - Display this help 23 | 24 | .. _configuring_receptor_with_a_config_file: 25 | 26 | Configuring Receptor with a config file 27 | ---------------------------------------- 28 | 29 | Receptor can be configured on the command-line, exemplified above, or via a yaml config file. All actions and parameters shown in ``receptor --help`` can be written to a config file. 30 | 31 | .. code-block:: yaml 32 | 33 | --- 34 | version: 2 35 | node: 36 | id: foo 37 | 38 | local-only: 39 | local: true 40 | 41 | log-level: 42 | level: Debug 43 | 44 | Start receptor using the config file 45 | 46 | .. code-block:: bash 47 | 48 | receptor --config foo.yml 49 | 50 | Changing the configuration file does take effect until the receptor process is restarted. 51 | 52 | .. _using_receptor_containers: 53 | 54 | Use Receptor through a container image 55 | --------------------------------------- 56 | 57 | .. code-block:: bash 58 | 59 | podman pull quay.io/ansible/receptor 60 | 61 | Start a container, which automatically runs receptor with the default config located at ``/etc/receptor/receptor.conf`` 62 | 63 | .. code-block:: bash 64 | 65 | podman run -it --rm --name receptor quay.io/ansible/receptor 66 | 67 | In another terminal, issue a basic "status" command to the running receptor process 68 | 69 | .. code-block:: bash 70 | 71 | $ podman exec receptor receptorctl status 72 | Node ID: d9b5a8e3c156 73 | Version: 1.0.0 74 | System CPU Count: 8 75 | System Memory MiB: 15865 76 | 77 | Node Service Type Last Seen Tags Work Types 78 | d9b5a8e3c156 control Stream 2021-08-04 19:26:14 - - 79 | 80 | Note: the config file does not specify a node ID, so the hostname (on the container) is chosen as the node ID. 81 | -------------------------------------------------------------------------------- /docs/source/user_guide/edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/receptor/175087548e366d375f0dc8d97b8c1c5217393dd5/docs/source/user_guide/edge.png -------------------------------------------------------------------------------- /docs/source/user_guide/edge_networks.rst: -------------------------------------------------------------------------------- 1 | Support for Edge Networks 2 | ========================= 3 | 4 | Recptor out-of-the-box has the ability to support complicated networking environments including edge networks. 5 | 6 | .. contents:: 7 | :local: 8 | 9 | Consider the following environment: 10 | 11 | .. image:: edge.png 12 | :alt: Network diagram with netceptor peers to edge network 13 | 14 | Configurable-Items 15 | ------------------- 16 | 17 | Receptor encapsulates the concepts of `below-the-mesh` and `above-the-mesh` connections. Please refer to :doc:`tls` for a better understanding of these networking layers. 18 | 19 | If a particular node in a network has higher than normal latency, we allow the users to define a finely-grained idle connection timeout value for any given Receptor node. This will help Receptor keep `below-the-mesh` tcp connections alive. Receptor will monitor backend connections for traffic and will timeout any connection that hasn't seen traffic for a period of time. Once the connection is dropped, a new connection is formed automatically. 20 | 21 | If a connection timeout occurs, the users can expect to see a message like this in their receptor logs. 22 | 23 | .. code-block:: text 24 | 25 | DEBUG 2022/04/07 12:48:56 Sending initial connection message 26 | ERROR 2022/04/07 12:48:56 Backend sending error read tcp 10.26.5 0.239:27199->10.102.21.131:35024: i/o **timeout** 27 | 28 | To circumvent this scenario from happening, users can leverage the `maxidleconnectiontimeout` parameter in their configuration files. 29 | 30 | `maxidleconnectiontimeout` A user-defined parameter in the configuration file that will set the `below-the-mesh` tcp connection timeout. 31 | 32 | The configuration files for the diagram above are listed below. 33 | 34 | foo.yml 35 | 36 | .. code-block:: yaml 37 | 38 | --- 39 | version: 2 40 | node: 41 | id: foo 42 | maxidleconnectiontimeout: 60s 43 | 44 | log-level: 45 | level: Debug 46 | 47 | tcp-listeners: 48 | - port: 2222 49 | 50 | bar.yml 51 | 52 | .. code-block:: yaml 53 | 54 | --- 55 | node: 56 | id: bar 57 | maxidleconnectiontimeout: 60s 58 | 59 | log-level: 60 | level: Debug 61 | 62 | tcp-peers: 63 | - address: localhost:2222 64 | 65 | fish.yml 66 | 67 | .. code-block:: yaml 68 | 69 | --- 70 | node: 71 | id: fish 72 | maxidleconnectiontimeout: 60s 73 | 74 | log-level: 75 | level: Debug 76 | 77 | tcp-peers: 78 | - address: localhost:2222 79 | 80 | *Note* - All Receptor nodes in the mesh must define a `maxidleconnectiontimeout` value, if this value is consumed on ANY node. The effective `maxidleconnectiontimeout` value is the minumum value between all the nodes in the mesh. 81 | -------------------------------------------------------------------------------- /docs/source/user_guide/firewall.rst: -------------------------------------------------------------------------------- 1 | .. _firewall_rules: 2 | 3 | Firewall Rules 4 | ============== 5 | 6 | Receptor has the ability to accept, drop, or reject traffic based on any combination of the following: 7 | 8 | - ``FromNode`` 9 | - ``ToNode`` 10 | - ``FromService`` 11 | - ``ToService`` 12 | 13 | Firewall rules are added under the ``node`` entry in a Receptor configuration file: 14 | 15 | .. code-block:: yaml 16 | 17 | # Accepts everything 18 | node: 19 | firewallrules: 20 | - action: "accept" 21 | 22 | .. code-block:: yaml 23 | 24 | # Drops traffic from `foo` to `bar`'s control service 25 | node: 26 | firewallrules: 27 | - action: "drop" 28 | fromnode: "foo" 29 | tonode: "bar" 30 | toservice: "control" 31 | 32 | .. code-block:: yaml 33 | 34 | # Rejects traffic originating from nodes like abcb, adfb, etc 35 | node: 36 | firewallrules: 37 | - action: "reject" 38 | fromnode: "/a.*b/" 39 | 40 | .. code-block:: yaml 41 | 42 | # Rejects traffic destined for nodes like abcb, AdfB, etc 43 | node: 44 | firewallrules: 45 | - action: "reject" 46 | tonode: "/(?i)a.*b/" 47 | -------------------------------------------------------------------------------- /docs/source/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | User guide 3 | ******************* 4 | 5 | This guide describes how to use receptor in multiple environments and uses the following terms: 6 | 7 | .. glossary:: 8 | 9 | backend 10 | A type of connection that receptor nodes can pass traffic over. Current backends include TCP, UDP and websockets. 11 | 12 | backend peers 13 | A node connected to another through receptor backends. 14 | 15 | control node 16 | A node running the receptor control service. 17 | 18 | control service 19 | A built-in service that usually runs under the name `control`. Used to report status and to launch and monitor work. 20 | 21 | netceptor 22 | The component of receptor that handles all networking functionality. 23 | 24 | netceptor peers 25 | A receptor node directly connected to another receptor node. 26 | 27 | node 28 | A single running instance of receptor. 29 | 30 | node ID 31 | An arbitrary string identifying a single node, analogous to an IP address. 32 | 33 | receptor 34 | The receptor application taken as a whole, which typically runs as a daemon. 35 | 36 | receptorctl 37 | A user-facing command line used to interact with receptor, typically over a Unix domain socket. 38 | 39 | workceptor 40 | The component of receptor that handles work units. 41 | 42 | .. toctree:: 43 | :maxdepth: 2 44 | 45 | basic_usage 46 | configuration_options 47 | connecting_nodes 48 | edge_networks 49 | firewall 50 | interacting_with_nodes 51 | k8s 52 | tls 53 | workceptor 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/source/user_guide/mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/receptor/175087548e366d375f0dc8d97b8c1c5217393dd5/docs/source/user_guide/mesh.png -------------------------------------------------------------------------------- /docs/source/user_guide/remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/receptor/175087548e366d375f0dc8d97b8c1c5217393dd5/docs/source/user_guide/remote.png -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:generate mockgen -source=pkg/backends/websockets.go -destination=pkg/backends/mock_backends/websockets.go 4 | //go:generate mockgen -source=pkg/certificates/oser.go -destination=pkg/certificates/mock_certificates/oser.go 5 | //go:generate mockgen -source=pkg/certificates/rsaer.go -destination=pkg/certificates/mock_certificates/rsaer.go 6 | //go:generate mockgen -source=pkg/controlsvc/controlsvc.go -destination=pkg/controlsvc/mock_controlsvc/controlsvc.go 7 | //go:generate mockgen -source=pkg/controlsvc/interfaces.go -destination=pkg/controlsvc/mock_controlsvc/interfaces.go 8 | //go:generate mockgen -source=pkg/framer/framer.go -destination=pkg/framer/mock_framer/framer.go 9 | //go:generate mockgen -source=pkg/netceptor/conn.go -destination=pkg/netceptor/mock_netceptor/conn.go 10 | //go:generate mockgen -source=pkg/netceptor/external_backend.go -destination=pkg/netceptor/mock_netceptor/external_backend.go 11 | //go:generate mockgen -source=pkg/netceptor/netceptor.go -destination=pkg/netceptor/mock_netceptor/netceptor.go 12 | //go:generate mockgen -source=pkg/netceptor/packetconn.go -destination=pkg/netceptor/mock_netceptor/packetconn.go 13 | //go:generate mockgen -source=pkg/netceptor/ping.go -destination=pkg/netceptor/mock_netceptor/ping.go 14 | //go:generate mockgen -source=pkg/services/interfaces/net_interfaces.go -destination=pkg/services/interfaces/mock_interfaces/net_interfaces.go 15 | //go:generate mockgen -source=pkg/services/command.go -destination=pkg/services/mock_services/command.go 16 | //go:generate mockgen -source=pkg/services/tcp_proxy.go -destination=pkg/services/mock_services/tcp_proxy.go 17 | //go:generate mockgen -source=pkg/services/udp_proxy.go -destination=pkg/services/mock_services/udp_proxy.go 18 | //go:generate mockgen -source=pkg/utils/net.go -destination=pkg/utils/mock_utils/net.go 19 | //go:generate mockgen -source=pkg/workceptor/command.go -destination=pkg/workceptor/mock_workceptor/command.go 20 | //go:generate mockgen -source=pkg/workceptor/interfaces.go -destination=pkg/workceptor/mock_workceptor/interfaces.go 21 | //go:generate mockgen -source=pkg/workceptor/kubernetes.go -destination=pkg/workceptor/mock_workceptor/kubernetes.go 22 | //go:generate mockgen -source=pkg/workceptor/stdio_utils.go -destination=pkg/workceptor/mock_workceptor/stdio_utils.go 23 | //go:generate mockgen -source=pkg/workceptor/workceptor.go -destination=pkg/workceptor/mock_workceptor/workceptor.go 24 | //go:generate mockgen -source=pkg/workceptor/workunitbase.go -destination=pkg/workceptor/mock_workceptor/workunitbase.go 25 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ghjm/cmdline" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | // Version is receptor app version. 11 | var Version string 12 | 13 | // cmdlineCfg is a cmdline-compatible struct for a --version command. 14 | type cmdlineCfg struct{} 15 | 16 | // Run runs the action. 17 | func (cfg cmdlineCfg) Run() error { 18 | validateVersion() 19 | fmt.Printf("%s\n", Version) 20 | 21 | return nil 22 | } 23 | 24 | func init() { 25 | version := viper.GetInt("version") 26 | if version > 1 { 27 | return 28 | } 29 | cmdline.RegisterConfigTypeForApp("receptor-version", 30 | "version", "Displays the Receptor version.", cmdlineCfg{}, cmdline.Exclusive) 31 | } 32 | 33 | func validateVersion() string { 34 | if Version == "" { 35 | return "Version unknown" 36 | } else { 37 | return Version 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValidateVersion(t *testing.T) { 10 | type test struct { 11 | name string 12 | input string 13 | expected string 14 | } 15 | 16 | for _, tt := range []*test{ 17 | { 18 | name: "version is empty string", 19 | input: "", 20 | expected: "Version unknown", 21 | }, 22 | { 23 | name: "version is zero", 24 | input: "0", 25 | expected: "0", 26 | }, 27 | { 28 | name: "version is one", 29 | input: "1", 30 | expected: "1", 31 | }, 32 | } { 33 | Version = tt.input 34 | output := validateVersion() 35 | assert.Equal(t, tt.expected, output) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packaging/container/.gitignore: -------------------------------------------------------------------------------- 1 | receptor 2 | *.whl 3 | source.tar.gz 4 | -------------------------------------------------------------------------------- /packaging/container/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi9/ubi:9.5 AS builder 2 | ARG VERSION 3 | 4 | RUN dnf -y update && \ 5 | dnf install -y golang make python3.12 python3.12-pip git 6 | RUN pip3.12 install wheel 7 | 8 | ADD source.tar.gz /source 9 | WORKDIR /source 10 | RUN make VERSION=${VERSION} 11 | 12 | FROM registry.access.redhat.com/ubi9/ubi:9.5 13 | ARG VERSION 14 | 15 | LABEL license="ASL2" 16 | LABEL name="receptor" 17 | LABEL vendor="ansible" 18 | LABEL version="${VERSION}" 19 | 20 | COPY receptorctl-${VERSION}-py3-none-any.whl /tmp 21 | COPY receptor_python_worker-${VERSION}-py3-none-any.whl /tmp 22 | COPY receptor.conf /etc/receptor/receptor.conf 23 | 24 | RUN dnf -y update && \ 25 | dnf -y install python3.12-pip && \ 26 | dnf clean all && \ 27 | pip3.12 install --no-cache-dir wheel && \ 28 | pip3.12 install --no-cache-dir dumb-init && \ 29 | pip3.12 install --no-cache-dir ansible-runner && \ 30 | pip3.12 install --no-cache-dir /tmp/*.whl && \ 31 | rm /tmp/*.whl 32 | 33 | COPY --from=builder /source/receptor /usr/bin/receptor 34 | 35 | ENV RECEPTORCTL_SOCKET=/tmp/receptor.sock 36 | 37 | EXPOSE 7323 38 | 39 | ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] 40 | CMD ["/usr/bin/receptor", "-c", "/etc/receptor/receptor.conf"] 41 | -------------------------------------------------------------------------------- /packaging/container/receptor.conf: -------------------------------------------------------------------------------- 1 | --- 2 | - control-service: 3 | service: control 4 | filename: /tmp/receptor.sock 5 | 6 | - tcp-listener: 7 | port: 7323 8 | -------------------------------------------------------------------------------- /packaging/tc-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM receptor:latest 2 | 3 | RUN dnf install tc -y 4 | 5 | ENTRYPOINT ["/bin/bash"] 6 | CMD ["-c", "/usr/bin/receptor --config /etc/receptor/receptor.conf > /etc/receptor/stdout 2> /etc/receptor/stderr"] 7 | -------------------------------------------------------------------------------- /pkg/backends/cmdline.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import "github.com/ghjm/cmdline" 4 | 5 | var backendSection = &cmdline.ConfigSection{ 6 | Description: "Commands to configure back-ends, which connect Receptor nodes together:", 7 | Order: 10, 8 | } 9 | -------------------------------------------------------------------------------- /pkg/backends/null.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "sync" 7 | 8 | "github.com/ansible/receptor/pkg/netceptor" 9 | ) 10 | 11 | type NullBackendCfg struct { 12 | Local bool 13 | } 14 | 15 | // make the nullBackendCfg object be usable as a do-nothing Backend. 16 | func (cfg NullBackendCfg) Start(_ context.Context, _ *sync.WaitGroup) (chan netceptor.BackendSession, error) { 17 | return make(chan netceptor.BackendSession), nil 18 | } 19 | 20 | // Run runs the action, in this case adding a null backend to keep the wait group alive. 21 | func (cfg NullBackendCfg) Run() error { 22 | err := netceptor.MainInstance.AddBackend(&NullBackendCfg{}) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (cfg *NullBackendCfg) GetAddr() string { 31 | return "" 32 | } 33 | 34 | func (cfg *NullBackendCfg) GetTLS() *tls.Config { 35 | return nil 36 | } 37 | 38 | func (cfg NullBackendCfg) Reload() error { 39 | return cfg.Run() 40 | } 41 | -------------------------------------------------------------------------------- /pkg/backends/utils.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/ansible/receptor/pkg/logger" 9 | "github.com/ansible/receptor/pkg/netceptor" 10 | "github.com/ansible/receptor/pkg/utils" 11 | ) 12 | 13 | const ( 14 | maxRedialDelay = 20 * time.Second 15 | ) 16 | 17 | type dialerFunc func(chan struct{}) (netceptor.BackendSession, error) 18 | 19 | // dialerSession is a convenience function for backends that use dial/retry logic. 20 | func dialerSession( 21 | ctx context.Context, 22 | wg *sync.WaitGroup, 23 | redial bool, 24 | redialDelay time.Duration, 25 | logger *logger.ReceptorLogger, 26 | df dialerFunc, 27 | ) (chan netceptor.BackendSession, error) { 28 | sessChan := make(chan netceptor.BackendSession) 29 | wg.Add(1) 30 | go func() { 31 | defer func() { 32 | wg.Done() 33 | close(sessChan) 34 | }() 35 | redialDelayInc := utils.NewIncrementalDuration(redialDelay, maxRedialDelay, 1.5) 36 | for { 37 | closeChan := make(chan struct{}) 38 | sess, err := df(closeChan) 39 | if err == nil { 40 | redialDelayInc.Reset() 41 | select { 42 | case sessChan <- sess: 43 | // continue 44 | case <-ctx.Done(): 45 | return 46 | } 47 | select { 48 | case <-closeChan: 49 | // continue 50 | case <-ctx.Done(): 51 | return 52 | } 53 | } 54 | if redial && ctx.Err() == nil { 55 | if err != nil { 56 | logger.Warning("Backend connection failed (will retry): %s\n", err) 57 | } else { 58 | logger.Warning("Backend connection exited (will retry)\n") 59 | } 60 | select { 61 | case <-redialDelayInc.NextTimeout(): 62 | continue 63 | case <-ctx.Done(): 64 | return 65 | } 66 | } else { 67 | if err != nil { 68 | logger.Error("Backend connection failed: %s\n", err) 69 | } else if ctx.Err() != nil { 70 | logger.Error("Backend connection exited\n") 71 | } 72 | 73 | return 74 | } 75 | } 76 | }() 77 | 78 | return sessChan, nil 79 | } 80 | 81 | type ( 82 | listenFunc func() error 83 | acceptFunc func() (netceptor.BackendSession, error) 84 | listenerCancelFunc func() 85 | ) 86 | 87 | // listenerSession is a convenience function for backends that use listen/accept logic. 88 | func listenerSession( 89 | ctx context.Context, 90 | wg *sync.WaitGroup, 91 | logger *logger.ReceptorLogger, 92 | lf listenFunc, 93 | af acceptFunc, 94 | lcf listenerCancelFunc, 95 | ) (chan netceptor.BackendSession, error) { 96 | if err := lf(); err != nil { 97 | return nil, err 98 | } 99 | sessChan := make(chan netceptor.BackendSession) 100 | wg.Add(1) 101 | go func() { 102 | defer func() { 103 | wg.Done() 104 | lcf() 105 | close(sessChan) 106 | }() 107 | for { 108 | c, err := af() 109 | select { 110 | case <-ctx.Done(): 111 | return 112 | default: 113 | } 114 | if err != nil { 115 | logger.Error("Error accepting connection: %s\n", err) 116 | 117 | return 118 | } 119 | select { 120 | case sessChan <- c: 121 | case <-ctx.Done(): 122 | return 123 | } 124 | } 125 | }() 126 | 127 | return sessChan, nil 128 | } 129 | -------------------------------------------------------------------------------- /pkg/certificates/cmdline.go: -------------------------------------------------------------------------------- 1 | package certificates 2 | 3 | import "github.com/ghjm/cmdline" 4 | 5 | var certSection = &cmdline.ConfigSection{ 6 | Description: "Commands to generate certificates and run a certificate authority", 7 | Order: 90, 8 | } 9 | -------------------------------------------------------------------------------- /pkg/certificates/mock_certificates/oser.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/certificates/oser.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=pkg/certificates/oser.go -destination=pkg/certificates/mock_certificates/oser.go 7 | // 8 | 9 | // Package mock_certificates is a generated GoMock package. 10 | package mock_certificates 11 | 12 | import ( 13 | fs "io/fs" 14 | reflect "reflect" 15 | 16 | gomock "go.uber.org/mock/gomock" 17 | ) 18 | 19 | // MockOser is a mock of Oser interface. 20 | type MockOser struct { 21 | ctrl *gomock.Controller 22 | recorder *MockOserMockRecorder 23 | isgomock struct{} 24 | } 25 | 26 | // MockOserMockRecorder is the mock recorder for MockOser. 27 | type MockOserMockRecorder struct { 28 | mock *MockOser 29 | } 30 | 31 | // NewMockOser creates a new mock instance. 32 | func NewMockOser(ctrl *gomock.Controller) *MockOser { 33 | mock := &MockOser{ctrl: ctrl} 34 | mock.recorder = &MockOserMockRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows the caller to indicate expected use. 39 | func (m *MockOser) EXPECT() *MockOserMockRecorder { 40 | return m.recorder 41 | } 42 | 43 | // ReadFile mocks base method. 44 | func (m *MockOser) ReadFile(name string) ([]byte, error) { 45 | m.ctrl.T.Helper() 46 | ret := m.ctrl.Call(m, "ReadFile", name) 47 | ret0, _ := ret[0].([]byte) 48 | ret1, _ := ret[1].(error) 49 | return ret0, ret1 50 | } 51 | 52 | // ReadFile indicates an expected call of ReadFile. 53 | func (mr *MockOserMockRecorder) ReadFile(name any) *gomock.Call { 54 | mr.mock.ctrl.T.Helper() 55 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadFile", reflect.TypeOf((*MockOser)(nil).ReadFile), name) 56 | } 57 | 58 | // WriteFile mocks base method. 59 | func (m *MockOser) WriteFile(name string, data []byte, perm fs.FileMode) error { 60 | m.ctrl.T.Helper() 61 | ret := m.ctrl.Call(m, "WriteFile", name, data, perm) 62 | ret0, _ := ret[0].(error) 63 | return ret0 64 | } 65 | 66 | // WriteFile indicates an expected call of WriteFile. 67 | func (mr *MockOserMockRecorder) WriteFile(name, data, perm any) *gomock.Call { 68 | mr.mock.ctrl.T.Helper() 69 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteFile", reflect.TypeOf((*MockOser)(nil).WriteFile), name, data, perm) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/certificates/mock_certificates/rsaer.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/certificates/rsaer.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=pkg/certificates/rsaer.go -destination=pkg/certificates/mock_certificates/rsaer.go 7 | // 8 | 9 | // Package mock_certificates is a generated GoMock package. 10 | package mock_certificates 11 | 12 | import ( 13 | rsa "crypto/rsa" 14 | io "io" 15 | reflect "reflect" 16 | 17 | gomock "go.uber.org/mock/gomock" 18 | ) 19 | 20 | // MockRsaer is a mock of Rsaer interface. 21 | type MockRsaer struct { 22 | ctrl *gomock.Controller 23 | recorder *MockRsaerMockRecorder 24 | isgomock struct{} 25 | } 26 | 27 | // MockRsaerMockRecorder is the mock recorder for MockRsaer. 28 | type MockRsaerMockRecorder struct { 29 | mock *MockRsaer 30 | } 31 | 32 | // NewMockRsaer creates a new mock instance. 33 | func NewMockRsaer(ctrl *gomock.Controller) *MockRsaer { 34 | mock := &MockRsaer{ctrl: ctrl} 35 | mock.recorder = &MockRsaerMockRecorder{mock} 36 | return mock 37 | } 38 | 39 | // EXPECT returns an object that allows the caller to indicate expected use. 40 | func (m *MockRsaer) EXPECT() *MockRsaerMockRecorder { 41 | return m.recorder 42 | } 43 | 44 | // GenerateKey mocks base method. 45 | func (m *MockRsaer) GenerateKey(random io.Reader, bits int) (*rsa.PrivateKey, error) { 46 | m.ctrl.T.Helper() 47 | ret := m.ctrl.Call(m, "GenerateKey", random, bits) 48 | ret0, _ := ret[0].(*rsa.PrivateKey) 49 | ret1, _ := ret[1].(error) 50 | return ret0, ret1 51 | } 52 | 53 | // GenerateKey indicates an expected call of GenerateKey. 54 | func (mr *MockRsaerMockRecorder) GenerateKey(random, bits any) *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateKey", reflect.TypeOf((*MockRsaer)(nil).GenerateKey), random, bits) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/certificates/oser.go: -------------------------------------------------------------------------------- 1 | package certificates 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | ) 7 | 8 | // Oser is the function calls interfaces for mocking os. 9 | type Oser interface { 10 | ReadFile(name string) ([]byte, error) 11 | WriteFile(name string, data []byte, perm fs.FileMode) error 12 | } 13 | 14 | // OsWrapper is the Wrapper structure for Oser. 15 | type OsWrapper struct{} 16 | 17 | // ReadFile for Oser defaults to os library call. 18 | func (ow *OsWrapper) ReadFile(name string) ([]byte, error) { 19 | return os.ReadFile(name) 20 | } 21 | 22 | // WriteFile for Oser defaults to os library call. 23 | func (ow *OsWrapper) WriteFile(name string, data []byte, perm fs.FileMode) error { 24 | return os.WriteFile(name, data, perm) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/certificates/rsaer.go: -------------------------------------------------------------------------------- 1 | package certificates 2 | 3 | import ( 4 | "crypto/rsa" 5 | "io" 6 | ) 7 | 8 | // Rsaer is the function calls interface for mocking rsa. 9 | type Rsaer interface { 10 | GenerateKey(random io.Reader, bits int) (*rsa.PrivateKey, error) 11 | } 12 | 13 | // RsaWrapper is the Wrapper structure for Rsaer. 14 | type RsaWrapper struct{} 15 | 16 | // GenerateKey for RsaWrapper defaults to rsa library call. 17 | func (rw *RsaWrapper) GenerateKey(random io.Reader, bits int) (*rsa.PrivateKey, error) { 18 | return rsa.GenerateKey(random, bits) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/controlsvc/connect.go: -------------------------------------------------------------------------------- 1 | package controlsvc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/ansible/receptor/pkg/netceptor" 9 | ) 10 | 11 | type ( 12 | ConnectCommandType struct{} 13 | ConnectCommand struct { 14 | targetNode string 15 | targetService string 16 | tlsConfigName string 17 | } 18 | ) 19 | 20 | func (t *ConnectCommandType) InitFromString(params string) (ControlCommand, error) { 21 | tokens := strings.Split(params, " ") 22 | if len(tokens) < 2 { 23 | return nil, fmt.Errorf("no connect target") 24 | } 25 | if len(tokens) > 3 { 26 | return nil, fmt.Errorf("too many parameters") 27 | } 28 | var tlsConfigName string 29 | if len(tokens) == 3 { 30 | tlsConfigName = tokens[2] 31 | } 32 | c := &ConnectCommand{ 33 | targetNode: tokens[0], 34 | targetService: tokens[1], 35 | tlsConfigName: tlsConfigName, 36 | } 37 | 38 | return c, nil 39 | } 40 | 41 | func (t *ConnectCommandType) InitFromJSON(config map[string]interface{}) (ControlCommand, error) { 42 | targetNode, ok := config["node"] 43 | if !ok { 44 | return nil, fmt.Errorf("no connect target node") 45 | } 46 | targetNodeStr, ok := targetNode.(string) 47 | if !ok { 48 | return nil, fmt.Errorf("connect target node must be string") 49 | } 50 | targetService, ok := config["service"] 51 | if !ok { 52 | return nil, fmt.Errorf("no connect target service") 53 | } 54 | targetServiceStr, ok := targetService.(string) 55 | if !ok { 56 | return nil, fmt.Errorf("connect target service must be string") 57 | } 58 | var tlsConfigStr string 59 | tlsConfig, ok := config["tls"] 60 | if ok { 61 | tlsConfigStr, ok = tlsConfig.(string) 62 | if !ok { 63 | return nil, fmt.Errorf("connect tls name must be string") 64 | } 65 | } else { 66 | tlsConfigStr = "" 67 | } 68 | c := &ConnectCommand{ 69 | targetNode: targetNodeStr, 70 | targetService: targetServiceStr, 71 | tlsConfigName: tlsConfigStr, 72 | } 73 | 74 | return c, nil 75 | } 76 | 77 | func (c *ConnectCommand) ControlFunc(_ context.Context, nc NetceptorForControlCommand, cfo ControlFuncOperations) (map[string]interface{}, error) { 78 | tlscfg, err := nc.GetClientTLSConfig(c.tlsConfigName, c.targetNode, netceptor.ExpectedHostnameTypeReceptor) 79 | if err != nil { 80 | return nil, err 81 | } 82 | rc, err := nc.Dial(c.targetNode, c.targetService, tlscfg) 83 | if err != nil { 84 | return nil, err 85 | } 86 | err = cfo.BridgeConn("Connecting\n", rc, "connected service", nc.GetLogger(), &Util{}) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | return nil, nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/controlsvc/controlsvc_stub.go: -------------------------------------------------------------------------------- 1 | //go:build no_controlsvc 2 | // +build no_controlsvc 3 | 4 | // Stub package to satisfy controlsvc dependencies while providing no functionality 5 | 6 | package controlsvc 7 | 8 | import ( 9 | "context" 10 | "crypto/tls" 11 | "fmt" 12 | "net" 13 | "os" 14 | 15 | "github.com/ansible/receptor/pkg/netceptor" 16 | ) 17 | 18 | // ErrNotImplemented is returned by most functions in this unit since it is a non-functional stub 19 | var ErrNotImplemented = fmt.Errorf("not implemented") 20 | 21 | // Server is an instance of a control service 22 | type Server struct{} 23 | 24 | // New returns a new instance of a control service. 25 | func New(stdServices bool, nc *netceptor.Netceptor) *Server { 26 | return &Server{} 27 | } 28 | 29 | // MainInstance is the global instance of the control service instantiated by the command-line main() function 30 | var MainInstance *Server 31 | 32 | // AddControlFunc registers a function that can be used from a control socket. 33 | func (s *Server) AddControlFunc(name string, cType ControlCommandType) error { 34 | return nil 35 | } 36 | 37 | // RunControlSession runs the server protocol on the given connection 38 | func (s *Server) RunControlSession(conn net.Conn) { 39 | } 40 | 41 | // RunControlSvc runs the main accept loop of the control service 42 | func (s *Server) RunControlSvc(ctx context.Context, service string, tlscfg *tls.Config, 43 | unixSocket string, unixSocketPermissions os.FileMode, tcpListen string, tcptls *tls.Config, 44 | ) error { 45 | return ErrNotImplemented 46 | } 47 | -------------------------------------------------------------------------------- /pkg/controlsvc/interfaces.go: -------------------------------------------------------------------------------- 1 | package controlsvc 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "io" 7 | "net" 8 | "time" 9 | 10 | "github.com/ansible/receptor/pkg/logger" 11 | "github.com/ansible/receptor/pkg/netceptor" 12 | ) 13 | 14 | // ControlCommandType is a type of command that can be run from the control service. 15 | type ControlCommandType interface { 16 | InitFromString(string) (ControlCommand, error) 17 | InitFromJSON(map[string]interface{}) (ControlCommand, error) 18 | } 19 | 20 | type NetceptorForControlCommand interface { 21 | GetClientTLSConfig(name string, expectedHostName string, expectedHostNameType netceptor.ExpectedHostnameType) (*tls.Config, error) 22 | Dial(node string, service string, tlscfg *tls.Config) (*netceptor.Conn, error) 23 | Ping(ctx context.Context, target string, hopsToLive byte) (time.Duration, string, error) 24 | MaxForwardingHops() byte 25 | Status() netceptor.Status 26 | Traceroute(ctx context.Context, target string) <-chan *netceptor.TracerouteResult 27 | NodeID() string 28 | GetLogger() *logger.ReceptorLogger 29 | CancelBackends() 30 | } 31 | 32 | // ControlCommand is an instance of a command that is being run from the control service. 33 | type ControlCommand interface { 34 | ControlFunc(context.Context, NetceptorForControlCommand, ControlFuncOperations) (map[string]interface{}, error) 35 | } 36 | 37 | // ControlFuncOperations provides callbacks for control services to take actions. 38 | type ControlFuncOperations interface { 39 | BridgeConn(message string, bc io.ReadWriteCloser, bcName string, logger *logger.ReceptorLogger, utils Utiler) error 40 | ReadFromConn(message string, out io.Writer, io Copier) error 41 | WriteToConn(message string, in chan []byte) error 42 | Close() error 43 | RemoteAddr() net.Addr 44 | } 45 | -------------------------------------------------------------------------------- /pkg/controlsvc/ping.go: -------------------------------------------------------------------------------- 1 | package controlsvc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type ( 9 | PingCommandType struct{} 10 | PingCommand struct { 11 | target string 12 | } 13 | ) 14 | 15 | func (t *PingCommandType) InitFromString(params string) (ControlCommand, error) { 16 | if params == "" { 17 | return nil, fmt.Errorf("no ping target") 18 | } 19 | c := &PingCommand{ 20 | target: params, 21 | } 22 | 23 | return c, nil 24 | } 25 | 26 | func (t *PingCommandType) InitFromJSON(config map[string]interface{}) (ControlCommand, error) { 27 | target, ok := config["target"] 28 | if !ok { 29 | return nil, fmt.Errorf("no ping target") 30 | } 31 | targetStr, ok := target.(string) 32 | if !ok { 33 | return nil, fmt.Errorf("ping target must be string") 34 | } 35 | c := &PingCommand{ 36 | target: targetStr, 37 | } 38 | 39 | return c, nil 40 | } 41 | 42 | func (c *PingCommand) ControlFunc(ctx context.Context, nc NetceptorForControlCommand, _ ControlFuncOperations) (map[string]interface{}, error) { 43 | pingTime, pingRemote, err := nc.Ping(ctx, c.target, nc.MaxForwardingHops()) 44 | cfr := make(map[string]interface{}) 45 | if err == nil { 46 | cfr["Success"] = true 47 | cfr["From"] = pingRemote 48 | cfr["Time"] = pingTime 49 | cfr["TimeStr"] = fmt.Sprint(pingTime) 50 | } else { 51 | cfr["Success"] = false 52 | cfr["Error"] = err.Error() 53 | } 54 | 55 | return cfr, nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/controlsvc/reload_test.go: -------------------------------------------------------------------------------- 1 | package controlsvc 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReload(t *testing.T) { 8 | type yamltest struct { 9 | filename string 10 | modifyError bool 11 | absentError bool 12 | } 13 | 14 | scenarios := []yamltest{ 15 | {filename: "reload_test_yml/init.yml", modifyError: false, absentError: false}, 16 | {filename: "reload_test_yml/add_cfg.yml", modifyError: true, absentError: false}, 17 | {filename: "reload_test_yml/drop_cfg.yml", modifyError: false, absentError: true}, 18 | {filename: "reload_test_yml/modify_cfg.yml", modifyError: true, absentError: true}, 19 | {filename: "reload_test_yml/syntax_error.yml", modifyError: true, absentError: true}, 20 | {filename: "reload_test_yml/successful_reload.yml", modifyError: false, absentError: false}, 21 | } 22 | err := parseConfigForReload("reload_test_yml/init.yml", false) 23 | if err != nil { 24 | t.Errorf("parseConfigForReload %s: Unexpected err: %v", "init.yml", err) 25 | } 26 | 27 | if len(cfgNotReloadable) != 5 { 28 | t.Errorf("cfNotReloadable length expected %d, got %d", 5, len(cfgNotReloadable)) 29 | } 30 | 31 | for _, s := range scenarios { 32 | t.Logf("%s", s.filename) 33 | err = parseConfigForReload(s.filename, true) 34 | if s.modifyError { 35 | if err == nil { 36 | t.Errorf("parseConfigForReload %s %s: Expected err, got %v", s.filename, "modifyError", err) 37 | } 38 | } else { 39 | if err != nil { 40 | t.Errorf("parseConfigForReload %s %s: Unexpected err: %v", s.filename, "modifyError", err) 41 | } 42 | } 43 | err = cfgAbsent() 44 | if s.absentError { 45 | if err == nil { 46 | t.Errorf("parseConfigForReload %s %s: Expected err, got %v", s.filename, "absentError", err) 47 | } 48 | } else { 49 | if err != nil { 50 | t.Errorf("parseConfigForReload %s %s: Unexpected err: %v", s.filename, "absentError", err) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/controlsvc/reload_test_yml/add_cfg.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: foo 4 | allowedpeers: bar 5 | 6 | - log-level: Info 7 | - trace 8 | 9 | - tcp-peer: 10 | address: localhost:8001 11 | 12 | - control-service: 13 | service: control 14 | filename: /tmp/foo.sock 15 | 16 | - work-command: 17 | workType: hello 18 | command: bash 19 | params: "-c \"echo hello\"" 20 | 21 | - work-command: # not reloadable 22 | workType: hello2 23 | command: bash 24 | params: "-c \"echo hello2\"" 25 | -------------------------------------------------------------------------------- /pkg/controlsvc/reload_test_yml/drop_cfg.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: foo 4 | allowedpeers: bar 5 | 6 | - log-level: Info 7 | - trace 8 | 9 | - tcp-peer: 10 | address: localhost:8001 11 | 12 | - control-service: 13 | service: control 14 | filename: /tmp/foo.sock 15 | 16 | # - work-command: 17 | # workType: hello 18 | # command: bash 19 | # params: "-c \"echo hello\"" 20 | -------------------------------------------------------------------------------- /pkg/controlsvc/reload_test_yml/init.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: foo 4 | allowedpeers: bar 5 | 6 | - log-level: Info 7 | - trace 8 | 9 | - tcp-peer: 10 | address: localhost:8001 11 | 12 | - control-service: 13 | service: control 14 | filename: /tmp/foo.sock 15 | 16 | - work-command: 17 | workType: hello 18 | command: bash 19 | params: "-c \"echo hello\"" 20 | -------------------------------------------------------------------------------- /pkg/controlsvc/reload_test_yml/modify_cfg.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: foo 4 | allowedpeers: bar 5 | 6 | - log-level: Info 7 | - trace 8 | 9 | - tcp-peer: 10 | address: localhost:8001 11 | 12 | - control-service: 13 | service: control 14 | filename: /tmp/foo.sock 15 | 16 | - work-command: 17 | workType: hello2 # changed workType name 18 | command: bash 19 | params: "-c \"echo hello\"" 20 | -------------------------------------------------------------------------------- /pkg/controlsvc/reload_test_yml/successful_reload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: foo 4 | allowedpeers: bar 5 | 6 | - log-level: Info 7 | - trace 8 | 9 | - tcp-peer: 10 | address: localhost:8001 11 | 12 | - tcp-peer: 13 | address: localhost:8002 # is reloadable 14 | 15 | - control-service: 16 | service: control 17 | filename: /tmp/foo.sock 18 | 19 | - work-command: 20 | workType: hello 21 | command: bash 22 | params: "-c \"echo hello\"" 23 | -------------------------------------------------------------------------------- /pkg/controlsvc/reload_test_yml/syntax_error.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 1234syntaxerror # syntax error 3 | - node: 4 | id: foo 5 | allowedpeers: bar 6 | 7 | - log-level: Info 8 | - trace 9 | 10 | - tcp-peer: 11 | address: localhost:8001 12 | 13 | - control-service: 14 | service: control 15 | filename: /tmp/foo.sock 16 | -------------------------------------------------------------------------------- /pkg/controlsvc/status.go: -------------------------------------------------------------------------------- 1 | package controlsvc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/ansible/receptor/internal/version" 8 | "github.com/ansible/receptor/pkg/utils" 9 | ) 10 | 11 | type ( 12 | StatusCommandType struct{} 13 | StatusCommand struct { 14 | requestedFields []string 15 | } 16 | ) 17 | 18 | func (t *StatusCommandType) InitFromString(params string) (ControlCommand, error) { 19 | if params != "" { 20 | return nil, fmt.Errorf("status command does not take parameters") 21 | } 22 | c := &StatusCommand{} 23 | 24 | return c, nil 25 | } 26 | 27 | func (t *StatusCommandType) InitFromJSON(config map[string]interface{}) (ControlCommand, error) { 28 | requestedFields, ok := config["requested_fields"] 29 | var requestedFieldsStr []string 30 | if ok { 31 | requestedFieldsStr = make([]string, 0) 32 | for _, v := range requestedFields.([]interface{}) { 33 | vStr, ok := v.(string) 34 | if !ok { 35 | return nil, fmt.Errorf("each element of requested_fields must be a string") 36 | } 37 | requestedFieldsStr = append(requestedFieldsStr, vStr) 38 | } 39 | } else { 40 | requestedFieldsStr = nil 41 | } 42 | c := &StatusCommand{ 43 | requestedFields: requestedFieldsStr, 44 | } 45 | 46 | return c, nil 47 | } 48 | 49 | func (c *StatusCommand) ControlFunc(_ context.Context, nc NetceptorForControlCommand, _ ControlFuncOperations) (map[string]interface{}, error) { 50 | status := nc.Status() 51 | statusGetters := make(map[string]func() interface{}) 52 | statusGetters["Version"] = func() interface{} { return version.Version } 53 | statusGetters["SystemCPUCount"] = func() interface{} { return utils.GetSysCPUCount() } 54 | statusGetters["SystemMemoryMiB"] = func() interface{} { return utils.GetSysMemoryMiB() } 55 | statusGetters["NodeID"] = func() interface{} { return status.NodeID } 56 | statusGetters["Connections"] = func() interface{} { return status.Connections } 57 | statusGetters["RoutingTable"] = func() interface{} { return status.RoutingTable } 58 | statusGetters["Advertisements"] = func() interface{} { return status.Advertisements } 59 | statusGetters["KnownConnectionCosts"] = func() interface{} { return status.KnownConnectionCosts } 60 | cfr := make(map[string]interface{}) 61 | if c.requestedFields == nil { // if nil, fill it with the keys in statusGetters 62 | for field := range statusGetters { 63 | c.requestedFields = append(c.requestedFields, field) 64 | } 65 | } 66 | for _, field := range c.requestedFields { 67 | getter, ok := statusGetters[field] 68 | if ok { 69 | cfr[field] = getter() 70 | } 71 | } 72 | 73 | return cfr, nil 74 | } 75 | -------------------------------------------------------------------------------- /pkg/controlsvc/traceroute.go: -------------------------------------------------------------------------------- 1 | package controlsvc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | type ( 10 | TracerouteCommandType struct{} 11 | TracerouteCommand struct { 12 | target string 13 | } 14 | ) 15 | 16 | func (t *TracerouteCommandType) InitFromString(params string) (ControlCommand, error) { 17 | if params == "" { 18 | return nil, fmt.Errorf("no traceroute target") 19 | } 20 | c := &TracerouteCommand{ 21 | target: params, 22 | } 23 | 24 | return c, nil 25 | } 26 | 27 | func (t *TracerouteCommandType) InitFromJSON(config map[string]interface{}) (ControlCommand, error) { 28 | target, ok := config["target"] 29 | if !ok { 30 | return nil, fmt.Errorf("no traceroute target") 31 | } 32 | targetStr, ok := target.(string) 33 | if !ok { 34 | return nil, fmt.Errorf("traceroute target must be string") 35 | } 36 | c := &TracerouteCommand{ 37 | target: targetStr, 38 | } 39 | 40 | return c, nil 41 | } 42 | 43 | func (c *TracerouteCommand) ControlFunc(ctx context.Context, nc NetceptorForControlCommand, _ ControlFuncOperations) (map[string]interface{}, error) { 44 | cfr := make(map[string]interface{}) 45 | results := nc.Traceroute(ctx, c.target) 46 | i := 0 47 | for res := range results { 48 | thisResult := make(map[string]interface{}) 49 | thisResult["From"] = res.From 50 | thisResult["Time"] = res.Time 51 | thisResult["TimeStr"] = fmt.Sprint(res.Time) 52 | if res.Err != nil { 53 | thisResult["Error"] = res.Err.Error() 54 | } 55 | cfr[strconv.Itoa(i)] = thisResult 56 | i++ 57 | } 58 | 59 | return cfr, nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/framer/framer.go: -------------------------------------------------------------------------------- 1 | package framer 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | // Framer provides framing of discrete data entities over a stream connection. 10 | type Framer interface { 11 | SendData(data []byte) []byte 12 | RecvData(buf []byte) 13 | MessageReady() bool 14 | GetMessage() ([]byte, error) 15 | } 16 | 17 | type framer struct { 18 | bufLock *sync.RWMutex 19 | buffer []byte 20 | } 21 | 22 | // New returns a new framer instance. 23 | func New() Framer { 24 | f := &framer{ 25 | bufLock: &sync.RWMutex{}, 26 | buffer: make([]byte, 0), 27 | } 28 | 29 | return f 30 | } 31 | 32 | // SendData takes a data buffer and returns a framed buffer. 33 | func (f *framer) SendData(data []byte) []byte { 34 | buf := make([]byte, len(data)+2) 35 | binary.LittleEndian.PutUint16(buf[0:2], uint16(len(data))) //nolint:gosec 36 | copy(buf[2:], data) 37 | 38 | return buf 39 | } 40 | 41 | // RecvData adds more data to the buffer from the network. 42 | func (f *framer) RecvData(buf []byte) { 43 | f.bufLock.Lock() 44 | defer f.bufLock.Unlock() 45 | f.buffer = append(f.buffer, buf...) 46 | } 47 | 48 | // Caller must already hold at least a read lock on f.bufLock. 49 | func (f *framer) messageReady() (int, bool) { 50 | if len(f.buffer) < 2 { 51 | return 0, false 52 | } 53 | msgSize := int(binary.LittleEndian.Uint16(f.buffer[:2])) 54 | 55 | return msgSize, len(f.buffer) >= msgSize+2 56 | } 57 | 58 | // MessageReady returns true if a full framed message is available to read. 59 | func (f *framer) MessageReady() bool { 60 | f.bufLock.RLock() 61 | defer f.bufLock.RUnlock() 62 | _, ready := f.messageReady() 63 | 64 | return ready 65 | } 66 | 67 | // GetMessage returns a single framed message, or an error if one is not available. 68 | func (f *framer) GetMessage() ([]byte, error) { 69 | f.bufLock.Lock() 70 | defer f.bufLock.Unlock() 71 | msgSize, ready := f.messageReady() 72 | if !ready { 73 | return nil, fmt.Errorf("message not ready") 74 | } 75 | data := f.buffer[2 : msgSize+2] 76 | f.buffer = f.buffer[msgSize+2:] 77 | 78 | return data, nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/framer/mock_framer/framer.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/framer/framer.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=pkg/framer/framer.go -destination=pkg/framer/mock_framer/framer.go 7 | // 8 | 9 | // Package mock_framer is a generated GoMock package. 10 | package mock_framer 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | gomock "go.uber.org/mock/gomock" 16 | ) 17 | 18 | // MockFramer is a mock of Framer interface. 19 | type MockFramer struct { 20 | ctrl *gomock.Controller 21 | recorder *MockFramerMockRecorder 22 | isgomock struct{} 23 | } 24 | 25 | // MockFramerMockRecorder is the mock recorder for MockFramer. 26 | type MockFramerMockRecorder struct { 27 | mock *MockFramer 28 | } 29 | 30 | // NewMockFramer creates a new mock instance. 31 | func NewMockFramer(ctrl *gomock.Controller) *MockFramer { 32 | mock := &MockFramer{ctrl: ctrl} 33 | mock.recorder = &MockFramerMockRecorder{mock} 34 | return mock 35 | } 36 | 37 | // EXPECT returns an object that allows the caller to indicate expected use. 38 | func (m *MockFramer) EXPECT() *MockFramerMockRecorder { 39 | return m.recorder 40 | } 41 | 42 | // GetMessage mocks base method. 43 | func (m *MockFramer) GetMessage() ([]byte, error) { 44 | m.ctrl.T.Helper() 45 | ret := m.ctrl.Call(m, "GetMessage") 46 | ret0, _ := ret[0].([]byte) 47 | ret1, _ := ret[1].(error) 48 | return ret0, ret1 49 | } 50 | 51 | // GetMessage indicates an expected call of GetMessage. 52 | func (mr *MockFramerMockRecorder) GetMessage() *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockFramer)(nil).GetMessage)) 55 | } 56 | 57 | // MessageReady mocks base method. 58 | func (m *MockFramer) MessageReady() bool { 59 | m.ctrl.T.Helper() 60 | ret := m.ctrl.Call(m, "MessageReady") 61 | ret0, _ := ret[0].(bool) 62 | return ret0 63 | } 64 | 65 | // MessageReady indicates an expected call of MessageReady. 66 | func (mr *MockFramerMockRecorder) MessageReady() *gomock.Call { 67 | mr.mock.ctrl.T.Helper() 68 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MessageReady", reflect.TypeOf((*MockFramer)(nil).MessageReady)) 69 | } 70 | 71 | // RecvData mocks base method. 72 | func (m *MockFramer) RecvData(buf []byte) { 73 | m.ctrl.T.Helper() 74 | m.ctrl.Call(m, "RecvData", buf) 75 | } 76 | 77 | // RecvData indicates an expected call of RecvData. 78 | func (mr *MockFramerMockRecorder) RecvData(buf any) *gomock.Call { 79 | mr.mock.ctrl.T.Helper() 80 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecvData", reflect.TypeOf((*MockFramer)(nil).RecvData), buf) 81 | } 82 | 83 | // SendData mocks base method. 84 | func (m *MockFramer) SendData(data []byte) []byte { 85 | m.ctrl.T.Helper() 86 | ret := m.ctrl.Call(m, "SendData", data) 87 | ret0, _ := ret[0].([]byte) 88 | return ret0 89 | } 90 | 91 | // SendData indicates an expected call of SendData. 92 | func (mr *MockFramerMockRecorder) SendData(data any) *gomock.Call { 93 | mr.mock.ctrl.T.Helper() 94 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendData", reflect.TypeOf((*MockFramer)(nil).SendData), data) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/netceptor/addr.go: -------------------------------------------------------------------------------- 1 | package netceptor 2 | 3 | import "fmt" 4 | 5 | // Addr represents an endpoint address on the Netceptor network. 6 | type Addr struct { 7 | network string 8 | node string 9 | service string 10 | } 11 | 12 | // Network returns the network name. 13 | func (a Addr) Network() string { 14 | return a.network 15 | } 16 | 17 | // String formats this address as a string. 18 | func (a Addr) String() string { 19 | return fmt.Sprintf("%s:%s", a.node, a.service) 20 | } 21 | 22 | // SetNetwork sets the network variable. 23 | func (a *Addr) SetNetwork(network string) { 24 | a.network = network 25 | } 26 | 27 | // SetNetwork sets the node variable. 28 | func (a *Addr) SetNode(node string) { 29 | a.node = node 30 | } 31 | 32 | // SetNetwork sets the service variable. 33 | func (a *Addr) SetService(service string) { 34 | a.service = service 35 | } 36 | -------------------------------------------------------------------------------- /pkg/netceptor/addr_test.go: -------------------------------------------------------------------------------- 1 | package netceptor_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ansible/receptor/pkg/netceptor" 7 | "github.com/ansible/receptor/pkg/netceptor/mock_netceptor" 8 | "go.uber.org/mock/gomock" 9 | ) 10 | 11 | func TestNetwork(t *testing.T) { 12 | networkResult := "netceptor-testNode1" 13 | strResult := "testNode2:testService" 14 | 15 | ctrl := gomock.NewController(t) 16 | mockNetceptor := mock_netceptor.NewMockNetcForPing(ctrl) 17 | 18 | mockNetceptor.EXPECT().NewAddr(gomock.Any(), gomock.Any()).Return(netceptor.Addr{}) 19 | 20 | addr := mockNetceptor.NewAddr("testNode2", "testService") 21 | addr.SetNetwork(networkResult) 22 | addr.SetNode("testNode2") 23 | addr.SetService("testService") 24 | 25 | network := addr.Network() 26 | str := addr.String() 27 | 28 | if network != networkResult { 29 | t.Errorf("Expected network to be %v, got %v", networkResult, network) 30 | } 31 | if str != strResult { 32 | t.Errorf("Expected network to be %v, got %v", strResult, str) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/netceptor/firewall_rules_test.go: -------------------------------------------------------------------------------- 1 | package netceptor 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFirewallRules(t *testing.T) { 8 | var frd FirewallRuleData 9 | 10 | // Rule #1 11 | frd = FirewallRuleData{} 12 | frd["action"] = "accept" 13 | rule, err := frd.ParseFirewallRule() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | if rule(&MessageData{}) != FirewallResultAccept { 18 | t.Fatal("rule #1 did not return Accept") 19 | } 20 | 21 | // // Rule #2 22 | frd = FirewallRuleData{} 23 | frd["Action"] = "drop" 24 | frd["FromNode"] = "foo" 25 | frd["ToNode"] = "bar" 26 | frd["ToService"] = "control" 27 | rule, err = frd.ParseFirewallRule() 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if rule(&MessageData{}) != FirewallResultContinue { 32 | t.Fatal("rule #2 did not return Continue") 33 | } 34 | if rule(&MessageData{ 35 | FromNode: "foo", 36 | ToNode: "bar", 37 | ToService: "control", 38 | }) != FirewallResultDrop { 39 | t.Fatal("rule #2 did not return Drop") 40 | } 41 | 42 | // Rule #3 43 | frd = FirewallRuleData{} 44 | frd["fromnode"] = "/a.*b/" 45 | frd["action"] = "reject" 46 | rule, err = frd.ParseFirewallRule() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | if rule(&MessageData{}) != FirewallResultContinue { 51 | t.Fatal("rule #3 did not return Continue") 52 | } 53 | if rule(&MessageData{ 54 | FromNode: "appleb", 55 | }) != FirewallResultReject { 56 | t.Fatal("rule #3 did not return Reject") 57 | } 58 | if rule(&MessageData{ 59 | FromNode: "Appleb", 60 | }) != FirewallResultContinue { 61 | t.Fatal("rule #3 did not return Continue") 62 | } 63 | 64 | // Rule #4 65 | frd = FirewallRuleData{} 66 | frd["TONODE"] = "/(?i)a.*b/" 67 | frd["ACTION"] = "reject" 68 | rule, err = frd.ParseFirewallRule() 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | if rule(&MessageData{}) != FirewallResultContinue { 73 | t.Fatal("rule #4 did not return Continue") 74 | } 75 | if rule(&MessageData{ 76 | ToNode: "appleb", 77 | }) != FirewallResultReject { 78 | t.Fatal("rule #4 did not return Reject") 79 | } 80 | if rule(&MessageData{ 81 | ToNode: "Appleb", 82 | }) != FirewallResultReject { 83 | t.Fatal("rule #4 did not return Reject") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/randstr/randstr.go: -------------------------------------------------------------------------------- 1 | package randstr 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | // RandomString returns a random string of a given length. 9 | func RandomString(length int) string { 10 | if length < 0 { 11 | return "" 12 | } 13 | charset := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 14 | randbytes := make([]byte, 0, length) 15 | for i := 0; i < length; i++ { 16 | idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) 17 | randbytes = append(randbytes, charset[idx.Int64()]) 18 | } 19 | 20 | return string(randbytes) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/randstr/randstr_test.go: -------------------------------------------------------------------------------- 1 | package randstr_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/ansible/receptor/pkg/randstr" 8 | ) 9 | 10 | func TestRandStrLength(t *testing.T) { 11 | randStringTestCases := []struct { 12 | name string 13 | inputLength int 14 | expectedLength int 15 | }{ 16 | { 17 | name: "length of 100", 18 | inputLength: 100, 19 | expectedLength: 100, 20 | }, 21 | { 22 | name: "length of 0", 23 | inputLength: 0, 24 | expectedLength: 0, 25 | }, 26 | { 27 | name: "length of -1", 28 | inputLength: -1, 29 | expectedLength: 0, 30 | }, 31 | } 32 | 33 | for _, testCase := range randStringTestCases { 34 | t.Run(testCase.name, func(t *testing.T) { 35 | randomStr := randstr.RandomString(testCase.inputLength) 36 | 37 | if len(randomStr) != testCase.expectedLength { 38 | t.Errorf("%s - expected: %+v, received: %+v", testCase.name, testCase.expectedLength, len(randomStr)) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestRandStrHasDifferentOutputThanCharset(t *testing.T) { 45 | charset := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 46 | randomStr := randstr.RandomString(len(charset)) 47 | 48 | if randomStr == charset { 49 | t.Errorf("output should be different than charset. charset: %+v, received: %+v", charset, randomStr) 50 | } 51 | } 52 | 53 | func TestRandStrHasNoContinuousSubStringOfCharset(t *testing.T) { 54 | randomStr := randstr.RandomString(10) 55 | charset := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 56 | 57 | charsetIndex := strings.Index(charset, string(randomStr[0])) 58 | for index, char := range randomStr { 59 | if index == 0 { 60 | continue 61 | } 62 | currentCharsetIndex := strings.Index(charset, string(char)) 63 | if charsetIndex+1 != currentCharsetIndex { 64 | break 65 | } 66 | if index+1 == len(randomStr) { 67 | t.Error("rand str is continuous") 68 | } 69 | charsetIndex = currentCharsetIndex 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/services/cmdline.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/ghjm/cmdline" 4 | 5 | var servicesSection = &cmdline.ConfigSection{ 6 | Description: "Commands to configure services that run on top of the Receptor mesh:", 7 | Order: 20, 8 | } 9 | -------------------------------------------------------------------------------- /pkg/services/command.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package services 5 | 6 | import ( 7 | "crypto/tls" 8 | "errors" 9 | "net" 10 | "os/exec" 11 | 12 | "github.com/ansible/receptor/pkg/logger" 13 | "github.com/ansible/receptor/pkg/netceptor" 14 | "github.com/creack/pty" 15 | "github.com/ghjm/cmdline" 16 | "github.com/google/shlex" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | type NetCForCommandService interface { 21 | GetLogger() *logger.ReceptorLogger 22 | ListenAndAdvertise(service string, tlscfg *tls.Config, tags map[string]string) (*netceptor.Listener, error) 23 | } 24 | 25 | func runCommand(qc net.Conn, command string, logger *logger.ReceptorLogger, utilsLib UtilsLib) error { 26 | // Note: shlex.Split does not return error for the empty string 27 | args, err := shlex.Split(command) 28 | if err != nil { 29 | return err 30 | } 31 | if len(args) == 0 { 32 | return errors.New("shell command is empty") 33 | } 34 | cmd := exec.Command(args[0], args[1:]...) 35 | tty, err := pty.Start(cmd) 36 | if err != nil { 37 | return err 38 | } 39 | utilsLib.BridgeConns(tty, "external command", qc, "command service", logger) 40 | 41 | return nil 42 | } 43 | 44 | // CommandService listens on the Receptor network and runs a local command. 45 | func CommandService(s NetCForCommandService, service string, tlscfg *tls.Config, command string, utilsLib UtilsLib) { 46 | if command == "" { 47 | s.GetLogger().Error("initializing command service: command not provided\n") 48 | 49 | return 50 | } 51 | 52 | qli, err := s.ListenAndAdvertise(service, tlscfg, map[string]string{ 53 | "type": "Command Service", 54 | }) 55 | if err != nil { 56 | s.GetLogger().Error("listening on Receptor network: %s\n", err) 57 | 58 | return 59 | } 60 | for { 61 | qc, err := qli.Accept() 62 | if err != nil { 63 | s.GetLogger().Error("accepting connection on Receptor network: %s\n", err) 64 | 65 | return 66 | } 67 | go func() { 68 | err := runCommand(qc, command, s.GetLogger(), utilsLib) 69 | if err != nil { 70 | s.GetLogger().Error("running command: %s\n", err) 71 | } 72 | _ = qc.Close() 73 | }() 74 | } 75 | } 76 | 77 | // commandSvcCfg is the cmdline configuration object for a command service. 78 | type CommandSvcCfg struct { 79 | Service string `required:"true" description:"Receptor service name to bind to"` 80 | Command string `required:"true" description:"Command to execute on a connection"` 81 | TLS string `description:"Name of TLS server config"` 82 | } 83 | 84 | // Run runs the action. 85 | func (cfg CommandSvcCfg) Run() error { 86 | netceptor.MainInstance.Logger.Info("Running command service %s\n", cfg) 87 | tlscfg, err := netceptor.MainInstance.GetServerTLSConfig(cfg.TLS) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | go CommandService(netceptor.MainInstance, cfg.Service, tlscfg, cfg.Command, &UtilsTCPWrapper{}) 93 | 94 | return nil 95 | } 96 | 97 | func init() { 98 | version := viper.GetInt("version") 99 | if version > 1 { 100 | return 101 | } 102 | cmdline.RegisterConfigTypeForApp("receptor-command-service", 103 | "command-service", "Run an interactive command via a Receptor service", CommandSvcCfg{}, cmdline.Section(servicesSection)) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/services/interfaces/net_interfaces.go: -------------------------------------------------------------------------------- 1 | package netinterface 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | "os" 7 | "syscall" 8 | "time" 9 | ) 10 | 11 | type NetterUDP interface { 12 | ResolveUDPAddr(network string, address string) (*net.UDPAddr, error) 13 | ListenUDP(network string, laddr *net.UDPAddr) (UDPConnInterface, error) 14 | DialUDP(network string, laddr *net.UDPAddr, raddr *net.UDPAddr) (UDPConnInterface, error) 15 | } 16 | 17 | // UDPConnInterface abstracts the methods of net.UDPConn used in the proxy. 18 | type UDPConnInterface interface { 19 | Close() error 20 | File() (f *os.File, err error) 21 | LocalAddr() net.Addr 22 | Read(b []byte) (int, error) 23 | ReadFrom(b []byte) (int, net.Addr, error) 24 | ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) 25 | ReadFromUDPAddrPort(b []byte) (n int, addr netip.AddrPort, err error) 26 | ReadMsgUDP(b []byte, oob []byte) (n int, oobn int, flags int, addr *net.UDPAddr, err error) 27 | ReadMsgUDPAddrPort(b []byte, oob []byte) (n int, oobn int, flags int, addr netip.AddrPort, err error) 28 | RemoteAddr() net.Addr 29 | SetDeadline(t time.Time) error 30 | SetReadBuffer(bytes int) error 31 | SetReadDeadline(t time.Time) error 32 | SetWriteBuffer(bytes int) error 33 | SetWriteDeadline(t time.Time) error 34 | SyscallConn() (syscall.RawConn, error) 35 | Write(b []byte) (int, error) 36 | WriteMsgUDP(b []byte, oob []byte, addr *net.UDPAddr) (n int, oobn int, err error) 37 | WriteMsgUDPAddrPort(b []byte, oob []byte, addr netip.AddrPort) (n int, oobn int, err error) 38 | WriteTo(b []byte, addr net.Addr) (int, error) 39 | WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) 40 | WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/services/ip_router_cfg.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | // ipRouterCfg is the cmdline configuration object for an IP router. 4 | type IPRouterCfg struct { 5 | NetworkName string `required:"true" description:"Name of this network and service."` 6 | Interface string `description:"Name of the local tun interface"` 7 | LocalNet string `required:"true" description:"Local /30 CIDR address"` 8 | Routes string `description:"Comma separated list of CIDR subnets to advertise"` 9 | } 10 | -------------------------------------------------------------------------------- /pkg/services/mock_services/command.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/services/command.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=pkg/services/command.go -destination=pkg/services/mock_services/command.go 7 | // 8 | 9 | // Package mock_services is a generated GoMock package. 10 | package mock_services 11 | 12 | import ( 13 | tls "crypto/tls" 14 | reflect "reflect" 15 | 16 | logger "github.com/ansible/receptor/pkg/logger" 17 | netceptor "github.com/ansible/receptor/pkg/netceptor" 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockNetCForCommandService is a mock of NetCForCommandService interface. 22 | type MockNetCForCommandService struct { 23 | ctrl *gomock.Controller 24 | recorder *MockNetCForCommandServiceMockRecorder 25 | isgomock struct{} 26 | } 27 | 28 | // MockNetCForCommandServiceMockRecorder is the mock recorder for MockNetCForCommandService. 29 | type MockNetCForCommandServiceMockRecorder struct { 30 | mock *MockNetCForCommandService 31 | } 32 | 33 | // NewMockNetCForCommandService creates a new mock instance. 34 | func NewMockNetCForCommandService(ctrl *gomock.Controller) *MockNetCForCommandService { 35 | mock := &MockNetCForCommandService{ctrl: ctrl} 36 | mock.recorder = &MockNetCForCommandServiceMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockNetCForCommandService) EXPECT() *MockNetCForCommandServiceMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // GetLogger mocks base method. 46 | func (m *MockNetCForCommandService) GetLogger() *logger.ReceptorLogger { 47 | m.ctrl.T.Helper() 48 | ret := m.ctrl.Call(m, "GetLogger") 49 | ret0, _ := ret[0].(*logger.ReceptorLogger) 50 | return ret0 51 | } 52 | 53 | // GetLogger indicates an expected call of GetLogger. 54 | func (mr *MockNetCForCommandServiceMockRecorder) GetLogger() *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogger", reflect.TypeOf((*MockNetCForCommandService)(nil).GetLogger)) 57 | } 58 | 59 | // ListenAndAdvertise mocks base method. 60 | func (m *MockNetCForCommandService) ListenAndAdvertise(service string, tlscfg *tls.Config, tags map[string]string) (*netceptor.Listener, error) { 61 | m.ctrl.T.Helper() 62 | ret := m.ctrl.Call(m, "ListenAndAdvertise", service, tlscfg, tags) 63 | ret0, _ := ret[0].(*netceptor.Listener) 64 | ret1, _ := ret[1].(error) 65 | return ret0, ret1 66 | } 67 | 68 | // ListenAndAdvertise indicates an expected call of ListenAndAdvertise. 69 | func (mr *MockNetCForCommandServiceMockRecorder) ListenAndAdvertise(service, tlscfg, tags any) *gomock.Call { 70 | mr.mock.ctrl.T.Helper() 71 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListenAndAdvertise", reflect.TypeOf((*MockNetCForCommandService)(nil).ListenAndAdvertise), service, tlscfg, tags) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/services/unix_proxy_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "os" 7 | "testing" 8 | 9 | "github.com/ansible/receptor/pkg/netceptor" 10 | ) 11 | 12 | func TestUnixProxyServiceInbound(t *testing.T) { 13 | type testCase struct { 14 | name string 15 | filename string 16 | permissions os.FileMode 17 | node string 18 | rservice string 19 | tlscfg *tls.Config 20 | expecterr bool 21 | } 22 | 23 | tests := []testCase{ 24 | { 25 | name: "Fail UnixSocketListen", 26 | expecterr: true, 27 | }, 28 | } 29 | 30 | for _, tc := range tests { 31 | t.Run(tc.name, func(t *testing.T) { 32 | ctx := context.Background() 33 | s := netceptor.New(ctx, "Unix Test Node") 34 | err := UnixProxyServiceInbound(s, tc.filename, tc.permissions, tc.node, tc.rservice, tc.tlscfg) 35 | if tc.expecterr { 36 | if err == nil { 37 | t.Errorf("net UnixProxyServiceInbound fail case error") 38 | } 39 | 40 | return 41 | } else if err != nil { 42 | t.Errorf("net UnixProxyServiceInbound error") 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestUnixProxyServiceOutbound(t *testing.T) { 49 | type testCase struct { 50 | name string 51 | expecterr bool 52 | service string 53 | tlscfg *tls.Config 54 | filename string 55 | } 56 | 57 | tests := []testCase{ 58 | { 59 | name: "Fail UnixSocketListen", 60 | }, 61 | } 62 | 63 | for _, tc := range tests { 64 | t.Run(tc.name, func(t *testing.T) { 65 | ctx := context.Background() 66 | s := netceptor.New(ctx, "Unix Test Node") 67 | err := UnixProxyServiceOutbound(s, tc.service, tc.tlscfg, tc.filename) 68 | if tc.expecterr { 69 | if err == nil { 70 | t.Errorf("net UnixProxyServiceInbound fail case error") 71 | } 72 | 73 | return 74 | } else if err != nil { 75 | t.Errorf("net UnixProxyServiceInbound error") 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/tickrunner/tickrunner.go: -------------------------------------------------------------------------------- 1 | package tickrunner 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Run runs a task at a given periodic interval, or as requested over a channel. 9 | // If many requests come in close to the same time, only run the task once. 10 | // Callers can ask for the task to be run within a given amount of time, which 11 | // overrides defaultReqDelay. Sending a zero to the channel runs it after defaukltReqDelay. 12 | func Run(ctx context.Context, f func(), periodicInterval time.Duration, defaultReqDelay time.Duration) chan time.Duration { 13 | runChan := make(chan time.Duration) 14 | go func() { 15 | nextRunTime := time.Now().Add(periodicInterval) 16 | for { 17 | select { 18 | case <-time.After(time.Until(nextRunTime)): 19 | nextRunTime = time.Now().Add(periodicInterval) 20 | f() 21 | case req := <-runChan: 22 | proposedTime := time.Now() 23 | if req == 0 { 24 | proposedTime = proposedTime.Add(defaultReqDelay) 25 | } else { 26 | proposedTime = proposedTime.Add(req) 27 | } 28 | if proposedTime.Before(nextRunTime) { 29 | nextRunTime = proposedTime 30 | } 31 | case <-ctx.Done(): 32 | return 33 | } 34 | } 35 | }() 36 | 37 | return runChan 38 | } 39 | -------------------------------------------------------------------------------- /pkg/types/main_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "testing" 4 | 5 | func TestMainInitNodeID(t *testing.T) { 6 | mainInitNodeIDTestCases := []struct { 7 | name string 8 | nodeID string 9 | expectedErr string 10 | }{ 11 | { 12 | name: "successful, no error", 13 | nodeID: "t.e-s_t@1:234", 14 | expectedErr: "", 15 | }, 16 | { 17 | name: "failed, charactered not allowed", 18 | nodeID: "test!#&123", 19 | expectedErr: "node id can only contain a-z, A-Z, 0-9 or special characters . - _ @ : but received: test!#&123", 20 | }, 21 | } 22 | 23 | for _, testCase := range mainInitNodeIDTestCases { 24 | t.Run(testCase.name, func(t *testing.T) { 25 | cfg := NodeCfg{ 26 | ID: testCase.nodeID, 27 | } 28 | err := cfg.Init() 29 | if err == nil && testCase.expectedErr != "" { 30 | t.Errorf("exected error but got no error") 31 | } else if err != nil && err.Error() != testCase.expectedErr { 32 | t.Errorf("expected error to be %s, but got: %s", testCase.expectedErr, err.Error()) 33 | } 34 | t.Cleanup(func() { 35 | cfg = NodeCfg{} 36 | }) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/utils/bridge.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "github.com/ansible/receptor/pkg/logger" 8 | ) 9 | 10 | // NormalBufferSize is the size of buffers used by various processes when copying data between sockets. 11 | const NormalBufferSize = 65536 12 | 13 | // BridgeConns bridges two connections, like netcat. 14 | func BridgeConns(c1 io.ReadWriteCloser, c1Name string, c2 io.ReadWriteCloser, c2Name string, logger *logger.ReceptorLogger) { 15 | doneChan := make(chan bool) 16 | go bridgeHalf(c1, c1Name, c2, c2Name, doneChan, logger) 17 | go bridgeHalf(c2, c2Name, c1, c1Name, doneChan, logger) 18 | <-doneChan 19 | <-doneChan 20 | } 21 | 22 | // BridgeHalf bridges the read side of c1 to the write side of c2. 23 | func bridgeHalf(c1 io.ReadWriteCloser, c1Name string, c2 io.ReadWriteCloser, c2Name string, done chan bool, logger *logger.ReceptorLogger) { 24 | logger.Trace(" Bridging %s to %s\n", c1Name, c2Name) 25 | defer func() { 26 | done <- true 27 | }() 28 | buf := make([]byte, NormalBufferSize) 29 | shouldClose := false 30 | for { 31 | n, err := c1.Read(buf) 32 | if err != nil { 33 | if err.Error() != "EOF" && !strings.Contains(err.Error(), "use of closed network connection") { 34 | logger.Error("Connection read error: %s\n", err) 35 | } 36 | shouldClose = true 37 | } 38 | if n > 0 { 39 | logger.Trace(" Copied %d bytes from %s to %s\n", n, c1Name, c2Name) 40 | wn, err := c2.Write(buf[:n]) 41 | if err != nil { 42 | logger.Error("Connection write error: %s\n", err) 43 | shouldClose = true 44 | } 45 | if wn != n { 46 | logger.Error("Not all bytes written\n") 47 | shouldClose = true 48 | } 49 | } 50 | if shouldClose { 51 | logger.Trace(" Stopping bridge %s to %s\n", c1Name, c2Name) 52 | _ = c2.Close() 53 | 54 | return 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/utils/broker.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "sync" 8 | ) 9 | 10 | // Broker code adapted from https://stackoverflow.com/questions/36417199/how-to-broadcast-message-using-channel 11 | // which is licensed under Creative Commons CC BY-SA 4.0. 12 | 13 | // Broker implements a simple pub-sub broadcast system. 14 | type Broker struct { 15 | ctx context.Context 16 | msgType reflect.Type 17 | publishCh chan interface{} 18 | subCh chan chan interface{} 19 | unsubCh chan chan interface{} 20 | } 21 | 22 | // NewBroker allocates a new Broker object. 23 | func NewBroker(ctx context.Context, msgType reflect.Type) *Broker { 24 | b := &Broker{ 25 | ctx: ctx, 26 | msgType: msgType, 27 | publishCh: make(chan interface{}), 28 | subCh: make(chan chan interface{}), 29 | unsubCh: make(chan chan interface{}), 30 | } 31 | go b.start() 32 | 33 | return b 34 | } 35 | 36 | // start starts the broker goroutine. 37 | func (b *Broker) start() { 38 | subs := map[chan interface{}]struct{}{} 39 | for { 40 | select { 41 | case <-b.ctx.Done(): 42 | for ch := range subs { 43 | close(ch) 44 | } 45 | 46 | return 47 | case msgCh := <-b.subCh: 48 | subs[msgCh] = struct{}{} 49 | case msgCh := <-b.unsubCh: 50 | delete(subs, msgCh) 51 | close(msgCh) 52 | case msg := <-b.publishCh: 53 | wg := sync.WaitGroup{} 54 | for msgCh := range subs { 55 | wg.Add(1) 56 | go func(msgCh chan interface{}) { 57 | defer wg.Done() 58 | select { 59 | case msgCh <- msg: 60 | case <-b.ctx.Done(): 61 | } 62 | }(msgCh) 63 | } 64 | wg.Wait() 65 | } 66 | } 67 | } 68 | 69 | // Subscribe registers to receive messages from the broker. 70 | func (b *Broker) Subscribe() chan interface{} { 71 | msgCh := make(chan interface{}) 72 | select { 73 | case <-b.ctx.Done(): 74 | return nil 75 | case b.subCh <- msgCh: 76 | return msgCh 77 | } 78 | } 79 | 80 | // Unsubscribe de-registers a message receiver. 81 | func (b *Broker) Unsubscribe(msgCh chan interface{}) { 82 | select { 83 | case <-b.ctx.Done(): 84 | case b.unsubCh <- msgCh: 85 | } 86 | } 87 | 88 | // Publish sends a message to all subscribers. 89 | func (b *Broker) Publish(msg interface{}) error { 90 | if reflect.TypeOf(msg) != b.msgType { 91 | return fmt.Errorf("messages to broker must be of type %s", b.msgType.String()) 92 | } 93 | select { 94 | case <-b.ctx.Done(): 95 | case b.publishCh <- msg: 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/utils/common.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/ansible/receptor/pkg/logger" 10 | ) 11 | 12 | func ParseReceptorNamesFromCert(cert *x509.Certificate, expectedHostname string, logger *logger.ReceptorLogger) (bool, []string, error) { 13 | var receptorNames []string 14 | receptorNames, err := ReceptorNames(cert.Extensions) 15 | if err != nil { 16 | logger.Error("RVF failed to get ReceptorNames: %s", err) 17 | 18 | return false, nil, err 19 | } 20 | found := false 21 | for _, receptorName := range receptorNames { 22 | if receptorName == expectedHostname { 23 | found = true 24 | 25 | break 26 | } 27 | } 28 | 29 | return found, receptorNames, nil 30 | } 31 | 32 | // AddressToHostPort splits an address(1.2.3.4:5000) into a Host(1.2.3.4) and a Port(5000). Enhances `net.SplitHostPort` to handle additional input formats and IPv6. 33 | func AddressToHostPort(address string) (string, string, error) { 34 | if !strings.Contains(address, "[") && strings.Count(address, ":") > 2 { 35 | idx := strings.LastIndexByte(address, ':') 36 | if idx == -1 { 37 | return "", "", fmt.Errorf("malformed remote address: %v", address) 38 | } 39 | host := address[:idx] 40 | port := address[idx+1:] 41 | 42 | return host, port, nil 43 | } 44 | host, port, err := net.SplitHostPort(address) 45 | if err != nil { 46 | return "", "", fmt.Errorf("%s is not a valid IP address + port", address) 47 | } 48 | 49 | return host, port, nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/utils/error_kind.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "fmt" 4 | 5 | // ErrorWithKind represents an error wrapped with a designation of what kind of error it is. 6 | type ErrorWithKind struct { 7 | Err error 8 | Kind string 9 | } 10 | 11 | // Error returns the error text as a string. 12 | func (ek ErrorWithKind) Error() string { 13 | return fmt.Sprintf("%s error: %v", ek.Kind, ek.Err) 14 | } 15 | 16 | // WrapErrorWithKind creates an ErrorWithKind that wraps an underlying error. 17 | func WrapErrorWithKind(err error, kind string) ErrorWithKind { 18 | return ErrorWithKind{ 19 | Err: err, 20 | Kind: kind, 21 | } 22 | } 23 | 24 | // ErrorIsKind returns true if err is an ErrorWithKind of the specified kind, or false otherwise (including if nil). 25 | func ErrorIsKind(err error, kind string) bool { 26 | ek, ok := err.(ErrorWithKind) 27 | if !ok { 28 | return false 29 | } 30 | 31 | return ek.Kind == kind 32 | } 33 | -------------------------------------------------------------------------------- /pkg/utils/error_kind_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/ansible/receptor/pkg/utils" 9 | ) 10 | 11 | const ( 12 | goodKind string = "connection" 13 | goodErrorString string = "unit was already started" 14 | ) 15 | 16 | var errUnitWasAlreadyStarted error = fmt.Errorf(goodErrorString) //nolint:staticcheck 17 | 18 | func TestErrorWithKind_Error(t *testing.T) { 19 | type fields struct { 20 | err error 21 | kind string 22 | } 23 | tests := []struct { 24 | name string 25 | fields fields 26 | want string 27 | }{ 28 | { 29 | name: "Positive", 30 | fields: fields{ 31 | err: errUnitWasAlreadyStarted, 32 | kind: goodKind, 33 | }, 34 | want: fmt.Sprintf("%s error: %s", goodKind, goodErrorString), 35 | }, 36 | { 37 | name: "Negative", 38 | fields: fields{ 39 | err: nil, 40 | kind: goodKind, 41 | }, 42 | want: fmt.Sprintf("%s error: ", goodKind), 43 | }, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | ek := utils.ErrorWithKind{ 48 | Err: tt.fields.err, 49 | Kind: tt.fields.kind, 50 | } 51 | if got := ek.Error(); got != tt.want { 52 | t.Errorf("ErrorWithKind.Error() = %v, want %v", got, tt.want) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | func TestWrapErrorWithKind(t *testing.T) { 59 | type args struct { 60 | err error 61 | kind string 62 | } 63 | tests := []struct { 64 | name string 65 | args args 66 | want utils.ErrorWithKind 67 | }{ 68 | { 69 | name: "Positive", 70 | args: args{ 71 | err: errUnitWasAlreadyStarted, 72 | kind: goodKind, 73 | }, 74 | want: utils.ErrorWithKind{ 75 | Err: errUnitWasAlreadyStarted, 76 | Kind: goodKind, 77 | }, 78 | }, 79 | { 80 | name: "Negative", 81 | args: args{ 82 | err: nil, 83 | kind: goodKind, 84 | }, 85 | want: utils.ErrorWithKind{ 86 | Err: nil, 87 | Kind: goodKind, 88 | }, 89 | }, 90 | } 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | if got := utils.WrapErrorWithKind(tt.args.err, tt.args.kind); !reflect.DeepEqual(got, tt.want) { 94 | t.Errorf("WrapErrorWithKind() = %v, want %v", got, tt.want) 95 | } 96 | }) 97 | } 98 | } 99 | 100 | func TestErrorIsKind(t *testing.T) { 101 | type args struct { 102 | err error 103 | kind string 104 | } 105 | tests := []struct { 106 | name string 107 | args args 108 | want bool 109 | }{ 110 | { 111 | name: "Positive", 112 | args: args{ 113 | err: utils.WrapErrorWithKind( 114 | errUnitWasAlreadyStarted, 115 | goodKind, 116 | ), 117 | kind: goodKind, 118 | }, 119 | want: true, 120 | }, 121 | { 122 | name: "Negative", 123 | args: args{ 124 | err: nil, 125 | kind: goodKind, 126 | }, 127 | want: false, 128 | }, 129 | } 130 | for _, tt := range tests { 131 | t.Run(tt.name, func(t *testing.T) { 132 | if got := utils.ErrorIsKind(tt.args.err, tt.args.kind); got != tt.want { 133 | t.Errorf("ErrorIsKind() = %v, want %v", got, tt.want) 134 | } 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /pkg/utils/flock.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | "syscall" 9 | ) 10 | 11 | // ErrLocked is returned when the flock is already held. 12 | var ErrLocked = fmt.Errorf("fslock is already locked") 13 | 14 | // FLock represents a file lock. 15 | type FLock struct { 16 | Fd int 17 | } 18 | 19 | // TryFLock non-blockingly attempts to acquire a lock on the file. 20 | func TryFLock(filename string) (*FLock, error) { 21 | fd, err := syscall.Open(filename, syscall.O_CREAT|syscall.O_RDONLY|syscall.O_CLOEXEC, syscall.S_IRUSR|syscall.S_IWUSR) 22 | if err != nil { 23 | return nil, err 24 | } 25 | err = syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB) 26 | if err == syscall.EWOULDBLOCK { 27 | err = ErrLocked 28 | } 29 | if err != nil { 30 | _ = syscall.Close(fd) 31 | 32 | return nil, err 33 | } 34 | 35 | return &FLock{Fd: fd}, nil 36 | } 37 | 38 | // Unlock unlocks the file lock. 39 | func (lock *FLock) Unlock() error { 40 | return syscall.Close(lock.Fd) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/utils/flock_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package utils_test 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "testing" 11 | 12 | "github.com/ansible/receptor/pkg/utils" 13 | ) 14 | 15 | func TestTryFLock(t *testing.T) { 16 | type args struct { 17 | filename string 18 | } 19 | tests := []struct { 20 | name string 21 | args args 22 | want *utils.FLock 23 | wantErr bool 24 | }{ 25 | { 26 | name: "Positive", 27 | args: args{ 28 | filename: filepath.Join(os.TempDir(), "good_flock_listener"), 29 | }, 30 | want: &utils.FLock{Fd: 0}, 31 | wantErr: false, 32 | }, 33 | { 34 | name: "Negative", 35 | args: args{ 36 | filename: "", 37 | }, 38 | want: &utils.FLock{}, 39 | wantErr: true, 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | got, err := utils.TryFLock(tt.args.filename) 45 | if (err != nil) != tt.wantErr { 46 | t.Errorf("%s: TryFLock(): error = %v, wantErr %v", tt.name, err, tt.wantErr) 47 | 48 | return 49 | } 50 | 51 | if err == nil { 52 | if got.Fd < 0 { 53 | t.Errorf("%s: UnixSocketListen(): Invalid got Fd = %+v", tt.name, got) 54 | } 55 | } 56 | }) 57 | } 58 | } 59 | 60 | func TestFLock_Unlock(t *testing.T) { 61 | f, err := os.CreateTemp("", "flock-test") 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | defer os.Remove(f.Name()) 66 | defer f.Close() 67 | 68 | var maxInt uintptr 69 | if strconv.IntSize == 32 { 70 | maxInt = uintptr(1<<31 - 1) 71 | } else { 72 | maxInt = uintptr(1<<63 - 1) 73 | } 74 | 75 | fd := f.Fd() 76 | if fd > maxInt { 77 | t.Error(err) 78 | } 79 | 80 | type fields struct { 81 | Fd int 82 | } 83 | tests := []struct { 84 | name string 85 | fields fields 86 | wantErr bool 87 | }{ 88 | { 89 | name: "Positive", 90 | fields: fields{ 91 | Fd: int(f.Fd()), // #nosec G115 92 | }, 93 | wantErr: false, 94 | }, 95 | { 96 | name: "Negative", 97 | fields: fields{ 98 | Fd: -1, 99 | }, 100 | wantErr: true, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | lock := &utils.FLock{ 106 | Fd: tt.fields.Fd, 107 | } 108 | if err := lock.Unlock(); (err != nil) != tt.wantErr { 109 | t.Errorf("%s: FLock.Unlock() error = %v, wantErr %v", tt.name, err, tt.wantErr) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/utils/flock_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package utils 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // ErrLocked is returned when the flock is already held 11 | var ErrLocked = fmt.Errorf("fslock is already locked") 12 | 13 | // FLock represents a Unix file lock, but is not usable on Windows 14 | type FLock struct{} 15 | 16 | // TryFLock is not implemented on Windows 17 | func TryFLock(filename string) (*FLock, error) { 18 | return nil, fmt.Errorf("file locks not implemented on Windows") 19 | } 20 | 21 | // Unlock is not implemented on Windows 22 | func (lock *FLock) Unlock() error { 23 | return fmt.Errorf("file locks not implemented on Windows") 24 | } 25 | -------------------------------------------------------------------------------- /pkg/utils/incremental_duration.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math" 5 | "time" 6 | ) 7 | 8 | // IncrementalDuration handles a time.Duration with max limits. 9 | type IncrementalDuration struct { 10 | Duration time.Duration 11 | InitialDuration time.Duration 12 | MaxDuration time.Duration 13 | multiplier float64 14 | } 15 | 16 | // NewIncrementalDuration returns an IncrementalDuration object with initialized values. 17 | func NewIncrementalDuration(duration, maxDuration time.Duration, multiplier float64) *IncrementalDuration { 18 | return &IncrementalDuration{ 19 | Duration: duration, 20 | InitialDuration: duration, 21 | MaxDuration: maxDuration, 22 | multiplier: multiplier, 23 | } 24 | } 25 | 26 | // Reset sets current duration to initial duration. 27 | func (id *IncrementalDuration) Reset() { 28 | id.Duration = id.InitialDuration 29 | } 30 | 31 | func (id *IncrementalDuration) IncreaseDuration() { 32 | id.Duration = time.Duration(math.Min(id.multiplier*float64(id.Duration), float64(id.MaxDuration))) 33 | } 34 | 35 | // NextTimeout returns a timeout channel based on current duration. 36 | func (id *IncrementalDuration) NextTimeout() <-chan time.Time { 37 | ch := time.After(id.Duration) 38 | id.IncreaseDuration() 39 | 40 | return ch 41 | } 42 | -------------------------------------------------------------------------------- /pkg/utils/incremental_duration_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ansible/receptor/pkg/utils" 8 | ) 9 | 10 | const newIncrementalDurationMessage string = "NewIncrementalDuration() = %v, want %v" 11 | 12 | func TestNewIncrementalDuration(t *testing.T) { 13 | type args struct { 14 | Duration time.Duration 15 | maxDuration time.Duration 16 | multiplier float64 17 | } 18 | 19 | tests := []struct { 20 | name string 21 | args args 22 | want time.Duration 23 | }{ 24 | { 25 | name: "NewIncrementalDuration1", 26 | args: args{ 27 | Duration: 1 * time.Second, 28 | maxDuration: 10 * time.Second, 29 | multiplier: 2.0, 30 | }, 31 | want: 1 * time.Second, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := utils.NewIncrementalDuration(tt.args.Duration, tt.args.maxDuration, tt.args.multiplier); got.Duration != tt.want { 37 | t.Errorf(newIncrementalDurationMessage, got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestIncrementalDurationReset(t *testing.T) { 44 | delay := utils.NewIncrementalDuration(1*time.Second, 10*time.Second, 2.0) 45 | want1 := 1 * time.Second 46 | if delay.Duration != want1 { 47 | t.Errorf(newIncrementalDurationMessage, delay.Duration, want1) 48 | } 49 | <-delay.NextTimeout() 50 | 51 | want2 := 2 * time.Second 52 | if delay.Duration != want2 { 53 | t.Errorf(newIncrementalDurationMessage, delay.Duration, want2) 54 | } 55 | delay.Reset() 56 | if delay.Duration != want1 { 57 | t.Errorf("Reset() = %v, want %v", delay.Duration, want1) 58 | } 59 | } 60 | 61 | func TestIncrementalDurationincreaseDuration(t *testing.T) { 62 | delay := utils.NewIncrementalDuration(1*time.Second, 10*time.Second, 2.0) 63 | for i := 0; i <= 10; i++ { 64 | delay.IncreaseDuration() 65 | } 66 | want10 := 10 * time.Second 67 | if delay.Duration != want10 { 68 | t.Errorf("increaseDuration() = %v, want %v", delay.Duration, want10) 69 | } 70 | } 71 | 72 | func TestIncrementalDurationNextTimeout(t *testing.T) { 73 | delay := utils.NewIncrementalDuration(1*time.Second, 10*time.Second, 2.0) 74 | want1 := 1 * time.Second 75 | if delay.Duration != want1 { 76 | t.Errorf(newIncrementalDurationMessage, delay.Duration, want1) 77 | } 78 | <-delay.NextTimeout() 79 | 80 | want2 := 2 * time.Second 81 | if delay.Duration != want2 { 82 | t.Errorf("NextTimeout() = %v, want %v", delay.Duration, want2) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/utils/net.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net" 4 | 5 | type NetListener interface { 6 | net.Listener 7 | } 8 | 9 | type NetConn interface { 10 | net.Conn 11 | } 12 | 13 | type NetAddr interface { 14 | net.Addr 15 | } 16 | -------------------------------------------------------------------------------- /pkg/utils/readstring_context.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | ) 7 | 8 | type readStringResult = struct { 9 | str string 10 | err error 11 | } 12 | 13 | // ReadStringContext calls bufio.Reader.ReadString() but uses a context. Note that if the 14 | // ctx.Done() fires, the ReadString() call is still active, and bufio is not re-entrant, so it is 15 | // important for callers to error out of further use of the bufio. Also, the goroutine will not 16 | // exit until the bufio's underlying connection is closed. 17 | func ReadStringContext(ctx context.Context, reader *bufio.Reader, delim byte) (string, error) { 18 | result := make(chan *readStringResult, 1) 19 | go func() { 20 | str, err := reader.ReadString(delim) 21 | result <- &readStringResult{ 22 | str: str, 23 | err: err, 24 | } 25 | }() 26 | select { 27 | case res := <-result: 28 | return res.str, res.err 29 | case <-ctx.Done(): 30 | return "", ctx.Err() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/utils/sysinfo.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/pbnjay/memory" 7 | ) 8 | 9 | // GetSysCPUCount returns number of logical CPU cores on the system. 10 | func GetSysCPUCount() int { 11 | return runtime.NumCPU() 12 | } 13 | 14 | // GetSysMemoryMiB returns the capacity (in mebibytes) of the physical memory installed on the system. 15 | func GetSysMemoryMiB() uint64 { 16 | return memory.TotalMemory() / 1048576 // bytes to MiB 17 | } 18 | -------------------------------------------------------------------------------- /pkg/utils/sysinfo_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "os/exec" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/ansible/receptor/pkg/utils" 11 | ) 12 | 13 | func TestGetSysCPUCount(t *testing.T) { 14 | got := utils.GetSysCPUCount() 15 | if got <= 0 { 16 | t.Errorf("Non-positive CPU count: %d\n", got) 17 | } 18 | 19 | if runtime.GOOS == "linux" { 20 | commandOutput, _ := exec.Command("nproc").CombinedOutput() 21 | 22 | commandOutputWithout := strings.TrimSpace(string(commandOutput)) 23 | want, _ := strconv.Atoi(commandOutputWithout) 24 | 25 | if got != want { 26 | t.Errorf("Expected CPU count: %d, got %d\n", want, got) 27 | } 28 | } 29 | } 30 | 31 | func TestGetSysMemoryMiB(t *testing.T) { 32 | got := utils.GetSysMemoryMiB() 33 | if got <= 0 { 34 | t.Errorf("Non-positive Memory: %d\n", got) 35 | } 36 | 37 | if runtime.GOOS == "linux" { 38 | commandOutput, _ := exec.Command("sed", "-n", "s/^MemTotal:[[:space:]]*\\([[:digit:]]*\\).*/\\1/p", "/proc/meminfo").CombinedOutput() 39 | 40 | commandOutputWithout := strings.TrimSpace(string(commandOutput)) 41 | wantKb, _ := strconv.ParseUint(commandOutputWithout, 10, 64) 42 | 43 | want := wantKb / 1024 44 | if got != want { 45 | t.Errorf("Expected Memory: %d, got %d\n", want, got) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/utils/unixsock.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package utils 5 | 6 | import ( 7 | "net" 8 | "os" 9 | ) 10 | 11 | // UnixSocketListen listens on a Unix socket, handling file locking and permissions. 12 | func UnixSocketListen(filename string, permissions os.FileMode) (net.Listener, *FLock, error) { 13 | lock, err := TryFLock(filename + ".lock") 14 | if err != nil { 15 | return nil, nil, MakeUnixSocketError(ErrSocketLockFileNotAcquired, err) 16 | } 17 | err = os.RemoveAll(filename) 18 | if err != nil { 19 | _ = lock.Unlock() 20 | 21 | return nil, nil, MakeUnixSocketError(ErrSocketFileNotOverwritten, err) 22 | } 23 | uli, err := net.Listen("unix", filename) 24 | if err != nil { 25 | _ = lock.Unlock() 26 | 27 | return nil, nil, MakeUnixSocketError(ErrSocketFileListen, err) 28 | } 29 | err = os.Chmod(filename, permissions) 30 | if err != nil { 31 | _ = uli.Close() 32 | _ = lock.Unlock() 33 | 34 | return nil, nil, MakeUnixSocketError(ErrSocketFilePermissionsNotSet, err) 35 | } 36 | 37 | return uli, lock, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/utils/unixsock_errors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | ErrSocketLockFileNotAcquired = errors.New("could not acquire lock on socket file") 10 | ErrSocketFileNotOverwritten = errors.New("could not overwrite socket file") 11 | ErrSocketFileListen = errors.New("could not listen on socket file") 12 | ErrSocketFilePermissionsNotSet = errors.New("error setting socket file permissions") 13 | ErrWindowsNotSupported = errors.New("unix sockets not available on Windows") 14 | ) 15 | 16 | func MakeUnixSocketError(err, underlyingErr error) error { 17 | return fmt.Errorf("%s: %s", err, underlyingErr) 18 | } 19 | 20 | func MakeWindowsSocketError() error { 21 | return ErrWindowsNotSupported 22 | } 23 | -------------------------------------------------------------------------------- /pkg/utils/unixsock_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package utils 5 | 6 | import ( 7 | "net" 8 | "os" 9 | ) 10 | 11 | // UnixSocketListen is not available on Windows 12 | func UnixSocketListen(filename string, permissions os.FileMode) (net.Listener, *FLock, error) { 13 | return nil, nil, MakeWindowsSocketError() 14 | } 15 | -------------------------------------------------------------------------------- /pkg/workceptor/cmdline.go: -------------------------------------------------------------------------------- 1 | //go:build !no_workceptor 2 | // +build !no_workceptor 3 | 4 | package workceptor 5 | 6 | import "github.com/ghjm/cmdline" 7 | 8 | var workersSection = &cmdline.ConfigSection{ 9 | Description: "Commands to configure workers that process units of work:", 10 | Order: 30, 11 | } 12 | -------------------------------------------------------------------------------- /pkg/workceptor/command_detach_unixlike.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !no_workceptor 2 | // +build !windows,!no_workceptor 3 | 4 | package workceptor 5 | 6 | import ( 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func cmdSetDetach(cmd *exec.Cmd) { 12 | cmd.SysProcAttr = &syscall.SysProcAttr{ 13 | Setsid: true, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pkg/workceptor/command_detach_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && !no_workceptor 2 | // +build windows,!no_workceptor 3 | 4 | package workceptor 5 | 6 | import ( 7 | "os/exec" 8 | ) 9 | 10 | func cmdSetDetach(cmd *exec.Cmd) { 11 | // Do nothing 12 | } 13 | -------------------------------------------------------------------------------- /pkg/workceptor/controlsvc_test.go: -------------------------------------------------------------------------------- 1 | //go:build !no_workceptor 2 | // +build !no_workceptor 3 | 4 | package workceptor 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/ansible/receptor/pkg/controlsvc" 10 | ) 11 | 12 | func Test_workceptorCommandTypeInitFromString(t *testing.T) { 13 | type fields struct { 14 | w *Workceptor 15 | } 16 | type args struct { 17 | params string 18 | } 19 | tests := []struct { 20 | name string 21 | fields fields 22 | args args 23 | want controlsvc.ControlCommand 24 | wantErr bool 25 | }{ 26 | { 27 | name: "Positive cancel", 28 | fields: fields{ 29 | w: nil, 30 | }, 31 | args: args{ 32 | params: "cancel u", 33 | }, 34 | wantErr: false, 35 | }, 36 | { 37 | name: "Positive force-release", 38 | fields: fields{ 39 | w: nil, 40 | }, 41 | args: args{ 42 | params: "force-release u", 43 | }, 44 | wantErr: false, 45 | }, 46 | { 47 | name: "Positive list", 48 | fields: fields{ 49 | w: nil, 50 | }, 51 | args: args{ 52 | params: "list", 53 | }, 54 | wantErr: false, 55 | }, 56 | { 57 | name: "Positive release", 58 | fields: fields{ 59 | w: nil, 60 | }, 61 | args: args{ 62 | params: "release u", 63 | }, 64 | wantErr: false, 65 | }, 66 | { 67 | name: "Positive results", 68 | fields: fields{ 69 | w: nil, 70 | }, 71 | args: args{ 72 | params: "results u", 73 | }, 74 | wantErr: false, 75 | }, 76 | { 77 | name: "Positive status", 78 | fields: fields{ 79 | w: nil, 80 | }, 81 | args: args{ 82 | params: "status u", 83 | }, 84 | wantErr: false, 85 | }, 86 | { 87 | name: "Positive submit", 88 | fields: fields{ 89 | w: nil, 90 | }, 91 | args: args{ 92 | params: "submit n w", 93 | }, 94 | wantErr: false, 95 | }, 96 | } 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | tr := &workceptorCommandType{ 100 | w: tt.fields.w, 101 | } 102 | got, err := tr.InitFromString(tt.args.params) 103 | if (err != nil) != tt.wantErr { 104 | t.Errorf("workceptorCommandType.InitFromString() error = %v, wantErr %v", err, tt.wantErr) 105 | 106 | return 107 | } 108 | if got == nil { 109 | t.Errorf("workceptorCommandType.InitFromString() returned nil") 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /pkg/workceptor/interfaces.go: -------------------------------------------------------------------------------- 1 | package workceptor 2 | 3 | // WorkUnit represents a local unit of work. 4 | type WorkUnit interface { 5 | ID() string 6 | UnitDir() string 7 | StatusFileName() string 8 | StdoutFileName() string 9 | Save() error 10 | Load() error 11 | SetFromParams(params map[string]string) error 12 | UpdateBasicStatus(state int, detail string, stdoutSize int64) 13 | UpdateFullStatus(statusFunc func(*StatusFileData)) 14 | LastUpdateError() error 15 | Status() *StatusFileData 16 | UnredactedStatus() *StatusFileData 17 | Start() error 18 | Restart() error 19 | Cancel() error 20 | Release(force bool) error 21 | } 22 | 23 | type WorkerConfig interface { 24 | GetWorkType() string 25 | GetVerifySignature() bool 26 | NewWorker(bwu BaseWorkUnitForWorkUnit, w *Workceptor, unitID string, workType string) WorkUnit 27 | } 28 | 29 | // NewWorkerFunc represents a factory of WorkUnit instances. 30 | type NewWorkerFunc func(bwu BaseWorkUnitForWorkUnit, w *Workceptor, unitID string, workType string) WorkUnit 31 | 32 | // StatusFileData is the structure of the JSON data saved to a status file. 33 | // This struct should only contain value types, except for ExtraData. 34 | type StatusFileData struct { 35 | State int 36 | Detail string 37 | StdoutSize int64 38 | WorkType string 39 | ExtraData interface{} 40 | } 41 | -------------------------------------------------------------------------------- /pkg/workceptor/json_test.go: -------------------------------------------------------------------------------- 1 | //go:build !no_workceptor 2 | // +build !no_workceptor 3 | 4 | package workceptor 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "testing" 10 | 11 | "github.com/ansible/receptor/pkg/netceptor" 12 | ) 13 | 14 | func newCommandWorker(_ BaseWorkUnitForWorkUnit, w *Workceptor, unitID string, workType string) WorkUnit { 15 | cw := &commandUnit{ 16 | BaseWorkUnitForWorkUnit: &BaseWorkUnit{ 17 | status: StatusFileData{ 18 | ExtraData: &CommandExtraData{}, 19 | }, 20 | }, 21 | command: "echo", 22 | baseParams: "foo", 23 | allowRuntimeParams: true, 24 | } 25 | cw.BaseWorkUnitForWorkUnit.Init(w, unitID, workType, FileSystem{}) 26 | 27 | return cw 28 | } 29 | 30 | func TestWorkceptorJson(t *testing.T) { 31 | tmpdir, err := os.MkdirTemp(os.TempDir(), "receptor-test-*") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer os.RemoveAll(tmpdir) 36 | nc := netceptor.New(context.TODO(), "test") 37 | w, err := New(context.Background(), nc, tmpdir) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | err = w.RegisterWorker("command", newCommandWorker, false) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | cw, err := w.AllocateUnit("command", "", make(map[string]string)) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | cw.UpdateFullStatus(func(status *StatusFileData) { 50 | ed, ok := status.ExtraData.(*CommandExtraData) 51 | if !ok { 52 | t.Fatal("ExtraData type assertion failed") 53 | } 54 | ed.Pid = 12345 55 | }) 56 | err = cw.Save() 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | cw2 := newCommandWorker(nil, w, cw.ID(), "command") 61 | err = cw2.Load() 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | ed2, ok := cw2.Status().ExtraData.(*CommandExtraData) 66 | if !ok { 67 | t.Fatal("ExtraData type assertion failed") 68 | } 69 | if ed2.Pid != 12345 { 70 | t.Fatal("PID did not make it through") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/workceptor/lock_test.go: -------------------------------------------------------------------------------- 1 | //go:build !no_workceptor 2 | // +build !no_workceptor 3 | 4 | package workceptor 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "path" 11 | "strconv" 12 | "sync" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestStatusFileLock(t *testing.T) { 18 | numWriterThreads := 8 19 | numReaderThreads := 8 20 | baseWaitTime := 200 * time.Millisecond 21 | 22 | tmpdir, err := os.MkdirTemp(os.TempDir(), "receptor-test-*") 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | defer os.RemoveAll(tmpdir) 27 | statusFilename := path.Join(tmpdir, "status") 28 | startTime := time.Now() 29 | var totalWaitTime time.Duration 30 | wg := sync.WaitGroup{} 31 | wg.Add(numWriterThreads) 32 | for i := 0; i < numWriterThreads; i++ { 33 | waitTime := time.Duration(i) * baseWaitTime 34 | totalWaitTime += waitTime 35 | go func(iter int, waitTime time.Duration) { 36 | sfd := StatusFileData{} 37 | sfd.UpdateFullStatus(statusFilename, func(status *StatusFileData) { 38 | time.Sleep(waitTime) 39 | status.State = iter 40 | status.StdoutSize = int64(iter) 41 | status.Detail = fmt.Sprintf("%d", iter) 42 | }) 43 | wg.Done() 44 | }(i, waitTime) 45 | } 46 | ctx, cancel := context.WithCancel(context.Background()) 47 | wg2 := sync.WaitGroup{} 48 | wg2.Add(numReaderThreads) 49 | for i := 0; i < numReaderThreads; i++ { 50 | go func() { 51 | sfd := StatusFileData{} 52 | fileHasExisted := false 53 | for { 54 | if ctx.Err() != nil { 55 | wg2.Done() 56 | 57 | return 58 | } 59 | err := sfd.Load(statusFilename) 60 | if os.IsNotExist(err) && !fileHasExisted { 61 | continue 62 | } 63 | fileHasExisted = true 64 | if err != nil { 65 | t.Fatalf("Error loading status file: %s", err) 66 | } 67 | detailIter, err := strconv.Atoi(sfd.Detail) 68 | if err != nil { 69 | t.Fatalf("Error converting status detail to int: %s", err) 70 | } 71 | if detailIter >= 0 { 72 | if int64(sfd.State) != sfd.StdoutSize || sfd.State != detailIter { 73 | t.Fatal("Mismatched data in struct") 74 | } 75 | } 76 | } 77 | }() 78 | } 79 | wg.Wait() 80 | cancel() 81 | totalTime := time.Since(startTime) 82 | if totalTime < totalWaitTime { 83 | t.Fatal("File locks apparently not locking") 84 | } 85 | wg2.Wait() 86 | } 87 | -------------------------------------------------------------------------------- /pkg/workceptor/remote_work_test.go: -------------------------------------------------------------------------------- 1 | package workceptor_test 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/ansible/receptor/pkg/workceptor" 9 | "github.com/ansible/receptor/pkg/workceptor/mock_workceptor" 10 | "go.uber.org/mock/gomock" 11 | ) 12 | 13 | func createRemoteWorkTestSetup(t *testing.T) (workceptor.WorkUnit, *mock_workceptor.MockBaseWorkUnitForWorkUnit, *mock_workceptor.MockNetceptorForWorkceptor, *workceptor.Workceptor) { 14 | ctrl := gomock.NewController(t) 15 | ctx := context.Background() 16 | 17 | mockBaseWorkUnit := mock_workceptor.NewMockBaseWorkUnitForWorkUnit(ctrl) 18 | mockNetceptor := mock_workceptor.NewMockNetceptorForWorkceptor(ctrl) 19 | mockNetceptor.EXPECT().NodeID().Return("NodeID") 20 | mockNetceptor.EXPECT().GetLogger() 21 | 22 | w, err := workceptor.New(ctx, mockNetceptor, "/tmp") 23 | if err != nil { 24 | t.Errorf("Error while creating Workceptor: %v", err) 25 | } 26 | 27 | mockBaseWorkUnit.EXPECT().Init(w, "", "", workceptor.FileSystem{}) 28 | mockBaseWorkUnit.EXPECT().SetStatusExtraData(gomock.Any()) 29 | workUnit := workceptor.NewRemoteWorker(mockBaseWorkUnit, w, "", "") 30 | 31 | return workUnit, mockBaseWorkUnit, mockNetceptor, w 32 | } 33 | 34 | func TestRemoteWorkUnredactedStatus(t *testing.T) { 35 | t.Parallel() 36 | wu, mockBaseWorkUnit, _, _ := createRemoteWorkTestSetup(t) 37 | restartTestCases := []struct { 38 | name string 39 | }{ 40 | {name: "test1"}, 41 | {name: "test2"}, 42 | } 43 | 44 | statusLock := &sync.RWMutex{} 45 | for _, testCase := range restartTestCases { 46 | t.Run(testCase.name, func(t *testing.T) { 47 | t.Parallel() 48 | mockBaseWorkUnit.EXPECT().GetStatusLock().Return(statusLock).Times(2) 49 | mockBaseWorkUnit.EXPECT().GetStatusWithoutExtraData().Return(&workceptor.StatusFileData{}) 50 | mockBaseWorkUnit.EXPECT().GetStatusCopy().Return(workceptor.StatusFileData{ 51 | ExtraData: &workceptor.RemoteExtraData{}, 52 | }) 53 | wu.UnredactedStatus() 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/workceptor/workceptor_stub.go: -------------------------------------------------------------------------------- 1 | //go:build no_workceptor 2 | // +build no_workceptor 3 | 4 | package workceptor 5 | 6 | // Stub file to satisfy dependencies when Workceptor is not compiled in 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | 12 | "github.com/ansible/receptor/pkg/controlsvc" 13 | "github.com/ansible/receptor/pkg/netceptor" 14 | ) 15 | 16 | // ErrNotImplemented is returned from functions that are stubbed out 17 | var ErrNotImplemented = fmt.Errorf("not implemented") 18 | 19 | // Workceptor is the main object that handles unit-of-work management 20 | type Workceptor struct{} 21 | 22 | // New constructs a new Workceptor instance 23 | func New(ctx context.Context, nc *netceptor.Netceptor, dataDir string) (*Workceptor, error) { 24 | return &Workceptor{}, nil 25 | } 26 | 27 | // MainInstance is the global instance of Workceptor instantiated by the command-line main() function 28 | var MainInstance *Workceptor 29 | 30 | // RegisterWithControlService registers this workceptor instance with a control service instance 31 | func (w *Workceptor) RegisterWithControlService(cs *controlsvc.Server) error { 32 | return nil 33 | } 34 | 35 | // RegisterWorker notifies the Workceptor of a new kind of work that can be done 36 | func (w *Workceptor) RegisterWorker(typeName string, newWorkerFunc NewWorkerFunc, verifySignature bool) error { 37 | return ErrNotImplemented 38 | } 39 | 40 | // AllocateUnit creates a new local work unit and generates an identifier for it 41 | func (w *Workceptor) AllocateUnit(workTypeName, params string) (WorkUnit, error) { 42 | return nil, ErrNotImplemented 43 | } 44 | 45 | // AllocateRemoteUnit creates a new remote work unit and generates a local identifier for it 46 | func (w *Workceptor) AllocateRemoteUnit(remoteNode string, remoteWorkType string, tlsclient string, ttl string, signWork bool, params string) (WorkUnit, error) { 47 | return nil, ErrNotImplemented 48 | } 49 | 50 | // StartUnit starts a unit of work 51 | func (w *Workceptor) StartUnit(unitID string) error { 52 | return ErrNotImplemented 53 | } 54 | 55 | // ListKnownUnitIDs returns a slice containing the known unit IDs 56 | func (w *Workceptor) ListKnownUnitIDs() []string { 57 | return []string{} 58 | } 59 | 60 | // UnitStatus returns the state of a unit 61 | func (w *Workceptor) UnitStatus(unitID string) (*StatusFileData, error) { 62 | return nil, ErrNotImplemented 63 | } 64 | 65 | // CancelUnit cancels a unit of work, killing any processes 66 | func (w *Workceptor) CancelUnit(unitID string) error { 67 | return ErrNotImplemented 68 | } 69 | 70 | // ReleaseUnit releases (deletes) resources from a unit of work, including stdout. Release implies Cancel. 71 | func (w *Workceptor) ReleaseUnit(unitID string, force bool) error { 72 | return ErrNotImplemented 73 | } 74 | 75 | // GetResults returns a live stream of the results of a unit 76 | func (w *Workceptor) GetResults(unitID string, startPos int64, doneChan chan struct{}) (chan []byte, error) { 77 | return nil, ErrNotImplemented 78 | } 79 | -------------------------------------------------------------------------------- /receptor-python-worker/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .mypy_cache 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | report.xml 47 | report.pylama 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # vim 95 | *.swp 96 | 97 | # mac OS 98 | *.DS_Store 99 | 100 | # pytest 101 | *.pytest_cache 102 | 103 | # PyCharm 104 | .idea/ 105 | -------------------------------------------------------------------------------- /receptor-python-worker/README.md: -------------------------------------------------------------------------------- 1 | The receptor-python-worker command is called by Receptor to supervise the operation of a Python worker plugin. 2 | -------------------------------------------------------------------------------- /receptor-python-worker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "receptor-python-worker" 3 | authors = [{name = "Red Hat", email = "info@ansible.com"}] 4 | description = "The receptor-python-worker command is called by Receptor to supervise the operation of a Python worker plugin." 5 | readme = "README.md" 6 | license = "Apache-2.0" 7 | dynamic = ["version"] 8 | 9 | [build-system] 10 | requires = ["setuptools", "setuptools-scm"] 11 | build-backend = "setuptools.build_meta" 12 | 13 | [tool.setuptools_scm] 14 | fallback_version = "0.0.0" 15 | 16 | [project.scripts] 17 | receptor-python-worker = "receptor_python_worker:run" -------------------------------------------------------------------------------- /receptor-python-worker/receptor_python_worker/__init__.py: -------------------------------------------------------------------------------- 1 | from .work import run -------------------------------------------------------------------------------- /receptor-python-worker/receptor_python_worker/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .work import run 3 | 4 | run() 5 | -------------------------------------------------------------------------------- /receptor-python-worker/receptor_python_worker/plugin_utils.py: -------------------------------------------------------------------------------- 1 | BYTES_PAYLOAD = "bytes" 2 | """ 3 | Inform Receptor that the given plugin expects BYTES for the message data 4 | """ 5 | 6 | BUFFER_PAYLOAD = "buffer" 7 | """ 8 | Inform Receptor that the given plugin expects a buffered reader for the message data 9 | """ 10 | 11 | FILE_PAYLOAD = "file" 12 | """ 13 | Inform Receptor that the given plugin expects a file path for the message data 14 | """ 15 | 16 | 17 | def plugin_export(payload_type): 18 | """ 19 | A decorator intended to be used by Receptor plugins in conjunction with 20 | entrypoints typically defined in your setup.py file:: 21 | 22 | entry_points={ 23 | 'receptor.worker': 24 | 'your_package_name = your_package_name.your_module', 25 | } 26 | 27 | ``your_package_name.your_module`` should then contain a function decorated with 28 | ``plugin_export`` as such:: 29 | 30 | @receptor.plugin_export(payload_type=receptor.BYTES_PAYLOAD): 31 | def execute(message, config, result_queue): 32 | result_queue.put("My plugin ran!") 33 | 34 | You can then send messages to this plugin across the Receptor mesh with the directive 35 | ``your_package_name:execute`` 36 | 37 | Depending on what kind of data you expect to receive you can select from one 38 | of 3 different incoming payload types. This determines the incoming type of the 39 | ``message`` data type: 40 | 41 | * BYTES_PAYLOAD: This will give you literal python bytes that you can then read 42 | and interpret. 43 | * BUFFER_PAYLOAD: This will send you a buffer that you can read(). This buffer 44 | will be automatically closed and its contents discarded when your plugin returns. 45 | * FILE_PAYLOAD: This will return you a file path that you can open() or manage 46 | in any way you see fit. It will be automatically removed after your plugin returns. 47 | 48 | For more information about developing plugins see :ref:`plugins`. 49 | """ 50 | 51 | def decorator(func): 52 | func.receptor_export = True 53 | func.payload_type = payload_type 54 | return func 55 | 56 | return decorator 57 | -------------------------------------------------------------------------------- /receptorctl/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .mypy_cache 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | report.xml 47 | report.pylama 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # vim 95 | *.swp 96 | 97 | # mac OS 98 | *.DS_Store 99 | 100 | # pytest 101 | *.pytest_cache 102 | 103 | # PyCharm 104 | .idea/ 105 | -------------------------------------------------------------------------------- /receptorctl/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include receptorctl *.py 2 | exclude .gitignore 3 | exclude noxfile.py 4 | -------------------------------------------------------------------------------- /receptorctl/README.md: -------------------------------------------------------------------------------- 1 | # Receptorctl 2 | 3 | Receptorctl is a front-end CLI and importable Python library that interacts with Receptor over its control socket interface. 4 | 5 | ## Setting up nox 6 | 7 | This project includes a `nox` configuration to automate tests, checks, and other functions in a reproducible way using isolated environments. 8 | Before you submit a PR, you should install `nox` and verify your changes. 9 | 10 | > To run `make receptorctl-lint` and `receptorctl-test` from the repository root, you must first install `nox`. 11 | 12 | 1. Install `nox` using `python3 -m pip install nox` or your distribution's package manager. 13 | 2. Run `nox --list` from the `receptorctl` directory to view available sessions. 14 | 15 | You can run `nox` with no arguments to execute all checks and tests. 16 | Alternatively, you can run only certain tasks as outlined in the following sections. 17 | 18 | > By default nox sessions install pinned dependencies from `pyproject.toml`. 19 | 20 | ## Checking changes to Receptorctl 21 | 22 | Run the following `nox` sessions to check for code style and formatting issues: 23 | 24 | * Run all checks. 25 | 26 | ```bash 27 | nox -s lint 28 | ``` 29 | 30 | * Check code style. 31 | 32 | ```bash 33 | nox -s check_style 34 | ``` 35 | 36 | * Check formatting. 37 | 38 | ```bash 39 | nox -s check_format 40 | ``` 41 | 42 | * Format code if the check fails. 43 | 44 | ```bash 45 | nox -s format 46 | ``` 47 | 48 | ## Running Receptorctl tests 49 | 50 | Run the following `nox` sessions to test Receptorctl changes: 51 | 52 | * Run tests against the complete matrix of Python versions. 53 | 54 | ```bash 55 | nox -s tests 56 | ``` 57 | 58 | * Run tests against a specific Python version. 59 | 60 | ```bash 61 | # For example, this command tests Receptorctl against Python 3.12. 62 | nox -s tests-3.12 63 | ``` 64 | 65 | ## Updating dependencies 66 | 67 | Update dependencies in the `requirements` directory as follows: 68 | 69 | 1. Add any packages or pins to the `*.in` file. 70 | 2. Do one of the following from the `receptorctl` directory: 71 | 72 | * Update all dependencies. 73 | 74 | ```bash 75 | nox -s pip-compile 76 | ``` 77 | 78 | * Generate the full dependency tree for a single set of dependencies, for example: 79 | 80 | ```bash 81 | nox -s "pip-compile-3.12(tests)" 82 | ``` 83 | 84 | > You can also pass the `--no-upgrade` flag when adding a new package. 85 | > This avoids bumping transitive dependencies for other packages in the `*.in` file. 86 | 87 | ```bash 88 | nox -s pip-compile -- --no-upgrade 89 | ``` 90 | -------------------------------------------------------------------------------- /receptorctl/noxfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from glob import iglob 4 | from pathlib import Path 5 | 6 | import nox.command 7 | 8 | LATEST_PYTHON_VERSION = ["3.12"] 9 | 10 | python_versions = ["3.8", "3.9", "3.10", "3.11", "3.12"] 11 | 12 | LINT_FILES: tuple[str, ...] = (*iglob("**/*.py"),) 13 | 14 | requirements_directory = Path("requirements").resolve() 15 | 16 | 17 | def install(session: nox.Session, *args, **kwargs): 18 | session.install(".[test]", *args, **kwargs) 19 | 20 | 21 | @nox.session(python=LATEST_PYTHON_VERSION) 22 | def coverage(session: nox.Session): 23 | """ 24 | Run receptorctl tests with code coverage 25 | """ 26 | install(session) 27 | session.install("-e", ".") 28 | session.run( 29 | "pytest", 30 | "--cov", 31 | "--cov-report", 32 | "term-missing:skip-covered", 33 | "--cov-report", 34 | "xml:receptorctl_coverage.xml", 35 | "--verbose", 36 | "tests", 37 | *session.posargs, 38 | ) 39 | 40 | 41 | @nox.session(python=python_versions) 42 | def tests(session: nox.Session): 43 | """ 44 | Run receptorctl tests 45 | """ 46 | install(session) 47 | session.install("-e", ".") 48 | session.run("pytest", "-v", "tests", *session.posargs) 49 | 50 | 51 | @nox.session 52 | def check_style(session: nox.Session): 53 | """ 54 | Check receptorctl Python code style 55 | """ 56 | install(session) 57 | session.run("ruff", "check", *session.posargs, *LINT_FILES) 58 | 59 | 60 | @nox.session 61 | def check_format(session: nox.Session): 62 | """ 63 | Check receptorctl Python file formatting without making changes 64 | """ 65 | install(session) 66 | session.run("ruff", "format", "--check", *session.posargs, *LINT_FILES) 67 | 68 | 69 | @nox.session 70 | def format(session: nox.Session): 71 | """ 72 | Format receptorctl Python files 73 | """ 74 | install(session) 75 | session.run("ruff", "format", *session.posargs, *LINT_FILES) 76 | 77 | 78 | @nox.session 79 | def lint(session: nox.Session): 80 | """ 81 | Check receptorctl for code style and formatting 82 | """ 83 | session.notify("check_style") 84 | session.notify("check_format") 85 | 86 | 87 | @nox.session(name="pip-compile", python=["3.12"]) 88 | def pip_compile(session: nox.Session): 89 | """Generate lock files from input files or upgrade packages in lock files.""" 90 | install(session) 91 | 92 | # Use --upgrade by default unless a user passes -P. 93 | upgrade_related_cli_flags = ("-P", "--upgrade-package", "--no-upgrade") 94 | has_upgrade_related_cli_flags = any(arg.startswith(upgrade_related_cli_flags) for arg in session.posargs) 95 | injected_extra_cli_args = () if has_upgrade_related_cli_flags else ("--upgrade",) 96 | 97 | output_file = os.path.relpath(Path(requirements_directory / "requirements.txt")) 98 | input_file = "pyproject.toml" 99 | 100 | session.run( 101 | "pip-compile", 102 | "--output-file", 103 | str(output_file), 104 | *session.posargs, 105 | *injected_extra_cli_args, 106 | str(input_file), 107 | ) 108 | -------------------------------------------------------------------------------- /receptorctl/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "receptorctl" 3 | authors = [{name = "Red Hat", email = "info@ansible.com"}] 4 | description = "Receptorctl is a front-end CLI and importable Python library that interacts with Receptor over its control socket interface." 5 | readme = "README.md" 6 | dynamic = ["version"] 7 | dependencies = [ 8 | "python-dateutil>=2.8.1", 9 | "click>=8.1.3, <8.2.0", 10 | "PyYAML>=5.4.1", 11 | ] 12 | 13 | [project.license] 14 | text = "Apache-2.0" 15 | 16 | [project.urls] 17 | Homepage = "https://ansible.readthedocs.io/projects/receptor/" 18 | Documentation = "https://ansible.readthedocs.io/projects/receptor/en/latest/" 19 | Repository = "https://github.com/ansible/receptor" 20 | Issues = "https://github.com/ansible/receptor/issues" 21 | 22 | [build-system] 23 | requires = ["setuptools>=75.3.2", "setuptools-scm>=7.1.0"] 24 | build-backend = "setuptools.build_meta" 25 | 26 | [tool.setuptools_scm] 27 | fallback_version = "0.0.0" 28 | 29 | [project.optional-dependencies] 30 | test = [ 31 | "coverage", 32 | "pip-tools>=7", 33 | "pytest", 34 | "pytest-cov", 35 | "ruff", 36 | ] 37 | 38 | [project.scripts] 39 | receptorctl = "receptorctl:run" 40 | 41 | [tool.ruff] 42 | line-length = 100 43 | 44 | [tool.pip-tools] 45 | resolver = "backtracking" 46 | allow-unsafe = true 47 | strip-extras = true 48 | quiet = true 49 | 50 | [tool.coverage.run] 51 | omit = ["tests/*"] -------------------------------------------------------------------------------- /receptorctl/receptorctl/__init__.py: -------------------------------------------------------------------------------- 1 | from .cli import run 2 | from .socket_interface import ReceptorControl 3 | 4 | __all__ = ["run", "ReceptorControl"] 5 | -------------------------------------------------------------------------------- /receptorctl/receptorctl/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import run 2 | 3 | run() 4 | -------------------------------------------------------------------------------- /receptorctl/requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile --allow-unsafe --output-file=requirements/requirements.txt --strip-extras pyproject.toml 6 | # 7 | click==8.1.8 8 | # via receptorctl (pyproject.toml) 9 | python-dateutil==2.9.0.post0 10 | # via receptorctl (pyproject.toml) 11 | pyyaml==6.0.2 12 | # via receptorctl (pyproject.toml) 13 | six==1.17.0 14 | # via python-dateutil 15 | -------------------------------------------------------------------------------- /receptorctl/setup.py: -------------------------------------------------------------------------------- 1 | # This file is only used by our downstream RPM builds. 2 | # Remove this once that tooling has been updated to work with setup.cfg. 3 | 4 | import setuptools 5 | 6 | if __name__ == "__main__": 7 | setuptools.setup() 8 | -------------------------------------------------------------------------------- /receptorctl/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | test_dir = os.path.dirname(os.path.realpath(__file__)) 4 | -------------------------------------------------------------------------------- /receptorctl/tests/mesh-definitions/access_control/node1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: node1 4 | 5 | - log-level: debug 6 | 7 | - tcp-listener: 8 | port: 12111 9 | 10 | - control-service: 11 | filename: /tmp/receptorctltest/access_control/node1.sock 12 | 13 | - work-signing: 14 | privatekey: /tmp/receptorctltest/access_control/signwork_key 15 | tokenexpiration: 10h30m 16 | 17 | - work-verification: 18 | publickey: /tmp/receptorctltest/access_control/signwork_key.pub 19 | 20 | - work-command: 21 | worktype: signed-echo 22 | command: bash 23 | params: "-c \"for w in {1..4}; do echo ${line^^}; sleep 1; done\"" 24 | verifysignature: true 25 | 26 | - work-command: 27 | workType: unsigned-echo 28 | command: bash 29 | params: "-c \"for w in {1..4}; do echo ${line^^}; sleep 1; done\"" 30 | ... 31 | -------------------------------------------------------------------------------- /receptorctl/tests/mesh-definitions/access_control/node2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: node2 4 | 5 | - log-level: debug 6 | 7 | - tcp-peer: 8 | address: localhost:12111 9 | 10 | - tcp-listener: 11 | port: 12121 12 | 13 | - control-service: 14 | filename: /tmp/receptorctltest/access_control/node2.sock 15 | ... 16 | -------------------------------------------------------------------------------- /receptorctl/tests/mesh-definitions/access_control/node3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: node3 4 | 5 | - log-level: debug 6 | 7 | - tcp-peer: 8 | address: localhost:12121 9 | 10 | - control-service: 11 | filename: /tmp/receptorctltest/access_control/node3.sock 12 | ... 13 | -------------------------------------------------------------------------------- /receptorctl/tests/mesh-definitions/mesh1/node1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: node1 4 | 5 | - tcp-listener: 6 | port: 11111 7 | 8 | - control-service: 9 | filename: /tmp/receptorctltest/mesh1/node1.sock 10 | 11 | - tcp-server: 12 | port: 11112 13 | remotenode: localhost 14 | remoteservice: control 15 | 16 | - tls-server: 17 | name: tlsserver 18 | key: /tmp/receptorctltest/mesh1/server.key 19 | cert: /tmp/receptorctltest/mesh1/server.crt 20 | requireclientcert: true 21 | clientcas: /tmp/receptorctltest/mesh1/ca.crt 22 | 23 | - control-service: 24 | service: ctltls 25 | tcplisten: 11113 26 | tcptls: tlsserver 27 | ... 28 | -------------------------------------------------------------------------------- /receptorctl/tests/mesh-definitions/mesh1/node2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: node2 4 | 5 | - tcp-peer: 6 | address: localhost:11111 7 | 8 | - tcp-listener: 9 | port: 11121 10 | 11 | - control-service: 12 | filename: /tmp/receptorctltest/mesh1/node2.sock 13 | ... 14 | -------------------------------------------------------------------------------- /receptorctl/tests/mesh-definitions/mesh1/node3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: node3 4 | 5 | - tcp-peer: 6 | address: localhost:11121 7 | 8 | - control-service: 9 | filename: /tmp/receptorctltest/mesh1/node3.sock 10 | 11 | - work-command: 12 | worktype: sleep 13 | command: bash 14 | params: "-c \"read N_ITER; for i in `seq 1 $N_ITER`; do echo $((${N_ITER}-${i}+1)) 'remaining'; sleep 1; done\"" 15 | allowruntimeparams: true 16 | 17 | - work-command: 18 | workType: echo-uppercase 19 | command: bash 20 | params: "-c \"read PAYLOAD; echo ${PAYLOAD^^}\"" 21 | ... 22 | -------------------------------------------------------------------------------- /receptorctl/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from receptorctl import cli as commands 2 | 3 | # The goal is to write tests following the click documentation: 4 | # https://click.palletsprojects.com/en/8.0.x/testing/ 5 | 6 | import pytest 7 | 8 | 9 | @pytest.mark.usefixtures("receptor_mesh_mesh1") 10 | class TestCLI: 11 | def test_cli_cmd_status(self, invoke_as_json): 12 | result, json_output = invoke_as_json(commands.status, []) 13 | assert result.exit_code == 0 14 | assert set( 15 | [ 16 | "Advertisements", 17 | "Connections", 18 | "KnownConnectionCosts", 19 | "NodeID", 20 | "RoutingTable", 21 | "SystemCPUCount", 22 | "SystemMemoryMiB", 23 | "Version", 24 | ] 25 | ) == set(json_output.keys()), "The command returned unexpected keys from json output" 26 | -------------------------------------------------------------------------------- /receptorctl/tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.usefixtures("receptor_mesh_mesh1") 5 | class TestReceptorCtlConnection: 6 | def test_connect_to_service(self, default_receptor_controller_unix): 7 | node1_controller = default_receptor_controller_unix 8 | node1_controller.connect_to_service("node2", "control", "") 9 | node1_controller.handshake() 10 | status = node1_controller.simple_command("status") 11 | node1_controller.close() 12 | assert status["NodeID"] == "node2" 13 | 14 | def test_simple_command(self, default_receptor_controller_unix): 15 | node1_controller = default_receptor_controller_unix 16 | status = node1_controller.simple_command("status") 17 | node1_controller.close() 18 | assert not ( 19 | set( 20 | [ 21 | "Advertisements", 22 | "Connections", 23 | "KnownConnectionCosts", 24 | "NodeID", 25 | "RoutingTable", 26 | ] 27 | ) 28 | - status.keys() 29 | ) 30 | 31 | def test_simple_command_fail(self, default_receptor_controller_unix): 32 | node1_controller = default_receptor_controller_unix 33 | with pytest.raises(RuntimeError): 34 | node1_controller.simple_command("doesnotexist") 35 | node1_controller.close() 36 | 37 | def test_tcp_control_service(self, default_receptor_controller_tcp): 38 | node1_controller = default_receptor_controller_tcp 39 | status = node1_controller.simple_command("status") 40 | node1_controller.close() 41 | assert not ( 42 | set( 43 | [ 44 | "Advertisements", 45 | "Connections", 46 | "KnownConnectionCosts", 47 | "NodeID", 48 | "RoutingTable", 49 | ] 50 | ) 51 | - status.keys() 52 | ) 53 | 54 | def test_tcp_control_service_tls(self, default_receptor_controller_tcp_tls): 55 | node1_controller = default_receptor_controller_tcp_tls 56 | status = node1_controller.simple_command("status") 57 | node1_controller.close() 58 | assert not ( 59 | set( 60 | [ 61 | "Advertisements", 62 | "Connections", 63 | "KnownConnectionCosts", 64 | "NodeID", 65 | "RoutingTable", 66 | ] 67 | ) 68 | - status.keys() 69 | ) 70 | -------------------------------------------------------------------------------- /receptorctl/tests/test_mesh.py: -------------------------------------------------------------------------------- 1 | from receptorctl import cli as commands 2 | 3 | # The goal is to write tests following the click documentation: 4 | # https://click.palletsprojects.com/en/8.0.x/testing/ 5 | 6 | import pytest 7 | import time 8 | 9 | 10 | @pytest.mark.usefixtures("receptor_mesh_access_control") 11 | class TestMeshFirewall: 12 | def test_work_unsigned(self, invoke, receptor_nodes): 13 | """Run a unsigned work-command 14 | 15 | Steps: 16 | 1. Create node1 with a unsigned work-command 17 | 2. Create node2 18 | 3. Run from node2 a unsigned work-command to node1 19 | 4. Expect to be accepted 20 | """ 21 | 22 | # Run an unsigned command 23 | result = invoke( 24 | commands.work, 25 | "submit unsigned-echo --node node1 --no-payload".split(), 26 | ) 27 | work_unit_id = result.stdout.split("Unit ID: ")[-1].replace("\n", "") 28 | 29 | time.sleep(5) 30 | assert result.exit_code == 0 31 | 32 | # Release unsigned work 33 | result = invoke(commands.work, f"release {work_unit_id}".split()) 34 | time.sleep(5) 35 | 36 | assert result.exit_code == 0 37 | 38 | # DISABLE UNTIL THE FIX BEING IMPLEMENTED 39 | # 40 | # def test_work_signed_expect_block(self, invoke, receptor_nodes): 41 | # """Run a signed work-command without the right key 42 | # and expect to be blocked. 43 | 44 | # Steps: 45 | # 1. Create node1 with a signed work-command 46 | # 2. Create node2 47 | # 3. Run from node2 a signed work-command to node1 48 | # 4. Expect to be blocked 49 | # """ 50 | # # Run an unsigned command 51 | # result = invoke( 52 | # commands.work, "submit signed-echo --node node1 --no-payload".split() 53 | # ) 54 | # work_unit_id = result.stdout.split("Unit ID: ")[-1].replace("\n", "") 55 | 56 | # time.sleep(5) 57 | # assert work_unit_id, "Work unit ID should not be empty" 58 | # assert result.exit_code != 0, "Work signed run should fail, but it worked" 59 | 60 | # # Release unsigned work 61 | # result = invoke(commands.work, f"release {work_unit_id}".split()) 62 | # assert result.exit_code == 0, "Work release failed" 63 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | receptorctl/setup.cfg -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts-output/ 2 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | CONTAINER_IMAGE_TAG_builder=ansible/receptor:builder 2 | CONTAINER_IMAGE_TAG_dev=ansible/receptor:dev 3 | 4 | CONTAINER_RUN:=$$(./receptor-tester.sh container-runtime) 5 | 6 | # Tests 7 | GO_FUNCTIONAL_TESTS_DIRS:=$$(find ./functional -type d) 8 | 9 | # Generate artifacts 10 | artifacts: container-image-builder 11 | mkdir -p $(PWD)/artifacts-output 12 | $(CONTAINER_RUN) run --rm \ 13 | -v $(PWD)/../:/source/ \ 14 | -v $(PWD)/artifacts-output/:/artifacts/:rw \ 15 | -v receptor_go_root_cache:/root/go:rw \ 16 | -e OUTPUT_UID=$$(id -u) \ 17 | $(CONTAINER_IMAGE_TAG_builder) \ 18 | /build-artifacts.sh 19 | 20 | artifacts-no-cache: container-image-builder 21 | mkdir -p $(PWD)/artifacts 22 | $(CONTAINER_RUN) run --rm \ 23 | -v $(PWD)/../:/source/:ro \ 24 | -v $(PWD)/artifacts/:/artifacts/:rw \ 25 | $(CONTAINER_IMAGE_TAG_builder) \ 26 | /build-artifacts.sh 27 | 28 | # Container environment 29 | container-image-dev: 30 | $(CONTAINER_RUN) build \ 31 | -t $(CONTAINER_IMAGE_TAG_dev) \ 32 | -f ./environments/container-dev/Containerfile \ 33 | ./../ 34 | 35 | container-image-builder: 36 | $(CONTAINER_RUN) build \ 37 | -t $(CONTAINER_IMAGE_TAG_builder) \ 38 | -f ./environments/container-builder/Containerfile \ 39 | ./../ 40 | 41 | container-image-dev-shell: 42 | $(CONTAINER_RUN) run -it --rm \ 43 | $(CONTAINER_IMAGE_TAG_dev) bash 44 | 45 | container-image-builder-shell: 46 | $(CONTAINER_RUN) run -it --rm \ 47 | -v $(PWD)/../:/source/:ro \ 48 | -v receptor_go_root_cache:/root/go:rw \ 49 | $(CONTAINER_IMAGE_TAG_builder) bash -c \ 50 | 'cp -r /source /build; cd /build; bash' 51 | 52 | # VM (vagrant) 53 | vm: vm-create 54 | 55 | vm-create: 56 | cd environments/vagrant && \ 57 | vagrant up 58 | 59 | shell: vm-shell 60 | vm-shell: 61 | @cd environments/vagrant && \ 62 | vagrant ssh -c "cd /vagrant/tests && /bin/bash" 63 | 64 | vm-destroy: 65 | cd environments/vagrant && \ 66 | vagrant destroy 67 | 68 | vm-provision: 69 | cd environments/vagrant && \ 70 | vagrant provision 71 | 72 | # Colored output 73 | ccblue=$(echo -e "\033[0;31m") 74 | ccred=$(echo -e "\033[0;31m") 75 | ccyellow=$(echo -e "\033[0;33m") 76 | ccend=$(echo -e "\033[0m") 77 | 78 | # Other configs 79 | SHELL:=/bin/bash 80 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Receptor tests 2 | 3 | All test files and test tools can be found on this directory. 4 | 5 | ```java 6 | . 7 | |-- artifacts : "receptor-tester.sh builds output" 8 | |-- environments : "Controlled environments for testing" 9 | | |-- container : "Containerfile recipe" 10 | | `-- vagrant : "VM recipe" 11 | `-- functional : "Functional test files" 12 | |-- cli 13 | |-- lib 14 | `-- mesh 15 | ``` 16 | 17 | ## Tests 18 | 19 | 1. Functional tests and correspondent docs can be found here: [./functional/README.md](./functional/README.md) 20 | 21 | ## Tools 22 | 23 | There are two parts of Receptor tools. Each one is used for different scenarios. 24 | 25 | Requirements: 26 | - podman 27 | - make 28 | 29 | ### receptor-tester 30 | 31 | All features will be showed on `help` argument: 32 | 33 | ```bash 34 | ./receptor-tester.sh help 35 | 36 | # Command list: 37 | # list-dirs - list all available tests directories 38 | # list-files - list all available tests files 39 | # run - run a specific test 40 | # run-all - run all tests. Returns 0 if pass 41 | # help - Displays this help text 42 | ``` 43 | 44 | List all available tests: 45 | 46 | ```bash 47 | # list all available tests directories 48 | ./receptor-tester.sh list-dirs 49 | # ./functional/mesh 50 | # ./functional/cli 51 | # ./functional/lib/utils 52 | 53 | # List all available tests files 54 | ./receptor-tester.sh list-files 55 | # ./functional/mesh/mesh_test.go 56 | # ./functional/mesh/work_test.go 57 | # ./functional/mesh/tls_test.go 58 | # ./functional/cli/cli_test.go 59 | # ./functional/lib/utils/utils_test.go 60 | ``` 61 | 62 | Run tests: 63 | 64 | ```bash 65 | # run a specific test 66 | ./receptor-tester.sh run ./functional/cli/cli_test.go 67 | # run all tests 68 | ./receptor-tester.sh run-all 69 | ``` 70 | 71 | ### Makefile 72 | 73 | Build artifacts (receptor and receptorctl) based on latest source code. 74 | The container recipe used can be found at `environments/container`. 75 | 76 | ```bash 77 | # Build artifacts 78 | make artifacts 79 | ``` 80 | 81 | Container commands can be used in conjunction with `make artifacts` or isolated to run ad-hoc commands inside a controlled environment. 82 | 83 | ```bash 84 | # Rebuild container image used 85 | # by `make artifacts` 86 | make container-image # OR 87 | make container-image-base 88 | 89 | # Jump into a container created from 90 | # the same container image used by 91 | # `make artifacts` 92 | make container-shell-base 93 | ``` 94 | 95 | If you're using Windows or macOS, the following commands can be useful to create a virtual machine and play aroung with containers inside there. 96 | 97 | Requirements: 98 | - vagrant 99 | 100 | ```bash 101 | # Creates a Vagrant VM 102 | make vm # OR 103 | make vm-create 104 | 105 | # SSH into VM 106 | make vm-shell 107 | 108 | # Destroy VM 109 | make vm-destroy 110 | 111 | # Reapply Ansible playbook into VM 112 | make vm-provision 113 | ``` 114 | -------------------------------------------------------------------------------- /tests/environments/container-builder/Containerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/centos/centos:stream9 2 | 3 | # Python options = [3.9, 3.11, 3.12] 4 | ARG PYTHON_VERSION=3.12 5 | 6 | ENV PATH=${PATH}:/usr/local/go/bin 7 | 8 | RUN set -x \ 9 | && echo 'fastestmirror=True' >> /etc/dnf/dnf.conf \ 10 | && dnf update -y \ 11 | # Receptor build tools 12 | && dnf install -y \ 13 | findutils \ 14 | git \ 15 | iproute \ 16 | make \ 17 | openssl \ 18 | wget \ 19 | # Install specific python version 20 | && dnf install -y \ 21 | python${PYTHON_VERSION} \ 22 | python${PYTHON_VERSION}-pip \ 23 | && pip${PYTHON_VERSION} install virtualenv \ 24 | # Install specific golang version 25 | && dnf install -y \ 26 | golang \ 27 | && dnf clean all 28 | 29 | # --- ALL IMAGE MUST BE THE SAME UNTIL NOW --- 30 | 31 | # Caching dependencies 32 | WORKDIR /dependencies 33 | ADD ./go.mod \ 34 | ./go.sum \ 35 | ../receptorctl/requirements/requirements.txt ./ 36 | RUN set -x \ 37 | # Go 38 | && go get -u golang.org/x/lint/golint \ 39 | && go get -d -v ./... \ 40 | # Python 41 | && virtualenv -p python${PYTHON_VERSION} /opt/venv \ 42 | && source /opt/venv/bin/activate \ 43 | && pip${PYTHON_VERSION} install \ 44 | --upgrade \ 45 | -r requirements.txt 46 | 47 | ADD ./tests/environments/container-builder/build-artifacts.sh / 48 | 49 | RUN chmod +x /build-artifacts.sh 50 | 51 | WORKDIR / 52 | -------------------------------------------------------------------------------- /tests/environments/container-builder/README.md: -------------------------------------------------------------------------------- 1 | # Receptor base container image 2 | 3 | This container image is used as a controlled environment to build and run Receptor for CI and automate tests. 4 | -------------------------------------------------------------------------------- /tests/environments/container-builder/build-artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | SOURCE_DIR=/source 6 | BUILD_DIR=/build 7 | ARTIFACTS_DIR=/artifacts 8 | OUTPUT_UID=${OUTPUT_UID:-1000} 9 | OUTPUT_GID=${OUTPUT_GID:-$OUTPUT_UID} 10 | 11 | # Copy all content 12 | cp -r ${SOURCE_DIR}/ ${BUILD_DIR} 13 | 14 | # Build receptor 15 | cd ${BUILD_DIR} 16 | make clean # prevent fail on dev environment 17 | make receptor 18 | 19 | # Build receptorctl 20 | cd ${BUILD_DIR}/receptorctl 21 | source /opt/venv/bin/activate # uses the currect Python version 22 | python -m build 23 | 24 | # Move packages 25 | mkdir -p ${ARTIFACTS_DIR}/dist 26 | rm -f ${ARTIFACTS_DIR}/receptor 27 | cp ${BUILD_DIR}/receptor ${ARTIFACTS_DIR}/receptor 28 | rm -rf ${ARTIFACTS_DIR}/dist 29 | cp -r ${BUILD_DIR}/receptorctl/dist/ ${ARTIFACTS_DIR}/dist 30 | 31 | # Fix permissions 32 | chown -R ${OUTPUT_UID}:${OUTPUT_GID} ${ARTIFACTS_DIR} 33 | -------------------------------------------------------------------------------- /tests/environments/container-dev/Containerfile: -------------------------------------------------------------------------------- 1 | FROM centos:8 2 | 3 | # Python options = [3.8, 3.9] 4 | ARG PYTHON_VERSION=3.8 5 | 6 | ENV PATH=${PATH}:/usr/local/go/bin 7 | 8 | RUN set -x \ 9 | && echo 'fastestmirror=True' >> /etc/dnf/dnf.conf \ 10 | && dnf update -y \ 11 | # Receptor build tools 12 | && dnf install -y \ 13 | git wget make iproute openssl findutils virtualenv \ 14 | # Install specific python version 15 | && export PYTHON_PKG_NAME=python$(echo ${PYTHON_VERSION} | sed 's/\.//g') \ 16 | && dnf module install -y ${PYTHON_PKG_NAME} \ 17 | && alternatives --set python /usr/bin/python${PYTHON_VERSION} \ 18 | # Install specific golang version 19 | && dnf install -y golang \ 20 | && dnf clean all 21 | 22 | # --- ALL IMAGE MUST BE THE SAME UNTIL NOW --- 23 | 24 | # Build steps 25 | 26 | WORKDIR /dependencies 27 | 28 | ADD . . 29 | 30 | RUN set -x \ 31 | # Go dependencies 32 | && go get -u golang.org/x/lint/golint \ 33 | && go get -d -v ./... \ 34 | # Python dependencies 35 | && virtualenv -p python${PYTHON_VERSION} /opt/venv \ 36 | && source /opt/venv/bin/activate \ 37 | && cd receptorctl \ 38 | && pip3 install -r requirements.txt \ 39 | && pip3 install --upgrade -r build-requirements.txt \ 40 | && cd - \ 41 | # Build receptor 42 | && make receptor \ 43 | # Build receptorctl 44 | && cd receptorctl \ 45 | && python -m build 46 | 47 | # Final image 48 | FROM centos:8 49 | 50 | ARG PYTHON_VERSION=3.8 51 | 52 | ENV PATH=${PATH}:/opt/venv/bin/ 53 | ENV RECEPTORCTL_SOCKET=/tmp/receptor.sock 54 | 55 | RUN set -x \ 56 | && echo 'fastestmirror=True' >> /etc/dnf/dnf.conf \ 57 | && dnf update -y \ 58 | # OS dependencies 59 | && dnf install -y \ 60 | virtualenv podman \ 61 | # Install specific python version 62 | && export PYTHON_PKG_NAME=python$(echo ${PYTHON_VERSION} | sed 's/\.//g') \ 63 | && dnf module install -y ${PYTHON_PKG_NAME} \ 64 | && alternatives --set python /usr/bin/python${PYTHON_VERSION} \ 65 | && dnf clean all \ 66 | # Python virtualenv 67 | && virtualenv -p python${PYTHON_VERSION} /opt/venv \ 68 | && source /opt/venv/bin/activate \ 69 | && pip install ansible-runner 70 | 71 | COPY --from=0 /dependencies/receptor /usr/local/bin/receptor 72 | COPY --from=0 /dependencies/receptorctl/dist/* /tmp/receptorctl_dist/ 73 | ADD ./tests/environments/container-dev/receptor.conf /etc/receptor.conf 74 | 75 | # Install 76 | RUN set -x \ 77 | && source /opt/venv/bin/activate \ 78 | && pip install /tmp/receptorctl_dist/receptorctl-*.whl 79 | 80 | CMD ["/usr/local/bin/receptor", "--config", "/etc/receptor.conf"] 81 | -------------------------------------------------------------------------------- /tests/environments/container-dev/receptor.conf: -------------------------------------------------------------------------------- 1 | # Default Receptor config file 2 | 3 | - log-level: debug 4 | 5 | # Receptor node name 6 | - node: 7 | id: worker 8 | 9 | # File socket 10 | - control-service: 11 | service: control 12 | filename: /tmp/receptor.sock 13 | 14 | - tcp-listener: 15 | port: 2345 16 | 17 | - work-command: 18 | workType: echocafe 19 | command: bash 20 | params: "-c \"echo cafe\"" 21 | -------------------------------------------------------------------------------- /tests/functional/mesh/firewall_test.go: -------------------------------------------------------------------------------- 1 | package mesh 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/ansible/receptor/pkg/netceptor" 9 | _ "github.com/fortytw2/leaktest" 10 | ) 11 | 12 | func TestFirewall(t *testing.T) { 13 | t.Parallel() 14 | 15 | for _, proto := range []string{"tcp", "ws", "udp"} { 16 | proto := proto 17 | 18 | t.Run(proto, func(t *testing.T) { 19 | t.Parallel() 20 | 21 | m := NewLibMesh() 22 | 23 | defer func() { 24 | t.Log(m.LogWriter.String()) 25 | }() 26 | 27 | node1 := m.NewLibNode("node1") 28 | node2 := m.NewLibNode("node2") 29 | 30 | node1.Connections = []Connection{ 31 | {RemoteNode: node2, Protocol: proto}, 32 | } 33 | m.GetNodes()[node1.GetID()] = node1 34 | 35 | node2.ListenerCfgs = map[listenerName]ListenerCfg{ 36 | listenerName(proto): newListenerCfg(proto, "", 1, nil), 37 | } 38 | m.GetNodes()[node2.GetID()] = node2 39 | node2.Config.FirewallRules = []netceptor.FirewallRuleData{ 40 | {"Action": "accept", "FromNode": "node2"}, 41 | {"Action": "reject", "ToNode": "node3"}, 42 | {"Action": "reject", "ToNode": "node1"}, 43 | } 44 | 45 | node3 := m.NewLibNode("node3") 46 | node3.Connections = []Connection{ 47 | {RemoteNode: node2, Protocol: proto}, 48 | } 49 | m.GetNodes()[node3.GetID()] = node3 50 | 51 | defer m.WaitForShutdown() 52 | defer m.Destroy() 53 | err := m.Start(t.Name()) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | ctx1, cancel1 := context.WithTimeout(context.Background(), 20*time.Second) 59 | defer cancel1() 60 | 61 | err = m.WaitForReady(ctx1) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | // Test that node1 and node3 can ping node2 67 | for _, nodeSender := range []*LibNode{m.GetNodes()["node1"], m.GetNodes()["node3"]} { 68 | controller := NewReceptorControl() 69 | err = controller.Connect(nodeSender.GetControlSocket()) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | response, err := controller.Ping("node2") 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | t.Logf("%v", response) 78 | controller.Close() 79 | } 80 | 81 | // Test that node1 and node3 cannot ping each other 82 | for strSender, strReceiver := range map[string]string{"node1": "node3", "node3": "node1"} { 83 | controller := NewReceptorControl() 84 | err = controller.Connect(m.GetNodes()[strSender].GetControlSocket()) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | _, err := controller.Ping(strReceiver) 89 | if err == nil { 90 | t.Error("firewall failed to block ping") 91 | } else if err.Error() != "blocked by firewall" { 92 | t.Errorf("got wrong error: %s", err) 93 | } 94 | 95 | t.Logf("%v", err) 96 | controller.Close() 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/functional/mesh/fixtures.go: -------------------------------------------------------------------------------- 1 | package mesh 2 | 3 | import "github.com/ansible/receptor/pkg/workceptor" 4 | 5 | var workPlugins = []workPlugin{"command", "kube"} 6 | 7 | var workTestConfigs = map[workPlugin]map[workType]workceptor.WorkerConfig{ 8 | "kube": { 9 | "echosleepshort": workceptor.KubeWorkerCfg{ 10 | WorkType: "echosleepshort", 11 | AuthMethod: "kubeconfig", 12 | Namespace: "default", 13 | Image: "alpine", 14 | Command: "sh -c 'for i in `seq 1 5`; do echo $i;done'", 15 | }, 16 | "echosleeplong": workceptor.KubeWorkerCfg{ 17 | WorkType: "echosleeplong", 18 | AuthMethod: "kubeconfig", 19 | Namespace: "default", 20 | Image: "alpine", 21 | Command: "sh -c 'for i in `seq 1 5`; do echo $i; sleep 3;done'", 22 | }, 23 | "echosleeplong50": workceptor.KubeWorkerCfg{ 24 | WorkType: "echosleeplong50", 25 | AuthMethod: "kubeconfig", 26 | Namespace: "default", 27 | Image: "alpine", 28 | Command: "sh -c 'for i in `seq 1 50`; do echo $i; sleep 4;done'", 29 | }, 30 | }, 31 | "command": { 32 | "echosleepshort": workceptor.CommandWorkerCfg{ 33 | WorkType: "echosleepshort", 34 | Command: "bash", 35 | Params: "-c 'for i in {1..5}; do echo $i;done'", 36 | }, 37 | "echosleeplong": workceptor.CommandWorkerCfg{ 38 | WorkType: "echosleeplong", 39 | Command: "bash", 40 | Params: "-c 'for i in {1..5}; do echo $i; sleep 3;done'", 41 | }, 42 | "echosleeplong50": workceptor.CommandWorkerCfg{ 43 | WorkType: "echosleeplong50", 44 | Command: "base", 45 | Params: "-c 'for i in {1..50}; do echo $i; sleep 4;done'", 46 | }, 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /tests/functional/mesh/testdata/echo-pod.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | spec: 5 | containers: 6 | - name: worker 7 | image: centos:8 8 | command: 9 | - bash 10 | args: 11 | - -c 12 | - for i in {1..5}; do echo $i;done 13 | -------------------------------------------------------------------------------- /tests/functional/mesh/work_utils.go: -------------------------------------------------------------------------------- 1 | package mesh 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | "time" 10 | 11 | "github.com/ansible/receptor/tests/utils" 12 | ) 13 | 14 | func workSetup(workPluginName workPlugin, t *testing.T) (map[string]*ReceptorControl, *LibMesh, []byte) { 15 | checkSkipKube(t) 16 | 17 | m := workTestMesh(workPluginName) 18 | 19 | err := m.Start(t.Name()) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | ctx1, cancel1 := context.WithTimeout(context.Background(), 120*time.Second) 25 | defer cancel1() 26 | 27 | err = m.WaitForReady(ctx1) 28 | if err != nil { 29 | t.Fatal(err, m.DataDir) 30 | } 31 | 32 | nodes := m.GetNodes() 33 | controllers := make(map[string]*ReceptorControl) 34 | for k := range nodes { 35 | controller := NewReceptorControl() 36 | err = controller.Connect(nodes[k].GetControlSocket()) 37 | if err != nil { 38 | t.Fatal(err, m.DataDir) 39 | } 40 | controllers[k] = controller 41 | } 42 | 43 | return controllers, m, []byte("1\n2\n3\n4\n5\n") 44 | } 45 | 46 | func assertFilesReleased(ctx context.Context, nodeDir, nodeID, unitID string) error { 47 | workPath := filepath.Join(nodeDir, "datadir", nodeID, unitID) 48 | check := func() bool { 49 | _, err := os.Stat(workPath) 50 | 51 | return os.IsNotExist(err) 52 | } 53 | if !utils.CheckUntilTimeout(ctx, 5*time.Second, check) { 54 | return fmt.Errorf("unitID %s on %s did not release", unitID, nodeID) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func assertStdoutFizeSize(ctx context.Context, dataDir, nodeID, unitID string, waitUntilSize int) error { 61 | stdoutFilename := filepath.Join(dataDir, nodeID, unitID, "stdout") 62 | check := func() bool { 63 | _, err := os.Stat(stdoutFilename) 64 | if os.IsNotExist(err) { 65 | return false 66 | } 67 | fstat, _ := os.Stat(stdoutFilename) 68 | 69 | return int(fstat.Size()) >= waitUntilSize 70 | } 71 | if !utils.CheckUntilTimeout(ctx, 3000*time.Millisecond, check) { 72 | return fmt.Errorf("file size not correct for %s", stdoutFilename) 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func checkSkipKube(t *testing.T) { 79 | if skip := os.Getenv("SKIP_KUBE"); skip == "1" { 80 | t.Skip("Kubernetes tests are set to skip, unset SKIP_KUBE to run them") 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/utils/logs.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | // testLogWriter provides a threadsafe way of reading and writing logs to a buffer. 9 | type TestLogWriter struct { 10 | Lock *sync.RWMutex 11 | Buffer *bytes.Buffer 12 | } 13 | 14 | func (lw *TestLogWriter) Write(p []byte) (n int, err error) { 15 | lw.Lock.Lock() 16 | defer lw.Lock.Unlock() 17 | 18 | n, err = lw.Buffer.Write(p) 19 | if err != nil { 20 | return 0, err 21 | } 22 | 23 | return n, nil 24 | } 25 | 26 | func (lw *TestLogWriter) String() string { 27 | lw.Lock.RLock() 28 | defer lw.Lock.RUnlock() 29 | 30 | return lw.Buffer.String() 31 | } 32 | 33 | func NewTestLogWriter() *TestLogWriter { 34 | return &TestLogWriter{ 35 | Lock: &sync.RWMutex{}, 36 | Buffer: &bytes.Buffer{}, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Receptor docs 2 | 3 | ## Examples 4 | 5 | - 1. [Simple receptor network](./examples/simple-network) 6 | -------------------------------------------------------------------------------- /tools/ansible/stage.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | vars: 5 | headers: 6 | Accept: "application/vnd.github+json" 7 | Authorization: "Bearer {{ github_token }}" 8 | X-GitHub-Api-Version: "2022-11-28" 9 | tasks: 10 | # The next 2 tasks can maybe get deleted if GitHub ever fixes this: 11 | # https://github.com/orgs/community/discussions/4924 12 | - name: Create tag object 13 | uri: 14 | url: "https://api.github.com/repos/{{ repo }}/git/tags" 15 | method: "POST" 16 | headers: "{{ headers }}" 17 | body: 18 | tag: "v{{ version }}" 19 | message: "Release for v{{ version }}" 20 | object: "{{ target_commitish }}" 21 | type: "commit" 22 | tagger: 23 | name: "{{ tagger_name }}" 24 | email: "{{ tagger_email }}" 25 | body_format: "json" 26 | status_code: 27 | - 200 28 | - 201 29 | register: tag 30 | 31 | - name: Create tag reference 32 | uri: 33 | url: "https://api.github.com/repos/{{ repo }}/git/refs" 34 | method: "POST" 35 | headers: "{{ headers }}" 36 | body: 37 | ref: "refs/tags/{{ tag.json.tag }}" 38 | sha: "{{ tag.json.sha }}" 39 | body_format: "json" 40 | status_code: 41 | - 200 42 | - 201 43 | 44 | - name: Publish draft Release 45 | uri: 46 | url: "https://api.github.com/repos/{{ repo }}/releases" 47 | method: "POST" 48 | headers: "{{ headers }}" 49 | body: 50 | name: "v{{ version }}" 51 | tag_name: "v{{ version }}" 52 | target_commitish: "{{ target_commitish }}" 53 | draft: true 54 | body_format: "json" 55 | status_code: 56 | - 200 57 | - 201 58 | -------------------------------------------------------------------------------- /tools/examples/simple-network/.gitignore: -------------------------------------------------------------------------------- 1 | !build/* 2 | -------------------------------------------------------------------------------- /tools/examples/simple-network/README.md: -------------------------------------------------------------------------------- 1 | # receptor - basic example 2 | 3 | This example creates two receptor nodes, called `arceus` and `celebi`. 4 | 5 | ## Diagram 6 | 7 | The `ctl.sh` script (equivalent to `receptorctl`) sends the message to Celebi through an unix domain socket `socks/celebi.sock`, then Celebi forwards that message to Arceus through the docker-compose network. 8 | 9 | ![Receptor simple network diagram](./simple-network-diagram.drawio.png) 10 | 11 | ## Commands 12 | 13 | ```bash 14 | # Build and run 15 | docker-compose up -d 16 | 17 | # Destroy and cleanup 18 | docker-compose down 19 | rm -rf ./socks/ 20 | 21 | # Run commands on receptorctl 22 | ./ctl.sh 23 | 24 | # Examples 25 | ./ctl.sh ping celebi 26 | ./ctl.sh ping arceus 27 | ``` 28 | 29 | ## Config files 30 | 31 | Celebi socket is exposed: 32 | 33 | Full Celebi config file can be found here: [configs/celebi.yaml](configs/celebi.yaml) 34 | 35 | ```yaml 36 | ... 37 | 38 | - control-service: 39 | service: control 40 | filename: /socks/celebi.sock 41 | ``` 42 | 43 | Arceus port is exposed: 44 | 45 | Full Arceus config file can be found here: [configs/arceus.yaml](configs/arceus.yaml) 46 | 47 | ```yaml 48 | ... 49 | 50 | - tcp-listener: 51 | port: 2223 52 | 53 | ... 54 | ``` 55 | -------------------------------------------------------------------------------- /tools/examples/simple-network/build/receptor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/ansible/receptor 2 | 3 | RUN set -x \ 4 | # Set fastest repo 5 | echo 'fastestmirror=1' >> /etc/dnf/dnf.conf \ 6 | # Add debug tools 7 | && yum install -y iputils nano htop vim 8 | -------------------------------------------------------------------------------- /tools/examples/simple-network/build/receptorctl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | WORKDIR /opt 4 | 5 | RUN set -x \ 6 | && apk add php py3-pip git bash \ 7 | && git clone --depth 1 https://github.com/ansible/receptor.git \ 8 | && pip3 install -e ./receptor/receptorctl 9 | 10 | WORKDIR /app 11 | 12 | CMD ["php", "-S", "0.0.0.0:8080"] 13 | -------------------------------------------------------------------------------- /tools/examples/simple-network/configs/arceus.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - node: 3 | id: arceus 4 | 5 | - tcp-listener: 6 | port: 2223 7 | 8 | - work-command: 9 | worktype: echo 10 | command: echo 11 | params: arceus 12 | allowruntimeparams: true 13 | -------------------------------------------------------------------------------- /tools/examples/simple-network/configs/celebi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - log-level: 3 | level: debug 4 | 5 | - node: 6 | id: celebi 7 | 8 | - tcp-peer: 9 | address: arceus:2223 10 | 11 | - control-service: 12 | service: control 13 | filename: /socks/celebi.sock 14 | -------------------------------------------------------------------------------- /tools/examples/simple-network/ctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker exec -it simple-network_receptorctl_1 receptorctl $@ 4 | -------------------------------------------------------------------------------- /tools/examples/simple-network/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | arceus: 4 | build: ./build/receptor/ 5 | command: receptor -c /configs/arceus.yaml 6 | volumes: 7 | - ./configs/:/configs 8 | networks: 9 | - receptor-network 10 | 11 | celebi: 12 | build: ./build/receptor/ 13 | command: receptor -c /configs/celebi.yaml 14 | volumes: 15 | - ./configs/:/configs 16 | - ./socks/:/socks 17 | networks: 18 | - receptor-network 19 | 20 | receptorctl: 21 | build: ./build/receptorctl/ 22 | volumes: 23 | - ./socks/:/socks 24 | environment: 25 | RECEPTORCTL_SOCKET: /socks/celebi.sock 26 | networks: 27 | - random 28 | 29 | networks: 30 | receptor-network: {} 31 | random: {} -------------------------------------------------------------------------------- /tools/examples/simple-network/simple-network-diagram.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/receptor/175087548e366d375f0dc8d97b8c1c5217393dd5/tools/examples/simple-network/simple-network-diagram.drawio.png -------------------------------------------------------------------------------- /tools/examples/simple-network/socks/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | --------------------------------------------------------------------------------