Showing preview only (528K chars total). Download the full file or copy to clipboard to get everything.
Repository: tinkerbell/boots
Branch: main
Commit: 5f31a4ab8025
Files: 104
Total size: 497.8 KB
Directory structure:
gitextract_aau8ld8h/
├── .dockerignore
├── .github/
│ ├── CODEOWNERS
│ ├── codecov.yml
│ ├── dependabot.yml
│ ├── mergify.yml
│ ├── settings.yml
│ └── workflows/
│ ├── ci-checks.sh
│ ├── ci.yaml
│ └── tags.yaml
├── .gitignore
├── .golangci.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── RELEASING.md
├── Tiltfile
├── cmd/
│ └── smee/
│ ├── backend.go
│ ├── flag.go
│ ├── flag_test.go
│ └── main.go
├── contrib/
│ └── tag-release.sh
├── docker-compose.yml
├── docs/
│ ├── Backend-File.md
│ ├── Code-Structure.md
│ ├── DCO.md
│ ├── DESIGN.md
│ ├── DESIGNPHILOSOPHY.md
│ ├── DHCP.md
│ ├── Design-Philosophy.md
│ ├── ISO-Static-IPAM.md
│ ├── images/
│ │ └── BYO_DHCP.uml
│ └── manifests/
│ ├── README.md
│ ├── k3d.md
│ ├── kind.md
│ ├── kubernetes.md
│ └── tilt.md
├── go.mod
├── go.sum
├── internal/
│ ├── backend/
│ │ ├── file/
│ │ │ ├── file.go
│ │ │ ├── file_test.go
│ │ │ └── testdata/
│ │ │ └── example.yaml
│ │ ├── kube/
│ │ │ ├── error.go
│ │ │ ├── index.go
│ │ │ ├── index_test.go
│ │ │ ├── kube.go
│ │ │ └── kube_test.go
│ │ └── noop/
│ │ ├── noop.go
│ │ └── noop_test.go
│ ├── dhcp/
│ │ ├── data/
│ │ │ ├── data.go
│ │ │ └── data_test.go
│ │ ├── dhcp.go
│ │ ├── dhcp_test.go
│ │ ├── handler/
│ │ │ ├── handler.go
│ │ │ ├── proxy/
│ │ │ │ └── proxy.go
│ │ │ └── reservation/
│ │ │ ├── handler.go
│ │ │ ├── handler_test.go
│ │ │ ├── noop.go
│ │ │ ├── noop_test.go
│ │ │ ├── option.go
│ │ │ ├── option_test.go
│ │ │ └── reservation.go
│ │ ├── otel/
│ │ │ ├── otel.go
│ │ │ └── otel_test.go
│ │ └── server/
│ │ ├── dhcp.go
│ │ └── dhcp_test.go
│ ├── ipxe/
│ │ ├── http/
│ │ │ ├── http.go
│ │ │ ├── middleware.go
│ │ │ ├── xff.go
│ │ │ └── xff_test.go
│ │ └── script/
│ │ ├── auto.go
│ │ ├── auto_test.go
│ │ ├── custom.go
│ │ ├── hook.go
│ │ ├── ipxe.go
│ │ ├── ipxe_test.go
│ │ └── static.go
│ ├── iso/
│ │ ├── internal/
│ │ │ ├── LICENSE
│ │ │ ├── acsii.go
│ │ │ ├── acsii_test.go
│ │ │ ├── context.go
│ │ │ ├── reverseproxy.go
│ │ │ └── reverseproxy_test.go
│ │ ├── ipam.go
│ │ ├── ipam_test.go
│ │ ├── iso.go
│ │ ├── iso_test.go
│ │ └── testdata/
│ │ └── output.iso
│ ├── metric/
│ │ └── metric.go
│ ├── otel/
│ │ └── otel.go
│ └── syslog/
│ ├── facility_string.go
│ ├── message.go
│ ├── receiver.go
│ └── severity_string.go
├── lint.mk
├── rules.mk
└── test/
├── Dockerfile
├── busybox-udhcpc-script.sh
├── extract-traceparent-from-opt43.sh
├── hardware.yaml
├── otel-collector.yaml
├── start-smee.sh
└── test-smee.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
*
!cmd/smee/smee-*-*
!cmd/smee/smee
!test/
================================================
FILE: .github/CODEOWNERS
================================================
/.github/settings.yml @chrisdoherty4 @jacobweinstock
/.github/CODEOWNERS @chrisdoherty4 @jacobweinstock
================================================
FILE: .github/codecov.yml
================================================
---
coverage:
precision: 0 # xx%
round: down # round down
range: 30..40 # red < yellow (this range) < green
status:
project:
default:
target: auto # automatically calculate coverage target - should increase
threshold: 2% # allow for 2% reduction without failing
patch:
default:
target: auto
changes: false
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "04:39"
timezone: "America/New_York"
reviewers:
- chrisdoherty4
- jacobweinstock
open-pull-requests-limit: 10
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "03:52"
timezone: "America/New_York"
reviewers:
- chrisdoherty4
- jacobweinstock
open-pull-requests-limit: 10
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "thursday"
time: "03:52"
timezone: "America/New_York"
reviewers:
- chrisdoherty4
- jacobweinstock
open-pull-requests-limit: 10
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "04:22"
timezone: "America/New_York"
reviewers:
- chrisdoherty4
- jacobweinstock
open-pull-requests-limit: 10
================================================
FILE: .github/mergify.yml
================================================
queue_rules:
- name: default
queue_conditions:
- base=main
- "#approved-reviews-by>=1"
- "#changes-requested-reviews-by=0"
- "#review-requested=0"
- check-success=DCO
- check-success=validation
- label!=do-not-merge
- label=ready-to-merge
merge_conditions:
# Conditions to get out of the queue (= merged)
- check-success=DCO
- check-success=validation
merge_method: merge
commit_message_template: |
{{ title }} (#{{ number }})
{{ body }}
pull_request_rules:
- name: refactored queue action rule
conditions: []
actions:
queue:
================================================
FILE: .github/settings.yml
================================================
# Collaborators: give specific users access to this repository.
# See https://docs.github.com/en/rest/reference/repos#add-a-repository-collaborator for available options
collaborators:
# Maintainers, should also be added to the .github/CODEOWNERS file as owners of this settings.yml file.
- username: jacobweinstock
permission: maintain
- username: chrisdoherty4
permission: maintain
# Approvers
# Reviewers
# Note: `permission` is only valid on organization-owned repositories.
# The permission to grant the collaborator. Can be one of:
# * `pull` - can pull, but not push to or administer this repository.
# * `push` - can pull and push, but not administer this repository.
# * `admin` - can pull, push and administer this repository.
# * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions.
# * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access.
================================================
FILE: .github/workflows/ci-checks.sh
================================================
#!/usr/bin/env bash
set -eux
failed=0
if [[ -n $(go run golang.org/x/tools/cmd/goimports@latest -d -e -l .) ]]; then
go run golang.org/x/tools/cmd/goimports@latest -w .
failed=1
fi
if ! go mod tidy; then
failed=true
fi
if ! git diff | (! grep .); then
failed=1
fi
exit "$failed"
================================================
FILE: .github/workflows/ci.yaml
================================================
name: For each commit and PR
on:
push:
branches:
- "*"
tags-ignore:
- "v*"
pull_request:
env:
REGISTRY: quay.io
IMAGE: quay.io/${{ github.repository }}
CGO_ENABLED: 0
GO_VERSION: "1.24"
jobs:
validation:
runs-on: ubuntu-latest
env:
CGO_ENABLED: 0
steps:
- name: Setup Dynamic Env
run: |
echo "MAKEFLAGS=-j$(nproc)" | tee $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "${{ env.GO_VERSION }}"
cache: true
- name: Fetch Deps
run: |
# fixes "write /run/user/1001/355792648: no space left on device" error
sudo mount -o remount,size=3G /run/user/1001 || true
go get -t ./... && go mod tidy
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate all files
run: make -j1 gen
- name: Run all the tests
run: make ci
- name: upload codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: compile binaries
run: make crosscompile
- name: Figure out Docker Tags
id: docker-image-tag
run: |
echo ::set-output name=tags::${{ env.IMAGE }}:latest,${{ env.IMAGE }}:sha-${GITHUB_SHA::8}
- name: Login to quay.io
uses: docker/login-action@v3
if: ${{ startsWith(github.ref, 'refs/heads/main') }}
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- name: Build Docker Images
uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile
cache-from: type=registry,ref=${{ env.IMAGE }}:latest
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker-image-tag.outputs.tags }}
# looks just like Build Docker Images except with push:true and this will only run for builds for main
- name: Push Docker Images
uses: docker/build-push-action@v6
if: ${{ startsWith(github.ref, 'refs/heads/main') }}
with:
context: ./
file: ./Dockerfile
cache-from: type=registry,ref=${{ env.IMAGE }}:latest
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.docker-image-tag.outputs.tags }}
================================================
FILE: .github/workflows/tags.yaml
================================================
on:
push:
tags:
- "v*"
name: Create release
env:
REGISTRY: quay.io
IMAGE_NAME: ${{ github.repository }}
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate Release Notes
run: |
release_notes=$(gh api repos/{owner}/{repo}/releases/generate-notes -F tag_name=${{ github.ref }} --jq .body)
echo 'RELEASE_NOTES<<EOF' >> $GITHUB_ENV
echo "${release_notes}" >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
- name: Docker manager metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: latest=false
tags: type=ref,event=tag
- name: Set the from image tag
run: echo "FROM_TAG=sha-${GITHUB_SHA::8}" >> $GITHUB_ENV
- name: Copy the image using skopeo
run: skopeo copy --all --dest-creds="${DST_REG_USER}":"${DST_REG_PASS}" docker://"${SRC_IMAGE}" docker://"${DST_IMAGE}"
env:
SRC_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.FROM_TAG }}
DST_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
DST_REG_USER: ${{ secrets.QUAY_USERNAME }}
DST_REG_PASS: ${{ secrets.QUAY_PASSWORD }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: ${{ env.RELEASE_NOTES }}
draft: false
prerelease: true
================================================
FILE: .gitignore
================================================
*.iml
*.orig
*.test
.idea*/**
/bin/
/cmd/smee/smee
/cmd/smee/smee-*-*
coverage.txt
.vscode
# added by lint-install
out/
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
# The default runtime timeout is 1m, which doesn't work well on Github Actions.
timeout: 4m
linters:
default: none
enable:
- asciicheck
- bodyclose
- copyloopvar
- cyclop
- dogsled
- dupl
- durationcheck
- errcheck
- errname
- errorlint
- exhaustive
- forcetypeassert
- gocognit
- goconst
- gocritic
- godot
- goheader
- goprintffuncname
- gosec
- govet
- importas
- ineffassign
- makezero
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- thelper
- tparallel
- unconvert
- unparam
- unused
- wastedassign
- whitespace
settings:
cyclop:
max-complexity: 37
package-average: 34
dupl:
threshold: 200
errorlint:
# Forcing %w in error wrapping forces authors to make errors part of their package APIs. The decision to make
# an error part of a package API should be a conscious decision by the author.
# Also see Hyrums Law.
errorf: false
asserts: false
exhaustive:
default-signifies-exhaustive: true
gocognit:
min-complexity: 98
goconst:
min-len: 4
min-occurrences: 5
gosec:
excludes:
- G107 # Potential HTTP request made with variable url
- G204 # Subprocess launched with function call as argument or cmd arguments
- G404 # Use of weak random number generator (math/rand instead of crypto/rand
nestif:
min-complexity: 8
nolintlint:
require-explanation: true
require-specific: true
allow-unused: false
revive:
severity: warning
rules:
- name: atomic
- name: blank-imports
- name: bool-literal-in-expr
- name: confusing-naming
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
- name: deep-exit
- name: defer
- name: range-val-in-closure
- name: range-val-address
- name: dot-imports
- name: error-naming
- name: error-return
- name: error-strings
- name: errorf
- name: exported
- name: identical-branches
- name: if-return
- name: import-shadowing
- name: increment-decrement
- name: indent-error-flow
- name: indent-error-flow
- name: package-comments
- name: range
- name: receiver-naming
- name: redefines-builtin-id
- name: superfluous-else
- name: struct-tag
- name: time-naming
- name: unexported-naming
- name: unexported-return
- name: unnecessary-stmt
- name: unreachable-code
- name: unused-parameter
- name: var-declaration
- name: var-naming
- name: unconditional-recursion
- name: waitgroup-by-value
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag
- name: struct-tag
arguments:
- json,inline
- yaml,omitzero
- protobuf,casttype
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- dupl
- errcheck
- forcetypeassert
- gocyclo
- gosec
- noctx
path: _test\.go
- linters:
# This check is of questionable value
- tparallel
text: call t.Parallel on the top level as well as its subtests
- linters:
- cyclop
- goconst
path: (.+)_test\.go
paths:
- third_party$
- builtin$
- examples$
- internal/iso/internal/reverseproxy.go
- internal/iso/internal/reverseproxy_test.go
- internal/iso/internal/acsii.go
- internal/iso/internal/acsii_test.go
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- internal/iso/internal/reverseproxy.go
- internal/iso/internal/reverseproxy_test.go
- internal/iso/internal/acsii.go
- internal/iso/internal/acsii_test.go
- third_party$
- builtin$
- examples$
================================================
FILE: CONTRIBUTING.md
================================================
# Contributor Guide
Welcome to Smee!
We are really excited to have you.
Please use the following guide on your contributing journey.
Thanks for contributing!
## Table of Contents
- [Context](#Context)
- [Architecture](#Architecture)
- [Design Docs](#Design-Docs)
- [Code Structure](#Code-Structure)
- [Prerequisites](#Prerequisites)
- [DCO Sign Off](#DCO-Sign-Off)
- [Code of Conduct](#Code-of-Conduct)
- [Setting up your development environment](#Setting-up-your-development-environment)
- [Development](#Development)
- [Building](#Building)
- [Unit testing](#Unit-testing)
- [Linting](#Linting)
- [Functional testing](#Functional-testing)
- [Running Smee locally](#Running-Smee-locally)
- [Pull Requests](#Pull-Requests)
- [Branching strategy](#Branching-strategy)
- [Quality](#Quality)
- [CI](#CI)
- [Code coverage](#Code-coverage)
- [Pre PR Checklist](#Pre-PR-Checklist)
---
## Context
Smee is a DHCP and PXE (TFTP & HTTP) service.
It is part of the [Tinkerbell stack](https://tinkerbell.org) and provides the first interaction for any machines being provisioned through Tinkerbell.
## Architecture
### Design Docs
Details and diagrams for Smee are found [here](docs/DESIGN.md).
### Code Structure
Details on Smee's code structure is found [here](docs/CODE_STRUCTURE.md) (WIP)
## Prerequisites
### DCO Sign Off
Please read and understand the DCO found [here](docs/DCO.md).
### Code of Conduct
Please read and understand the code of conduct found [here](https://github.com/tinkerbell/.github/blob/main/CODE_OF_CONDUCT.md).
### Setting up your development environment
---
### Dependencies
#### Build time dependencies
#### Runtime dependencies
At runtime Smee needs to communicate with a Tink server.
Follow this [guide](https://tinkerbell.org/docs/setup/getting_started/) for running Tink server.
## Development
### Building
> At the moment, these instructions are only stable on Linux environments
To build Smee, run:
```bash
# build all ipxe files, embed them, and build the Go binary
# Built binary can be found in the top level directory.
make build
```
To build the amd64 Smee container image, run:
```bash
# make the amd64 container image
# Built image will be named smee:latest
make image
```
To build the IPXE binaries and embed them into Go, run:
```bash
# Note, this will not build the Smee binary
make bindata
```
To build Smee binaries for all distro
### Unit testing
To execute the unit tests, run:
```bash
make test
# to get code coverage numbers, run:
make coverage
```
### Linting
To execute linting, run:
```bash
# runs golangci-lint
make lint
# runs goimports
make goimports
# runs go vet
make vet
```
## Linting of Non Go files
```bash
# lints non Go files like shell scripts, markdown files, etc
# this script is used in CI run, so be sure it passes before submitting a PR
./.github/workflows/ci-non-go.sh
```
### Functional testing
1. Create a hardware record in Tink server - follow the guide [here](https://tinkerbell.org/docs/concepts/hardware/)
2. boot the machine
### Running Smee
1. Be sure all documented runtime dependencies are satisfied.
2. Define all environment variables.
```bash
# MIRROR_HOST is for downloading kernel, initrd
export MIRROR_HOST=192.168.2.3
# PUBLIC_FQDN is for phone home endpoint
export PUBLIC_FQDN=192.168.2.4
# DOCKER_REGISTRY, REGISTRY_USERNAME, REGISTRY_PASSWORD, TINKERBELL_GRPC_AUTHORITY, TINKERBELL_CERT_URL are needed for auto.ipxe file generation
# TINKERBELL_GRPC_AUTHORITY, TINKERBELL_CERT_URL are needed for getting hardware data
export DOCKER_REGISTRY=192.168.2.1:5000
export REGISTRY_USERNAME=admin
export REGISTRY_PASSWORD=secret
export TINKERBELL_GRPC_AUTHORITY=tinkerbell.tinkerbell:42113
export TINKERBELL_CERT_URL=http://tinkerbell.tinkerbell:42114/cert
# FACILITY_CODE is needed for ?
export FACILITY_CODE=onprem
export DATA_MODEL_VERSION=1
# API_AUTH_TOKEN, API_CONSUMER_TOKEN are needed to by pass panicking in main.go main func
export API_AUTH_TOKEN=none
export API_CONSUMER_TOKEN=none
```
3. Run Smee
```bash
# Run the compiled smee
sudo ./smee -http-addr 192.168.2.225:80 -tftp-addr 192.168.2.225:69 -dhcp-addr 192.168.2.225:67
```
4. Faster iterating via `go run`
```bash
# after the ipxe binaries have been compiled you can use `go run` to iterate a little more quickly than building the binary every time
sudo go run ./smee -http-addr 192.168.2.225:80 -tftp-addr 192.168.2.225:69 -dhcp-addr 192.168.2.225:67
```
## Pull Requests
### Branching strategy
Smee uses a fork and pull request model.
See this [doc](https://guides.github.com/activities/forking/) for more details.
### Quality
#### CI
Smee uses GitHub Actions for CI.
The workflow is found in [.github/workflows/ci.yaml](.github/workflows/ci.yaml).
It is run for each commit and PR.
#### Code coverage
Smee does run code coverage with each PR.
Coverage thresholds are not currently enforced.
It is always nice and very welcomed to add tests and keep or increase the code coverage percentage.
### Pre PR Checklist
This checklist is a helper to make sure there's no gotchas that come up when you submit a PR.
- [ ] You've reviewed the [code of conduct](#Code-of-Conduct)
- [ ] All commits are DCO signed off
- [ ] Code is [formatted and linted](#Linting)
- [ ] Code [builds](#Building) successfully
- [ ] All tests are [passing](#Unit-testing)
- [ ] Code coverage [percentage](#Code-coverage). (main line is the base with which to compare)
================================================
FILE: Dockerfile
================================================
# run `make image` to build the binary + container
# if you're using `make build` this Dockerfile will not find the binary
# and you probably want `make smee-linux-amd64`
FROM alpine:3.22
ARG TARGETARCH
ARG TARGETVARIANT
ENTRYPOINT ["/usr/bin/smee"]
RUN apk add --update --upgrade --no-cache ca-certificates
COPY cmd/smee/smee-linux-${TARGETARCH:-amd64}${TARGETVARIANT} /usr/bin/smee
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Packet Host, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: Makefile
================================================
all: help
-include lint.mk
-include rules.mk
build: cmd/smee/smee ## Compile smee for host OS and Architecture
crosscompile: $(crossbinaries) ## Compile smee for all architectures
gen: $(generated_go_files) ## Generate go generate'd files
IMAGE_TAG ?= smee:latest
image: cmd/smee/smee-linux-amd64 ## Build docker image
docker build -t $(IMAGE_TAG) .
test: gen ## Run go test
CGO_ENABLED=1 go test -race -coverprofile=coverage.txt -covermode=atomic -v ${TEST_ARGS} ./...
coverage: test ## Show test coverage
go tool cover -func=coverage.txt
vet: ## Run go vet
go vet ./...
goimports: gen ## Run goimports
$(GOIMPORTS) -w .
ci-checks: .github/workflows/ci-checks.sh gen
./.github/workflows/ci-checks.sh
ci: ci-checks coverage goimports lint vet ## Runs all the same validations and tests that run in CI
help: ## Print this help
@grep --no-filename -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sed 's/:.*##/·/' | sort | column -ts '·' -c 120
================================================
FILE: README.md
================================================
> [!IMPORTANT]
> The Smee repo has been deprecated. All functionality has been moved to https://github.com/tinkerbell/tinkerbell.
> For more details, see the roadmap issue [#41](https://github.com/tinkerbell/roadmap/issues/41).
> This repository is scheduled for archive by the end of 2025.
# Smee
[](https://github.com/tinkerbell/smee/actions?query=workflow%3A%22For+each+commit+and+PR%22+branch%3Amain)
Smee is the network boot service in the [Tinkerbell stack](https://tinkerbell.org), formerly known as `Boots`. It is comprised of the following services.
- DHCP server
- host reservations only
- mac address based lookups
- netboot options support
- backend support
- Kubernetes
- file based
- ProxyDHCP support
- TFTP server
- serving iPXE binaries
- HTTP server
- serving iPXE binaries and iPXE scripts
- iPXE script serving uses IP authentication
- backend support
- Kubernetes
- file based
- Syslog server
- receives syslog messages and logs them
## Definitions
**DHCP Reservation:**
A fixed IP address that is reserved for a specific client.
**DHCP Lease:**
An IP address, that can potentially change, that is assigned to a client by the DHCP server.
The IP is typically pulled from a pool or subnet of available IP addresses.
**ProxyDHCP:**
"[A] Proxy DHCP server behaves much like a DHCP server by listening for ordinary DHCP client traffic and responding to certain client requests. However, unlike the DHCP server, the PXE Proxy DHCP server does not administer network addresses, and it only responds to clients that identify themselves as PXE clients.
The responses given by the PXE Proxy DHCP server contain the mechanism by which the client locates the boot servers or the network addresses and descriptions of the supported, compatible boot servers."
-- [IBM](https://www.ibm.com/docs/en/aix/7.1?topic=protocol-preboot-execution-environment-proxy-dhcp-daemon)
## Running Smee
### DHCP Modes
Smee's DHCP functionality can operate in one of the following modes:
1. **DHCP Reservation**
To enable this mode set `-dhcp-mode=reservation`.
Smee will respond to DHCP requests from clients and provide them with IP and next boot info when netbooting. This is the default mode. IP info is all reservation based. There must be a corresponding Hardware record for the requesting client's MAC address.
1. **Proxy DHCP**
To enable this mode set `-dhcp-mode=proxy`.
Smee will respond to PXE enabled DHCP requests from clients and provide them with next boot info when netbooting. In this mode an existing DHCP server that does not serve network boot information is required. Smee will respond to PXE enabled DHCP requests and provide the client with the next boot info. There must be a corresponding Hardware record for the requesting client's MAC address. The `auto.ipxe` script will be served with the MAC address in the URL and the MAC address will be used to lookup the corresponding Hardware record. Layer 2 access to machines or a DHCP relay agent that will forward the DHCP requests to Smee is required.
1. **Auto Proxy DHCP**
To enable this mode set `-dhcp-mode=auto-proxy`.
Smee will respond to PXE enabled DHCP requests from clients and provide them with next boot info when netbooting. In this mode an existing DHCP server that does not serve network boot information is required. In this mode, if no corresponding Hardware record is found for the requesting client's MAC address, Smee will provide the client with a statically defined iPXE script. If a Hardware record is found, then the normal `auto.ipxe` script will be served. Use `-backend-noop-enabled` to disable all backend look ups. Layer 2 access to machines or a DHCP relay agent that will forward the DHCP requests to Smee is required.
- When using Smee's auto.ipxe, you'll generally want to set the following flags:
- `-dhcp-mode=auto-proxy`
- `-osie-url <URL to HookOS kernel and initrd>`
- `-tink-server <IP and port of Tink server>`
- `-extra-kernel-args="tink_worker_image=quay.io/tinkerbell/tink-worker:<use a version/commit tag>"`
- When not using Smee's auto.ipxe, you'll generally want to set the following flags:
- `-dhcp-mode=auto-proxy`
- `-dhcp-http-ipxe-script-url=https://boot.netboot.xyz`
- `-dhcp-http-ipxe-script-prepend-mac=false`
1. **DHCP disabled**
To enable this mode set `-dhcp-enabled=false`.
Smee will not respond to DHCP requests from clients. This is useful when the network has an existing DHCP server that will provide both IP and next boot info and Smee's TFTP and HTTP functionality will be used. The IP address in the Hardware record must be the same as the IP address of the client requesting the `auto.ipxe` script. See this [doc](docs/DHCP.md) for more details. In most situations`--dhcp-http-ipxe-script-prepend-mac=false` should also be set when in this mode.
### Interoperability with other DHCP servers
When a DHCP server exists on the network, Smee should be set to run `proxy` or `auto-proxy` mode. This will allow Smee to provide the next boot information to clients that request it. The existing DHCP server will provide the IP address and other network boot details. Layer 2 access to machines or a DHCP relay agent that will forward the DHCP requests to Smee is required.
It is not recommended, but it is possible for Smee to be run in `reservation` mode in networks with another DHCP server(s). To get the intended behavior from Smee one of the following must be true.
1. All DHCP servers besides Smee are configured to ignore the MAC addresses that Smee is configured to serve.
1. All DHCP servers are configured to serve the same IP address and network boot details as Smee. In this scenario the DHCP functionality of Smee is redundant. It would be recommended to run Smee with the DHCP server functionality disabled (`-dhcp=false`). See the [doc](./docs/DHCP.md) on using your existing DHCP service for more details.
### Environment Variables and CLI Flags
It's important to note that CLI flags take precedence over environment variables. All CLI flags can be set as environment variables. Environment variable names are the same as the flag names with some modifications. For example, the flag `-dhcp-addr` has the environment variable of `SMEE_DHCP_ADDR`. The modifications of CLI flags to environment variables are as follows:
- prefixed with `SMEE_`
- all uppercase
- hyphens (`-`) are replaced with underscores (`_`)
There is one environment variable that does not have a corresponding CLI flag. The environment variable is `SMEE_PUBLIC_IP_INTERFACE`. This environment variable takes a local network interface name and uses it to auto detect the IP address to use as the default in all other CLI flags that require an IP address. This is useful when the machine running Smee has multiple network interfaces and you want the default detected IP to be from this specified interface.
### Local Setup
Running the Tests
```bash
# run the tests
make test
```
Build/Run Smee
```bash
# make the binary
make build
# run Smee
./smee -h
Smee is the DHCP and Network boot service for use in the Tinkerbell stack.
USAGE
smee [flags]
FLAGS
-log-level log level (debug, info) (default "info")
-backend-file-enabled [backend] enable the file backend for DHCP and the HTTP iPXE script (default "false")
-backend-file-path [backend] the hardware yaml file path for the file backend
-backend-kube-api [backend] the Kubernetes API URL, used for in-cluster client construction, kube backend only
-backend-kube-config [backend] the Kubernetes config file location, kube backend only
-backend-kube-enabled [backend] enable the kubernetes backend for DHCP and the HTTP iPXE script (default "true")
-backend-kube-namespace [backend] an optional Kubernetes namespace override to query hardware data from, kube backend only
-backend-noop-enabled [backend] enable the noop backend for DHCP and the HTTP iPXE script (default "false")
-dhcp-addr [dhcp] local IP:Port to listen on for DHCP requests (default "0.0.0.0:67")
-dhcp-enabled [dhcp] enable DHCP server (default "true")
-dhcp-http-ipxe-binary-host [dhcp] HTTP iPXE binaries host or IP to use in DHCP packets (default "172.17.0.3")
-dhcp-http-ipxe-binary-path [dhcp] HTTP iPXE binaries path to use in DHCP packets (default "/ipxe/")
-dhcp-http-ipxe-binary-port [dhcp] HTTP iPXE binaries port to use in DHCP packets (default "8080")
-dhcp-http-ipxe-binary-scheme [dhcp] HTTP iPXE binaries scheme to use in DHCP packets (default "http")
-dhcp-http-ipxe-script-host [dhcp] HTTP iPXE script host or IP to use in DHCP packets (default "172.17.0.3")
-dhcp-http-ipxe-script-path [dhcp] HTTP iPXE script path to use in DHCP packets (default "/auto.ipxe")
-dhcp-http-ipxe-script-port [dhcp] HTTP iPXE script port to use in DHCP packets (default "8080")
-dhcp-http-ipxe-script-prepend-mac [dhcp] prepend the hardware MAC address to iPXE script URL base, http://1.2.3.4/auto.ipxe -> http://1.2.3.4/40:15:ff:89:cc:0e/auto.ipxe (default "true")
-dhcp-http-ipxe-script-scheme [dhcp] HTTP iPXE script scheme to use in DHCP packets (default "http")
-dhcp-http-ipxe-script-url [dhcp] HTTP iPXE script URL to use in DHCP packets, this overrides the flags for dhcp-http-ipxe-script-{scheme, host, port, path}
-dhcp-iface [dhcp] interface to bind to for DHCP requests
-dhcp-ip-for-packet [dhcp] IP address to use in DHCP packets (opt 54, etc) (default "172.17.0.3")
-dhcp-mode [dhcp] DHCP mode (reservation, proxy, auto-proxy) (default "reservation")
-dhcp-syslog-ip [dhcp] Syslog server IP address to use in DHCP packets (opt 7) (default "172.17.0.3")
-dhcp-tftp-ip [dhcp] TFTP server IP address to use in DHCP packets (opt 66, etc) (default "172.17.0.3")
-dhcp-tftp-port [dhcp] TFTP server port to use in DHCP packets (opt 66, etc) (default "69")
-extra-kernel-args [http] extra set of kernel args (k=v k=v) that are appended to the kernel cmdline iPXE script
-http-addr [http] local IP to listen on for iPXE HTTP script requests (default "172.17.0.3")
-http-ipxe-binary-enabled [http] enable iPXE HTTP binary server (default "true")
-http-ipxe-script-enabled [http] enable iPXE HTTP script server (default "true")
-http-port [http] local port to listen on for iPXE HTTP script requests (default "8080")
-ipxe-script-retries [http] number of retries to attempt when fetching kernel and initrd files in the iPXE script (default "0")
-ipxe-script-retry-delay [http] delay (in seconds) between retries when fetching kernel and initrd files in the iPXE script (default "2")
-osie-url [http] URL where OSIE (HookOS) images are located
-tink-server [http] IP:Port for the Tink server
-tink-server-tls [http] use TLS for Tink server (default "false")
-trusted-proxies [http] comma separated list of trusted proxies in CIDR notation
-iso-enabled [iso] enable patching an OSIE ISO (default "false")
-iso-magic-string [iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS
-iso-static-ipam-enabled [iso] enable static IPAM for HookOS (default "false")
-iso-url [iso] an ISO source URL target for patching
-otel-endpoint [otel] OpenTelemetry collector endpoint
-otel-insecure [otel] OpenTelemetry collector insecure (default "true")
-syslog-addr [syslog] local IP to listen on for Syslog messages (default "172.17.0.3")
-syslog-enabled [syslog] enable Syslog server(receiver) (default "true")
-syslog-port [syslog] local port to listen on for Syslog messages (default "514")
-ipxe-script-patch [tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP
-tftp-addr [tftp] local IP to listen on for iPXE TFTP binary requests (default "172.17.0.3")
-tftp-block-size [tftp] TFTP block size a value between 512 (the default block size for TFTP) and 65456 (the max size a UDP packet payload can be) (default "512")
-tftp-enabled [tftp] enable iPXE TFTP binary server) (default "true")
-tftp-port [tftp] local port to listen on for iPXE TFTP binary requests (default "69")
-tftp-timeout [tftp] iPXE TFTP binary server requests timeout (default "5s")
```
### Developing using the file backend
The quickest way to get started is `docker-compose up`. This will start Smee using the file backend. This uses the example Yaml file (hardware.yaml) in the `test/` directory. It also starts a client container that runs some tests.
```sh
docker compose up --build # build images and start the network & services
# it's fine to hit control-C twice for fast shutdown
docker compose down # stop the network & containers
```
Alternatively Smee can be run by itself. It requires a few
flags or environment variables for configuration.
`test/hardware.yaml` should be safe enough for most developers to
use on the command line locally without getting a call from your network
administrator. That said, you might want to contact them before running a DHCP
server on their network. Best to isolate it in Docker or a VM if you're not
sure.
```sh
# build the binary
make build
export SMEE_OSIE_URL=<http url to the OSIE (Operating System Installation Environment) artifacts>
# For more info on the default OSIE (Hook) artifacts, please see https://github.com/tinkerbell/hook
export SMEE_BACKEND_KUBE_ENABLED=false
export SMEE_BACKEND_FILE_ENABLED=true
export SMEE_BACKEND_FILE_PATH=./test/hardware.yaml
export SMEE_EXTRA_KERNEL_ARGS="tink_worker_image=quay.io/tinkerbell/tink-worker:latest"
# By default, Smee needs to bind to low ports (67, 69, 514) so it needs root.
sudo -E ./cmd/smee/smee
# clean up the environment variables
unset SMEE_OSIE_URL
unset SMEE_BACKEND_KUBE_ENABLED
unset SMEE_BACKEND_FILE_ENABLED
unset SMEE_BACKEND_FILE_PATH
unset SMEE_EXTRA_KERNEL_ARGS
```
================================================
FILE: RELEASING.md
================================================
# Releasing
## Process
For version v0.x.y:
1. Create the annotated tag
> NOTE: To use your GPG signature when pushing the tag, use `SIGN_TAG=1 ./contrib/tag-release.sh v0.x.y` instead)
- `./contrib/tag-release.sh v0.x.y`
1. Push the tag to the GitHub repository. This will automatically trigger a [Github Action](https://github.com/tinkerbell/smee/actions) to create a release.
> NOTE: `origin` should be the name of the remote pointing to `github.com/tinkerbell/smee`
- `git push origin v0.x.y`
1. Review the release on GitHub.
### Permissions
Releasing requires a particular set of permissions.
- Tag push access to the GitHub repository
================================================
FILE: Tiltfile
================================================
load('ext://restart_process', 'docker_build_with_restart')
load('ext://local_output', 'local_output')
load('ext://helm_resource', 'helm_resource')
local_resource('compile smee',
cmd='make cmd/smee/smee-linux-amd64',
deps=["go.mod", "go.sum", "internal", "Dockerfile", "cmd/smee/main.go", "cmd/smee/flag.go", "cmd/smee/backend.go"],
)
docker_build_with_restart(
'quay.io/tinkerbell/smee',
'.',
dockerfile='Dockerfile',
entrypoint=['/usr/bin/smee'],
live_update=[
sync('cmd/smee/smee-linux-amd64', '/usr/bin/smee'),
],
)
default_registry('ttl.sh/meohmy-dghentld')
default_trusted_proxies = local_output("kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' | tr ' ' ','")
trusted_proxies = os.getenv('TRUSTED_PROXIES', default_trusted_proxies)
lb_ip = os.getenv('LB_IP', '')
stack_version = os.getenv('STACK_CHART_VERSION', '0.5.0')
stack_location = os.getenv('STACK_LOCATION', 'oci://ghcr.io/tinkerbell/charts/stack') # or a local path like '/home/tink/repos/tinkerbell/charts/tinkerbell/stack'
namespace = 'tink'
if lb_ip == '':
fail('Please set the LB_IP environment variable. This is required to deploy the stack.')
# to use a KinD cluster, add a macvlan interface into the KinD docker container. for example: `docker network connect macvlan kind-control-plane`
# Then uncomment the 2 interface lines below.
helm_resource('stack',
chart=stack_location,
namespace=namespace,
image_deps=['quay.io/tinkerbell/smee'],
image_keys=[('smee.image')],
flags=[
'--create-namespace',
'--version=%s' % stack_version,
'--set=global.trustedProxies={%s}' % trusted_proxies,
'--set=global.publicIP=%s' % lb_ip,
#'--set=stack.kubevip.interface=eth1',
#'--set=stack.relay.sourceInterface=eth1',
],
release_name='stack'
)
================================================
FILE: cmd/smee/backend.go
================================================
package main
import (
"context"
"github.com/go-logr/logr"
"github.com/tinkerbell/smee/internal/backend/file"
"github.com/tinkerbell/smee/internal/backend/kube"
"github.com/tinkerbell/smee/internal/backend/noop"
"github.com/tinkerbell/smee/internal/dhcp/handler"
"github.com/tinkerbell/tink/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/client-go/scale/scheme"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/cluster"
)
type Kube struct {
// ConfigFilePath is the path to a kubernetes config file (kubeconfig).
ConfigFilePath string
// APIURL is the Kubernetes API URL.
APIURL string
// Namespace is an override for the Namespace the kubernetes client will watch.
// The default is the Namespace the pod is running in.
Namespace string
Enabled bool
}
type File struct {
// FilePath is the path to a JSON FilePath containing hardware data.
FilePath string
Enabled bool
}
type Noop struct {
Enabled bool
}
func (n *Noop) backend() handler.BackendReader {
return &noop.Backend{}
}
func (k *Kube) getClient() (*rest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = k.ConfigFilePath
overrides := &clientcmd.ConfigOverrides{
ClusterInfo: clientcmdapi.Cluster{
Server: k.APIURL,
},
Context: clientcmdapi.Context{
Namespace: k.Namespace,
},
}
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
return loader.ClientConfig()
}
func (k *Kube) backend(ctx context.Context) (handler.BackendReader, error) {
config, err := k.getClient()
if err != nil {
return nil, err
}
rs := runtime.NewScheme()
if err := scheme.AddToScheme(rs); err != nil {
return nil, err
}
if err := v1alpha1.AddToScheme(rs); err != nil {
return nil, err
}
conf := func(opts *cluster.Options) {
opts.Scheme = rs
if k.Namespace != "" {
opts.Cache.DefaultNamespaces = map[string]cache.Config{k.Namespace: {}}
}
}
kb, err := kube.NewBackend(config, conf)
if err != nil {
return nil, err
}
go func() {
err = kb.Start(ctx)
if err != nil {
panic(err)
}
}()
return kb, nil
}
func (s *File) backend(ctx context.Context, logger logr.Logger) (handler.BackendReader, error) {
f, err := file.NewWatcher(logger, s.FilePath)
if err != nil {
return nil, err
}
go f.Start(ctx)
return f, nil
}
================================================
FILE: cmd/smee/flag.go
================================================
package main
import (
"errors"
"flag"
"fmt"
"net"
"os"
"regexp"
"sort"
"strings"
"text/tabwriter"
"time"
"golang.org/x/sys/unix"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/vishvananda/netlink"
)
// customUsageFunc is a custom UsageFunc used for all commands.
func customUsageFunc(c *ffcli.Command) string {
var b strings.Builder
if c.LongHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
}
fmt.Fprintf(&b, "USAGE\n")
if c.ShortUsage != "" {
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
} else {
fmt.Fprintf(&b, " %s\n", c.Name)
}
fmt.Fprintf(&b, "\n")
if len(c.Subcommands) > 0 {
fmt.Fprintf(&b, "SUBCOMMANDS\n")
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
for _, subcommand := range c.Subcommands {
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
}
tw.Flush()
fmt.Fprintf(&b, "\n")
}
if countFlags(c.FlagSet) > 0 {
fmt.Fprintf(&b, "FLAGS\n")
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
type flagUsage struct {
name string
usage string
defaultValue string
}
flags := []flagUsage{}
c.FlagSet.VisitAll(func(f *flag.Flag) {
f1 := flagUsage{name: f.Name, usage: f.Usage, defaultValue: f.DefValue}
flags = append(flags, f1)
})
sort.SliceStable(flags, func(i, j int) bool {
// sort by the service name between the brackets "[]" found in the usage string.
r := regexp.MustCompile(`^\[(.*?)\]`)
return r.FindString(flags[i].usage) < r.FindString(flags[j].usage)
})
for _, elem := range flags {
if elem.defaultValue != "" {
fmt.Fprintf(tw, " -%s\t%s (default %q)\n", elem.name, elem.usage, elem.defaultValue)
} else {
fmt.Fprintf(tw, " -%s\t%s\n", elem.name, elem.usage)
}
}
tw.Flush()
fmt.Fprintf(&b, "\n")
}
return strings.TrimSpace(b.String()) + "\n"
}
func countFlags(fs *flag.FlagSet) (n int) {
fs.VisitAll(func(*flag.Flag) { n++ })
return n
}
func syslogFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.syslog.enabled, "syslog-enabled", true, "[syslog] enable Syslog server(receiver)")
fs.StringVar(&c.syslog.bindAddr, "syslog-addr", detectPublicIPv4(), "[syslog] local IP to listen on for Syslog messages")
fs.IntVar(&c.syslog.bindPort, "syslog-port", 514, "[syslog] local port to listen on for Syslog messages")
}
func tftpFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.tftp.enabled, "tftp-enabled", true, "[tftp] enable iPXE TFTP binary server)")
fs.StringVar(&c.tftp.bindAddr, "tftp-addr", detectPublicIPv4(), "[tftp] local IP to listen on for iPXE TFTP binary requests")
fs.IntVar(&c.tftp.bindPort, "tftp-port", 69, "[tftp] local port to listen on for iPXE TFTP binary requests")
fs.DurationVar(&c.tftp.timeout, "tftp-timeout", time.Second*5, "[tftp] iPXE TFTP binary server requests timeout")
fs.StringVar(&c.tftp.ipxeScriptPatch, "ipxe-script-patch", "", "[tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP")
fs.IntVar(&c.tftp.blockSize, "tftp-block-size", 512, "[tftp] TFTP block size a value between 512 (the default block size for TFTP) and 65456 (the max size a UDP packet payload can be)")
}
func ipxeHTTPBinaryFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.ipxeHTTPBinary.enabled, "http-ipxe-binary-enabled", true, "[http] enable iPXE HTTP binary server")
}
func ipxeHTTPScriptFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.ipxeHTTPScript.enabled, "http-ipxe-script-enabled", true, "[http] enable iPXE HTTP script server")
fs.StringVar(&c.ipxeHTTPScript.bindAddr, "http-addr", detectPublicIPv4(), "[http] local IP to listen on for iPXE HTTP script requests")
fs.IntVar(&c.ipxeHTTPScript.bindPort, "http-port", 8080, "[http] local port to listen on for iPXE HTTP script requests")
fs.StringVar(&c.ipxeHTTPScript.extraKernelArgs, "extra-kernel-args", "", "[http] extra set of kernel args (k=v k=v) that are appended to the kernel cmdline iPXE script")
fs.StringVar(&c.ipxeHTTPScript.trustedProxies, "trusted-proxies", "", "[http] comma separated list of trusted proxies in CIDR notation")
fs.StringVar(&c.ipxeHTTPScript.hookURL, "osie-url", "", "[http] URL where OSIE (HookOS) images are located")
fs.StringVar(&c.ipxeHTTPScript.tinkServer, "tink-server", "", "[http] IP:Port for the Tink server")
fs.BoolVar(&c.ipxeHTTPScript.tinkServerUseTLS, "tink-server-tls", false, "[http] use TLS for Tink server")
fs.BoolVar(&c.ipxeHTTPScript.tinkServerInsecureTLS, "tink-server-insecure-tls", false, "[http] use insecure TLS for Tink server")
fs.IntVar(&c.ipxeHTTPScript.retries, "ipxe-script-retries", 0, "[http] number of retries to attempt when fetching kernel and initrd files in the iPXE script")
fs.IntVar(&c.ipxeHTTPScript.retryDelay, "ipxe-script-retry-delay", 2, "[http] delay (in seconds) between retries when fetching kernel and initrd files in the iPXE script")
}
func dhcpFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.dhcp.enabled, "dhcp-enabled", true, "[dhcp] enable DHCP server")
fs.StringVar(&c.dhcp.mode, "dhcp-mode", dhcpModeReservation.String(), fmt.Sprintf("[dhcp] DHCP mode (%s, %s, %s)", dhcpModeReservation, dhcpModeProxy, dhcpModeAutoProxy))
fs.StringVar(&c.dhcp.bindAddr, "dhcp-addr", "0.0.0.0:67", "[dhcp] local IP:Port to listen on for DHCP requests")
fs.StringVar(&c.dhcp.bindInterface, "dhcp-iface", "", "[dhcp] interface to bind to for DHCP requests")
fs.StringVar(&c.dhcp.ipForPacket, "dhcp-ip-for-packet", detectPublicIPv4(), "[dhcp] IP address to use in DHCP packets (opt 54, etc)")
fs.StringVar(&c.dhcp.syslogIP, "dhcp-syslog-ip", detectPublicIPv4(), "[dhcp] Syslog server IP address to use in DHCP packets (opt 7)")
fs.StringVar(&c.dhcp.tftpIP, "dhcp-tftp-ip", detectPublicIPv4(), "[dhcp] TFTP server IP address to use in DHCP packets (opt 66, etc)")
fs.IntVar(&c.dhcp.tftpPort, "dhcp-tftp-port", 69, "[dhcp] TFTP server port to use in DHCP packets (opt 66, etc)")
fs.StringVar(&c.dhcp.httpIpxeBinaryURL.Scheme, "dhcp-http-ipxe-binary-scheme", "http", "[dhcp] HTTP iPXE binaries scheme to use in DHCP packets")
fs.StringVar(&c.dhcp.httpIpxeBinaryURL.Host, "dhcp-http-ipxe-binary-host", detectPublicIPv4(), "[dhcp] HTTP iPXE binaries host or IP to use in DHCP packets")
fs.IntVar(&c.dhcp.httpIpxeBinaryURL.Port, "dhcp-http-ipxe-binary-port", 8080, "[dhcp] HTTP iPXE binaries port to use in DHCP packets")
fs.StringVar(&c.dhcp.httpIpxeBinaryURL.Path, "dhcp-http-ipxe-binary-path", "/ipxe/", "[dhcp] HTTP iPXE binaries path to use in DHCP packets")
fs.StringVar(&c.dhcp.httpIpxeScript.Scheme, "dhcp-http-ipxe-script-scheme", "http", "[dhcp] HTTP iPXE script scheme to use in DHCP packets")
fs.StringVar(&c.dhcp.httpIpxeScript.Host, "dhcp-http-ipxe-script-host", detectPublicIPv4(), "[dhcp] HTTP iPXE script host or IP to use in DHCP packets")
fs.IntVar(&c.dhcp.httpIpxeScript.Port, "dhcp-http-ipxe-script-port", 8080, "[dhcp] HTTP iPXE script port to use in DHCP packets")
fs.StringVar(&c.dhcp.httpIpxeScript.Path, "dhcp-http-ipxe-script-path", "/auto.ipxe", "[dhcp] HTTP iPXE script path to use in DHCP packets")
fs.StringVar(&c.dhcp.httpIpxeScriptURL, "dhcp-http-ipxe-script-url", "", "[dhcp] HTTP iPXE script URL to use in DHCP packets, this overrides the flags for dhcp-http-ipxe-script-{scheme, host, port, path}")
fs.BoolVar(&c.dhcp.httpIpxeScript.injectMacAddress, "dhcp-http-ipxe-script-prepend-mac", true, "[dhcp] prepend the hardware MAC address to iPXE script URL base, http://1.2.3.4/auto.ipxe -> http://1.2.3.4/40:15:ff:89:cc:0e/auto.ipxe")
}
func backendFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.backends.file.Enabled, "backend-file-enabled", false, "[backend] enable the file backend for DHCP and the HTTP iPXE script")
fs.StringVar(&c.backends.file.FilePath, "backend-file-path", "", "[backend] the hardware yaml file path for the file backend")
fs.BoolVar(&c.backends.kubernetes.Enabled, "backend-kube-enabled", true, "[backend] enable the kubernetes backend for DHCP and the HTTP iPXE script")
fs.StringVar(&c.backends.kubernetes.ConfigFilePath, "backend-kube-config", "", "[backend] the Kubernetes config file location, kube backend only")
fs.StringVar(&c.backends.kubernetes.APIURL, "backend-kube-api", "", "[backend] the Kubernetes API URL, used for in-cluster client construction, kube backend only")
fs.StringVar(&c.backends.kubernetes.Namespace, "backend-kube-namespace", "", "[backend] an optional Kubernetes namespace override to query hardware data from, kube backend only")
fs.BoolVar(&c.backends.Noop.Enabled, "backend-noop-enabled", false, "[backend] enable the noop backend for DHCP and the HTTP iPXE script")
}
func otelFlags(c *config, fs *flag.FlagSet) {
fs.StringVar(&c.otel.endpoint, "otel-endpoint", "", "[otel] OpenTelemetry collector endpoint")
fs.BoolVar(&c.otel.insecure, "otel-insecure", true, "[otel] OpenTelemetry collector insecure")
}
func isoFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.iso.enabled, "iso-enabled", false, "[iso] enable patching an OSIE ISO")
fs.StringVar(&c.iso.url, "iso-url", "", "[iso] an ISO source URL target for patching")
fs.StringVar(&c.iso.magicString, "iso-magic-string", "", "[iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS")
fs.BoolVar(&c.iso.staticIPAMEnabled, "iso-static-ipam-enabled", false, "[iso] enable static IPAM for HookOS")
}
func setFlags(c *config, fs *flag.FlagSet) {
fs.StringVar(&c.logLevel, "log-level", "info", "log level (debug, info)")
dhcpFlags(c, fs)
tftpFlags(c, fs)
ipxeHTTPBinaryFlags(c, fs)
ipxeHTTPScriptFlags(c, fs)
syslogFlags(c, fs)
backendFlags(c, fs)
otelFlags(c, fs)
isoFlags(c, fs)
}
func newCLI(cfg *config, fs *flag.FlagSet) *ffcli.Command {
setFlags(cfg, fs)
return &ffcli.Command{
Name: name,
ShortUsage: "smee [flags]",
LongHelp: "Smee is the DHCP and Network boot service for use in the Tinkerbell stack.",
FlagSet: fs,
Options: []ff.Option{ff.WithEnvVarPrefix(name)},
UsageFunc: customUsageFunc,
}
}
// ipByInterface returns the first IPv4 address on the named network interface.
func ipByInterface(name string) string {
iface, err := net.InterfaceByName(name)
if err != nil {
return ""
}
addrs, err := iface.Addrs()
if err != nil {
return ""
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ipNet.IP.To4() != nil {
return ipNet.IP.String()
}
}
return ""
}
func detectPublicIPv4() string {
if netint := os.Getenv("SMEE_PUBLIC_IP_INTERFACE"); netint != "" {
if ip := ipByInterface(netint); ip != "" {
return ip
}
}
ipDgw, err := autoDetectPublicIpv4WithDefaultGateway()
if err == nil {
return ipDgw.String()
}
ip, err := autoDetectPublicIPv4()
if err != nil {
return ""
}
return ip.String()
}
func autoDetectPublicIPv4() (net.IP, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil, fmt.Errorf("unable to auto-detect public IPv4: %w", err)
}
for _, addr := range addrs {
ip, ok := addr.(*net.IPNet)
if !ok {
continue
}
v4 := ip.IP.To4()
if v4 == nil || !v4.IsGlobalUnicast() {
continue
}
return v4, nil
}
return nil, errors.New("unable to auto-detect public IPv4")
}
// autoDetectPublicIpv4WithDefaultGateway finds the network interface with a default gateway
// and returns the first net.IP address of the first interface that has a default gateway.
func autoDetectPublicIpv4WithDefaultGateway() (net.IP, error) {
// Get the list of routes from netlink
routes, err := netlink.RouteList(nil, unix.AF_INET)
if err != nil {
return nil, fmt.Errorf("failed to list routes: %v", err)
}
// Find the route with a default gateway (Dst == nil)
for _, route := range routes {
if route.Dst == nil && route.Gw != nil {
// Get the interface associated with this route
iface, err := net.InterfaceByIndex(route.LinkIndex)
if err != nil {
return nil, fmt.Errorf("failed to get interface by index: %v", err)
}
// Get the addresses assigned to this interface
addrs, err := iface.Addrs()
if err != nil {
return nil, fmt.Errorf("failed to get addresses for interface %v: %v", iface.Name, err)
}
// Return the first valid IP address found
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
return ipNet.IP, nil
}
}
}
}
}
return nil, fmt.Errorf("no default gateway found")
}
================================================
FILE: cmd/smee/flag_test.go
================================================
package main
import (
"flag"
"fmt"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestParser(t *testing.T) {
want := config{
syslog: syslogConfig{
enabled: true,
bindAddr: "192.168.2.4",
bindPort: 514,
},
tftp: tftp{
blockSize: 512,
enabled: true,
timeout: 5 * time.Second,
bindAddr: "192.168.2.4",
bindPort: 69,
},
ipxeHTTPBinary: ipxeHTTPBinary{
enabled: true,
},
ipxeHTTPScript: ipxeHTTPScript{
enabled: true,
bindAddr: "192.168.2.4",
bindPort: 8080,
retryDelay: 2,
},
dhcp: dhcpConfig{
enabled: true,
mode: "reservation",
bindAddr: "0.0.0.0:67",
ipForPacket: "192.168.2.4",
syslogIP: "192.168.2.4",
tftpIP: "192.168.2.4",
tftpPort: 69,
httpIpxeBinaryURL: urlBuilder{
Scheme: "http",
Host: "192.168.2.4",
Port: 8080,
Path: "/ipxe/",
},
httpIpxeScript: httpIpxeScript{
urlBuilder: urlBuilder{
Scheme: "http",
Host: "192.168.2.4",
Port: 8080,
Path: "/auto.ipxe",
},
injectMacAddress: true,
},
},
iso: isoConfig{
enabled: true,
url: "http://10.10.10.10:8787/hook.iso",
magicString: magicString,
},
logLevel: "info",
backends: dhcpBackends{
file: File{},
kubernetes: Kube{Enabled: true},
},
otel: otelConfig{
insecure: true,
},
}
got := config{}
fs := flag.NewFlagSet(name, flag.ContinueOnError)
args := []string{
"-log-level", "info",
"-syslog-addr", "192.168.2.4",
"-tftp-addr", "192.168.2.4",
"-http-addr", "192.168.2.4",
"-dhcp-ip-for-packet", "192.168.2.4",
"-dhcp-syslog-ip", "192.168.2.4",
"-dhcp-tftp-ip", "192.168.2.4",
"-dhcp-http-ipxe-binary-host", "192.168.2.4",
"-dhcp-http-ipxe-script-host", "192.168.2.4",
"-iso-enabled=true",
"-iso-magic-string", magicString,
"-iso-url", "http://10.10.10.10:8787/hook.iso",
}
cli := newCLI(&got, fs)
cli.Parse(args)
opts := cmp.Options{
cmp.AllowUnexported(config{}),
cmp.AllowUnexported(syslogConfig{}),
cmp.AllowUnexported(tftp{}),
cmp.AllowUnexported(ipxeHTTPBinary{}),
cmp.AllowUnexported(ipxeHTTPScript{}),
cmp.AllowUnexported(dhcpConfig{}),
cmp.AllowUnexported(dhcpBackends{}),
cmp.AllowUnexported(httpIpxeScript{}),
cmp.AllowUnexported(isoConfig{}),
cmp.AllowUnexported(otelConfig{}),
cmp.AllowUnexported(urlBuilder{}),
}
if diff := cmp.Diff(want, got, opts); diff != "" {
t.Fatal(diff)
}
}
func TestCustomUsageFunc(t *testing.T) {
defaultIP := detectPublicIPv4()
want := fmt.Sprintf(`Smee is the DHCP and Network boot service for use in the Tinkerbell stack.
USAGE
smee [flags]
FLAGS
-log-level log level (debug, info) (default "info")
-backend-file-enabled [backend] enable the file backend for DHCP and the HTTP iPXE script (default "false")
-backend-file-path [backend] the hardware yaml file path for the file backend
-backend-kube-api [backend] the Kubernetes API URL, used for in-cluster client construction, kube backend only
-backend-kube-config [backend] the Kubernetes config file location, kube backend only
-backend-kube-enabled [backend] enable the kubernetes backend for DHCP and the HTTP iPXE script (default "true")
-backend-kube-namespace [backend] an optional Kubernetes namespace override to query hardware data from, kube backend only
-backend-noop-enabled [backend] enable the noop backend for DHCP and the HTTP iPXE script (default "false")
-dhcp-addr [dhcp] local IP:Port to listen on for DHCP requests (default "0.0.0.0:67")
-dhcp-enabled [dhcp] enable DHCP server (default "true")
-dhcp-http-ipxe-binary-host [dhcp] HTTP iPXE binaries host or IP to use in DHCP packets (default "%[1]v")
-dhcp-http-ipxe-binary-path [dhcp] HTTP iPXE binaries path to use in DHCP packets (default "/ipxe/")
-dhcp-http-ipxe-binary-port [dhcp] HTTP iPXE binaries port to use in DHCP packets (default "8080")
-dhcp-http-ipxe-binary-scheme [dhcp] HTTP iPXE binaries scheme to use in DHCP packets (default "http")
-dhcp-http-ipxe-script-host [dhcp] HTTP iPXE script host or IP to use in DHCP packets (default "%[1]v")
-dhcp-http-ipxe-script-path [dhcp] HTTP iPXE script path to use in DHCP packets (default "/auto.ipxe")
-dhcp-http-ipxe-script-port [dhcp] HTTP iPXE script port to use in DHCP packets (default "8080")
-dhcp-http-ipxe-script-prepend-mac [dhcp] prepend the hardware MAC address to iPXE script URL base, http://1.2.3.4/auto.ipxe -> http://1.2.3.4/40:15:ff:89:cc:0e/auto.ipxe (default "true")
-dhcp-http-ipxe-script-scheme [dhcp] HTTP iPXE script scheme to use in DHCP packets (default "http")
-dhcp-http-ipxe-script-url [dhcp] HTTP iPXE script URL to use in DHCP packets, this overrides the flags for dhcp-http-ipxe-script-{scheme, host, port, path}
-dhcp-iface [dhcp] interface to bind to for DHCP requests
-dhcp-ip-for-packet [dhcp] IP address to use in DHCP packets (opt 54, etc) (default "%[1]v")
-dhcp-mode [dhcp] DHCP mode (reservation, proxy, auto-proxy) (default "reservation")
-dhcp-syslog-ip [dhcp] Syslog server IP address to use in DHCP packets (opt 7) (default "%[1]v")
-dhcp-tftp-ip [dhcp] TFTP server IP address to use in DHCP packets (opt 66, etc) (default "%[1]v")
-dhcp-tftp-port [dhcp] TFTP server port to use in DHCP packets (opt 66, etc) (default "69")
-extra-kernel-args [http] extra set of kernel args (k=v k=v) that are appended to the kernel cmdline iPXE script
-http-addr [http] local IP to listen on for iPXE HTTP script requests (default "%[1]v")
-http-ipxe-binary-enabled [http] enable iPXE HTTP binary server (default "true")
-http-ipxe-script-enabled [http] enable iPXE HTTP script server (default "true")
-http-port [http] local port to listen on for iPXE HTTP script requests (default "8080")
-ipxe-script-retries [http] number of retries to attempt when fetching kernel and initrd files in the iPXE script (default "0")
-ipxe-script-retry-delay [http] delay (in seconds) between retries when fetching kernel and initrd files in the iPXE script (default "2")
-osie-url [http] URL where OSIE (HookOS) images are located
-tink-server [http] IP:Port for the Tink server
-tink-server-insecure-tls [http] use insecure TLS for Tink server (default "false")
-tink-server-tls [http] use TLS for Tink server (default "false")
-trusted-proxies [http] comma separated list of trusted proxies in CIDR notation
-iso-enabled [iso] enable patching an OSIE ISO (default "false")
-iso-magic-string [iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS
-iso-static-ipam-enabled [iso] enable static IPAM for HookOS (default "false")
-iso-url [iso] an ISO source URL target for patching
-otel-endpoint [otel] OpenTelemetry collector endpoint
-otel-insecure [otel] OpenTelemetry collector insecure (default "true")
-syslog-addr [syslog] local IP to listen on for Syslog messages (default "%[1]v")
-syslog-enabled [syslog] enable Syslog server(receiver) (default "true")
-syslog-port [syslog] local port to listen on for Syslog messages (default "514")
-ipxe-script-patch [tftp/http] iPXE script fragment to patch into served iPXE binaries served via TFTP or HTTP
-tftp-addr [tftp] local IP to listen on for iPXE TFTP binary requests (default "%[1]v")
-tftp-block-size [tftp] TFTP block size a value between 512 (the default block size for TFTP) and 65456 (the max size a UDP packet payload can be) (default "512")
-tftp-enabled [tftp] enable iPXE TFTP binary server) (default "true")
-tftp-port [tftp] local port to listen on for iPXE TFTP binary requests (default "69")
-tftp-timeout [tftp] iPXE TFTP binary server requests timeout (default "5s")
`, defaultIP)
c := &config{}
fs := flag.NewFlagSet(name, flag.ContinueOnError)
cli := newCLI(c, fs)
got := customUsageFunc(cli)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatal(diff)
}
}
================================================
FILE: cmd/smee/main.go
================================================
package main
import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net"
"net/netip"
"net/url"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/go-logr/logr"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/tinkerbell/ipxedust"
"github.com/tinkerbell/ipxedust/ihttp"
"github.com/tinkerbell/smee/internal/dhcp/handler"
"github.com/tinkerbell/smee/internal/dhcp/handler/proxy"
"github.com/tinkerbell/smee/internal/dhcp/handler/reservation"
"github.com/tinkerbell/smee/internal/dhcp/server"
"github.com/tinkerbell/smee/internal/ipxe/http"
"github.com/tinkerbell/smee/internal/ipxe/script"
"github.com/tinkerbell/smee/internal/iso"
"github.com/tinkerbell/smee/internal/metric"
"github.com/tinkerbell/smee/internal/otel"
"github.com/tinkerbell/smee/internal/syslog"
"golang.org/x/sync/errgroup"
)
var (
// GitRev is the git revision of the build. It is set by the Makefile.
GitRev = "unknown (use make)"
startTime = time.Now()
)
const (
name = "smee"
dhcpModeProxy dhcpMode = "proxy"
dhcpModeReservation dhcpMode = "reservation"
dhcpModeAutoProxy dhcpMode = "auto-proxy"
// magicString comes from the HookOS repo
// ref: https://github.com/tinkerbell/hook/blob/main/linuxkit-templates/hook.template.yaml
magicString = `464vn90e7rbj08xbwdjejmdf4it17c5zfzjyfhthbh19eij201hjgit021bmpdb9ctrc87x2ymc8e7icu4ffi15x1hah9iyaiz38ckyap8hwx2vt5rm44ixv4hau8iw718q5yd019um5dt2xpqqa2rjtdypzr5v1gun8un110hhwp8cex7pqrh2ivh0ynpm4zkkwc8wcn367zyethzy7q8hzudyeyzx3cgmxqbkh825gcak7kxzjbgjajwizryv7ec1xm2h0hh7pz29qmvtgfjj1vphpgq1zcbiiehv52wrjy9yq473d9t1rvryy6929nk435hfx55du3ih05kn5tju3vijreru1p6knc988d4gfdz28eragvryq5x8aibe5trxd0t6t7jwxkde34v6pj1khmp50k6qqj3nzgcfzabtgqkmeqhdedbvwf3byfdma4nkv3rcxugaj2d0ru30pa2fqadjqrtjnv8bu52xzxv7irbhyvygygxu1nt5z4fh9w1vwbdcmagep26d298zknykf2e88kumt59ab7nq79d8amnhhvbexgh48e8qc61vq2e9qkihzt1twk1ijfgw70nwizai15iqyted2dt9gfmf2gg7amzufre79hwqkddc1cd935ywacnkrnak6r7xzcz7zbmq3kt04u2hg1iuupid8rt4nyrju51e6uejb2ruu36g9aibmz3hnmvazptu8x5tyxk820g2cdpxjdij766bt2n3djur7v623a2v44juyfgz80ekgfb9hkibpxh3zgknw8a34t4jifhf116x15cei9hwch0fye3xyq0acuym8uhitu5evc4rag3ui0fny3qg4kju7zkfyy8hwh537urd5uixkzwu5bdvafz4jmv7imypj543xg5em8jk8cgk7c4504xdd5e4e71ihaumt6u5u2t1w7um92fepzae8p0vq93wdrd1756npu1pziiur1payc7kmdwyxg3hj5n4phxbc29x0tcddamjrwt260b0w`
)
type config struct {
syslog syslogConfig
tftp tftp
ipxeHTTPBinary ipxeHTTPBinary
ipxeHTTPScript ipxeHTTPScript
dhcp dhcpConfig
iso isoConfig
// loglevel is the log level for smee.
logLevel string
backends dhcpBackends
otel otelConfig
}
type syslogConfig struct {
enabled bool
bindAddr string
bindPort int
}
type tftp struct {
bindAddr string
bindPort int
blockSize int
enabled bool
ipxeScriptPatch string
timeout time.Duration
}
type ipxeHTTPBinary struct {
enabled bool
}
type ipxeHTTPScript struct {
enabled bool
bindAddr string
bindPort int
extraKernelArgs string
hookURL string
tinkServer string
tinkServerUseTLS bool
tinkServerInsecureTLS bool
trustedProxies string
retries int
retryDelay int
}
type dhcpMode string
type dhcpConfig struct {
enabled bool
mode string
bindAddr string
bindInterface string
ipForPacket string
syslogIP string
tftpIP string
tftpPort int
httpIpxeBinaryURL urlBuilder
httpIpxeScript httpIpxeScript
httpIpxeScriptURL string
}
type urlBuilder struct {
Scheme string
Host string
Port int
Path string
}
type httpIpxeScript struct {
urlBuilder
// injectMacAddress will prepend the hardware mac address to the ipxe script URL file name.
// For example: http://1.2.3.4/my/loc/auto.ipxe -> http://1.2.3.4/my/loc/40:15:ff:89:cc:0e/auto.ipxe
// Setting this to false is useful when you are not using the auto.ipxe script in Smee.
injectMacAddress bool
}
type dhcpBackends struct {
file File
kubernetes Kube
Noop Noop
}
type otelConfig struct {
endpoint string
insecure bool
}
type isoConfig struct {
enabled bool
url string
magicString string
staticIPAMEnabled bool
}
func main() {
cfg := &config{}
cli := newCLI(cfg, flag.NewFlagSet(name, flag.ExitOnError))
_ = cli.Parse(os.Args[1:])
log := defaultLogger(cfg.logLevel)
log.Info("starting", "version", GitRev)
ctx, done := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGHUP, syscall.SIGTERM)
defer done()
oCfg := otel.Config{
Servicename: "smee",
Endpoint: cfg.otel.endpoint,
Insecure: cfg.otel.insecure,
Logger: log,
}
ctx, otelShutdown, err := otel.Init(ctx, oCfg)
if err != nil {
log.Error(err, "failed to initialize OpenTelemetry")
panic(err)
}
defer otelShutdown()
metric.Init()
g, ctx := errgroup.WithContext(ctx)
// syslog
if cfg.syslog.enabled {
addr := fmt.Sprintf("%s:%d", cfg.syslog.bindAddr, cfg.syslog.bindPort)
log.Info("starting syslog server", "bind_addr", addr)
g.Go(func() error {
if err := syslog.StartReceiver(ctx, log, addr, 1); err != nil {
log.Error(err, "syslog server failure")
return err
}
<-ctx.Done()
log.Info("syslog server stopped")
return nil
})
}
// tftp
if cfg.tftp.enabled {
tftpServer := &ipxedust.Server{
Log: log.WithValues("service", "github.com/tinkerbell/smee").WithName("github.com/tinkerbell/ipxedust"),
HTTP: ipxedust.ServerSpec{Disabled: true}, // disabled because below we use the http handlerfunc instead.
EnableTFTPSinglePort: true,
}
tftpServer.EnableTFTPSinglePort = true
addr := fmt.Sprintf("%s:%d", cfg.tftp.bindAddr, cfg.tftp.bindPort)
if ip, err := netip.ParseAddrPort(addr); err == nil {
tftpServer.TFTP = ipxedust.ServerSpec{
Disabled: false,
Addr: ip,
Timeout: cfg.tftp.timeout,
Patch: []byte(cfg.tftp.ipxeScriptPatch),
BlockSize: cfg.tftp.blockSize,
}
// start the ipxe binary tftp server
log.Info("starting tftp server", "bind_addr", addr)
g.Go(func() error {
return tftpServer.ListenAndServe(ctx)
})
} else {
log.Error(err, "invalid bind address")
panic(fmt.Errorf("invalid bind address: %w", err))
}
}
handlers := http.HandlerMapping{}
// http ipxe binaries
if cfg.ipxeHTTPBinary.enabled {
// serve ipxe binaries from the "/ipxe/" URI.
handlers["/ipxe/"] = ihttp.Handler{
Log: log.WithValues("service", "github.com/tinkerbell/smee").WithName("github.com/tinkerbell/ipxedust"),
Patch: []byte(cfg.tftp.ipxeScriptPatch),
}.Handle
}
// http ipxe script
if cfg.ipxeHTTPScript.enabled {
br, err := cfg.backend(ctx, log)
if err != nil {
panic(fmt.Errorf("failed to create backend: %w", err))
}
jh := script.Handler{
Logger: log,
Backend: br,
OSIEURL: cfg.ipxeHTTPScript.hookURL,
ExtraKernelParams: strings.Split(cfg.ipxeHTTPScript.extraKernelArgs, " "),
PublicSyslogFQDN: cfg.dhcp.syslogIP,
TinkServerTLS: cfg.ipxeHTTPScript.tinkServerUseTLS,
TinkServerInsecureTLS: cfg.ipxeHTTPScript.tinkServerInsecureTLS,
TinkServerGRPCAddr: cfg.ipxeHTTPScript.tinkServer,
IPXEScriptRetries: cfg.ipxeHTTPScript.retries,
IPXEScriptRetryDelay: cfg.ipxeHTTPScript.retryDelay,
StaticIPXEEnabled: (dhcpMode(cfg.dhcp.mode) == dhcpModeAutoProxy),
}
// serve ipxe script from the "/" URI.
handlers["/"] = jh.HandlerFunc()
}
if cfg.iso.enabled {
br, err := cfg.backend(ctx, log)
if err != nil {
panic(fmt.Errorf("failed to create backend: %w", err))
}
ih := iso.Handler{
Logger: log,
Backend: br,
SourceISO: cfg.iso.url,
ExtraKernelParams: strings.Split(cfg.ipxeHTTPScript.extraKernelArgs, " "),
Syslog: cfg.dhcp.syslogIP,
TinkServerTLS: cfg.ipxeHTTPScript.tinkServerUseTLS,
TinkServerGRPCAddr: cfg.ipxeHTTPScript.tinkServer,
StaticIPAMEnabled: cfg.iso.staticIPAMEnabled,
MagicString: func() string {
if cfg.iso.magicString == "" {
return magicString
}
return cfg.iso.magicString
}(),
}
isoHandler, err := ih.HandlerFunc()
if err != nil {
panic(fmt.Errorf("failed to create iso handler: %w", err))
}
handlers["/iso/"] = isoHandler
}
if len(handlers) > 0 {
// start the http server for ipxe binaries and scripts
tp := parseTrustedProxies(cfg.ipxeHTTPScript.trustedProxies)
httpServer := &http.Config{
GitRev: GitRev,
StartTime: startTime,
Logger: log,
TrustedProxies: tp,
}
bindAddr := fmt.Sprintf("%s:%d", cfg.ipxeHTTPScript.bindAddr, cfg.ipxeHTTPScript.bindPort)
log.Info("serving http", "addr", bindAddr, "trusted_proxies", tp)
g.Go(func() error {
return httpServer.ServeHTTP(ctx, bindAddr, handlers)
})
}
// dhcp serving
if cfg.dhcp.enabled {
dh, err := cfg.dhcpHandler(ctx, log)
if err != nil {
log.Error(err, "failed to create dhcp listener")
panic(fmt.Errorf("failed to create dhcp listener: %w", err))
}
log.Info("starting dhcp server", "bind_addr", cfg.dhcp.bindAddr)
g.Go(func() error {
bindAddr, err := netip.ParseAddrPort(cfg.dhcp.bindAddr)
if err != nil {
panic(fmt.Errorf("invalid tftp address for DHCP server: %w", err))
}
conn, err := server4.NewIPv4UDPConn(cfg.dhcp.bindInterface, net.UDPAddrFromAddrPort(bindAddr))
if err != nil {
panic(err)
}
defer conn.Close()
ds := &server.DHCP{Logger: log, Conn: conn, Handlers: []server.Handler{dh}}
return ds.Serve(ctx)
})
}
if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {
log.Error(err, "failed running all Smee services")
panic(err)
}
log.Info("smee is shutting down")
}
func numTrue(b ...bool) int {
n := 0
for _, v := range b {
if v {
n++
}
}
return n
}
func (c *config) backend(ctx context.Context, log logr.Logger) (handler.BackendReader, error) {
if c.backends.file.Enabled || c.backends.Noop.Enabled {
// the kubernetes backend is enabled by default so we disable it
// if another backend is enabled so that users don't have to explicitly
// set the CLI flag to disable it when using another backend.
c.backends.kubernetes.Enabled = false
}
var be handler.BackendReader
switch {
case numTrue(c.backends.file.Enabled, c.backends.kubernetes.Enabled, c.backends.Noop.Enabled) > 1:
return nil, errors.New("only one backend can be enabled at a time")
case c.backends.Noop.Enabled:
if c.dhcp.mode != string(dhcpModeAutoProxy) {
return nil, errors.New("noop backend can only be used with --dhcp-mode=auto-proxy")
}
be = c.backends.Noop.backend()
case c.backends.file.Enabled:
b, err := c.backends.file.backend(ctx, log)
if err != nil {
return nil, fmt.Errorf("failed to create file backend: %w", err)
}
be = b
default: // default backend is kubernetes
b, err := c.backends.kubernetes.backend(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create kubernetes backend: %w", err)
}
be = b
}
return be, nil
}
func (c *config) dhcpHandler(ctx context.Context, log logr.Logger) (server.Handler, error) {
// 1. create the handler
// 2. create the backend
// 3. add the backend to the handler
pktIP, err := netip.ParseAddr(c.dhcp.ipForPacket)
if err != nil {
return nil, fmt.Errorf("invalid bind address: %w", err)
}
tftpIP, err := netip.ParseAddrPort(fmt.Sprintf("%s:%d", c.dhcp.tftpIP, c.dhcp.tftpPort))
if err != nil {
return nil, fmt.Errorf("invalid tftp address for DHCP server: %w", err)
}
httpBinaryURL := &url.URL{
Scheme: c.dhcp.httpIpxeBinaryURL.Scheme,
Host: fmt.Sprintf("%s:%d", c.dhcp.httpIpxeBinaryURL.Host, c.dhcp.httpIpxeBinaryURL.Port),
Path: c.dhcp.httpIpxeBinaryURL.Path,
}
if _, err := url.Parse(httpBinaryURL.String()); err != nil {
return nil, fmt.Errorf("invalid http ipxe binary url: %w", err)
}
var httpScriptURL *url.URL
if c.dhcp.httpIpxeScriptURL != "" {
httpScriptURL, err = url.Parse(c.dhcp.httpIpxeScriptURL)
if err != nil {
return nil, fmt.Errorf("invalid http ipxe script url: %w", err)
}
} else {
httpScriptURL = &url.URL{
Scheme: c.dhcp.httpIpxeScript.Scheme,
Host: func() string {
switch c.dhcp.httpIpxeScript.Scheme {
case "http":
if c.dhcp.httpIpxeScript.Port == 80 {
return c.dhcp.httpIpxeScript.Host
}
case "https":
if c.dhcp.httpIpxeScript.Port == 443 {
return c.dhcp.httpIpxeScript.Host
}
}
return fmt.Sprintf("%s:%d", c.dhcp.httpIpxeScript.Host, c.dhcp.httpIpxeScript.Port)
}(),
Path: c.dhcp.httpIpxeScript.Path,
}
}
if _, err := url.Parse(httpScriptURL.String()); err != nil {
return nil, fmt.Errorf("invalid http ipxe script url: %w", err)
}
ipxeScript := func(*dhcpv4.DHCPv4) *url.URL {
return httpScriptURL
}
if c.dhcp.httpIpxeScript.injectMacAddress {
ipxeScript = func(d *dhcpv4.DHCPv4) *url.URL {
u := *httpScriptURL
p := path.Base(u.Path)
u.Path = path.Join(path.Dir(u.Path), d.ClientHWAddr.String(), p)
return &u
}
}
backend, err := c.backend(ctx, log)
if err != nil {
return nil, fmt.Errorf("failed to create backend: %w", err)
}
switch dhcpMode(c.dhcp.mode) {
case dhcpModeReservation:
syslogIP, err := netip.ParseAddr(c.dhcp.syslogIP)
if err != nil {
return nil, fmt.Errorf("invalid syslog address: %w", err)
}
dh := &reservation.Handler{
Backend: backend,
IPAddr: pktIP,
Log: log,
Netboot: reservation.Netboot{
IPXEBinServerTFTP: tftpIP,
IPXEBinServerHTTP: httpBinaryURL,
IPXEScriptURL: ipxeScript,
Enabled: true,
},
OTELEnabled: true,
SyslogAddr: syslogIP,
}
return dh, nil
case dhcpModeProxy:
dh := &proxy.Handler{
Backend: backend,
IPAddr: pktIP,
Log: log,
Netboot: proxy.Netboot{
IPXEBinServerTFTP: tftpIP,
IPXEBinServerHTTP: httpBinaryURL,
IPXEScriptURL: ipxeScript,
Enabled: true,
},
OTELEnabled: true,
AutoProxyEnabled: false,
}
return dh, nil
case dhcpModeAutoProxy:
dh := &proxy.Handler{
Backend: backend,
IPAddr: pktIP,
Log: log,
Netboot: proxy.Netboot{
IPXEBinServerTFTP: tftpIP,
IPXEBinServerHTTP: httpBinaryURL,
IPXEScriptURL: ipxeScript,
Enabled: true,
},
OTELEnabled: true,
AutoProxyEnabled: true,
}
return dh, nil
}
return nil, errors.New("invalid dhcp mode")
}
// defaultLogger uses the slog logr implementation.
func defaultLogger(level string) logr.Logger {
// source file and function can be long. This makes the logs less readable.
// truncate source file and function to last 3 parts for improved readability.
customAttr := func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.SourceKey {
ss, ok := a.Value.Any().(*slog.Source)
if !ok || ss == nil {
return a
}
f := strings.Split(ss.Function, "/")
if len(f) > 3 {
ss.Function = filepath.Join(f[len(f)-3:]...)
}
p := strings.Split(ss.File, "/")
if len(p) > 3 {
ss.File = filepath.Join(p[len(p)-3:]...)
}
return a
}
return a
}
opts := &slog.HandlerOptions{AddSource: true, ReplaceAttr: customAttr}
switch level {
case "debug":
opts.Level = slog.LevelDebug
default:
opts.Level = slog.LevelInfo
}
log := slog.New(slog.NewJSONHandler(os.Stdout, opts))
return logr.FromSlogHandler(log.Handler())
}
func parseTrustedProxies(trustedProxies string) (result []string) {
for _, cidr := range strings.Split(trustedProxies, ",") {
cidr = strings.TrimSpace(cidr)
if cidr == "" {
continue
}
_, _, err := net.ParseCIDR(cidr)
if err != nil {
// Its not a cidr, but maybe its an IP
if ip := net.ParseIP(cidr); ip != nil {
if ip.To4() != nil {
cidr += "/32"
} else {
cidr += "/128"
}
} else {
// not an IP, panic
panic("invalid ip cidr in TRUSTED_PROXIES cidr=" + cidr)
}
}
result = append(result, cidr)
}
return result
}
func (d dhcpMode) String() string {
return string(d)
}
================================================
FILE: contrib/tag-release.sh
================================================
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
if [ -z "${1-}" ]; then
echo "Must specify new tag"
exit 1
fi
new_tag=${1-}
[[ $new_tag =~ ^v[0-9]*\.[0-9]*\.[0-9]*$ ]] || (
echo "Tag must be in the form of vX.Y.Z"
exit 1
)
if [[ $(git symbolic-ref HEAD) != refs/heads/main ]] && [[ -z ${ALLOW_NON_MAIN:-} ]]; then
echo "Must be on main branch" >&2
exit 1
fi
if [[ $(git describe --dirty) != $(git describe) ]]; then
echo "Repo must be in a clean state" >&2
exit 1
fi
git fetch --all
last_tag=$(git describe --abbrev=0)
last_tag_commit=$(git rev-list -n1 "$last_tag")
last_specific_tag=$(git tag --contains="$last_tag_commit" | grep -E "^v[0-9]*\.[0-9]*\.[0-9]*$" | tail -n 1)
last_specific_tag_commit=$(git rev-list -n1 "$last_specific_tag")
if [[ $last_specific_tag_commit == $(git rev-list -n1 HEAD) ]]; then
echo "No commits since last tag" >&2
exit 1
fi
if [[ -n ${SIGN_TAG-} ]]; then
git tag -s -m "${new_tag}" "${new_tag}" &>/dev/null && echo "created signed tag ${new_tag}" >&2 && exit
else
git tag -a -m "${new_tag}" "${new_tag}" &>/dev/null && echo "created annotated tag ${new_tag}" >&2 && exit
fi
================================================
FILE: docker-compose.yml
================================================
---
# Provides a docker-compose configuration for local fast iteration when
# hacking on smee alone.
# TODO: figure out if NET_ADMIN capability is really necessary
version: "3.8"
# use a custom network configuration to enable macvlan mode and set explicit
# IPs and MACs as well as support mainstream DHCP clients for easier testing
# standalone-hardware.json references these IPs and MACs so we can write
# (simpler) assertions against behavior on the client side.
networks:
smee-test:
# enables a more realistic L2 network for the containers
driver: macvlan
ipam:
driver: default
config:
- subnet: 192.168.99.0/24
gateway: 192.168.99.1
services:
smee:
build: .
# entrypoint: ["/usr/bin/smee", "--dhcp-addr", "0.0.0.0:67"]
entrypoint: ["/start-smee.sh"]
networks:
smee-test:
ipv4_address: 192.168.99.42
mac_address: 02:00:00:00:00:01
environment:
SMEE_TINK_SERVER: tink-server:42113
SMEE_BACKEND_KUBE_ENABLED: false
SMEE_BACKEND_FILE_ENABLED: true
SMEE_BACKEND_FILE_PATH: /hardware.yaml
SMEE_OSIE_URL: "http://192.168.8.5/osie/artifacts/"
OTEL_EXPORTER_OTLP_ENDPOINT: otel-collector:4317
OTEL_EXPORTER_OTLP_INSECURE: "true"
volumes:
- ./test/hardware.yaml:/hardware.yaml
- ./test/start-smee.sh:/start-smee.sh
cap_add:
- NET_ADMIN
# eventually want to add more client containers, including one that smee will
# not recognize so we can validate it won't serve content to IPs it's not
# managing
client:
depends_on:
- smee
build: test
networks:
smee-test:
ipv4_address: 192.168.99.43
mac_address: 02:00:00:00:00:ff
cap_add:
- NET_ADMIN
otel-collector:
image: otel/opentelemetry-collector-contrib:0.38.0
networks:
smee-test:
ipv4_address: 192.168.99.44
volumes:
- ./test/otel-collector.yaml:/etc/otel-collector.yaml
command: --config /etc/otel-collector.yaml
ports:
- "4317:4317"
================================================
FILE: docs/Backend-File.md
================================================
# File Watcher Backend
This document gives an overview of the file watcher backend.
This backend will read in and watch a file on disk for changes.
The data from this file will then be used for serving DHCP requests.
## Why
This backend exists mainly for testing and development.
It allows the DHCP server to be run without having to spin up any additional backend servers, like [Tink](https://github.com/tinkerbell/tink) or [Cacher](https://github.com/packethost/cacher).
## Usage
```bash
# See the file example/main.go for details on how to select and use this backend in code.
go run example/main.go
```
Below is an example of the format used for this file watcher backend.
See this [example.yaml](../backend/file/testdata/example.yaml) for a full working example of the data model.
```yaml
---
08:00:27:29:4E:67:
ipAddress: "192.168.2.153"
subnetMask: "255.255.255.0"
defaultGateway: "192.168.2.1"
nameServers:
- "8.8.8.8"
- "1.1.1.1"
hostname: "pxe-virtualbox"
domainName: "example.com"
broadcastAddress: "192.168.2.255"
ntpServers:
- "132.163.96.2"
- "132.163.96.3"
leaseTime: 86400
domainSearch:
- "example.com"
netboot:
allowPxe: true
ipxeScriptUrl: "https://boot.netboot.xyz"
52:54:00:aa:88:2a:
ipAddress: "192.168.2.15"
subnetMask: "255.255.255.0"
defaultGateway: "192.168.2.1"
nameServers:
- "8.8.8.8"
- "1.1.1.1"
hostname: "sandbox"
domainName: "example.com"
broadcastAddress: "192.168.2.255"
ntpServers:
- "132.163.96.2"
- "132.163.96.3"
leaseTime: 86400
domainSearch:
- "example.com"
netboot:
allowPxe: true
ipxeScriptUrl: "https://boot.netboot.xyz"
```
================================================
FILE: docs/Code-Structure.md
================================================
# Code Structure
## Backend
Responsible for communicating with an external persistence source and returning data from said source.
Backends live in the `backend/` directory.
## Handler
Responsible for reading a DHCP packet from a source, calling a backend, and responding to the source.
All business logic for responding or reacting to DHCP messages lives here.
Handlers live in the `handler/` directory.
## Listener
Responsible for listening for UDP packets on the specified address and port.
A default listener can be used.
## Server
Responsible for filtering for DHCP packets received by the listener and calling the specified handler.
## Functional description
Server(listener, handler(backend))
================================================
FILE: docs/DCO.md
================================================
# DCO Sign Off
All authors to the project retain copyright to their work. However, to ensure
that they are only submitting work that they have rights to, we are requiring
everyone to acknowledge this by signing their work.
Since this signature indicates your rights to the contribution and
certifies the statements below, it must contain your real name and
email address. Various forms of noreply email address must not be used.
Any copyright notices in this repository should specify the authors as "The
project authors".
To sign your work, just add a line like this at the end of your commit message:
```bash
Signed-off-by: Jess Owens <jowens@tinkerbell.org>
```
This can easily be done with the `--signoff` option to `git commit`.
By doing this you state that you can certify the following (from [https://developercertificate.org/][1]):
```text
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
1 Letterman Drive
Suite D4700
San Francisco, CA, 94129
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
================================================
FILE: docs/DESIGN.md
================================================
# Smee Design Details
## Table of Contents
- [Smee Flow](#Smee-Flow)
- [Smee Installers](#Smee-Installers)
- [IPXE](#IPXE)
---
## Smee Flow
High-level traffic flow for Smee.

<details>
<summary>Smee Flow Code</summary>
Copy and paste the code below into [https://www.websequencediagrams.com](https://www.websequencediagrams.com) to modify
```flow
title Smee Flow
# DHCP
note over Machine: DHCP start
Machine->Smee: 1. DHCP Discover
Smee->Tink: 2. Get Hardware data from MAC
Tink->Smee: 3. Send Hardware data
Smee->Machine: 4. DHCP Offer
Machine->Smee: 5. DHCP Request
Smee->Tink: 6. Get Hardware data from MAC
Tink->Smee: 7. Send Hardware data
Smee->Machine: 8. DHCP Ack
note over Machine: DHCP end
# TFTP
note over Machine: TFTP start
Machine->Smee: 9. TFTP Get ipxe binary
Smee->Tink: 10. Get Hardware data from IP
Tink->Smee: 11. Send Hardware data
Smee->Machine: 12. Send ipxe binary
note over Machine: TFTP end
# DHCP
note over Machine: DHCP start
Machine->Smee: 13. DHCP Discover
Smee->Tink: 14. Get Hardware data from MAC
Tink->Smee: 15. Send Hardware data
Smee->Machine: 16. DHCP Offer
Machine->Smee: 17. DHCP Request
Smee->Tink: 18. Get Hardware data from MAC
Tink->Smee: 19. Send Hardware data
Smee->Machine: 20. DHCP Ack
note over Machine: DHCP end
# HTTP
note over Machine: HTTP start
Machine->Smee: 21. HTTP Get ipxe script
Smee->Tink: 22. Get Hardware data from IP
Tink->Smee: 23. Send Hardware data
Smee->Machine: 24. Send ipxe script
note over Machine: HTTP start
```
</details>
## Smee Installers
A Smee Installer is a custom iPXE script.
The code for each Installer lives in `installers/`
The idea of iPXE Installers that live in-tree here is an idea that doesn't follow the existing template/workflow paradigm.
Installers should eventually be deprecated.
The deprecation process is forthcoming.
### How an Installers is requested
During a PXE boot request, an iPXE script is provided to a PXE-ing machine through a dynamically generated endpoint (http://smee.addr/auto.ipxe).
The contents of the auto.ipxe script is determined through the following steps:
1. A hardware record is retrieved based on the PXE-ing machines mac address.
2. The following are tried, in order, to determine the content of the iPXE script ([code ref](https://github.com/tinkerbell/smee/blob/b2f4d15f9b55806f4636003948ed95975e1d475e/job/ipxe.go#L71))
1. If the `metadata.instance.operating_system.slug` matches a registered Installer, the iPXE script from that Installer is returned
2. If the `metadata.instance.operating_system.distro` matches a registered Installer, the iPXE script from that Installer
3. If neither of the first 2 is matched, then the default (OSIE) iPXE script is used
### Registering an Installer
To register an Installer, at a minimum, the following is required
1. A [blank import](https://github.com/golang/go/wiki/CodeReviewComments#import-blank) for your Installer should be added to `main.go`
2. Your Installer pkg needs an `func init()` that calls `job.RegisterSlug("InstallerName", funcThatReturnsAnIPXEScript)`
### Testing Installers
Unit tests should be created to validate that your registered func returns the iPXE script you're expecting.
Functional tests would be great but depending on what is in your iPXE script might be difficult because of external dependencies.
At a minimum try to create documentation that details these dependencies so that others can make them available for testing changes.
## IPXE
Smee serves the upstream IPXE binaries built from [https://github.com/ipxe/ipxe](https://github.com/ipxe/ipxe).
The IPXE binaries are built from source and then embedded into the Smee Go binary to be served via TFTP.
### Building the IPXE binary
The IPXE binaries from [https://github.com/ipxe/ipxe](https://github.com/ipxe/ipxe) are built via a Make target.
```make
make bindata
```
================================================
FILE: docs/DESIGNPHILOSOPHY.md
================================================
# Design Philosophy
This living document describes some Go design philosophies we endeavor to incorporate when working, building, or writing in Go.
## General
1. Prefer easy to understand over easy to do
2. First do it, then do it right, then do it better, then make it testable [14]
3. When you spawn goroutines, make it clear when - or whether - they exit. [2]
4. Packages that are imported only for their side effects should be avoided [4]
5. Package level and global variables should be avoided
6. magic is bad; global state is magic → no package level vars; no func init [13]
## Dependencies
1. External dependencies should be tried and fail fast or just keep trying
- For example, external connections, port binding, environment variables, secrets, etc
- Examples of "failing fast"
- Try external connections immediately
- Binding to ports immediately
- Examples of "keep trying"
- Block ingress traffic or calls until external connections are successful
- Should be accompanied by some way to check health status of external connections
2. Make all dependencies explicit [11]
## Naming
1. Naming general rules [12]
- Structs are plain nouns: API, Replica, Object
- Interfaces are active nouns: Reader, Writer, JobProcessor
- Functions and methods are verbs: Read, Process, Sync
2. Package names [15]
- Short: no more than one word
- No plural
- Lower case
- Informative about the service it provides
- Avoid packages named utility/utilities or model/models
3. Avoid renaming imports except to avoid a name collision; good package names should not require renaming [3]
## Interfaces
1. Accept interfaces, return structs [5]
2. Small interfaces are better [6]
3. Define an interface when you actually need it, not when you foresee needing it [7]
4. Interfaces [15]
- Use interfaces as function/method arguments & as field types
- Small interfaces are better
## Functions/Methods
1. All top-level, exported names should have doc comments, as should non-trivial unexported type or function declarations. [1]
2. Methods/functions [15]
- One function has one goal
- Simple names
- Reduce the number of nesting levels
3. Only func main has the right to decide which flags, env variables, config files are available to the user [10a],[10b]
4. `context.Context` should, in most cases, be the first argument of all functions or methods
5. Prefer synchronous functions - functions which return their results directly or finish any callbacks or channel ops before returning - over asynchronous ones. [8]
## Errors
1. Error Handling [15]
- Func `main` should normally be the only one calling fatal errors or `os.Exit`
## Source files
1. One file should be named like the package [9]
2. One file = One responsibility [9]
3. If you only have one command prefer a top level `main.go`, if you have more than one command put them in a `cmd/` package
---
[1]: https://github.com/golang/go/wiki/CodeReviewComments#doc-comments
[2]: https://github.com/golang/go/wiki/CodeReviewComments#goroutine-lifetimes
[3]: https://github.com/golang/go/wiki/CodeReviewComments#imports
[4]: https://github.com/golang/go/wiki/CodeReviewComments#import-blank
[5]: https://medium.com/@cep21/what-accept-interfaces-return-structs-means-in-go-2fe879e25ee8
[6]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#use-interfaces
[7]: http://c2.com/xp/YouArentGonnaNeedIt.html
[8]: https://github.com/golang/go/wiki/CodeReviewComments#synchronous-functions
[9]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#source-files
[10a]: https://thoughtbot.com/blog/where-to-define-command-line-flags-in-go
[10b]: https://peter.bourgon.org/go-best-practices-2016/#configuration
[11]: https://peter.bourgon.org/go-best-practices-2016/#top-tip-9
[12]: https://twitter.com/peterbourgon/status/1121023995107782656
[13]: https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html
[14]: https://code.tutsplus.com/articles/master-developers-addy-osmani--net-31661
[15]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#key-takeaways
================================================
FILE: docs/DHCP.md
================================================
# Use an existing DHCP service
There can be numerous reasons why you may want to use an existing DHCP service instead of Smee: Security, compliance, access issues, existing layer 2 constraints, existing automation, and so on.
In environments where there is an existing DHCP service, this DHCP service can be configured to interoperate with Smee. This document will cover how to make your existing DHCP service interoperate with Smee. In this scenario Smee will have no layer 2 DHCP responsibilities.
> Note: Currently, Smee is responsible for more than just DHCP. So generally speaking, Smee can't be entirely avoided in the provisioning process.
## Additional Services in Smee
- HTTP and TFTP servers for iPXE binaries
- HTTP server for iPXE script
- Syslog server (receiver)
## Process
As a prerequisite, your existing DHCP must serve [host/address/static reservations](https://kb.isc.org/docs/what-are-host-reservations-how-to-use-them) for all machines. The IP address you select will need to be used in a corresponding Hardware object.
Configure your existing DHCP service to provide the location of the iPXE binary and script. This is a two-step interaction between machines and the DHCP service and enables the network boot process to start.
- **Step 1**: The machine broadcasts a request to network boot. Your existing DHCP service then provides the machine with all IPAM info as well as the location of the Tinkerbell iPXE binary (`ipxe.efi`). The machine configures its network interface with the IPAM info then downloads the Tinkerbell iPXE binary from the location provided by the DHCP service and runs it.
- **Step 2**: Now with the Tinkerbell iPXE binary loaded and running, iPXE again broadcasts a request to network boot. The DHCP service again provides all IPAM info as well as the location of the Tinkerbell iPXE script (`auto.ipxe`). iPXE configures its network interface using the IPAM info and then downloads the Tinkerbell iPXE script from the location provided by the DHCP service and runs it.
> Note The `auto.ipxe` is an [iPXE script](https://ipxe.org/scripting) that tells iPXE from where to download the [HookOS](https://github.com/tinkerbell/hook) kernel and initrd so that they can be loaded into memory.
The following diagram illustrates the process described above. Note that the diagram only describes the network booting parts of the DHCP interaction, not the exchange of IPAM info.

## Configuration
Below you will find code snippets showing how to add the two-step process from above to an existing DHCP service. Each config checks if DHCP option 77 ([user class option](https://www.rfc-editor.org/rfc/rfc3004.html)) equals "`Tinkerbell`". If it does match, then the Tinkerbell iPXE script (`auto.ipxe`) will be served. If option 77 does not match, then the iPXE binary (`ipxe.efi`) will be served.
### DHCP option: `next server`
Most DHCP services all customization of a `next server` option. This option generally corresponds to either DHCP option 66 or the DHCP header `sname`, [reference.](https://www.rfc-editor.org/rfc/rfc2132.html#section-9.4) This option is used to tell a machine where to download the initial bootloader, [reference.](https://networkboot.org/fundamentals/)
### Code snippets
The following code snippets are generic examples of the config needed to enable the two-step process to an existing DHCP service. It does not cover the IPAM info that is also required.
[dnsmasq](https://linux.die.net/man/8/dnsmasq)
`dnsmasq.conf`
```text
dhcp-match=tinkerbell, option:user-class, Tinkerbell
dhcp-boot=tag:!tinkerbell,ipxe.efi,none,192.168.2.112
dhcp-boot=tag:tinkerbell,http://192.168.2.112/auto.ipxe
```
[Kea DHCP](https://www.isc.org/kea/)
`kea.json`
```json
{
"Dhcp4": {
"client-classes": [
{
"name": "tinkerbell",
"test": "substring(option[77].hex,0,10) == 'Tinkerbell'",
"boot-file-name": "http://192.168.2.112/auto.ipxe"
},
{
"name": "default",
"test": "not(substring(option[77].hex,0,10) == 'Tinkerbell')",
"boot-file-name": "ipxe.efi"
}
],
"subnet4": [
{
"next-server": "192.168.2.112"
}
]
}
}
```
[ISC DHCP](https://ipxe.org/howto/dhcpd)
`dhcpd.conf`
```text
if exists user-class and option user-class = "Tinkerbell" {
filename "http://192.168.2.112/auto.ipxe";
} else {
filename "ipxe.efi";
}
next-server "192.168.1.112";
```
[Microsoft DHCP server](https://learn.microsoft.com/en-us/windows-server/networking/technologies/dhcp/dhcp-top)
Please follow the ipxe.org [guide](https://ipxe.org/howto/msdhcp) on how to configure Microsoft DHCP server.
================================================
FILE: docs/Design-Philosophy.md
================================================
# Design Philosophy
This living document describes some Go design philosophies we endeavor to incorporate when working, building, or writing in Go.
## General
1. Prefer easy to understand over easy to do
2. First do it, then do it right, then do it better, then make it testable [14]
3. When you spawn goroutines, make it clear when - or whether - they exit. [2]
4. Packages that are imported only for their side effects should be avoided [4]
5. Package level and global variables should be avoided
6. magic is bad; global state is magic → no package level vars; no func init [13]
## Dependencies
1. External dependencies should be tried and fail fast or just keep trying
- For example, external connections, port binding, environment variables, secrets, etc
- Examples of "failing fast"
- Try external connections immediately
- Binding to ports immediately
- Examples of "keep trying"
- Block ingress traffic or calls until external connections are successful
- Should be accompanied by some way to check health status of external connections
2. Make all dependencies explicit [11]
## Naming
1. Naming general rules [12]
- Structs are plain nouns: API, Replica, Object
- Interfaces are active nouns: Reader, Writer, JobProcessor
- Functions and methods are verbs: Read, Process, Sync
2. Package names [15]
- Short: no more than one word
- No plural
- Lower case
- Informative about the service it provides
- Avoid packages named utility/utilities or model/models
3. Avoid renaming imports except to avoid a name collision; good package names should not require renaming [3]
## Interfaces
1. Accept interfaces, return structs [5]
2. Small interfaces are better [6]
3. Define an interface when you actually need it, not when you foresee needing it [7]
4. Interfaces [15]
- Use interfaces as function/method arguments & as field types
- Small interfaces are better
## Functions/Methods
1. All top-level, exported names should have doc comments, as should non-trivial unexported type or function declarations. [1]
2. Methods/functions [15]
- One function has one goal
- Simple names
- Reduce the number of nesting levels
3. Only func main has the right to decide which flags, env variables, config files are available to the user [10a],[10b]
4. `context.Context` should, in most cases, be the first argument of all functions or methods
5. Prefer synchronous functions - functions which return their results directly or finish any callbacks or channel ops before returning - over asynchronous ones. [8]
## Errors
1. Error Handling [15]
- Func `main` should normally be the only one calling fatal errors or `os.Exit`
## Source files
1. One file should be named like the package [9]
2. One file = One responsibility [9]
3. If you only have one command prefer a top level `main.go`, if you have more than one command put them in a `cmd/` package
---
[1]: https://github.com/golang/go/wiki/CodeReviewComments#doc-comments
[2]: https://github.com/golang/go/wiki/CodeReviewComments#goroutine-lifetimes
[3]: https://github.com/golang/go/wiki/CodeReviewComments#imports
[4]: https://github.com/golang/go/wiki/CodeReviewComments#import-blank
[5]: https://medium.com/@cep21/what-accept-interfaces-return-structs-means-in-go-2fe879e25ee8
[6]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#use-interfaces
[7]: http://c2.com/xp/YouArentGonnaNeedIt.html
[8]: https://github.com/golang/go/wiki/CodeReviewComments#synchronous-functions
[9]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#source-files
[10a]: https://thoughtbot.com/blog/where-to-define-command-line-flags-in-go
[10b]: https://peter.bourgon.org/go-best-practices-2016/#configuration
[11]: https://peter.bourgon.org/go-best-practices-2016/#top-tip-9
[12]: https://twitter.com/peterbourgon/status/1121023995107782656
[13]: https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html
[14]: https://code.tutsplus.com/articles/master-developers-addy-osmani--net-31661
[15]: https://www.practical-go-lessons.com/chap-40-design-recommendations?s=03#key-takeaways
================================================
FILE: docs/ISO-Static-IPAM.md
================================================
# Static IP Address Management in the OSIE ISO
OSIE stands for operating system installation environment. In Tinkerbell we currently have just one, [HookOS](https://github.com/tinkerbell/hook).
Smee has the capability to Patch the HookOS ISO at runtime to include information about the target machine's network configuration. This is enabled by setting the CLI flag `-iso-static-ipam-enabled=true` along with both `-iso-enabled` and `-iso-url`.
This document defines the specification/data format for passing this info to the HookOS ISO.
## Specification/Data format
This is the spec/ data format for passing the static IP address management information to the HookOS ISO.
```ipam=<mac-address>:<vlan-id>:<ip-address>:<netmask>:<gateway>:<hostname>:<dns>:<search-domains>:<ntp>```
Example:
```ipam=de-ad-be-ef-fe-ed:30:192.168.2.193:255.255.255.0:192.168.2.1:server.example.com:1.1.1.1,8.8.8.8:example.com,team.example.com:132.163.97.1,132.163.96.1```
### Fields
Some fields are required so that basic network communication can function properly.
| Field | Description | Required | Example |
|-------|-------------|----------|---------|
| mac-address | MAC address. Must be in dash notation. | Yes |`00-00-00-00-00-00` |
| vlan-id | VLAN ID. Must be a string integer between 0 and 4096 or an empty string for no VLAN tagging. | No | `30` |
| ip-address | IPv4 address. | Yes | `10.148.56.3` |
| netmask | Netmask. | Yes | `255.255.240.0` |
| gateway | IPv4 Gateway. | No | `10.148.56.1` |
| hostname | Hostname for the system. Can be fully qualified or not. | No | `hookos` or `hookos.example.com` |
| dns | Comma separated list of IPv4 DNS nameservers. Must be IPv4 addresses, not hostnames. | Yes | `1.1.1.1,8.8.8.8` |
| search-domains | Comma separated list of search domains. | No | `example.com,example.org` |
| ntp | Comma separated list of IPv4 NTP servers. Must be IPv4 addresses, not hostnames. | No | `132.163.97.1,132.163.96.1` |
## Implementation details
Smee will set the kernel commandline parameter `ipam=` with the above format. In HookOS, there is a service that reads this cmdline parameter and writes the file(s) and runs the command(s) necessary to configure HookOS the use of all the values. See HookOS for more details on the service and how it works.
================================================
FILE: docs/images/BYO_DHCP.uml
================================================
title Bring your own DHCP service
participant Machine
participant DHCP
participant Smee
rbox over Machine,DHCP: 192.168.5.5 represents the IP from which the Smee service is available
group #2f2e7b In firmware iPXE #white
autonumber 1
Machine->DHCP: DHCP discover
DHCP->Machine: DHCP OFFER\nnext server: 192.168.2.5.5\nboot file: ipxe.efi
Machine->DHCP: DHCP REQUEST
DHCP->Machine: DHCP ACK\nnext server: 192.168.5.5\nboot file: ipxe.efi
Machine->Smee: Download and boot **ipxe.efi** (TFTP or HTTP)
end
group #2f2e7b In Tinkerbell iPXE #white
Machine->DHCP: DHCP DISCOVER
DHCP->Machine: DHCP OFFER\nnext server: 192.168.5.5\nboot file: http://192.168.5.5/auto.ipxe
Machine->DHCP: DHCP REQUEST
DHCP->Machine: DHCP ACK\nnext server: 192.168.5.5\nboot file: http://192.168.5.5/auto.ipxe
Machine->Smee: Download and execute **auto.ipxe** iPXE script (HTTP)
destroysilent Machine
destroysilent DHCP
destroysilent Smee
end
================================================
FILE: docs/manifests/README.md
================================================
# Deploying Smee
This directory contains the manifests for deploying Smee to various environments. This document will describe how to use the different Smee deployment options.
## Variables
Regardless of the option you choose it is recommended you get started by updating the following environment variables in the [`manifests/kustomize/base/deployment.yaml`](./kustomize/base/deployment.yaml) file to match your setup.
| Variable | Description |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `TINKERBELL_GRPC_AUTHORITY` | This is the IP:Port that a Tink worker will use for communicated with the Tink server |
| `MIRROR_BASE_URL` | The URL from where the "OSIE" or Hook kernel(s) and initrd(s) will be downloaded by netboot clients |
| `PUBLIC_IP` | This is the IP that netboot clients and/or DHCP relay's will use to reach Smee |
| `PUBLIC_SYSLOG_FQDN` | This is the IP that syslog clients will use to send messages |
## Deployment Options
- [Kind](kind.md)
- [Kubernetes](kubernetes.md)
- [K3D](k3d.md)
- [Tilt](tilt.md)
================================================
FILE: docs/manifests/k3d.md
================================================
# K3D (K3S in Docker)
This describes deploying Smee into a K3S in Docker (K3D) cluster.
## Prerequisites
- [K3D >= v5.4.1](https://k3d.io/v5.4.1/#installation)
- [Kubectl >= v1.23.4](https://www.downloadkubernetes.com/)
- Supported platforms: Linux
### Steps
1. Create K3D cluster
```bash
# Create the K3D cluster
k3d cluster create --network host --no-lb --k3s-arg "--disable=traefik"
```
2. Deploy Smee
Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the [`manifests/kustomize/base/deployment.yaml`](../../manifests/kustomize/base/deployment.yaml) file.
```bash
# Deploy Smee to K3D
kubectl kustomize manifests/kustomize/overlays/k3d | kubectl apply -f -
```
3. Watch the logs
```bash
kubectl -n tinkerbell logs -f -l app=tinkerbell-smee
```
================================================
FILE: docs/manifests/kind.md
================================================
# KinD (Kubernetes in Docker)
This describes deploying Smee into a Kubernetes in Docker (KinD) cluster.
## Prerequisites
- [KinD >= v0.12.0](https://kind.sigs.k8s.io/docs/user/quick-start#installation)
- [Kubectl >= v1.23.4](https://www.downloadkubernetes.com/)
## Steps
1. Create KinD cluster
```bash
# Create the KinD cluster
kind create cluster --config ./manifests/kind/config.yaml
```
2. Deploy Smee
Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the [`manifests/kustomize/base/deployment.yaml`](../../manifests/kustomize/base/deployment.yaml) file.
```bash
# Deploy Smee to KinD
kubectl kustomize manifests/kustomize/overlays/kind | kubectl apply -f -
```
3. Watch the logs
```bash
kubectl -n tinkerbell logs -f -l app=tinkerbell-smee
```
> **Note:** KinD will not be able to listen for DHCP broadcast traffic. Using a DHCP relay is recommended.
>
> ```bash
> # Linux direct
> ipaddr=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' kind-control-plane)
> sudo -E dhcrelay -id <interface to listen on for DHCP broadcast> -iu $(ip -o route get ${ipaddr} | cut -d" " -f3) -d ${ipaddr}
>
> # Linux Container
> ipaddr=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' kind-control-plane)
> docker run -d --network host --name dhcrelay modem7/dhcprelay:latest -id <interface to listen on for DHCP broadcast> -iu $(ip -o route get ${ipaddr} | cut -d" " -f3) -d ${ipaddr}
>
> # MacOS TBD
> ```
================================================
FILE: docs/manifests/kubernetes.md
================================================
# Kubernetes
This deployment requires a running Kubernetes cluster. It can be a single node cluster. It is required to be running directly on a Linux machine, not in a container. This deployment is under development and is not guaranteed to work at this time.
## Prerequisites
TBD
## Steps
1. Deploy Smee
Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the [`manifests/kustomize/base/deployment.yaml`](../../manifests/kustomize/base/deployment.yaml) file.
```bash
# Deploy Smee to Kubernetes
kubectl kustomize manifests/kustomize/overlays/dev | kubectl apply -f -
```
2. Watch the logs
```bash
kubectl -n tinkerbell logs -f -l app=tinkerbell-smee
```
================================================
FILE: docs/manifests/tilt.md
================================================
# Tilt
This deployment method is for quick local development. Tilt will build and deploy Smee to the Kubernetes cluster pointed to in the current context of your Kubernetes config file. It will use the KinD manifest, documented [here](KIND.md), for deployment.
## Prerequisites
- [Tilt >= v0.28.1](https://docs.tilt.dev/install.html)
- Go >= 1.18
- [Kubectl >= v1.23.4](https://www.downloadkubernetes.com/)
- KinD cluster
## Steps
1. Deploy Smee
Be sure you have updated `MIRROR_BASE_URL`, `PUBLIC_IP`, `PUBLIC_SYSLOG_FQDN`, and `TINKERBELL_GRPC_AUTHORITY` env variables in the `manifests/kustomize/base/deployment.yaml` file.
This deployment method uses the kustomize kind overlay (`manifests/kustomize/overlays/kind`). See the `Tiltfile` modify this.
```bash
# Deploy Smee with Tilt
tilt up --stream
```
2. Watch the logs
```bash
kubectl -n tinkerbell logs -f -l app=tinkerbell-smee
```
================================================
FILE: go.mod
================================================
module github.com/tinkerbell/smee
go 1.24.0
toolchain go1.24.1
require (
github.com/ccoveille/go-safecast v1.6.1
github.com/diskfs/go-diskfs v1.6.0
github.com/fsnotify/fsnotify v1.9.0
github.com/ghodss/yaml v1.0.0
github.com/go-logr/logr v1.4.3
github.com/go-logr/stdr v1.2.2
github.com/google/go-cmp v0.7.0
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
github.com/peterbourgon/ff/v3 v3.4.0
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0
github.com/tinkerbell/ipxedust v0.0.0-20250129162407-3c29a914f8be
github.com/tinkerbell/tink v0.12.2
github.com/vishvananda/netlink v1.3.1
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/net v0.41.0
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0
google.golang.org/grpc v1.73.0
k8s.io/apimachinery v0.33.2
k8s.io/client-go v0.33.2
sigs.k8s.io/controller-runtime v0.21.0
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/go-logr/zerologr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/packet v1.1.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pierrec/lz4/v4 v4.1.19 // indirect
github.com/pin/tftp/v3 v3.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/xattr v0.4.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
================================================
FILE: go.sum
================================================
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=
github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diskfs/go-diskfs v1.6.0 h1:YmK5+vLSfkwC6kKKRTRPGaDGNF+Xh8FXeiNHwryDfu4=
github.com/diskfs/go-diskfs v1.6.0/go.mod h1:bRFumZeGFCO8C2KNswrQeuj2m1WCVr4Ms5IjWMczMDk=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU=
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinkerbell/ipxedust v0.0.0-20250129162407-3c29a914f8be h1:PRUY/EEvGGjwohNGn1ncj5y8BlU5p42C/GUwYzmJI/4=
github.com/tinkerbell/ipxedust v0.0.0-20250129162407-3c29a914f8be/go.mod h1:gO18k34se3edSoBttsayVjT9lPA7xTZ+yiXMU1oQAC8=
github.com/tinkerbell/tink v0.12.2 h1:ROe5SAx5X8hHEROm9OJzc6XLhEzOhUcdGpY2bLVAOnk=
github.com/tinkerbell/tink v0.12.2/go.mod h1:Cpv7pSazMhq6HYVAByHJu2tkLIsR9K/mBY1S87RQbC4=
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
================================================
FILE: internal/backend/file/file.go
================================================
// Package file watches a file for changes and updates the in memory DHCP data.
package file
import (
"context"
"fmt"
"net"
"net/netip"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"github.com/ccoveille/go-safecast"
"github.com/fsnotify/fsnotify"
"github.com/ghodss/yaml"
"github.com/go-logr/logr"
"github.com/tinkerbell/smee/internal/dhcp/data"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
)
const tracerName = "github.com/tinkerbell/smee/dhcp"
// Errors used by the file watcher.
var (
// errFileFormat is returned when the file is not in the correct format, e.g. not valid YAML.
errFileFormat = fmt.Errorf("invalid file format")
errRecordNotFound = fmt.Errorf("record not found")
errParseIP = fmt.Errorf("failed to parse IP from File")
errParseSubnet = fmt.Errorf("failed to parse subnet mask from File")
errParseURL = fmt.Errorf("failed to parse URL")
)
// netboot is the structure for the data expected in a file.
type netboot struct {
AllowPXE bool `yaml:"allowPxe"` // If true, the client will be provided netboot options in the DHCP offer/ack.
IPXEScriptURL string `yaml:"ipxeScriptUrl"` // Overrides default value of that is passed into DHCP on startup.
IPXEScript string `yaml:"ipxeScript"` // Overrides a default value that is passed into DHCP on startup.
Console string `yaml:"console"`
Facility string `yaml:"facility"`
}
// dhcp is the structure for the data expected in a file.
type dhcp struct {
MACAddress net.HardwareAddr // The MAC address of the client.
IPAddress string `yaml:"ipAddress"` // yiaddr DHCP header.
SubnetMask string `yaml:"subnetMask"` // DHCP option 1.
DefaultGateway string `yaml:"defaultGateway"` // DHCP option 3.
NameServers []string `yaml:"nameServers"` // DHCP option 6.
Hostname string `yaml:"hostname"` // DHCP option 12.
DomainName string `yaml:"domainName"` // DHCP option 15.
BroadcastAddress string `yaml:"broadcastAddress"` // DHCP option 28.
NTPServers []string `yaml:"ntpServers"` // DHCP option 42.
VLANID string `yaml:"vlanID"` // DHCP option 43.116.
LeaseTime int `yaml:"leaseTime"` // DHCP option 51.
Arch string `yaml:"arch"` // DHCP option 93.
DomainSearch []string `yaml:"domainSearch"` // DHCP option 119.
Disabled bool // If true, no DHCP response should be sent.
Netboot netboot `yaml:"netboot"`
}
// Watcher represents the backend for watching a file for changes and updating the in memory DHCP data.
type Watcher struct {
fileMu sync.RWMutex // protects FilePath for reads
// FilePath is the path to the file to watch.
FilePath string
// Log is the logger to be used in the File backend.
Log logr.Logger
dataMu sync.RWMutex // protects data
data []byte // data from file
watcher *fsnotify.Watcher
}
// NewWatcher creates a new file watcher.
func NewWatcher(l logr.Logger, f string) (*Watcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
if err := watcher.Add(f); err != nil {
return nil, err
}
w := &Watcher{
FilePath: f,
watcher: watcher,
Log: l,
}
w.fileMu.RLock()
w.data, err = os.ReadFile(filepath.Clean(f))
w.fileMu.RUnlock()
if err != nil {
return nil, err
}
return w, nil
}
// GetByMac is the implementation of the Backend interface.
// It reads a given file from the in memory data (w.data).
func (w *Watcher) GetByMac(ctx context.Context, mac net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {
tracer := otel.Tracer(tracerName)
_, span := tracer.Start(ctx, "backend.file.GetByMac")
defer span.End()
// get data from file, translate it, then pass it into setDHCPOpts and setNetworkBootOpts
w.dataMu.RLock()
d := w.data
w.dataMu.RUnlock()
r := make(map[string]dhcp)
if err := yaml.Unmarshal(d, &r); err != nil {
err := fmt.Errorf("%w: %w", err, errFileFormat)
w.Log.Error(err, "failed to unmarshal file data")
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
for k, v := range r {
if strings.EqualFold(k, mac.String()) {
// found a record for this mac address
v.MACAddress = mac
d, n, err := w.translate(v)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
span.SetAttributes(d.EncodeToAttributes()...)
span.SetAttributes(n.EncodeToAttributes()...)
span.SetStatus(codes.Ok, "")
return d, n, nil
}
}
err := fmt.Errorf("%w: %s", errRecordNotFound, mac.String())
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
// GetByIP is the implementation of the Backend interface.
// It reads a given file from the in memory data (w.data).
func (w *Watcher) GetByIP(ctx context.Context, ip net.IP) (*data.DHCP, *data.Netboot, error) {
tracer := otel.Tracer(tracerName)
_, span := tracer.Start(ctx, "backend.file.GetByIP")
defer span.End()
// get data from file, translate it, then pass it into setDHCPOpts and setNetworkBootOpts
w.dataMu.RLock()
d := w.data
w.dataMu.RUnlock()
r := make(map[string]dhcp)
if err := yaml.Unmarshal(d, &r); err != nil {
err := fmt.Errorf("%w: %w", err, errFileFormat)
w.Log.Error(err, "failed to unmarshal file data")
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
for k, v := range r {
if v.IPAddress == ip.String() {
// found a record for this ip address
v.IPAddress = ip.String()
mac, err := net.ParseMAC(k)
if err != nil {
err := fmt.Errorf("%w: %w", err, errFileFormat)
w.Log.Error(err, "failed to parse mac address")
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
v.MACAddress = mac
d, n, err := w.translate(v)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
span.SetAttributes(d.EncodeToAttributes()...)
span.SetAttributes(n.EncodeToAttributes()...)
span.SetStatus(codes.Ok, "")
return d, n, nil
}
}
err := fmt.Errorf("%w: %s", errRecordNotFound, ip.String())
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
// Start starts watching a file for changes and updates the in memory data (w.data) on changes.
// Start is a blocking method. Use a context cancellation to exit.
func (w *Watcher) Start(ctx context.Context) {
for {
select {
case <-ctx.Done():
w.Log.Info("stopping watcher")
return
case event, ok := <-w.watcher.Events:
if !ok {
continue
}
if event.Op&fsnotify.Write == fsnotify.Write {
w.Log.Info("file changed, updating cache")
w.fileMu.RLock()
d, err := os.ReadFile(w.FilePath)
w.fileMu.RUnlock()
if err != nil {
w.Log.Error(err, "failed to read file", "file", w.FilePath)
break
}
w.dataMu.Lock()
w.data = d
w.dataMu.Unlock()
}
case err, ok := <-w.watcher.Errors:
if !ok {
continue
}
w.Log.Info("error watching file", "err", err)
}
}
}
// translate converts the data from the file into a data.DHCP and data.Netboot structs.
func (w *Watcher) translate(r dhcp) (*data.DHCP, *data.Netboot, error) {
d := new(data.DHCP)
n := new(data.Netboot)
d.MACAddress = r.MACAddress
// ip address, required
ip, err := netip.ParseAddr(r.IPAddress)
if err != nil {
return nil, nil, fmt.Errorf("%w: %w", err, errParseIP)
}
d.IPAddress = ip
// subnet mask, required
sm := net.ParseIP(r.SubnetMask)
if sm == nil {
return nil, nil, errParseSubnet
}
d.SubnetMask = net.IPMask(sm.To4())
// default gateway, optional
if dg, err := netip.ParseAddr(r.DefaultGateway); err != nil {
w.Log.Info("failed to parse default gateway", "defaultGateway", r.DefaultGateway, "err", err)
} else {
d.DefaultGateway = dg
}
// name servers, optional
for _, s := range r.NameServers {
ip := net.ParseIP(s)
if ip == nil {
w.Log.Info("failed to parse name server", "nameServer", s)
break
}
d.NameServers = append(d.NameServers, ip)
}
// hostname, optional
d.Hostname = r.Hostname
// domain name, optional
d.DomainName = r.DomainName
// broadcast address, optional
if ba, err := netip.ParseAddr(r.BroadcastAddress); err != nil {
w.Log.Info("failed to parse broadcast address", "broadcastAddress", r.BroadcastAddress, "err", err)
} else {
d.BroadcastAddress = ba
}
// ntp servers, optional
for _, s := range r.NTPServers {
ip := net.ParseIP(s)
if ip == nil {
w.Log.Info("failed to parse ntp server", "ntpServer", s)
break
}
d.NTPServers = append(d.NTPServers, ip)
}
// vlanid
d.VLANID = r.VLANID
// lease time
// Default to one week
d.LeaseTime = 604800
if v, err := safecast.ToUint32(r.LeaseTime); err == nil {
d.LeaseTime = v
}
// arch
d.Arch = r.Arch
// domain search
d.DomainSearch = r.DomainSearch
// disabled
d.Disabled = r.Disabled
// allow machine to netboot
n.AllowNetboot = r.Netboot.AllowPXE
// ipxe script url is optional but if provided, it must be a valid url
if r.Netboot.IPXEScriptURL != "" {
u, err := url.Parse(r.Netboot.IPXEScriptURL)
if err != nil {
return nil, nil, fmt.Errorf("%w: %w", err, errParseURL)
}
n.IPXEScriptURL = u
}
// ipxe script
if r.Netboot.IPXEScript != "" {
n.IPXEScript = r.Netboot.IPXEScript
}
// console
if r.Netboot.Console != "" {
n.Console = r.Netboot.Console
}
// facility
if r.Netboot.Facility != "" {
n.Facility = r.Netboot.Facility
}
return d, n, nil
}
================================================
FILE: internal/backend/file/file_test.go
================================================
package file
import (
"bytes"
"context"
"errors"
"fmt"
"io/fs"
"log"
"net"
"net/netip"
"net/url"
"os"
"testing"
"time"
"github.com/fsnotify/fsnotify"
"github.com/go-logr/logr"
"github.com/go-logr/stdr"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/tinkerbell/smee/internal/dhcp/data"
)
func TestNewWatcher(t *testing.T) {
tests := map[string]struct {
createFile bool
want string
wantErr error
}{
"contents equal": {createFile: true, want: "test content here"},
"file not found": {createFile: false, wantErr: &fs.PathError{}},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
var name string
if tt.createFile {
var err error
name, err = createFile([]byte(tt.want))
if err != nil {
t.Fatal(err)
}
defer os.Remove(name)
}
w, err := NewWatcher(logr.Discard(), name)
if (err != nil) != (tt.wantErr != nil) {
t.Fatalf("NewWatcher() error = %v; type = %[1]T, wantErr %v; type = %[2]T", err, tt.wantErr)
}
var got string
if tt.wantErr != nil {
got = ""
} else {
got = string(w.data)
}
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Fatal(diff)
}
})
}
}
func createFile(content []byte) (string, error) {
file, err := os.CreateTemp("", "prefix")
if err != nil {
return "", err
}
defer file.Close()
if _, err := file.Write(content); err != nil {
return "", err
}
return file.Name(), nil
}
type testData struct {
initial string
after string
action string
expectedOut string
}
func TestStartAndStop(t *testing.T) {
tt := &testData{action: "cancel", expectedOut: `"level"=0 "msg"="stopping watcher"` + "\n"}
out := &bytes.Buffer{}
l := stdr.New(log.New(out, "", 0))
ctx, cancel := context.WithCancel(context.Background())
cancel()
watcher, err := fsnotify.NewWatcher()
if err != nil {
t.Fatal(err)
}
w := &Watcher{Log: l, watcher: watcher}
w.Start(ctx)
if diff := cmp.Diff(out.String(), tt.expectedOut); diff != "" {
t.Fatal(diff)
}
}
func TestStartFileUpdateError(t *testing.T) {
tt := &testData{expectedOut: `"level"=0 "msg"="file changed, updating cache"` + "\n" + `"msg"="failed to read file" "error"="open not-found.txt: no such file or directory" "file"="not-found.txt"` + "\n" + `"level"=0 "msg"="stopping watcher"` + "\n"}
out := &bytes.Buffer{}
l := stdr.New(log.New(out, "", 0))
got, name := tt.helper(t, l)
defer os.Remove(name)
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-time.After(time.Millisecond)
got.FilePath = "not-found.txt"
got.watcher.Events <- fsnotify.Event{Op: fsnotify.Write}
cancel()
}()
got.Start(ctx)
time.Sleep(time.Second)
if diff := cmp.Diff(out.String(), tt.expectedOut); diff != "" {
t.Fatal(diff)
}
}
func TestStartFileUpdate(t *testing.T) {
tt := &testData{initial: "once upon a time", after: "\nhello world", expectedOut: "once upon a time\nhello world"}
got, name := tt.helper(t, logr.Discard())
defer os.Remove(name)
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-time.After(time.Millisecond)
got.fileMu.Lock()
f, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
t.Log(err)
}
f.Write([]byte(tt.after))
f.Close()
got.fileMu.Unlock()
time.Sleep(time.Millisecond)
cancel()
}()
got.Start(ctx)
got.dataMu.RLock()
d := got.data
got.dataMu.RUnlock()
if diff := cmp.Diff(string(d), tt.expectedOut); diff != "" {
t.Log(string(d))
t.Fatal(diff)
}
}
func TestStartFileUpdateClosedChan(t *testing.T) {
out := &bytes.Buffer{}
l := stdr.New(log.New(out, "", 0))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
watcher, err := fsnotify.NewWatcher()
if err != nil {
t.Fatal(err)
}
w := &Watcher{Log: l, watcher: watcher}
go w.Start(ctx)
close(w.watcher.Events)
time.Sleep(time.Millisecond)
if diff := cmp.Diff(out.String(), ""); diff != "" {
t.Fatal(diff)
}
}
func TestStartError(t *testing.T) {
tt := &testData{expectedOut: `"level"=0 "msg"="error watching file" "err"="test error"` + "\n" + `"level"=0 "msg"="stopping watcher"` + "\n"}
out := &bytes.Buffer{}
l := stdr.New(log.New(out, "", 0))
ctx, cancel := context.WithCancel(context.Background())
watcher, err := fsnotify.NewWatcher()
if err != nil {
t.Fatal(err)
}
w := &Watcher{Log: l, watcher: watcher}
go func() {
time.Sleep(time.Millisecond)
w.watcher.Errors <- fmt.Errorf("test error")
cancel()
}()
w.Start(ctx)
if diff := cmp.Diff(out.String(), tt.expectedOut); diff != "" {
t.Fatal(diff)
}
}
func TestStartErrorContinue(t *testing.T) {
out := &bytes.Buffer{}
l := stdr.New(log.New(out, "", 0))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
watcher, err := fsnotify.NewWatcher()
if err != nil {
t.Fatal(err)
}
w := &Watcher{Log: l, watcher: watcher}
go w.Start(ctx)
close(w.watcher.Errors)
time.Sleep(time.Millisecond)
if diff := cmp.Diff(out.String(), ""); diff != "" {
t.Fatal(diff)
}
}
func (tt *testData) helper(t *testing.T, l logr.Logger) (*Watcher, string) {
t.Helper()
name, err := createFile([]byte(tt.initial))
if err != nil {
t.Fatal(err)
}
w, err := NewWatcher(l, name)
if err != nil {
t.Fatal(err)
}
w.dataMu.RLock()
before := string(w.data)
w.dataMu.RUnlock()
if diff := cmp.Diff(before, tt.initial); diff != "" {
t.Fatal("before", diff)
}
return w, name
}
func TestTranslate(t *testing.T) {
input := dhcp{
MACAddress: []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05},
IPAddress: "192.168.2.150",
SubnetMask: "255.255.255.0",
DefaultGateway: "192.168.2.1",
NameServers: []string{"1.1.1.1", "8.8.8.8"},
Hostname: "test-server",
DomainName: "example.com",
BroadcastAddress: "192.168.2.255",
NTPServers: []string{"132.163.96.2"},
VLANID: "100",
LeaseTime: 86400,
Arch: "x86_64",
DomainSearch: []string{"example.com"},
Netboot: netboot{
AllowPXE: true,
IPXEScriptURL: "http://boot.netboot.xyz",
IPXEScript: "#!ipxe\nchain http://boot.netboot.xyz",
Console: "ttyS0",
Facility: "onprem",
},
}
wantDHCP := &data.DHCP{
MACAddress: []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05},
IPAddress: netip.MustParseAddr("192.168.2.150"),
SubnetMask: net.IPv4Mask(255, 255, 255, 0),
DefaultGateway: netip.MustParseAddr("192.168.2.1"),
NameServers: []net.IP{{1, 1, 1, 1}, {8, 8, 8, 8}},
Hostname: "test-server",
DomainName: "example.com",
BroadcastAddress: netip.MustParseAddr("192.168.2.255"),
NTPServers: []net.IP{{132, 163, 96, 2}},
VLANID: "100",
LeaseTime: 86400,
Arch: "x86_64",
DomainSearch: []string{"example.com"},
}
wantNetboot := &data.Netboot{
AllowNetboot: true,
IPXEScriptURL: &url.URL{Scheme: "http", Host: "boot.netboot.xyz"},
IPXEScript: "#!ipxe\nchain http://boot.netboot.xyz",
Console: "ttyS0",
Facility: "onprem",
}
w := &Watcher{Log: logr.Discard()}
gotDHCP, gotNetboot, err := w.translate(input)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(gotDHCP, wantDHCP, cmpopts.IgnoreUnexported(netip.Addr{})); diff != "" {
t.Error(diff)
}
if diff := cmp.Diff(gotNetboot, wantNetboot); diff != "" {
t.Error(diff)
}
}
func TestTranslateErrors(t *testing.T) {
tests := map[string]struct {
input dhcp
wantErr error
}{
"invalid IP": {input: dhcp{IPAddress: "not an IP"}, wantErr: errParseIP},
"invalid subnet mask": {input: dhcp{IPAddress: "1.1.1.1", SubnetMask: "not a mask"}, wantErr: errParseSubnet},
"invalid gateway": {input: dhcp{IPAddress: "1.1.1.1", SubnetMask: "192.168.1.255", DefaultGateway: "not a gateway"}, wantErr: nil},
"invalid broadcast address": {input: dhcp{IPAddress: "1.1.1.1", SubnetMask: "192.168.1.255"}, wantErr: nil},
"invalid NameServers": {input: dhcp{IPAddress: "1.1.1.1", SubnetMask: "192.168.1.255", NameServers: []string{"no good"}}, wantErr: nil},
"invalid ntpservers": {input: dhcp{IPAddress: "1.1.1.1", SubnetMask: "192.168.1.255", NTPServers: []string{"no good"}}, wantErr: nil},
"invalid ipxe script url": {input: dhcp{IPAddress: "1.1.1.1", SubnetMask: "255.255.255.0", Netboot: netboot{IPXEScriptURL: ":not a url"}}, wantErr: errParseURL},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
w := &Watcher{Log: stdr.New(log.New(os.Stdout, "", log.Lshortfile))}
if _, _, err := w.translate(tt.input); !errors.Is(err, tt.wantErr) {
t.Errorf("translate() = %T, want %T", err, tt.wantErr)
}
})
}
}
func TestGetByMac(t *testing.T) {
tests := map[string]struct {
mac net.HardwareAddr
badData bool
wantErr error
}{
"no record found": {mac: net.HardwareAddr{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}, wantErr: errRecordNotFound},
"record found": {mac: net.HardwareAddr{0x08, 0x00, 0x27, 0x29, 0x4e, 0x67}, wantErr: nil},
"fail error translating": {mac: net.HardwareAddr{0x08, 0x00, 0x27, 0x29, 0x4e, 0x68}, wantErr: errParseIP},
"fail parsing file": {badData: true, wantErr: errFileFormat},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
data := "testdata/example.yaml"
if tt.badData {
var err error
data, err = createFile([]byte("not a yaml file"))
if err != nil {
t.Fatal(err)
}
defer os.Remove(data)
}
w, err := NewWatcher(logr.Discard(), data)
if err != nil {
t.Fatal(err)
}
_, _, err = w.GetByMac(context.Background(), tt.mac)
if !errors.Is(err, tt.wantErr) {
t.Fatal(err)
}
})
}
}
func TestGetByIP(t *testing.T) {
tests := map[string]struct {
ip net.IP
badData bool
wantErr error
}{
"no record found": {ip: net.IPv4(172, 168, 2, 1), wantErr: errRecordNotFound},
"record found": {ip: net.IPv4(192, 168, 2, 153), wantErr: nil},
"fail parsing file": {badData: true, wantErr: errFileFormat},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
data := "testdata/example.yaml"
if tt.badData {
var err error
data, err = createFile([]byte("not a yaml file"))
if err != nil {
t.Fatal(err)
}
defer os.Remove(data)
}
w, err := NewWatcher(logr.Discard(), data)
if err != nil {
t.Fatal(err)
}
_, _, err = w.GetByIP(context.Background(), tt.ip)
if !errors.Is(err, tt.wantErr) {
t.Fatal(err)
}
})
}
}
================================================
FILE: internal/backend/file/testdata/example.yaml
================================================
---
08:00:27:29:4E:67:
ipAddress: "192.168.2.153"
subnetMask: "255.255.255.0"
defaultGateway: "192.168.2.1"
nameServers:
- "8.8.8.8"
- "1.1.1.1"
hostname: "pxe-virtualbox"
domainName: "example.com"
broadcastAddress: "192.168.2.255"
ntpServers:
- "132.163.96.2"
- "132.163.96.3"
leaseTime: 86400
domainSearch:
- "example.com"
netboot:
allowPxe: true
ipxeScriptUrl: "https://boot.netboot.xyz"
52:54:00:aa:88:2a:
ipAddress: "192.168.2.15"
subnetMask: "255.255.255.0"
defaultGateway: "192.168.2.1"
nameServers:
- "8.8.8.8"
- "1.1.1.1"
hostname: "sandbox"
domainName: "example.com"
broadcastAddress: "192.168.2.255"
ntpServers:
- "132.163.96.2"
- "132.163.96.3"
leaseTime: 86400
domainSearch:
- "example.com"
netboot:
allowPxe: true
ipxeScriptUrl: "https://boot.netboot.xyz"
86:96:b0:6e:ca:36:
ipAddress: "192.168.2.158"
subnetMask: "255.255.255.0"
defaultGateway: "192.168.2.1"
nameServers:
- "8.8.8.8"
- "1.1.1.1"
hostname: "pxe-proxmox"
domainName: "example.com"
broadcastAddress: "192.168.2.255"
ntpServers:
- "132.163.96.2"
- "132.163.96.3"
leaseTime: 86400
domainSearch:
- "example.com"
netboot:
allowPxe: true
ipxeScriptUrl: "http://boot.netboot.xyz"
b4:96:91:6f:33:d0:
ipAddress: "192.168.56.15"
subnetMask: "255.255.255.0"
defaultGateway: "192.168.56.4"
nameServers:
- "8.8.8.8"
- "1.1.1.1"
hostname: "dhcp-testing"
domainName: "example.com"
broadcastAddress: "192.168.56.255"
ntpServers:
- "132.163.96.2"
- "132.163.96.3"
leaseTime: 86400
domainSearch:
- "example.com"
netboot:
allowPxe: true
ipxeScriptUrl: "https://boot.netboot.xyz"
08:00:27:29:4E:68: # bad data
ipAddress: "3"
subnetMask: "255.255.255.0"
================================================
FILE: internal/backend/kube/error.go
================================================
package kube
import (
"net/http"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type hardwareNotFoundError struct{}
func (hardwareNotFoundError) NotFound() bool { return true }
func (hardwareNotFoundError) Error() string { return "hardware not found" }
// Status() implements the APIStatus interface from apimachinery/pkg/api/errors
// so that IsNotFound function could be used against this error type.
func (hardwareNotFoundError) Status() metav1.Status {
return metav1.Status{
Reason: metav1.StatusReasonNotFound,
Code: http.StatusNotFound,
}
}
================================================
FILE: internal/backend/kube/index.go
================================================
package kube
import (
"github.com/tinkerbell/tink/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// MACAddrIndex is an index used with a controller-runtime client to lookup hardware by MAC.
const MACAddrIndex = ".Spec.Interfaces.MAC"
// MACAddrs returns a list of MAC addresses for a Hardware object.
func MACAddrs(obj client.Object) []string {
hw, ok := obj.(*v1alpha1.Hardware)
if !ok {
return nil
}
return GetMACs(hw)
}
// GetMACs retrieves all MACs associated with h.
func GetMACs(h *v1alpha1.Hardware) []string {
var macs []string
for _, i := range h.Spec.Interfaces {
if i.DHCP != nil && i.DHCP.MAC != "" {
macs = append(macs, i.DHCP.MAC)
}
}
return macs
}
// IPAddrIndex is an index used with a controller-runtime client to lookup hardware by IP.
const IPAddrIndex = ".Spec.Interfaces.DHCP.IP"
// IPAddrs returns a list of IP addresses for a Hardware object.
func IPAddrs(obj client.Object) []string {
hw, ok := obj.(*v1alpha1.Hardware)
if !ok {
return nil
}
return GetIPs(hw)
}
// GetIPs retrieves all IP addresses.
func GetIPs(h *v1alpha1.Hardware) []string {
var ips []string
for _, i := range h.Spec.Interfaces {
if i.DHCP != nil && i.DHCP.IP != nil && i.DHCP.IP.Address != "" {
ips = append(ips, i.DHCP.IP.Address)
}
}
return ips
}
================================================
FILE: internal/backend/kube/index_test.go
================================================
package kube
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/tinkerbell/tink/api/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func TestMACAddrs(t *testing.T) {
tests := map[string]struct {
hw client.Object
want []string
}{
"not a v1alpha1.Hardware object": {hw: &v1alpha1.Workflow{}, want: nil},
"2 MACs": {hw: &v1alpha1.Hardware{
Spec: v1alpha1.HardwareSpec{
Interfaces: []v1alpha1.Interface{
{
DHCP: &v1alpha1.DHCP{
MAC: "00:00:00:00:00:00",
},
},
{
DHCP: &v1alpha1.DHCP{
MAC: "00:00:00:00:00:01",
},
},
{
DHCP: &v1alpha1.DHCP{},
},
},
},
}, want: []string{"00:00:00:00:00:00", "00:00:00:00:00:01"}},
"no interfaces": {hw: &v1alpha1.Hardware{}, want: nil},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
macs := MACAddrs(tc.hw)
if diff := cmp.Diff(macs, tc.want); diff != "" {
t.Errorf("unexpected MACs (+want -got):\n%s", diff)
}
})
}
}
func TestIPAddrs(t *testing.T) {
tests := map[string]struct {
hw client.Object
want []string
}{
"not a v1alpha1.Hardware object": {hw: &v1alpha1.Workflow{}, want: nil},
"2 IPs": {hw: &v1alpha1.Hardware{
Spec: v1alpha1.HardwareSpec{
Interfaces: []v1alpha1.Interface{
{
DHCP: &v1alpha1.DHCP{
IP: &v1alpha1.IP{
Address: "192.168.2.1",
},
},
},
{
DHCP: &v1alpha1.DHCP{
IP: &v1alpha1.IP{
Address: "192.168.2.2",
},
},
},
{
DHCP: &v1alpha1.DHCP{},
},
{
DHCP: &v1alpha1.DHCP{
IP: &v1alpha1.IP{},
},
},
},
},
}, want: []string{"192.168.2.1", "192.168.2.2"}},
"no interfaces": {hw: &v1alpha1.Hardware{}, want: nil},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := IPAddrs(tc.hw)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("unexpected IPs (-want +got):\n%s", diff)
}
})
}
}
================================================
FILE: internal/backend/kube/kube.go
================================================
// Package kube is a backend implementation that uses the Tinkerbell CRDs to get DHCP data.
package kube
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"net/url"
"github.com/ccoveille/go-safecast"
"github.com/tinkerbell/smee/internal/dhcp/data"
"github.com/tinkerbell/tink/api/v1alpha1"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"
)
const tracerName = "github.com/tinkerbell/smee/dhcp"
// Backend is a backend implementation that uses the Tinkerbell CRDs to get DHCP data.
type Backend struct {
cluster cluster.Cluster
}
// NewBackend returns a controller-runtime cluster.Cluster with the Tinkerbell runtime
// scheme registered, and indexers for:
// * Hardware by MAC address
// * Hardware by IP address
//
// Callers must instantiate the client-side cache by calling Start() before use.
func NewBackend(conf *rest.Config, opts ...cluster.Option) (*Backend, error) {
c, err := cluster.New(conf, opts...)
if err != nil {
return nil, fmt.Errorf("failed to create new cluster config: %w", err)
}
if err := c.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.Hardware{}, MACAddrIndex, MACAddrs); err != nil {
return nil, fmt.Errorf("failed to setup indexer: %w", err)
}
if err := c.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.Hardware{}, IPAddrIndex, IPAddrs); err != nil {
return nil, fmt.Errorf("failed to setup indexer(.spec.interfaces.dhcp.ip.address): %w", err)
}
return &Backend{cluster: c}, nil
}
// Start starts the client-side cache.
func (b *Backend) Start(ctx context.Context) error {
return b.cluster.Start(ctx)
}
// GetByMac implements the handler.BackendReader interface and returns DHCP and netboot data based on a mac address.
func (b *Backend) GetByMac(ctx context.Context, mac net.HardwareAddr) (*data.DHCP, *data.Netboot, error) {
tracer := otel.Tracer(tracerName)
ctx, span := tracer.Start(ctx, "backend.kube.GetByMac")
defer span.End()
hardwareList := &v1alpha1.HardwareList{}
if err := b.cluster.GetClient().List(ctx, hardwareList, &client.MatchingFields{MACAddrIndex: mac.String()}); err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, nil, fmt.Errorf("failed listing hardware for (%v): %w", mac, err)
}
if len(hardwareList.Items) == 0 {
err := hardwareNotFoundError{}
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
if len(hardwareList.Items) > 1 {
err := fmt.Errorf("got %d hardware objects for mac %s, expected only 1", len(hardwareList.Items), mac)
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
i := v1alpha1.Interface{}
for _, iface := range hardwareList.Items[0].Spec.Interfaces {
if iface.DHCP.MAC == mac.String() {
i = iface
break
}
}
d, n, err := transform(i, hardwareList.Items[0].Spec.Metadata)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
span.SetAttributes(d.EncodeToAttributes()...)
span.SetAttributes(n.EncodeToAttributes()...)
span.SetStatus(codes.Ok, "")
return d, n, nil
}
// GetByIP implements the handler.BackendReader interface and returns DHCP and netboot data based on an IP address.
func (b *Backend) GetByIP(ctx context.Context, ip net.IP) (*data.DHCP, *data.Netboot, error) {
tracer := otel.Tracer(tracerName)
ctx, span := tracer.Start(ctx, "backend.kube.GetByIP")
defer span.End()
hardwareList := &v1alpha1.HardwareList{}
if err := b.cluster.GetClient().List(ctx, hardwareList, &client.MatchingFields{IPAddrIndex: ip.String()}); err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, nil, fmt.Errorf("failed listing hardware for (%v): %w", ip, err)
}
if len(hardwareList.Items) == 0 {
err := hardwareNotFoundError{}
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
if len(hardwareList.Items) > 1 {
err := fmt.Errorf("got %d hardware objects for ip: %s, expected only 1", len(hardwareList.Items), ip)
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
i := v1alpha1.Interface{}
for _, iface := range hardwareList.Items[0].Spec.Interfaces {
if iface.DHCP.IP.Address == ip.String() {
i = iface
break
}
}
d, n, err := transform(i, hardwareList.Items[0].Spec.Metadata)
if err != nil {
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
}
span.SetAttributes(d.EncodeToAttributes()...)
span.SetAttributes(n.EncodeToAttributes()...)
span.SetStatus(codes.Ok, "")
return d, n, nil
}
// toDHCPData converts a v1alpha1.DHCP to a data.DHCP data structure.
// if required fields are missing, an error is returned.
// Required fields: v1alpha1.Interface.DHCP.MAC, v1alpha1.Interface.DHCP.IP.Address, v1alpha1.Interface.DHCP.IP.Netmask.
func toDHCPData(h *v1alpha1.DHCP) (*data.DHCP, error) {
if h == nil {
return nil, errors.New("no DHCP data")
}
d := new(data.DHCP)
var err error
// MACAddress is required
if d.MACAddress, err = net.ParseMAC(h.MAC); err != nil {
return nil, err
}
if h.IP != nil {
// IPAddress is required
if d.IPAddress, err = netip.ParseAddr(h.IP.Address); err != nil {
return nil, err
}
// Netmask is required
sm := net.ParseIP(h.IP.Netmask)
if sm == nil {
return nil, errors.New("no netmask")
}
d.SubnetMask = net.IPMask(sm.To4())
} else {
return nil, errors.New("no IP data")
}
// Gateway is optional, but should be a valid IP address if present
if h.IP.Gateway != "" {
if d.DefaultGateway, err = netip.ParseAddr(h.IP.Gateway); err != nil {
return nil, err
}
}
// name servers, optional
for _, s := range h.NameServers {
ip := net.ParseIP(s)
if ip == nil {
break
}
d.NameServers = append(d.NameServers, ip)
}
// timeservers, optional
for _, s := range h.TimeServers {
ip := net.ParseIP(s)
if ip == nil {
break
}
d.NTPServers = append(d.NTPServers, ip)
}
// hostname, optional
d.Hostname = h.Hostname
// lease time required
// Default to one week
d.LeaseTime = 604800
if v, err := safecast.ToUint32(h.LeaseTime); err == nil {
d.LeaseTime = v
}
// arch
d.Arch = h.Arch
// vlanid
d.VLANID = h.VLANID
return d, nil
}
// toNetbootData converts a hardware interface to a data.Netboot data structure.
func toNetbootData(i *v1alpha1.Netboot, facility string) (*data.Netboot, error) {
if i == nil {
return nil, errors.New("no netboot data")
}
n := new(data.Netboot)
// allow machine to netboot
if i.AllowPXE != nil {
n.AllowNetboot = *i.AllowPXE
}
// ipxe script url is optional but if provided, it must be a valid url
if i.IPXE != nil {
if i.IPXE.URL != "" {
u, err := url.ParseRequestURI(i.IPXE.URL)
if err != nil {
return nil, err
}
n.IPXEScriptURL = u
}
}
// ipxescript
if i.IPXE != nil {
n.IPXEScript = i.IPXE.Contents
}
// console
n.Console = ""
// facility
n.Facility = facility
// OSIE data
n.OSIE = data.OSIE{}
if i.OSIE != nil {
if b, err := url.Parse(i.OSIE.BaseURL); err == nil {
n.OSIE.BaseURL = b
}
n.OSIE.Kernel = i.OSIE.Kernel
n.OSIE.Initrd = i.OSIE.Initrd
}
return n, nil
}
// transform returns data.DHCP and data.Netboot from part a v1alpha1.Interface and *v1alpha1.HardwareMetadata.
func transform(i v1alpha1.Interface, m *v1alpha1.HardwareMetadata) (*data.DHCP, *data.Netboot, error) {
d, err := toDHCPData(i.DHCP)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert hardware to DHCP data: %w", err)
}
d.Disabled = i.DisableDHCP
// Facility is used in the default HookOS iPXE script so we get it from the hardware metadata, if set.
facility := ""
if m != nil {
if m.Facility != nil {
facility = m.Facility.FacilityCode
}
}
n, err := toNetbootData(i.Netboot, facility)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert hardware to netboot data: %w", err)
}
return d, n, nil
}
================================================
FILE: internal/backend/kube/kube_test.go
================================================
package kube
import (
"context"
"net"
"net/http"
"net/netip"
"net/url"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/tinkerbell/smee/internal/dhcp/data"
"github.com/tinkerbell/tink/api/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/cache/informertest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/cluster"
)
func TestNewBackend(t *testing.T) {
tests := map[string]struct {
conf *rest.Config
opt cluster.Option
shouldErr bool
}{
"no config": {shouldErr: true},
"failed index field": {shouldErr: true, conf: new(rest.Config), opt: func(o *cluster.Options) {
cl := fake.NewClientBuilder().Build()
o.NewClient = func(*rest.Config, client.Options) (client.Client, error) {
return cl, nil
}
o.MapperProvider = func(*rest.Config, *http.Client) (meta.RESTMapper, error) {
return cl.RESTMapper(), nil
}
}},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
b, err := NewBackend(tt.conf, tt.opt)
if tt.shouldErr && err == nil {
t.Fatal("expected error")
}
if !tt.shouldErr && err != nil {
t.Fatal(err)
}
if !tt.shouldErr && b == nil {
t.Fatal("expected backend")
}
})
}
}
func TestToDHCPData(t *testing.T) {
tests := map[string]struct {
in *v1alpha1.DHCP
want *data.DHCP
shouldErr bool
}{
"nil input": {
in: nil,
shouldErr: true,
},
"no mac": {
in: &v1alpha1.DHCP{},
shouldErr: true,
},
"bad mac": {
in: &v1alpha1.DHCP{MAC: "bad"},
shouldErr: true,
},
"no ip": {
in: &v1alpha1.DHCP{MAC: "aa:bb:cc:dd:ee:ff", IP: &v1alpha1.IP{}},
shouldErr: true,
},
"no subnet": {
in: &v1alpha1.DHCP{MAC: "aa:bb:cc:dd:ee:ff", IP: &v1alpha1.IP{Address: "192.168.2.4"}},
shouldErr: true,
},
"v1alpha1.IP == nil": {
in: &v1alpha1.DHCP{MAC: "aa:bb:cc:dd:ee:ff", IP: nil},
shouldErr: true,
},
"bad gateway": {
in: &v1alpha1.DHCP{MAC: "aa:bb:cc:dd:ee:ff", IP: &v1alpha1.IP{Address: "192.168.2.4", Netmask: "255.255.254.0", Gateway: "bad"}},
shouldErr: true,
},
"one bad nameserver": {
in: &v1alpha1.DHCP{
MAC: "00:00:00:00:00:04",
NameServers: []string{"1.1.1.1", "bad"},
IP: &v1alpha1.IP{
Address: "192.168.2.4",
Netmask: "255.255.0.0",
Gateway: "192.168.2.1",
},
},
want: &data.DHCP{
SubnetMask: net.IPv4Mask(255, 255, 0, 0),
DefaultGateway: netip.MustParseAddr("192.168.2.1"),
NameServers: []net.IP{net.IPv4(1, 1, 1, 1)},
IPAddress: netip.MustParseAddr("192.168.2.4"),
MACAddress: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x04},
},
},
"full": {
in: &v1alpha1.DHCP{
MAC: "00:00:00:00:00:04",
Hostname: "test",
LeaseTime: 3600,
NameServers: []string{"1.1.1.1"},
IP: &v1alpha1.IP{
Address: "192.168.1.4",
Netmask: "255.255.255.0",
Gateway: "192.168.1.1",
},
},
want: &data.DHCP{
SubnetMask: net.IPv4Mask(255, 255, 255, 0),
DefaultGateway: netip.MustParseAddr("192.168.1.1"),
NameServers: []net.IP{net.IPv4(1, 1, 1, 1)},
Hostname: "test",
LeaseTime: 3600,
IPAddress: netip.MustParseAddr("192.168.1.4"),
MACAddress: net.HardwareAddr{0x00, 0x00, 0x00, 0x00, 0x00, 0x04},
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := toDHCPData(tt.in)
if tt.shouldErr && err == nil {
t.Fatal("expected error")
}
if diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(netip.Addr{})); diff != "" {
t.Fatal(diff)
}
})
}
}
func TestToNetbootData(t *testing.T) {
tests := map[string]struct {
in *v1alpha1.Netboot
want *data.Netboot
shouldErr bool
}{
"nil input": {in: nil, shouldErr: true},
"bad ipxe url": {in: &v1alpha1.Netboot{IPXE: &v1alpha1.IPXE{URL: "bad"}}, shouldErr: true},
"successful": {in: &v1alpha1.Netboot{IPXE: &v1alpha1.IPXE{URL: "http://example.com/ipxe.ipxe"}}, want: &data.Netboot{IPXEScriptURL: &url.URL{Scheme: "http", Host: "example.com", Path: "/ipxe.ipxe"}}},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := toNetbootData(tt.in, "")
if tt.shouldErr && err == nil {
t.Fatal("expected error")
}
if diff := cmp.Diff(got, tt.want, cmpopts.IgnoreUnexported(netip.Addr{})); diff != "" {
t.Fatal(diff)
}
})
}
}
func TestGetByIP(t *testing.T) {
tests := map[string]struct {
hwObject []v1alpha1.Hardware
wantDHCP *data.DHCP
wantNetboot *data.Netboot
shouldErr bool
failToList bool
}{
"empty hard
gitextract_aau8ld8h/
├── .dockerignore
├── .github/
│ ├── CODEOWNERS
│ ├── codecov.yml
│ ├── dependabot.yml
│ ├── mergify.yml
│ ├── settings.yml
│ └── workflows/
│ ├── ci-checks.sh
│ ├── ci.yaml
│ └── tags.yaml
├── .gitignore
├── .golangci.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── RELEASING.md
├── Tiltfile
├── cmd/
│ └── smee/
│ ├── backend.go
│ ├── flag.go
│ ├── flag_test.go
│ └── main.go
├── contrib/
│ └── tag-release.sh
├── docker-compose.yml
├── docs/
│ ├── Backend-File.md
│ ├── Code-Structure.md
│ ├── DCO.md
│ ├── DESIGN.md
│ ├── DESIGNPHILOSOPHY.md
│ ├── DHCP.md
│ ├── Design-Philosophy.md
│ ├── ISO-Static-IPAM.md
│ ├── images/
│ │ └── BYO_DHCP.uml
│ └── manifests/
│ ├── README.md
│ ├── k3d.md
│ ├── kind.md
│ ├── kubernetes.md
│ └── tilt.md
├── go.mod
├── go.sum
├── internal/
│ ├── backend/
│ │ ├── file/
│ │ │ ├── file.go
│ │ │ ├── file_test.go
│ │ │ └── testdata/
│ │ │ └── example.yaml
│ │ ├── kube/
│ │ │ ├── error.go
│ │ │ ├── index.go
│ │ │ ├── index_test.go
│ │ │ ├── kube.go
│ │ │ └── kube_test.go
│ │ └── noop/
│ │ ├── noop.go
│ │ └── noop_test.go
│ ├── dhcp/
│ │ ├── data/
│ │ │ ├── data.go
│ │ │ └── data_test.go
│ │ ├── dhcp.go
│ │ ├── dhcp_test.go
│ │ ├── handler/
│ │ │ ├── handler.go
│ │ │ ├── proxy/
│ │ │ │ └── proxy.go
│ │ │ └── reservation/
│ │ │ ├── handler.go
│ │ │ ├── handler_test.go
│ │ │ ├── noop.go
│ │ │ ├── noop_test.go
│ │ │ ├── option.go
│ │ │ ├── option_test.go
│ │ │ └── reservation.go
│ │ ├── otel/
│ │ │ ├── otel.go
│ │ │ └── otel_test.go
│ │ └── server/
│ │ ├── dhcp.go
│ │ └── dhcp_test.go
│ ├── ipxe/
│ │ ├── http/
│ │ │ ├── http.go
│ │ │ ├── middleware.go
│ │ │ ├── xff.go
│ │ │ └── xff_test.go
│ │ └── script/
│ │ ├── auto.go
│ │ ├── auto_test.go
│ │ ├── custom.go
│ │ ├── hook.go
│ │ ├── ipxe.go
│ │ ├── ipxe_test.go
│ │ └── static.go
│ ├── iso/
│ │ ├── internal/
│ │ │ ├── LICENSE
│ │ │ ├── acsii.go
│ │ │ ├── acsii_test.go
│ │ │ ├── context.go
│ │ │ ├── reverseproxy.go
│ │ │ └── reverseproxy_test.go
│ │ ├── ipam.go
│ │ ├── ipam_test.go
│ │ ├── iso.go
│ │ ├── iso_test.go
│ │ └── testdata/
│ │ └── output.iso
│ ├── metric/
│ │ └── metric.go
│ ├── otel/
│ │ └── otel.go
│ └── syslog/
│ ├── facility_string.go
│ ├── message.go
│ ├── receiver.go
│ └── severity_string.go
├── lint.mk
├── rules.mk
└── test/
├── Dockerfile
├── busybox-udhcpc-script.sh
├── extract-traceparent-from-opt43.sh
├── hardware.yaml
├── otel-collector.yaml
├── start-smee.sh
└── test-smee.sh
SYMBOL INDEX (522 symbols across 55 files)
FILE: cmd/smee/backend.go
type Kube (line 21) | type Kube struct
method getClient (line 45) | func (k *Kube) getClient() (*rest.Config, error) {
method backend (line 62) | func (k *Kube) backend(ctx context.Context) (handler.BackendReader, er...
type File (line 31) | type File struct
method backend (line 100) | func (s *File) backend(ctx context.Context, logger logr.Logger) (handl...
type Noop (line 37) | type Noop struct
method backend (line 41) | func (n *Noop) backend() handler.BackendReader {
FILE: cmd/smee/flag.go
function customUsageFunc (line 23) | func customUsageFunc(c *ffcli.Command) string {
function countFlags (line 81) | func countFlags(fs *flag.FlagSet) (n int) {
function syslogFlags (line 87) | func syslogFlags(c *config, fs *flag.FlagSet) {
function tftpFlags (line 93) | func tftpFlags(c *config, fs *flag.FlagSet) {
function ipxeHTTPBinaryFlags (line 102) | func ipxeHTTPBinaryFlags(c *config, fs *flag.FlagSet) {
function ipxeHTTPScriptFlags (line 106) | func ipxeHTTPScriptFlags(c *config, fs *flag.FlagSet) {
function dhcpFlags (line 120) | func dhcpFlags(c *config, fs *flag.FlagSet) {
function backendFlags (line 141) | func backendFlags(c *config, fs *flag.FlagSet) {
function otelFlags (line 151) | func otelFlags(c *config, fs *flag.FlagSet) {
function isoFlags (line 156) | func isoFlags(c *config, fs *flag.FlagSet) {
function setFlags (line 163) | func setFlags(c *config, fs *flag.FlagSet) {
function newCLI (line 175) | func newCLI(cfg *config, fs *flag.FlagSet) *ffcli.Command {
function ipByInterface (line 188) | func ipByInterface(name string) string {
function detectPublicIPv4 (line 213) | func detectPublicIPv4() string {
function autoDetectPublicIPv4 (line 232) | func autoDetectPublicIPv4() (net.IP, error) {
function autoDetectPublicIpv4WithDefaultGateway (line 255) | func autoDetectPublicIpv4WithDefaultGateway() (net.IP, error) {
FILE: cmd/smee/flag_test.go
function TestParser (line 12) | func TestParser(t *testing.T) {
function TestCustomUsageFunc (line 110) | func TestCustomUsageFunc(t *testing.T) {
FILE: cmd/smee/main.go
constant name (line 46) | name = "smee"
constant dhcpModeProxy (line 47) | dhcpModeProxy dhcpMode = "proxy"
constant dhcpModeReservation (line 48) | dhcpModeReservation dhcpMode = "reservation"
constant dhcpModeAutoProxy (line 49) | dhcpModeAutoProxy dhcpMode = "auto-proxy"
constant magicString (line 52) | magicString = `464vn90e7rbj08xbwdjejmdf4it17c5zfzjyfhthbh19eij201hjgit02...
type config (line 55) | type config struct
method backend (line 338) | func (c *config) backend(ctx context.Context, log logr.Logger) (handle...
method dhcpHandler (line 371) | func (c *config) dhcpHandler(ctx context.Context, log logr.Logger) (se...
type syslogConfig (line 69) | type syslogConfig struct
type tftp (line 75) | type tftp struct
type ipxeHTTPBinary (line 84) | type ipxeHTTPBinary struct
type ipxeHTTPScript (line 88) | type ipxeHTTPScript struct
type dhcpMode (line 102) | type dhcpMode
method String (line 554) | func (d dhcpMode) String() string {
type dhcpConfig (line 104) | type dhcpConfig struct
type urlBuilder (line 118) | type urlBuilder struct
type httpIpxeScript (line 125) | type httpIpxeScript struct
type dhcpBackends (line 133) | type dhcpBackends struct
type otelConfig (line 139) | type otelConfig struct
type isoConfig (line 144) | type isoConfig struct
function main (line 151) | func main() {
function numTrue (line 328) | func numTrue(b ...bool) int {
function defaultLogger (line 493) | func defaultLogger(level string) logr.Logger {
function parseTrustedProxies (line 528) | func parseTrustedProxies(trustedProxies string) (result []string) {
FILE: internal/backend/file/file.go
constant tracerName (line 24) | tracerName = "github.com/tinkerbell/smee/dhcp"
type netboot (line 37) | type netboot struct
type dhcp (line 46) | type dhcp struct
type Watcher (line 65) | type Watcher struct
method GetByMac (line 106) | func (w *Watcher) GetByMac(ctx context.Context, mac net.HardwareAddr) ...
method GetByIP (line 149) | func (w *Watcher) GetByIP(ctx context.Context, ip net.IP) (*data.DHCP,...
method Start (line 201) | func (w *Watcher) Start(ctx context.Context) {
method translate (line 234) | func (w *Watcher) translate(r dhcp) (*data.DHCP, *data.Netboot, error) {
function NewWatcher (line 79) | func NewWatcher(l logr.Logger, f string) (*Watcher, error) {
FILE: internal/backend/file/file_test.go
function TestNewWatcher (line 25) | func TestNewWatcher(t *testing.T) {
function createFile (line 63) | func createFile(content []byte) (string, error) {
type testData (line 75) | type testData struct
method helper (line 204) | func (tt *testData) helper(t *testing.T, l logr.Logger) (*Watcher, str...
function TestStartAndStop (line 82) | func TestStartAndStop(t *testing.T) {
function TestStartFileUpdateError (line 99) | func TestStartFileUpdateError(t *testing.T) {
function TestStartFileUpdate (line 119) | func TestStartFileUpdate(t *testing.T) {
function TestStartFileUpdateClosedChan (line 147) | func TestStartFileUpdateClosedChan(t *testing.T) {
function TestStartError (line 165) | func TestStartError(t *testing.T) {
function TestStartErrorContinue (line 186) | func TestStartErrorContinue(t *testing.T) {
function TestTranslate (line 224) | func TestTranslate(t *testing.T) {
function TestTranslateErrors (line 282) | func TestTranslateErrors(t *testing.T) {
function TestGetByMac (line 305) | func TestGetByMac(t *testing.T) {
function TestGetByIP (line 340) | func TestGetByIP(t *testing.T) {
FILE: internal/backend/kube/error.go
type hardwareNotFoundError (line 9) | type hardwareNotFoundError struct
method NotFound (line 11) | func (hardwareNotFoundError) NotFound() bool { return true }
method Error (line 13) | func (hardwareNotFoundError) Error() string { return "hardware not fou...
method Status (line 17) | func (hardwareNotFoundError) Status() metav1.Status {
FILE: internal/backend/kube/index.go
constant MACAddrIndex (line 9) | MACAddrIndex = ".Spec.Interfaces.MAC"
function MACAddrs (line 12) | func MACAddrs(obj client.Object) []string {
function GetMACs (line 21) | func GetMACs(h *v1alpha1.Hardware) []string {
constant IPAddrIndex (line 33) | IPAddrIndex = ".Spec.Interfaces.DHCP.IP"
function IPAddrs (line 36) | func IPAddrs(obj client.Object) []string {
function GetIPs (line 45) | func GetIPs(h *v1alpha1.Hardware) []string {
FILE: internal/backend/kube/index_test.go
function TestMACAddrs (line 11) | func TestMACAddrs(t *testing.T) {
function TestIPAddrs (line 48) | func TestIPAddrs(t *testing.T) {
FILE: internal/backend/kube/kube.go
constant tracerName (line 22) | tracerName = "github.com/tinkerbell/smee/dhcp"
type Backend (line 25) | type Backend struct
method Start (line 53) | func (b *Backend) Start(ctx context.Context) error {
method GetByMac (line 58) | func (b *Backend) GetByMac(ctx context.Context, mac net.HardwareAddr) ...
method GetByIP (line 107) | func (b *Backend) GetByIP(ctx context.Context, ip net.IP) (*data.DHCP,...
function NewBackend (line 35) | func NewBackend(conf *rest.Config, opts ...cluster.Option) (*Backend, er...
function toDHCPData (line 158) | func toDHCPData(h *v1alpha1.DHCP) (*data.DHCP, error) {
function toNetbootData (line 230) | func toNetbootData(i *v1alpha1.Netboot, facility string) (*data.Netboot,...
function transform (line 277) | func transform(i v1alpha1.Interface, m *v1alpha1.HardwareMetadata) (*dat...
FILE: internal/backend/kube/kube_test.go
function TestNewBackend (line 27) | func TestNewBackend(t *testing.T) {
function TestToDHCPData (line 61) | func TestToDHCPData(t *testing.T) {
function TestToNetbootData (line 149) | func TestToNetbootData(t *testing.T) {
function TestGetByIP (line 172) | func TestGetByIP(t *testing.T) {
function TestGetByMac (line 270) | func TestGetByMac(t *testing.T) {
FILE: internal/backend/noop/noop.go
type Backend (line 13) | type Backend struct
method GetByMac (line 15) | func (n Backend) GetByMac(context.Context, net.HardwareAddr) (*data.DH...
method GetByIP (line 19) | func (n Backend) GetByIP(context.Context, net.IP) (*data.DHCP, *data.N...
FILE: internal/backend/noop/noop_test.go
function TestBackend (line 9) | func TestBackend(t *testing.T) {
FILE: internal/dhcp/data/data.go
type Packet (line 15) | type Packet struct
type Metadata (line 25) | type Metadata struct
type DHCP (line 34) | type DHCP struct
method EncodeToAttributes (line 72) | func (d *DHCP) EncodeToAttributes() []attribute.KeyValue {
type Netboot (line 52) | type Netboot struct
method EncodeToAttributes (line 119) | func (n *Netboot) EncodeToAttributes() []attribute.KeyValue {
type OSIE (line 62) | type OSIE struct
FILE: internal/dhcp/data/data_test.go
function TestDHCPEncodeToAttributes (line 13) | func TestDHCPEncodeToAttributes(t *testing.T) {
function TestNetbootEncodeToAttributes (line 75) | func TestNetbootEncodeToAttributes(t *testing.T) {
FILE: internal/dhcp/dhcp.go
constant PXEClient (line 18) | PXEClient ClientType = "PXEClient"
constant HTTPClient (line 19) | HTTPClient ClientType = "HTTPClient"
constant IPXE (line 29) | IPXE UserClass = "iPXE"
constant Tinkerbell (line 34) | Tinkerbell UserClass = "Tinkerbell"
type UserClass (line 38) | type UserClass
method String (line 169) | func (u UserClass) String() string {
type ClientType (line 41) | type ClientType
method String (line 164) | func (c ClientType) String() string {
type Info (line 68) | type Info struct
method IPXEBinaryFrom (line 154) | func (i Info) IPXEBinaryFrom() string {
method UserClassFrom (line 173) | func (i Info) UserClassFrom() UserClass {
method ClientTypeFrom (line 184) | func (i Info) ClientTypeFrom() ClientType {
method Bootfile (line 266) | func (i Info) Bootfile(customUC UserClass, ipxeScript, ipxeHTTPBinServ...
method NextServer (line 303) | func (i Info) NextServer(ipxeHTTPBinServer *url.URL, ipxeTFTPBinServer...
method AddRPIOpt43 (line 322) | func (i Info) AddRPIOpt43(opts dhcpv4.Options) []byte {
function NewInfo (line 86) | func NewInfo(pkt *dhcpv4.DHCPv4) Info {
function isRaspberryPI (line 104) | func isRaspberryPI(mac net.HardwareAddr) bool {
function Arch (line 122) | func Arch(d *dhcpv4.DHCPv4) iana.Arch {
function IsNetbootClient (line 211) | func IsNetbootClient(pkt *dhcpv4.DHCPv4) error {
function wrapNonNil (line 257) | func wrapNonNil(err error, format string) error {
FILE: internal/dhcp/dhcp_test.go
constant examplePXEClient (line 18) | examplePXEClient = "PXEClient:Arch:00007:UNDI:003001"
constant exampleHTTPClient (line 19) | exampleHTTPClient = "HTTPClient:Arch:00016:UNDI:003001"
function TestNewInfo (line 22) | func TestNewInfo(t *testing.T) {
function TestArch (line 78) | func TestArch(t *testing.T) {
function TestIsNetbootClient (line 110) | func TestIsNetbootClient(t *testing.T) {
function TestBootfile (line 161) | func TestBootfile(t *testing.T) {
function TestNextServer (line 222) | func TestNextServer(t *testing.T) {
function TestOpt43 (line 268) | func TestOpt43(t *testing.T) {
function TestUserClassString (line 306) | func TestUserClassString(t *testing.T) {
function TestIsRaspberryPI (line 313) | func TestIsRaspberryPI(t *testing.T) {
FILE: internal/dhcp/handler/handler.go
type BackendReader (line 14) | type BackendReader interface
FILE: internal/dhcp/handler/proxy/proxy.go
constant tracerName (line 37) | tracerName = "github.com/tinkerbell/smee/internal/dhcp/handler/proxy"
type Handler (line 40) | type Handler struct
method Handle (line 86) | func (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, d...
method encodeToAttributes (line 240) | func (h *Handler) encodeToAttributes(d *dhcpv4.DHCPv4, namespace strin...
type Netboot (line 68) | type Netboot struct
function setMessageType (line 246) | func setMessageType(reply *dhcpv4.DHCPv4, reqMsg dhcpv4.MessageType) err...
type IgnorePacketError (line 259) | type IgnorePacketError struct
method Error (line 265) | func (e IgnorePacketError) Error() string {
function replyDestination (line 277) | func replyDestination(directPeer net.Addr, giaddr net.IP) net.Addr {
FILE: internal/dhcp/handler/reservation/handler.go
constant tracerName (line 21) | tracerName = "github.com/tinkerbell/smee"
method setDefaults (line 25) | func (h *Handler) setDefaults() {
method Handle (line 35) | func (h *Handler) Handle(ctx context.Context, conn *ipv4.PacketConn, p d...
function replyDestination (line 166) | func replyDestination(directPeer net.Addr, giaddr net.IP) net.Addr {
method readBackend (line 175) | func (h *Handler) readBackend(ctx context.Context, mac net.HardwareAddr)...
method updateMsg (line 197) | func (h *Handler) updateMsg(ctx context.Context, pkt *dhcpv4.DHCPv4, d *...
method encodeToAttributes (line 218) | func (h *Handler) encodeToAttributes(d *dhcpv4.DHCPv4, namespace string)...
function hardwareNotFound (line 226) | func hardwareNotFound(err error) bool {
FILE: internal/dhcp/handler/reservation/handler_test.go
type mockBackend (line 30) | type mockBackend struct
method GetByMac (line 42) | func (m *mockBackend) GetByMac(context.Context, net.HardwareAddr) (*da...
method GetByIP (line 76) | func (m *mockBackend) GetByIP(context.Context, net.IP) (*data.DHCP, *d...
type hwNotFoundError (line 37) | type hwNotFoundError struct
method NotFound (line 39) | func (hwNotFoundError) NotFound() bool { return true }
method Error (line 40) | func (hwNotFoundError) Error() string { return "not found" }
function TestHandle (line 83) | func TestHandle(t *testing.T) {
function client (line 352) | func client(pc net.PacketConn) (*dhcpv4.DHCPv4, error) {
function TestUpdateMsg (line 366) | func TestUpdateMsg(t *testing.T) {
function TestOne (line 438) | func TestOne(t *testing.T) {
function TestReadBackend (line 445) | func TestReadBackend(t *testing.T) {
function TestEncodeToAttributes (line 519) | func TestEncodeToAttributes(t *testing.T) {
FILE: internal/dhcp/handler/reservation/noop.go
type noop (line 13) | type noop struct
method GetByMac (line 16) | func (h noop) GetByMac(_ context.Context, _ net.HardwareAddr) (*data.D...
method GetByIP (line 21) | func (h noop) GetByIP(_ context.Context, _ net.IP) (*data.DHCP, *data....
FILE: internal/dhcp/handler/reservation/noop_test.go
function TestNoop (line 11) | func TestNoop(t *testing.T) {
FILE: internal/dhcp/handler/reservation/option.go
method setDHCPOpts (line 21) | func (h *Handler) setDHCPOpts(_ context.Context, _ *dhcpv4.DHCPv4, d *da...
method setNetworkBootOpts (line 68) | func (h *Handler) setNetworkBootOpts(ctx context.Context, m *dhcpv4.DHCP...
method bootfileAndNextServer (line 111) | func (h *Handler) bootfileAndNextServer(ctx context.Context, pkt *dhcpv4...
FILE: internal/dhcp/handler/reservation/option_test.go
constant examplePXEClient (line 26) | examplePXEClient = "PXEClient:Arch:00007:UNDI:003001"
constant exampleHTTPClient (line 27) | exampleHTTPClient = "HTTPClient:Arch:00016:UNDI:003001"
function TestSetDHCPOpts (line 30) | func TestSetDHCPOpts(t *testing.T) {
function TestBootfileAndNextServer (line 126) | func TestBootfileAndNextServer(t *testing.T) {
function TestSetNetworkBootOpts (line 253) | func TestSetNetworkBootOpts(t *testing.T) {
FILE: internal/dhcp/handler/reservation/reservation.go
type Handler (line 15) | type Handler struct
type Netboot (line 42) | type Netboot struct
FILE: internal/dhcp/otel/otel.go
constant keyNamespace (line 16) | keyNamespace = "DHCP"
type Encoder (line 19) | type Encoder struct
method Encode (line 46) | func (e *Encoder) Encode(pkt *dhcpv4.DHCPv4, namespace string, encoder...
type notFoundError (line 23) | type notFoundError struct
method Error (line 27) | func (e *notFoundError) Error() string {
method found (line 31) | func (e *notFoundError) found() bool {
type found (line 35) | type found interface
function OptNotFound (line 40) | func OptNotFound(err error) bool {
function AllEncoders (line 64) | func AllEncoders() []func(d *dhcpv4.DHCPv4, namespace string) (attribute...
function EncodeFlags (line 79) | func EncodeFlags(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeTransactionID (line 90) | func EncodeTransactionID(d *dhcpv4.DHCPv4, namespace string) (attribute....
function EncodeOpt1 (line 101) | func EncodeOpt1(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue,...
function EncodeOpt3 (line 114) | func EncodeOpt3(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue,...
function EncodeOpt6 (line 131) | func EncodeOpt6(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue,...
function EncodeOpt12 (line 148) | func EncodeOpt12(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt15 (line 159) | func EncodeOpt15(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt28 (line 170) | func EncodeOpt28(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt42 (line 181) | func EncodeOpt42(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt51 (line 198) | func EncodeOpt51(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt53 (line 209) | func EncodeOpt53(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt54 (line 220) | func EncodeOpt54(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt60 (line 231) | func EncodeOpt60(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt93 (line 242) | func EncodeOpt93(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt94 (line 258) | func EncodeOpt94(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt97 (line 275) | func EncodeOpt97(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue...
function EncodeOpt119 (line 292) | func EncodeOpt119(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValu...
function EncodeYIADDR (line 305) | func EncodeYIADDR(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValu...
function EncodeSIADDR (line 316) | func EncodeSIADDR(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValu...
function EncodeCHADDR (line 327) | func EncodeCHADDR(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValu...
function EncodeFILE (line 338) | func EncodeFILE(d *dhcpv4.DHCPv4, namespace string) (attribute.KeyValue,...
function TraceparentFromContext (line 350) | func TraceparentFromContext(ctx context.Context) []byte {
FILE: internal/dhcp/otel/otel_test.go
function TestEncode (line 19) | func TestEncode(t *testing.T) {
function TestEncodeError (line 47) | func TestEncodeError(t *testing.T) {
function TestSetOpt1 (line 65) | func TestSetOpt1(t *testing.T) {
function TestSetOpt3 (line 92) | func TestSetOpt3(t *testing.T) {
function TestSetOpt6 (line 119) | func TestSetOpt6(t *testing.T) {
function TestSetOpt12 (line 146) | func TestSetOpt12(t *testing.T) {
function TestSetOpt15 (line 173) | func TestSetOpt15(t *testing.T) {
function TestSetOpt28 (line 200) | func TestSetOpt28(t *testing.T) {
function TestSetOpt42 (line 227) | func TestSetOpt42(t *testing.T) {
function TestSetOpt51 (line 254) | func TestSetOpt51(t *testing.T) {
function TestSetOpt53 (line 281) | func TestSetOpt53(t *testing.T) {
function TestSetOpt54 (line 308) | func TestSetOpt54(t *testing.T) {
function TestSetOpt60 (line 335) | func TestSetOpt60(t *testing.T) {
function TestSetOpt93 (line 362) | func TestSetOpt93(t *testing.T) {
function TestSetOpt94 (line 390) | func TestSetOpt94(t *testing.T) {
function TestSetOpt97 (line 418) | func TestSetOpt97(t *testing.T) {
function TestSetOpt119 (line 446) | func TestSetOpt119(t *testing.T) {
function TestSetHeaderFlags (line 473) | func TestSetHeaderFlags(t *testing.T) {
function TestSetHeaderTransactionID (line 498) | func TestSetHeaderTransactionID(t *testing.T) {
function TestSetHeaderYIADDR (line 523) | func TestSetHeaderYIADDR(t *testing.T) {
function TestSetHeaderSIADDR (line 548) | func TestSetHeaderSIADDR(t *testing.T) {
function TestSetHeaderCHADDR (line 573) | func TestSetHeaderCHADDR(t *testing.T) {
function TestSetHeaderFILE (line 598) | func TestSetHeaderFILE(t *testing.T) {
function TestTraceparentFromContext (line 623) | func TestTraceparentFromContext(t *testing.T) {
FILE: internal/dhcp/server/dhcp.go
type Handler (line 18) | type Handler interface
type DHCP (line 23) | type DHCP struct
method Serve (line 30) | func (s *DHCP) Serve(ctx context.Context) error {
method Close (line 92) | func (s *DHCP) Close() error {
function NewServer (line 97) | func NewServer(ifname string, addr *net.UDPAddr, handler ...Handler) (*D...
FILE: internal/dhcp/server/dhcp_test.go
type mock (line 17) | type mock struct
method Handle (line 27) | func (m *mock) Handle(_ context.Context, conn *ipv4.PacketConn, d data...
method setOpts (line 57) | func (m *mock) setOpts() []dhcpv4.Modifier {
function dhcp (line 71) | func dhcp(ctx context.Context) (*dhcpv4.DHCPv4, error) {
function TestServe (line 88) | func TestServe(t *testing.T) {
FILE: internal/ipxe/http/http.go
type Config (line 19) | type Config struct
method ServeHTTP (line 31) | func (s *Config) ServeHTTP(ctx context.Context, addr string, handlers ...
method serveHealthchecker (line 91) | func (s *Config) serveHealthchecker(rev string, start time.Time) http....
type HandlerMapping (line 27) | type HandlerMapping
function otelFuncWrapper (line 112) | func otelFuncWrapper(route string, h func(w http.ResponseWriter, req *ht...
FILE: internal/ipxe/http/middleware.go
type loggingMiddleware (line 12) | type loggingMiddleware struct
method ServeHTTP (line 18) | func (h *loggingMiddleware) ServeHTTP(w http.ResponseWriter, req *http...
type responseWriter (line 41) | type responseWriter struct
method Write (line 46) | func (w *responseWriter) Write(b []byte) (int, error) {
method WriteHeader (line 58) | func (w *responseWriter) WriteHeader(code int) {
function clientIP (line 65) | func clientIP(str string) string {
FILE: internal/ipxe/http/xff.go
type xffOptions (line 33) | type xffOptions struct
type xff (line 43) | type xff struct
method Handler (line 65) | func (xff *xff) Handler(h http.Handler) http.Handler {
method allowed (line 134) | func (xff *xff) allowed(sip string) bool {
function newXFF (line 51) | func newXFF(options xffOptions) (*xff, error) {
function getRemoteAddrIfAllowed (line 74) | func getRemoteAddrIfAllowed(r *http.Request, allowed func(sip string) bo...
function parse (line 88) | func parse(ipList string, allowed func(string) bool) string {
function toMasks (line 121) | func toMasks(ips []string) (masks []net.IPNet, err error) {
function ipInMasks (line 144) | func ipInMasks(ip net.IP, masks []net.IPNet) bool {
FILE: internal/ipxe/http/xff_test.go
function TestParse_none (line 33) | func TestParse_none(t *testing.T) {
function allowAll (line 38) | func allowAll(string) bool { return true }
function TestParse_localhost (line 40) | func TestParse_localhost(t *testing.T) {
function TestParse_invalid (line 45) | func TestParse_invalid(t *testing.T) {
function TestParse_invalid_sioux (line 50) | func TestParse_invalid_sioux(t *testing.T) {
function TestParse_invalid_private_lookalike (line 55) | func TestParse_invalid_private_lookalike(t *testing.T) {
function TestParse_valid (line 60) | func TestParse_valid(t *testing.T) {
function TestParse_multi_first (line 65) | func TestParse_multi_first(t *testing.T) {
function TestParse_multi_with_invalid (line 70) | func TestParse_multi_with_invalid(t *testing.T) {
function TestParse_multi_with_invalid2 (line 75) | func TestParse_multi_with_invalid2(t *testing.T) {
function TestParse_multi_with_invalid_sioux (line 80) | func TestParse_multi_with_invalid_sioux(t *testing.T) {
function TestParse_ipv6_with_port (line 85) | func TestParse_ipv6_with_port(t *testing.T) {
function TestToMasks_empty (line 90) | func TestToMasks_empty(t *testing.T) {
function TestToMasks (line 97) | func TestToMasks(t *testing.T) {
function TestToMasks_error (line 106) | func TestToMasks_error(t *testing.T) {
function TestAllowed_all (line 113) | func TestAllowed_all(t *testing.T) {
function TestAllowed_yes (line 120) | func TestAllowed_yes(t *testing.T) {
function TestAllowed_no (line 132) | func TestAllowed_no(t *testing.T) {
function TestParseUnallowedMidway (line 144) | func TestParseUnallowedMidway(t *testing.T) {
function TestParseMany (line 152) | func TestParseMany(t *testing.T) {
FILE: internal/ipxe/script/auto.go
function GenerateTemplate (line 8) | func GenerateTemplate(d any, script string) (string, error) {
FILE: internal/ipxe/script/auto_test.go
function TestGenerateTemplate (line 9) | func TestGenerateTemplate(t *testing.T) {
FILE: internal/ipxe/script/custom.go
type Custom (line 20) | type Custom struct
FILE: internal/ipxe/script/hook.go
type Hook (line 51) | type Hook struct
FILE: internal/ipxe/script/ipxe.go
type Handler (line 21) | type Handler struct
method HandlerFunc (line 108) | func (h *Handler) HandlerFunc() http.HandlerFunc {
method serveStaticIPXEScript (line 173) | func (h *Handler) serveStaticIPXEScript(w http.ResponseWriter) {
method serveBootScript (line 215) | func (h *Handler) serveBootScript(ctx context.Context, w http.Response...
method defaultScript (line 262) | func (h *Handler) defaultScript(span trace.Span, hw data) (string, err...
method customScript (line 307) | func (h *Handler) customScript(hw data) (string, error) {
type data (line 35) | type data struct
type OSIE (line 49) | type OSIE struct
function getByMac (line 60) | func getByMac(ctx context.Context, mac net.HardwareAddr, br handler.Back...
function getByIP (line 83) | func getByIP(ctx context.Context, ip net.IP, br handler.BackendReader) (...
function getIP (line 195) | func getIP(remoteAddr string) (net.IP, error) {
function getMAC (line 205) | func getMAC(urlPath string) (net.HardwareAddr, error) {
FILE: internal/ipxe/script/ipxe_test.go
function TestCustomScript (line 15) | func TestCustomScript(t *testing.T) {
function TestDefaultScript (line 48) | func TestDefaultScript(t *testing.T) {
function TestStaticScript (line 117) | func TestStaticScript(t *testing.T) {
FILE: internal/iso/internal/acsii.go
function EqualFold (line 9) | func EqualFold(s, t string) bool {
function lower (line 22) | func lower(b byte) byte {
function IsPrint (line 31) | func IsPrint(s string) bool {
FILE: internal/iso/internal/acsii_test.go
function TestEqualFold (line 9) | func TestEqualFold(t *testing.T) {
function TestIsPrint (line 47) | func TestIsPrint(t *testing.T) {
FILE: internal/iso/internal/context.go
type patchCtxKeyType (line 5) | type patchCtxKeyType
constant isoPatchCtxKey (line 7) | isoPatchCtxKey patchCtxKeyType = "iso-patch"
function WithPatch (line 9) | func WithPatch(ctx context.Context, patch []byte) context.Context {
function GetPatch (line 13) | func GetPatch(ctx context.Context) []byte {
FILE: internal/iso/internal/reverseproxy.go
type ProxyRequest (line 29) | type ProxyRequest struct
method SetURL (line 53) | func (r *ProxyRequest) SetURL(target *url.URL) {
method SetXForwarded (line 77) | func (r *ProxyRequest) SetXForwarded() {
type ReverseProxy (line 102) | type ReverseProxy struct
method defaultErrorHandler (line 313) | func (p *ReverseProxy) defaultErrorHandler(rw http.ResponseWriter, req...
method getErrorHandler (line 318) | func (p *ReverseProxy) getErrorHandler() func(http.ResponseWriter, *ht...
method modifyResponse (line 327) | func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *htt...
method ServeHTTP (line 339) | func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Req...
method flushInterval (line 607) | func (p *ReverseProxy) flushInterval(res *http.Response) time.Duration {
method copyResponse (line 624) | func (p *ReverseProxy) copyResponse(ctx context.Context, dst http.Resp...
method copyBuffer (line 660) | func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []...
method logf (line 691) | func (p *ReverseProxy) logf(format string, args ...any) {
method handleUpgradeResponse (line 755) | func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, r...
type BufferPool (line 207) | type BufferPool interface
type CopyBuffer (line 212) | type CopyBuffer interface
function singleJoiningSlash (line 216) | func singleJoiningSlash(a, b string) string {
function joinURLPath (line 228) | func joinURLPath(a, b *url.URL) (path, rawpath string) {
function NewSingleHostReverseProxy (line 269) | func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
function rewriteRequestURL (line 276) | func rewriteRequestURL(req *http.Request, target *url.URL) {
function copyHeader (line 288) | func copyHeader(dst, src http.Header) {
function shouldPanicOnCopyError (line 572) | func shouldPanicOnCopyError(req *http.Request) bool {
function removeHopByHopHeaders (line 588) | func removeHopByHopHeaders(h http.Header) {
type maxLatencyWriter (line 699) | type maxLatencyWriter struct
method Write (line 709) | func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
method delayedFlush (line 729) | func (m *maxLatencyWriter) delayedFlush() {
method stop (line 739) | func (m *maxLatencyWriter) stop() {
function upgradeType (line 748) | func upgradeType(h http.Header) string {
type switchProtocolCopier (line 818) | type switchProtocolCopier struct
method copyFromBackend (line 822) | func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
method copyToBackend (line 827) | func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
function cleanQueryParams (line 832) | func cleanQueryParams(s string) string {
function ishex (line 853) | func ishex(c byte) bool {
FILE: internal/iso/internal/reverseproxy_test.go
constant fakeHopHeader (line 32) | fakeHopHeader = "X-Fake-Hop-Header-For-Test"
function init (line 34) | func init() {
function TestReverseProxy (line 39) | func TestReverseProxy(t *testing.T) {
function TestReverseProxyStripHeadersPresentInConnection (line 157) | func TestReverseProxyStripHeadersPresentInConnection(t *testing.T) {
function TestReverseProxyStripEmptyConnection (line 241) | func TestReverseProxyStripEmptyConnection(t *testing.T) {
function TestXForwardedFor (line 299) | func TestXForwardedFor(t *testing.T) {
function TestXForwardedFor_Omit (line 340) | func TestXForwardedFor_Omit(t *testing.T) {
function TestReverseProxyRewriteStripsForwarded (line 372) | func TestReverseProxyRewriteStripsForwarded(t *testing.T) {
function TestReverseProxyQuery (line 423) | func TestReverseProxyQuery(t *testing.T) {
function TestReverseProxyFlushInterval (line 450) | func TestReverseProxyFlushInterval(t *testing.T) {
type mockFlusher (line 480) | type mockFlusher struct
method Flush (line 485) | func (m *mockFlusher) Flush() {
type wrappedRW (line 489) | type wrappedRW struct
method Unwrap (line 493) | func (w *wrappedRW) Unwrap() http.ResponseWriter {
function TestReverseProxyResponseControllerFlushInterval (line 497) | func TestReverseProxyResponseControllerFlushInterval(t *testing.T) {
function TestReverseProxyFlushIntervalHeaders (line 536) | func TestReverseProxyFlushIntervalHeaders(t *testing.T) {
function TestReverseProxyCancellation (line 577) | func TestReverseProxyCancellation(t *testing.T) {
function req (line 633) | func req(t *testing.T, v string) *http.Request {
function TestNilBody (line 642) | func TestNilBody(t *testing.T) {
function TestUserAgentHeader (line 672) | func TestUserAgentHeader(t *testing.T) {
type bufferPool (line 707) | type bufferPool struct
method Get (line 712) | func (bp bufferPool) Get() []byte { return bp.get() }
method Put (line 713) | func (bp bufferPool) Put(v []byte) { bp.put(v) }
function TestReverseProxyGetPutBuffer (line 715) | func TestReverseProxyGetPutBuffer(t *testing.T) {
function TestReverseProxy_Post (line 772) | func TestReverseProxy_Post(t *testing.T) {
type RoundTripperFunc (line 812) | type RoundTripperFunc
method RoundTrip (line 814) | func (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Respons...
function TestReverseProxy_NilBody (line 819) | func TestReverseProxy_NilBody(t *testing.T) {
function TestReverseProxy_AllocatedHeader (line 843) | func TestReverseProxy_AllocatedHeader(t *testing.T) {
function TestReverseProxyModifyResponse (line 864) | func TestReverseProxyModifyResponse(t *testing.T) {
type failingRoundTripper (line 903) | type failingRoundTripper struct
method RoundTrip (line 905) | func (failingRoundTripper) RoundTrip(*http.Request) (*http.Response, e...
type staticResponseRoundTripper (line 909) | type staticResponseRoundTripper struct
method RoundTrip (line 911) | func (rt staticResponseRoundTripper) RoundTrip(*http.Request) (*http.R...
function TestReverseProxyErrorHandler (line 915) | func TestReverseProxyErrorHandler(t *testing.T) {
function TestReverseProxy_CopyBuffer (line 991) | func TestReverseProxy_CopyBuffer(t *testing.T) {
type staticTransport (line 1035) | type staticTransport struct
method RoundTrip (line 1039) | func (t *staticTransport) RoundTrip(r *http.Request) (*http.Response, ...
function BenchmarkServeHTTP (line 1043) | func BenchmarkServeHTTP(b *testing.B) {
function TestServeHTTPDeepCopy (line 1062) | func TestServeHTTPDeepCopy(t *testing.T) {
function TestClonesRequestHeaders (line 1102) | func TestClonesRequestHeaders(t *testing.T) {
type roundTripperFunc (line 1130) | type roundTripperFunc
method RoundTrip (line 1132) | func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Respons...
function TestModifyResponseClosesBody (line 1136) | func TestModifyResponseClosesBody(t *testing.T) {
type checkCloser (line 1167) | type checkCloser struct
method Close (line 1171) | func (cc *checkCloser) Close() error {
method Read (line 1176) | func (cc *checkCloser) Read(b []byte) (int, error) {
function TestReverseProxy_PanicBodyError (line 1181) | func TestReverseProxy_PanicBodyError(t *testing.T) {
function TestSelectFlushInterval (line 1255) | func TestSelectFlushInterval(t *testing.T) {
function TestReverseProxyWebSocket (line 1335) | func TestReverseProxyWebSocket(t *testing.T) {
function TestReverseProxyWebSocketCancellation (line 1421) | func TestReverseProxyWebSocketCancellation(t *testing.T) {
function TestUnannouncedTrailer (line 1552) | func TestUnannouncedTrailer(t *testing.T) {
function TestSetURL (line 1582) | func TestSetURL(t *testing.T) {
function TestSingleJoinSlash (line 1616) | func TestSingleJoinSlash(t *testing.T) {
function TestJoinURLPath (line 1639) | func TestJoinURLPath(t *testing.T) {
function TestReverseProxyRewriteReplacesOut (line 1666) | func TestReverseProxyRewriteReplacesOut(t *testing.T) {
function Test1xxHeadersNotModifiedAfterRoundTrip (line 1691) | func Test1xxHeadersNotModifiedAfterRoundTrip(t *testing.T) {
function Test1xxResponses (line 1732) | func Test1xxResponses(t *testing.T) {
constant testWantsCleanQuery (line 1813) | testWantsCleanQuery = true
constant testWantsRawQuery (line 1814) | testWantsRawQuery = false
function TestReverseProxyQueryParameterSmugglingDirectorDoesNotParseForm (line 1817) | func TestReverseProxyQueryParameterSmugglingDirectorDoesNotParseForm(t *...
function TestReverseProxyQueryParameterSmugglingDirectorParsesForm (line 1828) | func TestReverseProxyQueryParameterSmugglingDirectorParsesForm(t *testin...
function TestReverseProxyQueryParameterSmugglingRewrite (line 1842) | func TestReverseProxyQueryParameterSmugglingRewrite(t *testing.T) {
function TestReverseProxyQueryParameterSmugglingRewritePreservesRawQuery (line 1852) | func TestReverseProxyQueryParameterSmugglingRewritePreservesRawQuery(t *...
function testReverseProxyQueryParameterSmuggling (line 1863) | func testReverseProxyQueryParameterSmuggling(t *testing.T, wantCleanQuer...
type testResponseWriter (line 1907) | type testResponseWriter struct
method Header (line 1913) | func (rw *testResponseWriter) Header() http.Header {
method WriteHeader (line 1920) | func (rw *testResponseWriter) WriteHeader(statusCode int) {
method Write (line 1926) | func (rw *testResponseWriter) Write(p []byte) (int, error) {
FILE: internal/iso/ipam.go
function parseIPAM (line 12) | func parseIPAM(d *data.DHCP) string {
FILE: internal/iso/ipam_test.go
function TestParseIPAM (line 12) | func TestParseIPAM(t *testing.T) {
FILE: internal/iso/iso.go
constant defaultConsoles (line 26) | defaultConsoles = "console=ttyAMA0 console=ttyS0 console=tty0 console=tt...
type BackendReader (line 30) | type BackendReader interface
type Handler (line 38) | type Handler struct
method HandlerFunc (line 61) | func (h *Handler) HandlerFunc() (http.HandlerFunc, error) {
method Copy (line 86) | func (h *Handler) Copy(ctx context.Context, dst io.Writer, src io.Read...
method RoundTrip (line 130) | func (h *Handler) RoundTrip(req *http.Request) (*http.Response, error) {
method constructPatch (line 226) | func (h *Handler) constructPatch(console, mac string, d *data.DHCP) st...
method getFacility (line 256) | func (h *Handler) getFacility(ctx context.Context, mac net.HardwareAdd...
function getMAC (line 246) | func getMAC(urlPath string) (net.HardwareAddr, error) {
function randomPercentage (line 269) | func randomPercentage(precision int64) float64 {
FILE: internal/iso/iso_test.go
constant magicString (line 24) | magicString = `464vn90e7rbj08xbwdjejmdf4it17c5zfzjyfhthbh19eij201hjgit02...
function TestReqPathInvalid (line 26) | func TestReqPathInvalid(t *testing.T) {
function TestCreateISO (line 58) | func TestCreateISO(t *testing.T) {
function TestPatching (line 107) | func TestPatching(t *testing.T) {
type mockBackend (line 178) | type mockBackend struct
method GetByMac (line 180) | func (m *mockBackend) GetByMac(context.Context, net.HardwareAddr) (*da...
method GetByIP (line 188) | func (m *mockBackend) GetByIP(context.Context, net.IP) (*data.DHCP, *d...
FILE: internal/metric/metric.go
function Init (line 20) | func Init() {
function initCounterLabels (line 98) | func initCounterLabels(m *prometheus.CounterVec, l []prometheus.Labels) {
function initGaugeLabels (line 104) | func initGaugeLabels(m *prometheus.GaugeVec, l []prometheus.Labels) {
function initObserverLabels (line 110) | func initObserverLabels(m prometheus.ObserverVec, l []prometheus.Labels) {
FILE: internal/otel/otel.go
type SimpleCarrier (line 40) | type SimpleCarrier
method Get (line 43) | func (otp SimpleCarrier) Get(key string) string {
method Set (line 48) | func (otp SimpleCarrier) Set(key, value string) {
method Keys (line 53) | func (otp SimpleCarrier) Keys() []string {
method Clear (line 62) | func (otp SimpleCarrier) Clear() {
function TraceparentStringFromContext (line 70) | func TraceparentStringFromContext(ctx context.Context) string {
function ContextWithEnvTraceparent (line 82) | func ContextWithEnvTraceparent(ctx context.Context) context.Context {
function ContextWithTraceparentString (line 93) | func ContextWithTraceparentString(ctx context.Context, traceparent strin...
type Config (line 103) | type Config struct
method initTracing (line 122) | func (c Config) initTracing(ctx context.Context) (context.Context, con...
method Handle (line 203) | func (c Config) Handle(err error) {
function Init (line 112) | func Init(ctx context.Context, c Config) (context.Context, context.Cance...
FILE: internal/syslog/facility_string.go
function _ (line 7) | func _() {
constant _facility_name (line 37) | _facility_name = "kernusermaildaemonauthsysloglprnewsuucpclockauthprivft...
method String (line 41) | func (i facility) String() string {
FILE: internal/syslog/message.go
type facility (line 12) | type facility
constant kern (line 15) | kern facility = iota
constant user (line 16) | user
constant mail (line 17) | mail
constant daemon (line 18) | daemon
constant auth (line 19) | auth
constant syslog (line 20) | syslog
constant lpr (line 21) | lpr
constant news (line 22) | news
constant uucp (line 23) | uucp
constant clock (line 24) | clock
constant authpriv (line 25) | authpriv
constant ftp (line 26) | ftp
constant ntp (line 27) | ntp
constant audit (line 28) | audit
constant alert (line 29) | alert
constant cron (line 30) | cron
constant local0 (line 31) | local0
constant local1 (line 32) | local1
constant local2 (line 33) | local2
constant local3 (line 34) | local3
constant local4 (line 35) | local4
constant local5 (line 36) | local5
constant local6 (line 37) | local6
constant local7 (line 38) | local7
type severity (line 42) | type severity
constant EMERG (line 45) | EMERG severity = iota
constant ALERT (line 46) | ALERT
constant CRIT (line 47) | CRIT
constant ERR (line 48) | ERR
constant WARNING (line 49) | WARNING
constant NOTICE (line 50) | NOTICE
constant INFO (line 51) | INFO
constant DEBUG (line 52) | DEBUG
type message (line 55) | type message struct
method Facility (line 70) | func (m *message) Facility() facility {
method Host (line 74) | func (m *message) Host() string {
method Severity (line 78) | func (m *message) Severity() severity {
method String (line 84) | func (m *message) String() string {
method Timestamp (line 120) | func (m *message) Timestamp() time.Time {
method correctLegacyTime (line 124) | func (m *message) correctLegacyTime(t time.Time) {
method parse (line 139) | func (m *message) parse() bool {
method parseHeader (line 153) | func (m *message) parseHeader() bool {
method parseStructuredData (line 169) | func (m *message) parseStructuredData() bool {
method parseLegacyHeader (line 179) | func (m *message) parseLegacyHeader() bool {
method parseLegacyTag (line 207) | func (m *message) parseLegacyTag() {
method parsePriority (line 249) | func (m *message) parsePriority() bool {
method parseTimestamp (line 270) | func (m *message) parseTimestamp(b []byte) bool {
method parseVersion (line 291) | func (m *message) parseVersion() bool {
method reset (line 306) | func (m *message) reset() {
method trimSeverityPrefix (line 315) | func (m *message) trimSeverityPrefix() {
method trimTimePrefix (line 320) | func (m *message) trimTimePrefix() {
method trimCarriageReturns (line 324) | func (m *message) trimCarriageReturns() {
function ignoreNil (line 331) | func ignoreNil(b []byte) []byte {
FILE: internal/syslog/receiver.go
type Receiver (line 20) | type Receiver struct
method Done (line 59) | func (r *Receiver) Done() <-chan struct{} {
method Err (line 63) | func (r *Receiver) Err() error {
method cleanup (line 67) | func (r *Receiver) cleanup() {
method run (line 74) | func (r *Receiver) run(ctx context.Context) {
method runParser (line 152) | func (r *Receiver) runParser() {
function StartReceiver (line 29) | func StartReceiver(ctx context.Context, logger logr.Logger, laddr string...
function parse (line 117) | func parse(m *message) map[string]interface{} {
FILE: internal/syslog/severity_string.go
function _ (line 7) | func _() {
constant _severity_name (line 21) | _severity_name = "EMERGALERTCRITERRWARNINGNOTICEINFODEBUG"
method String (line 25) | func (i severity) String() string {
Condensed preview — 104 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (557K chars).
[
{
"path": ".dockerignore",
"chars": 43,
"preview": "*\n!cmd/smee/smee-*-*\n!cmd/smee/smee\n!test/\n"
},
{
"path": ".github/CODEOWNERS",
"chars": 104,
"preview": "/.github/settings.yml @chrisdoherty4 @jacobweinstock\n/.github/CODEOWNERS @chrisdoherty4 @jacobweinstock\n"
},
{
"path": ".github/codecov.yml",
"chars": 363,
"preview": "---\ncoverage:\n precision: 0 # xx%\n round: down # round down\n range: 30..40 # red < yellow (this range) < green\n\n sta"
},
{
"path": ".github/dependabot.yml",
"chars": 1063,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n "
},
{
"path": ".github/mergify.yml",
"chars": 640,
"preview": "queue_rules:\n - name: default\n queue_conditions:\n - base=main\n - \"#approved-reviews-by>=1\"\n - \"#chang"
},
{
"path": ".github/settings.yml",
"chars": 1028,
"preview": "# Collaborators: give specific users access to this repository.\n# See https://docs.github.com/en/rest/reference/repos#ad"
},
{
"path": ".github/workflows/ci-checks.sh",
"chars": 289,
"preview": "#!/usr/bin/env bash\n\nset -eux\n\nfailed=0\n\nif [[ -n $(go run golang.org/x/tools/cmd/goimports@latest -d -e -l .) ]]; then\n"
},
{
"path": ".github/workflows/ci.yaml",
"chars": 2549,
"preview": "name: For each commit and PR\non:\n push:\n branches:\n - \"*\"\n tags-ignore:\n - \"v*\"\n pull_request:\n\nenv:\n "
},
{
"path": ".github/workflows/tags.yaml",
"chars": 1916,
"preview": "on:\n push:\n tags:\n - \"v*\"\nname: Create release\nenv:\n REGISTRY: quay.io\n IMAGE_NAME: ${{ github.repository }}\n"
},
{
"path": ".gitignore",
"chars": 121,
"preview": "*.iml\n*.orig\n*.test\n.idea*/**\n/bin/\n/cmd/smee/smee\n/cmd/smee/smee-*-*\ncoverage.txt\n.vscode\n\n# added by lint-install\nout/"
},
{
"path": ".golangci.yml",
"chars": 4429,
"preview": "version: \"2\"\nrun:\n # The default runtime timeout is 1m, which doesn't work well on Github Actions.\n timeout: 4m\nlinter"
},
{
"path": "CONTRIBUTING.md",
"chars": 5561,
"preview": "# Contributor Guide\n\nWelcome to Smee!\nWe are really excited to have you.\nPlease use the following guide on your contribu"
},
{
"path": "Dockerfile",
"chars": 387,
"preview": "# run `make image` to build the binary + container\n# if you're using `make build` this Dockerfile will not find the bina"
},
{
"path": "LICENSE",
"chars": 11348,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 965,
"preview": "all: help\n\n-include lint.mk\n-include rules.mk\n\nbuild: cmd/smee/smee ## Compile smee for host OS and Architecture\n\ncrossc"
},
{
"path": "README.md",
"chars": 14811,
"preview": "> [!IMPORTANT] \n> The Smee repo has been deprecated. All functionality has been moved to https://github.com/tinkerbell/"
},
{
"path": "RELEASING.md",
"chars": 658,
"preview": "# Releasing\n\n## Process\n\nFor version v0.x.y:\n\n1. Create the annotated tag\n > NOTE: To use your GPG signature when push"
},
{
"path": "Tiltfile",
"chars": 1774,
"preview": "load('ext://restart_process', 'docker_build_with_restart')\nload('ext://local_output', 'local_output')\nload('ext://helm_r"
},
{
"path": "cmd/smee/backend.go",
"chars": 2502,
"preview": "package main\n\nimport (\n\t\"context\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/tinkerbell/smee/internal/backend/file\"\n\t\"gith"
},
{
"path": "cmd/smee/flag.go",
"chars": 12432,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"time\"\n\n\t\"g"
},
{
"path": "cmd/smee/flag_test.go",
"chars": 8804,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestParser(t *testing."
},
{
"path": "cmd/smee/main.go",
"chars": 16216,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\""
},
{
"path": "contrib/tag-release.sh",
"chars": 1137,
"preview": "#!/usr/bin/env bash\n\nset -o errexit -o nounset -o pipefail\n\nif [ -z \"${1-}\" ]; then\n\techo \"Must specify new tag\"\n\texit 1"
},
{
"path": "docker-compose.yml",
"chars": 2034,
"preview": "---\n# Provides a docker-compose configuration for local fast iteration when\n# hacking on smee alone.\n# TODO: figure out "
},
{
"path": "docs/Backend-File.md",
"chars": 1675,
"preview": "# File Watcher Backend\n\nThis document gives an overview of the file watcher backend.\nThis backend will read in and watch"
},
{
"path": "docs/Code-Structure.md",
"chars": 710,
"preview": "# Code Structure\n\n## Backend\n\nResponsible for communicating with an external persistence source and returning data from "
},
{
"path": "docs/DCO.md",
"chars": 2281,
"preview": "# DCO Sign Off\n\nAll authors to the project retain copyright to their work. However, to ensure\nthat they are only submitt"
},
{
"path": "docs/DESIGN.md",
"chars": 3889,
"preview": "# Smee Design Details\n\n## Table of Contents\n\n- [Smee Flow](#Smee-Flow)\n- [Smee Installers](#Smee-Installers)\n- [IPXE](#I"
},
{
"path": "docs/DESIGNPHILOSOPHY.md",
"chars": 4151,
"preview": "# Design Philosophy\n\nThis living document describes some Go design philosophies we endeavor to incorporate when working,"
},
{
"path": "docs/DHCP.md",
"chars": 4697,
"preview": "# Use an existing DHCP service\n\nThere can be numerous reasons why you may want to use an existing DHCP service instead o"
},
{
"path": "docs/Design-Philosophy.md",
"chars": 4151,
"preview": "# Design Philosophy\n\nThis living document describes some Go design philosophies we endeavor to incorporate when working,"
},
{
"path": "docs/ISO-Static-IPAM.md",
"chars": 2285,
"preview": "# Static IP Address Management in the OSIE ISO\n\nOSIE stands for operating system installation environment. In Tinkerbell"
},
{
"path": "docs/images/BYO_DHCP.uml",
"chars": 930,
"preview": "title Bring your own DHCP service\n\nparticipant Machine\nparticipant DHCP\nparticipant Smee\n\nrbox over Machine,DHCP: 192.16"
},
{
"path": "docs/manifests/README.md",
"chars": 1334,
"preview": "# Deploying Smee\n\nThis directory contains the manifests for deploying Smee to various environments. This document will d"
},
{
"path": "docs/manifests/k3d.md",
"chars": 871,
"preview": "# K3D (K3S in Docker)\n\nThis describes deploying Smee into a K3S in Docker (K3D) cluster.\n\n## Prerequisites\n\n- [K3D >= v5"
},
{
"path": "docs/manifests/kind.md",
"chars": 1582,
"preview": "# KinD (Kubernetes in Docker)\n\nThis describes deploying Smee into a Kubernetes in Docker (KinD) cluster.\n\n## Prerequisit"
},
{
"path": "docs/manifests/kubernetes.md",
"chars": 764,
"preview": "# Kubernetes\n\nThis deployment requires a running Kubernetes cluster. It can be a single node cluster. It is required to "
},
{
"path": "docs/manifests/tilt.md",
"chars": 925,
"preview": "# Tilt\n\nThis deployment method is for quick local development. Tilt will build and deploy Smee to the Kubernetes cluster"
},
{
"path": "go.mod",
"chars": 5044,
"preview": "module github.com/tinkerbell/smee\n\ngo 1.24.0\n\ntoolchain go1.24.1\n\nrequire (\n\tgithub.com/ccoveille/go-safecast v1.6.1\n\tgi"
},
{
"path": "go.sum",
"chars": 28731,
"preview": "dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=\ndario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobSt"
},
{
"path": "internal/backend/file/file.go",
"chars": 9624,
"preview": "// Package file watches a file for changes and updates the in memory DHCP data.\npackage file\n\nimport (\n\t\"context\"\n\t\"fmt\""
},
{
"path": "internal/backend/file/file_test.go",
"chars": 10515,
"preview": "package file\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"testi"
},
{
"path": "internal/backend/file/testdata/example.yaml",
"chars": 1817,
"preview": "---\n08:00:27:29:4E:67:\n ipAddress: \"192.168.2.153\"\n subnetMask: \"255.255.255.0\"\n defaultGateway: \"192.168.2.1\"\n name"
},
{
"path": "internal/backend/kube/error.go",
"chars": 564,
"preview": "package kube\n\nimport (\n\t\"net/http\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\ntype hardwareNotFoundError struct{"
},
{
"path": "internal/backend/kube/index.go",
"chars": 1301,
"preview": "package kube\n\nimport (\n\t\"github.com/tinkerbell/tink/api/v1alpha1\"\n\t\"sigs.k8s.io/controller-runtime/pkg/client\"\n)\n\n// MAC"
},
{
"path": "internal/backend/kube/index_test.go",
"chars": 2000,
"preview": "package kube\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/tinkerbell/tink/api/v1alpha1\"\n\t\"sigs.k8s"
},
{
"path": "internal/backend/kube/kube.go",
"chars": 7917,
"preview": "// Package kube is a backend implementation that uses the Tinkerbell CRDs to get DHCP data.\npackage kube\n\nimport (\n\t\"con"
},
{
"path": "internal/backend/kube/kube_test.go",
"chars": 15767,
"preview": "package kube\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\""
},
{
"path": "internal/backend/noop/noop.go",
"chars": 441,
"preview": "package noop\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\nvar errAlways = "
},
{
"path": "internal/backend/noop/noop_test.go",
"chars": 438,
"preview": "package noop\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestBackend(t *testing.T) {\n\tb := Backend{}\n\tctx := conte"
},
{
"path": "internal/dhcp/data/data.go",
"chars": 4241,
"preview": "// Package data is an interface between DHCP backend implementations and the DHCP server.\npackage data\n\nimport (\n\t\"net\"\n"
},
{
"path": "internal/dhcp/data/data_test.go",
"chars": 3508,
"preview": "package data\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"go.opentelemetry.io/"
},
{
"path": "internal/dhcp/dhcp.go",
"chars": 11715,
"preview": "package dhcp\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com"
},
{
"path": "internal/dhcp/dhcp_test.go",
"chars": 10030,
"preview": "package dhcp\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/c"
},
{
"path": "internal/dhcp/handler/handler.go",
"chars": 688,
"preview": "// Package handler holds the interface that backends implement, handlers take in, and the top level dhcp package passes "
},
{
"path": "internal/dhcp/handler/proxy/proxy.go",
"chars": 10782,
"preview": "/*\nPackage proxy implements a DHCP handler that provides proxyDHCP functionality.\n\n\"[A] Proxy DHCP server behaves much l"
},
{
"path": "internal/dhcp/handler/reservation/handler.go",
"chars": 7624,
"preview": "package reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk/d"
},
{
"path": "internal/dhcp/handler/reservation/handler_test.go",
"chars": 18178,
"preview": "package reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\""
},
{
"path": "internal/dhcp/handler/reservation/noop.go",
"chars": 626,
"preview": "// Package noop is a backend handler that does nothing.\npackage reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\n\t\"git"
},
{
"path": "internal/dhcp/handler/reservation/noop_test.go",
"chars": 462,
"preview": "package reservation\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestNoop(t *testi"
},
{
"path": "internal/dhcp/handler/reservation/option.go",
"chars": 5196,
"preview": "package reservation\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/insomniacslk/dhc"
},
{
"path": "internal/dhcp/handler/reservation/option_test.go",
"chars": 11057,
"preview": "package reservation\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n\t"
},
{
"path": "internal/dhcp/handler/reservation/reservation.go",
"chars": 1922,
"preview": "// Package reservation is the handler for responding to DHCPv4 messages with only host reservations.\npackage reservation"
},
{
"path": "internal/dhcp/otel/otel.go",
"chars": 14200,
"preview": "// Package otel handles translating DHCP headers and options to otel key/value attributes.\npackage otel\n\nimport (\n\t\"cont"
},
{
"path": "internal/dhcp/otel/otel_test.go",
"chars": 18954,
"preview": "package otel\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/googl"
},
{
"path": "internal/dhcp/server/dhcp.go",
"chars": 2833,
"preview": "// Package dhcp providers UDP listening and serving functionality.\npackage server\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github."
},
{
"path": "internal/dhcp/server/dhcp_test.go",
"chars": 2778,
"preview": "package server\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/go-logr/logr\"\n\t\"github.com/insomniacslk"
},
{
"path": "internal/ipxe/http/http.go",
"chars": 3359,
"preview": "// package bhttp is the http server for smee.\npackage http\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/"
},
{
"path": "internal/ipxe/http/middleware.go",
"chars": 1601,
"preview": "package http\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-logr/logr\"\n)\n\ntype loggingMiddleware struct {\n"
},
{
"path": "internal/ipxe/http/xff.go",
"chars": 4282,
"preview": "/*\nhttps://github.com/sebest/xff\nCopyright (c) 2015 Sebastien Estienne (sebastien.estienne@gmail.com)\n\nPermission is her"
},
{
"path": "internal/ipxe/http/xff_test.go",
"chars": 4387,
"preview": "/*\nhttps://github.com/sebest/xff\nCopyright (c) 2015 Sebastien Estienne (sebastien.estienne@gmail.com)\n\nPermission is her"
},
{
"path": "internal/ipxe/script/auto.go",
"chars": 339,
"preview": "package script\n\nimport (\n\t\"bytes\"\n\t\"text/template\"\n)\n\nfunc GenerateTemplate(d any, script string) (string, error) {\n\tt :"
},
{
"path": "internal/ipxe/script/auto_test.go",
"chars": 4742,
"preview": "package script\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc TestGenerateTemplate(t *testing.T) {\n\ttests"
},
{
"path": "internal/ipxe/script/custom.go",
"chars": 472,
"preview": "package script\n\nimport \"net/url\"\n\n// CustomScript is the template for the custom script.\n// It will either chain to a UR"
},
{
"path": "internal/ipxe/script/hook.go",
"chars": 2842,
"preview": "package script\n\n// HookScript is the default iPXE script for loading Hook.\nvar HookScript = `#!ipxe\n\necho Loading the Ti"
},
{
"path": "internal/ipxe/script/ipxe.go",
"chars": 9748,
"preview": "package script\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\n\t\"github.com/go-logr/logr\"\n\t"
},
{
"path": "internal/ipxe/script/ipxe_test.go",
"chars": 5829,
"preview": "package script\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t"
},
{
"path": "internal/ipxe/script/static.go",
"chars": 2165,
"preview": "package script\n\n// StaticScript is the iPXE script used when in the auto-proxy mode.\n// It is built to be generic enough"
},
{
"path": "internal/iso/internal/LICENSE",
"chars": 1453,
"preview": "Copyright 2009 The Go Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are per"
},
{
"path": "internal/iso/internal/acsii.go",
"chars": 865,
"preview": "// Copyright 2021 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
},
{
"path": "internal/iso/internal/acsii_test.go",
"chars": 1788,
"preview": "// Copyright 2021 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
},
{
"path": "internal/iso/internal/context.go",
"chars": 377,
"preview": "package internal\n\nimport \"context\"\n\ntype patchCtxKeyType string\n\nconst isoPatchCtxKey patchCtxKeyType = \"iso-patch\"\n\nfun"
},
{
"path": "internal/iso/internal/reverseproxy.go",
"chars": 25947,
"preview": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
},
{
"path": "internal/iso/internal/reverseproxy_test.go",
"chars": 55933,
"preview": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
},
{
"path": "internal/iso/ipam.go",
"chars": 1530,
"preview": "package iso\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\n\t\"github.com/tinkerbell/smee/internal/dhcp/data\"\n)\n\nfunc pa"
},
{
"path": "internal/iso/ipam_test.go",
"chars": 1288,
"preview": "package iso\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/tinkerbell/smee/inter"
},
{
"path": "internal/iso/iso.go",
"chars": 9470,
"preview": "package iso\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url"
},
{
"path": "internal/iso/iso_test.go",
"chars": 8162,
"preview": "package iso\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testin"
},
{
"path": "internal/metric/metric.go",
"chars": 3725,
"preview": "package metric\n\nimport (\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometh"
},
{
"path": "internal/otel/otel.go",
"chars": 6733,
"preview": "/*\nhttps://github.com/equinix-labs/otel-init-go\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache Li"
},
{
"path": "internal/syslog/facility_string.go",
"chars": 1195,
"preview": "// Code generated by \"stringer -type=facility -output=facility_string.go\"; DO NOT EDIT.\n\npackage syslog\n\nimport \"strconv"
},
{
"path": "internal/syslog/message.go",
"chars": 5941,
"preview": "package syslog\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n)\n\n//go:generate go run golang.org/x/tools/cmd/string"
},
{
"path": "internal/syslog/receiver.go",
"chars": 3127,
"preview": "package syslog\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/go"
},
{
"path": "internal/syslog/severity_string.go",
"chars": 776,
"preview": "// Code generated by \"stringer -type=severity -output=severity_string.go\"; DO NOT EDIT.\n\npackage syslog\n\nimport \"strconv"
},
{
"path": "lint.mk",
"chars": 1476,
"preview": "# BEGIN: lint-install github.com/tinkerbell/smee\n# http://github.com/tinkerbell/lint-install\n\n.PHONY: lint\nlint: _lint "
},
{
"path": "rules.mk",
"chars": 1705,
"preview": "# Only use the recipes defined in these makefiles\nMAKEFLAGS += --no-builtin-rules\n.SUFFIXES:\n# Delete target files if th"
},
{
"path": "test/Dockerfile",
"chars": 302,
"preview": "FROM alpine:3.14\nEXPOSE 67 69\n\nRUN apk add --update --upgrade --no-cache net-tools busybox tftp-hpa curl tcpdump\n\nCOPY b"
},
{
"path": "test/busybox-udhcpc-script.sh",
"chars": 196,
"preview": "#!/bin/sh\n# instead of messing with the actual interface configuration\n# this just dumps the environment variables to a "
},
{
"path": "test/extract-traceparent-from-opt43.sh",
"chars": 2659,
"preview": "#!/bin/sh\n# shellcheck shell=dash\n\n# extract_traceparent_from_opt43 takes a hex string from busybox udhcpc's opt43\n# and"
},
{
"path": "test/hardware.yaml",
"chars": 361,
"preview": "---\n02:00:00:00:00:ff:\n ipAddress: \"192.168.99.43\"\n subnetMask: \"255.255.255.0\"\n defaultGateway: \"192.168.99.1\"\n nam"
},
{
"path": "test/otel-collector.yaml",
"chars": 1291,
"preview": "# opentelemetry-collector is a proxy for telemetry events.\n#\n# This configuration is set up for use in smee development."
},
{
"path": "test/start-smee.sh",
"chars": 737,
"preview": "#!/bin/sh\n# the docker-compose overrides the smee container's ENTRYPOINT\n# with this script so it's a little easier to d"
},
{
"path": "test/test-smee.sh",
"chars": 2563,
"preview": "#!/bin/sh\n# shellcheck shell=dash disable=SC1091,SC2154\n\n# useful for debugging sometimes\n# tcpdump -ni eth0 &\n# alterna"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the tinkerbell/boots GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 104 files (497.8 KB), approximately 163.2k tokens, and a symbol index with 522 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.