100 Commits

Author SHA1 Message Date
renovate[bot]
1161cf3ea2 chore(deps): Update marocchino/sticky-pull-request-comment action to v3 2026-04-01 16:55:19 +00:00
renovate[bot]
42faaf332e chore(deps): Update module google.golang.org/grpc to v1.80.0 (#173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-01 13:11:51 +00:00
renovate[bot]
33458c075d chore(deps): Update module google.golang.org/grpc to v1.79.3 [SECURITY] (#172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-19 10:09:40 +00:00
renovate[bot]
d09abe14f2 chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.4 (#169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 18:08:17 +00:00
renovate[bot]
baa7f813c1 chore(deps): Update fgrosse/go-coverage-report action to v1.3.0 (#168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-12 06:00:23 +00:00
renovate[bot]
bc86e413fc chore(deps): Update module golang.org/x/sync to v0.20.0 (#167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-08 17:55:20 +00:00
renovate[bot]
105d39be89 chore(deps): Update module google.golang.org/grpc to v1.79.2 (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 13:44:51 +00:00
github-actions[bot]
1f6fecfefe chore(main): release 0.11.2 (#164)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-04 15:33:04 +01:00
weslson
03541febb2 feat(hal): add RK3588 (Radxa CM5) HAL with sysfs fan control (#155)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Specht <cedric@specht-labs.de>
2026-03-04 15:30:45 +01:00
renovate[bot]
ed39f8320b chore(deps): Update docker/setup-qemu-action action to v4 (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:29:21 +01:00
renovate[bot]
6c1fb4aeb6 chore(deps): Update github.com/sierrasoftworks/humane-errors-go digest to a7be4ff (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:29:05 +01:00
renovate[bot]
75c7df898f chore(deps): Update GitHub Artifact Actions (#150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:28:42 +01:00
renovate[bot]
2b2aab20d4 chore(deps): Update goreleaser/goreleaser-action action to v7 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:28:29 +01:00
renovate[bot]
e8b9d888fa chore(deps): Update docker/login-action action to v4 (#163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:28:11 +01:00
weslson
84346089ca fix(fancontroller): support more than 2 steps in fan curve (#156)
* fix(fancontroller): support more than 2 steps in fan curve

The GetFanSpeedPercent function only used the first 2 steps from the
config, ignoring all subsequent steps. This caused the fan to cap at
step[1]'s percent value regardless of actual temperature.

For example, with a typical 5-step curve:
  - 40°C → 30%
  - 50°C → 50%
  - 60°C → 70%
  - 70°C → 90%
  - 75°C → 100%

Any temperature ≥50°C would return 50% instead of the correct value.
At 77°C the fan should be at 100%, but was stuck at 50%.

The fix properly iterates through all configured steps to find the
correct temperature bracket and interpolates within it.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Apply suggestions from code review

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Specht <cedric@specht-labs.de>
2026-03-04 15:26:29 +01:00
weslson
9477cc71c2 feat(hal): add BCM2712 (CM5/Pi 5) HAL support (#154)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Cedric Specht <cedric@specht-labs.de>
2026-03-04 15:21:39 +01:00
renovate[bot]
0c8efc3e54 chore(deps): Update module google.golang.org/grpc to v1.79.1 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 13:56:03 +00:00
renovate[bot]
d3018868c8 chore(deps): Update module google.golang.org/grpc to v1.79.0 (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-12 21:52:40 +00:00
renovate[bot]
85a65ff4fc chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.3 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 00:38:16 +00:00
renovate[bot]
217a796fb9 chore(deps): Update module google.golang.org/grpc to v1.78.0 (#152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 14:33:59 +00:00
renovate[bot]
4772faaa46 chore(deps): Update module tinygo.org/x/drivers to v0.34.0 (#151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 22:09:22 +00:00
renovate[bot]
764a530c67 chore(deps): Update module google.golang.org/protobuf to v1.36.11 (#149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 15:30:59 +00:00
renovate[bot]
8459b80451 chore(deps): Update module golang.org/x/sync to v0.19.0 (#148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 23:36:46 +00:00
renovate[bot]
7508715397 chore(deps): Update module github.com/spf13/cobra to v1.10.2 (#147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 07:28:42 +00:00
renovate[bot]
659ef04d3f chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.2 (#146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-30 17:10:23 +00:00
renovate[bot]
3b8362a640 chore(deps): Update actions/setup-go action to v6 (#131) 2025-11-26 22:22:12 +00:00
renovate[bot]
0ae498cfd2 chore(deps): Update amannn/action-semantic-pull-request action to v6 (#117) 2025-11-26 08:49:23 +00:00
renovate[bot]
2e75867a2c chore(deps): Update actions/labeler action to v6 (#129) 2025-11-26 08:43:49 +00:00
renovate[bot]
da33d0767a chore(deps): Update GitHub Artifact Actions (#112) 2025-11-26 08:38:32 +00:00
renovate[bot]
940b8851f3 chore(deps): Update github.com/sierrasoftworks/humane-errors-go digest to 6b4ca9d (#116) 2025-11-26 08:32:54 +00:00
renovate[bot]
9d37e4d0e0 chore(deps): Update golangci/golangci-lint-action action to v9 (#140) 2025-11-26 08:27:18 +00:00
renovate[bot]
ee845c96da chore(deps): Update actions/checkout action to v6 (#145) 2025-11-26 09:22:19 +01:00
renovate[bot]
682d15abe1 chore(deps): Update module go.uber.org/zap to v1.27.1 (#144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-20 05:54:51 +00:00
renovate[bot]
c891384ed3 chore(deps): Update module google.golang.org/grpc to v1.77.0 (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 22:44:40 +00:00
renovate[bot]
731de17934 chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.1 (#142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-09 14:32:55 +00:00
renovate[bot]
20af75e36a chore(deps): Update module golang.org/x/sync to v0.18.0 (#141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 18:40:21 +00:00
renovate[bot]
bca920c510 chore(deps): Update module github.com/grpc-ecosystem/go-grpc-middleware/v2 to v2.3.3 (#139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 01:51:40 +00:00
renovate[bot]
4610f83e56 chore(deps): Update module google.golang.org/grpc to v1.76.0 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 05:07:26 +00:00
renovate[bot]
10839c5bf6 chore(deps): Update module google.golang.org/protobuf to v1.36.10 (#137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 14:46:11 +00:00
renovate[bot]
c6c7166a87 chore(deps): Update module github.com/olekukonko/tablewriter to v1.1.0 (#136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 21:15:11 +00:00
renovate[bot]
19c58fda60 chore(deps): Update module golang.org/x/sync to v0.17.0 (#135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-21 01:57:37 +00:00
renovate[bot]
551c3b50ba chore(deps): Update module github.com/spf13/viper to v1.21.0 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 20:42:31 +00:00
renovate[bot]
4f56e1a568 chore(deps): Update module github.com/spf13/cobra to v1.10.1 (#133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 17:28:42 +00:00
renovate[bot]
de8ca4c27b chore(deps): Update module google.golang.org/protobuf to v1.36.9 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 13:38:55 +00:00
renovate[bot]
339b6881f2 chore(deps): Update module google.golang.org/grpc to v1.75.1 (#130)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 09:07:40 +00:00
renovate[bot]
7e2cb1d9c3 chore(deps): Update module github.com/spf13/pflag to v1.0.10 (#128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 04:27:33 +00:00
renovate[bot]
c30a64acd9 chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelzap to v0.0.15 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-20 01:15:17 +00:00
renovate[bot]
055fcb98b2 chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelprovider to v0.0.15 (#126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 21:52:32 +00:00
renovate[bot]
a4e4e468d1 chore(deps): Update module github.com/prometheus/client_golang to v1.23.2 (#125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 17:25:50 +00:00
renovate[bot]
a710b17abf chore(deps): Update module github.com/stretchr/testify to v1.11.1 (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 23:30:10 +00:00
renovate[bot]
eaf26cabd2 chore(deps): Update module github.com/stretchr/testify to v1.11.0 (#123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 05:34:52 +00:00
renovate[bot]
b0da67eaae chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelzap to v0.0.13 (#122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 00:49:46 +00:00
renovate[bot]
0cfdb935b5 chore(deps): Update module github.com/spechtlabs/go-otel-utils/otelprovider to v0.0.13 (#121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-24 20:50:37 +00:00
renovate[bot]
2679c21423 chore(deps): Update module google.golang.org/protobuf to v1.36.8 (#120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 00:54:35 +00:00
renovate[bot]
4f3bea45fe chore(deps): Update module tinygo.org/x/drivers to v0.33.0 (#119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 01:54:31 +00:00
renovate[bot]
a2f06d99ec chore(deps): Update module google.golang.org/protobuf to v1.36.7 (#113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-07 17:23:04 +00:00
renovate[bot]
24cb5f0d4a chore(deps): Update module github.com/prometheus/client_golang to v1.23.0 (#111)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-01 00:39:48 +00:00
renovate[bot]
19805dd5ae chore(deps): Update module github.com/olekukonko/tablewriter to v1.0.9 (#110)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 23:27:58 +00:00
github-actions[bot]
354c7b62c6 chore(main): release 0.11.1 (#109)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-23 20:51:36 +02:00
Cedric Specht
ddee9b2c14 fix(cmd_root.go): change alias for --all flag from lowercase 'a' to uppercase 'A' for consistency with other flags (#108) 2025-07-23 20:49:29 +02:00
renovate[bot]
1471ac9376 chore(deps): Update module google.golang.org/grpc to v1.74.2 (#107)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 19:49:48 +00:00
renovate[bot]
1865cc3163 chore(deps): Update module google.golang.org/grpc to v1.74.0 (#106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-17 06:36:40 +00:00
renovate[bot]
97e9dc4e5e chore(deps): Update module github.com/spf13/pflag to v1.0.7 (#105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-17 03:12:16 +00:00
renovate[bot]
25758de65d chore(deps): Update module golang.org/x/sync to v0.16.0 (#104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 23:57:09 +00:00
renovate[bot]
c5c330ffa3 chore(deps): Update module github.com/olekukonko/tablewriter to v1.0.8 (#103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:42:02 +00:00
renovate[bot]
2d9fa62ac0 chore(deps): Update module tinygo.org/x/drivers to v0.32.0 (#102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 23:50:24 +00:00
Cedric Kienzler
b176f86394 refactor(bladectl): use short-commit sha in version cmd 2025-06-07 00:53:01 +02:00
Cedric Kienzler
f6234b5a3d chore(build): bump-patch-for-minor-pre-major
pre 1.0.0 we bump the minor for breaking and the patch for feature
changes.
2025-06-07 00:19:03 +02:00
github-actions[bot]
ebb492b71a chore(main): release 0.11.0 (#101)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-07 00:16:46 +02:00
Cedric Kienzler
062e36e33a feat(bladectl): add server version information to output (#100)
add server version information to output
ensure the blade-name is always set

---------

Co-authored-by: Cedric Kienzler <cedric@specht-labs.de>
2025-06-06 22:14:29 +00:00
github-actions[bot]
4acfa27158 chore(main): release 0.10.0 (#99)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 23:05:27 +02:00
Cedric Kienzler
781ded8e43 feat(bladectl)!: add more bladectl commands (#91)
This PR introduces a comprehensive set of new subcommands to bladectl, expanding its capabilities for querying and managing compute blade state. It also includes an internal refactor to simplify interface management across the gRPC API.

* `get`
	* `fan`: Returns current fan speed.
	* `identify`: Indicates whether the identify mode is active.
	* `stealth`: Shows if stealth mode is currently enabled.
	* `status`: Prints a full blade status report.
	* `temperature`: Retrieves current SoC temperature.
	* `critical`: Shows whether critical mode is active.
	* `power`: Reports the current power source (e.g., PoE+ or USB).
* `set`
	* `stealth`: Enables stealth mode.
* `remove`
	* `stealth`: Disables stealth mode.
* `describe`
	* `fan`: Outputs the current fan curve configuration.
* `monitor`: plot some charts about the state of the compute-blade-agent

* **gRPC API refactor**: The gRPC service definitions previously located in `internal/api` have been folded into `internal/agent`. This eliminates redundant interface declarations and ensures that all ComputeBladeAgent implementations are directly compatible with the gRPC API.
This reduces duplication and improves long-term maintainability and clarity of the interface contract.

```bash
bladectl set fan --percent 90 --blade 1 --blade 2
bladectl unset identify --blade 1 --blade 2 --blade 3 --blade 4
bladectl set stealth --blade 1 --blade 2 --blade 3 --blade 4
bladectl get status --blade 1 --blade 2 --blade 3 --blade 4
┌───────┬─────────────┬────────────────────┬───────────────┬──────────────┬──────────┬───────────────┬──────────────┐
│ BLADE │ TEMPERATURE │ FAN SPEED OVERRIDE │ FAN SPEED     │ STEALTH MODE │ IDENTIFY │ CRITICAL MODE │ POWER STATUS │
├───────┼─────────────┼────────────────────┼───────────────┼──────────────┼──────────┼───────────────┼──────────────┤
│ 1     │ 50°C        │ 90%                │ 5825 RPM(90%) │ Active       │ Off      │ Off           │ poe+         │
│ 2     │ 48°C        │ 90%                │ 5825 RPM(90%) │ Active       │ Off      │ Off           │ poe+         │
│ 3     │ 49°C        │ Not set            │ 4643 RPM(56%) │ Active       │ Off      │ Off           │ poe+         │
│ 4     │ 49°C        │ Not set            │ 4774 RPM(58%) │ Active       │ Off      │ Off           │ poe+         │
└───────┴─────────────┴────────────────────┴───────────────┴──────────────┴──────────┴───────────────┴──────────────┘
bladectl rm stealth --blade 1 --blade 2 --blade 3 --blade 4
bladectl rm fan --blade 1 --blade 2 --blade 3 --blade 4
bladectl get status --blade 1 --blade 2 --blade 3 --blade 4
┌───────┬─────────────┬────────────────────┬───────────────┬──────────────┬──────────┬───────────────┬──────────────┐
│ BLADE │ TEMPERATURE │ FAN SPEED OVERRIDE │ FAN SPEED     │ STEALTH MODE │ IDENTIFY │ CRITICAL MODE │ POWER STATUS │
├───────┼─────────────┼────────────────────┼───────────────┼──────────────┼──────────┼───────────────┼──────────────┤
│ 1     │ 51°C        │ Not set            │ 5177 RPM(66%) │ Off          │ Off      │ Off           │ poe+         │
│ 2     │ 49°C        │ Not set            │ 5177 RPM(58%) │ Off          │ Off      │ Off           │ poe+         │
│ 3     │ 50°C        │ Not set            │ 4659 RPM(60%) │ Off          │ Off      │ Off           │ poe+         │
│ 4     │ 48°C        │ Not set            │ 4659 RPM(54%) │ Off          │ Off      │ Off           │ poe+         │
└───────┴─────────────┴────────────────────┴───────────────┴──────────────┴──────────┴───────────────┴──────────────┘
```

when having multiple compute-blades in your bladeconfig:

```yaml
blades:
    - name: 1
      blade:
        server: blade-pi1:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 2
      blade:
        server: blade-pi2:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 3
      blade:
        server: blade-pi3:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 4
      blade:
        server: blade-pi4:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
    - name: 4
      blade:
        server: blade-pi4:8081
        cert:
            certificate-authority-data: <redacted>
            client-certificate-data: <redacted>
            client-key-data: <redacted>
current-blade: 1
```

Fixes #4, #9 (partially), should help with #5

* test: improve unit-testing

* fix: pin github.com/warthog618/gpiod

---------

Co-authored-by: Cedric Kienzler <cedric@specht-labs.de>
2025-06-06 23:03:43 +02:00
Cedric Kienzler
7ec49ce05c feat(OpenTelemetry): Integrate OpenTelemetry into agent (#90)
* feat(OpenTelemetry): Integrate OpenTelemetry into agent

- integrate OpenTelemetry logging with zap logger for better observability
- add OpenTelemetry gRPC middleware for enhanced tracing capabilities
- document new OTLP exporter endpoint for better configuration guidance

* docs: document OTEL env var

---------

Co-authored-by: Cedric Kienzler <cedric@specht-labs.de>
2025-06-06 22:43:37 +02:00
renovate[bot]
ca5e9258fa chore(deps): Update module github.com/warthog618/gpiod to v0.9.1 (#98)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 20:28:57 +00:00
renovate[bot]
ced33b0514 chore(deps): Update goreleaser/goreleaser-action action to v6 (#96)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 20:38:31 +02:00
renovate[bot]
9e795d0a22 chore(deps): Update module google.golang.org/grpc to v1.73.0 (#94)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 18:34:54 +00:00
renovate[bot]
39b41e0cdd chore(deps): Update module golang.org/x/sync to v0.15.0 (#93)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 18:33:55 +00:00
Cedric Kienzler
8fb8a0e100 chore: adjust renovate to use the correct semantics for commit 2025-06-06 20:32:57 +02:00
Cedric Kienzler
2b01c25d0a setup renovate 2025-06-06 20:18:25 +02:00
github-actions[bot]
56f72613b2 chore(main): release 0.9.1 (#89)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 18:54:39 +02:00
Cedric Kienzler
b022133860 fix(systemd): Add User to systemd service
Add User to systemd service to prevent failure during startup

Currently, the compute-blade-agent fails to start:

```log
May 28 12:56:02 blade-pi1 compute-blade-agent[2066699]: {"level":"fatal","ts":1748433362.147768,"caller":"agent/main.go:104","msg":"Failed to create agent","app":"compute-blade-agent","error":"Failed to extract home directory"}
```

This is a result of #54 being tested with a locally modified systemd service file that had the user ecplicitly set.

In #54 a dependency to the user home directory was made to store the `bladectl` configuration file. If the systemd-service unit runs without an explicit user set, `os.UserHomeDir()` will return `err` preventing the agent from starting up
2025-06-06 18:37:18 +02:00
github-actions[bot]
0733dd594a chore(main): release 0.9.0 (#88)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 18:30:40 +02:00
Cedric Kienzler
f6a70fa6a3 docs(release)!: document release process 2025-06-06 18:28:15 +02:00
Cedric Kienzler
631ddfedd4 chore: update repository references from uptime-industries to computeblade-community (#70)
* chore: update repository references from uptime-industries to compute-blade-community

chore: update repository references from uptime-industries to compute-blade-community for consistency and clarity across all files
fix: update links in CHANGELOG.md and README.md to point to the new repository location for accurate documentation
fix: update Dockerfile and systemd service file to reflect the new repository URL for proper source tracking
refactor: change import paths in Go files to use the new repository name for correct package referencing

* chore: Add CODEOWNERS

* feat: add auto-labeling

---------

Co-authored-by: Cedric Kienzler <cedric@specht-labs.de>
2025-06-06 14:40:06 +02:00
Luke Mallon
27a87f3c0f Fix the source label for ghcr.io (#68) 2025-06-06 14:32:04 +02:00
github-actions[bot]
501caeb750 chore(main): release 0.8.2 (#64)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 14:31:48 +02:00
Matthias Riegler
6faf63c76f fix: auth to ghcr.io (#63)
Signed-off-by: Matthias Riegler <me@xvzf.tech>
2025-06-06 14:29:35 +02:00
github-actions[bot]
459ab10bec chore(main): release 0.8.1 (#62)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 14:29:24 +02:00
Matthias Riegler
790ea2089a fix: set goreleaser version to v2.x (#61)
* fix: set goreleaser version to v2.x

Signed-off-by: Matthias Riegler <me@xvzf.tech>

* fix: goreleaser issues

---------

Signed-off-by: Matthias Riegler <me@xvzf.tech>
Co-authored-by: Luke Mallon (Nalum) <luke@mallon.ie>
2025-06-06 14:26:53 +02:00
github-actions[bot]
2ff46c62b7 chore(main): release 0.8.0 (#60)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 14:26:45 +02:00
Cedric Kienzler
ff6898f514 chore(go version)!: Bump go version to 1.24 (#58)
* refactor(workflows): Improve GitHub Action workflows

* bump go version to 1.24

* set coverage report baseline to correct workflow

* nit: keep same

* require older go version

* let semantic-prs write to PR

* let semantic-prs write to PR

* bump go version to 1.24

* bump dependencies

---------

Co-authored-by: Cedric Kienzler <cedric@specht-labs.de>
2025-06-06 14:19:42 +02:00
github-actions[bot]
ac573c805f chore(main): release 0.7.0 (#52)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-06 14:19:21 +02:00
Cedric Kienzler
70541d86ba feat(agent)!: add support for mTLS authentication in gRPC server (#54)
* refactor(fancontroller): improve fan controller validation logic and error handling for temperature steps

* refactor(agent): restructure gRPC server implementation by moving it to a new api package for better organization and maintainability

* feat(agent): implement gRPC server for managing compute blade agents and add graceful shutdown support
refactor(agent): restructure agent code by moving API logic to a dedicated file and improving error handling
fix(agent): update logging messages for clarity and consistency across the agent's operations
chore(agent): remove unused API code and consolidate event handling logic for better maintainability
style(agent): improve code formatting and organization for better readability and adherence to conventions

* feat(agent): add support for TLS configuration in gRPC server

* feat(api): add gRPC server authentication

* fix

* feat(config): add listen mode configuration to support tcp or unix sockets
feat(agent): implement listen mode in gRPC service to allow flexible socket types
feat(bladectl): enhance configuration loading and add support for TLS credentials
fix(bladectl): improve error handling for gRPC connection and event emission
style(logging): change log level from Warn to Info for better clarity in logs

* add logging middleware + fixes

* fix remote-connection to gRPC API Server

debugging the SAN issues took the soul out of me... And then the stupid
mistake in cmd_root where I didn't construct the TLS credentials
correctly... Oh dear...

* cleanup

* cleanup

* cleanup commands

* cleanup

* make README.md nicer

* Update cmd/agent/main.go

Co-authored-by: Matthias Riegler <github@m4tbit.de>

* Update cmd/bladectl/cmd_root.go

Co-authored-by: Matthias Riegler <github@m4tbit.de>

* move bladectl config into correct directory

* fix bugs

* // FIXME: No dead code

* nit: code style

* nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be

* nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be

* nit(YAGNI): you aint gonna need it. Don't make life harder than it needs to be

* nit(cmd_identify)

---------

Co-authored-by: Matthias Riegler <github@m4tbit.de>
2025-05-12 00:00:55 +02:00
Cedric Kienzler
ec6229ad86 chore: some improvements in bladectl sub-command handling, error logging, and CI (#51)
chore(ci): update Go setup action to v5 and simplify caching configuration for improved performance
chore(release): update Go setup action to v5 and simplify caching configuration for improved performance
fix(.gitignore): add .idea directory to ignore list to prevent IDE files from being tracked
feat(goreleaser): add versioning information to builds for better traceability
feat(agent): expose version, commit, and date information in logs for better tracking
feat(bladectl): implement command structure for managing compute-blade features
fix(bladectl): improve error handling in identify command for better user feedback
chore(go.mod): update dependencies to latest versions for improved stability and features
2025-05-03 11:13:37 +02:00
github-actions[bot]
c5ff21d522 chore(main): release 0.6.6 (#48)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-01-14 22:26:13 +01:00
Matthias Duve
67b3411e32 fix: correct package name from computeblade-agent to compute-blade-agent (#47)
* fix: correct package name from computeblade-agent to compute-blade-agent

* fix: update autoinstall script to use correct GitHub repository format and improve error handling

---------

Co-authored-by: Matthias Duve <morzan1001@mail.runforest.run>
2025-01-14 22:24:47 +01:00
github-actions[bot]
485f5ac79b chore(main): release 0.6.5 (#44)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-31 15:08:21 +02:00
Matthias Riegler
ca690d418f fix: pin golang/tinygo versions
Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>
2024-08-31 15:07:31 +02:00
github-actions[bot]
9048a4afca chore(main): release 0.6.4 (#43)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-08-31 15:03:44 +02:00
Matthias Riegler
158e7fc1bd fix: finalize renaming
Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>
2024-08-31 15:02:45 +02:00
86 changed files with 6332 additions and 1738 deletions

70
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
# API-related files
api:
- changed-files:
- any-glob-to-any-file:
- 'api/**'
- 'internal/api/**'
- 'pkg/certificate/**'
- 'buf.*'
# CLI-related files
cli:
- changed-files:
- any-glob-to-any-file:
- 'api/**'
- 'cmd/bladectl/**'
# Agent
agent:
- changed-files:
- any-glob-to-any-file:
- 'api/**'
- 'cmd/agent/**'
- 'internal/agent/**'
- 'pkg/agent/**'
- 'pkg/events/**'
# Firmware-related files
firmware:
- changed-files:
- any-glob-to-any-file:
- 'cmd/fanunit/**'
- 'pkg/smartfanunit/**'
# Hardware-specific packages
hardware:
- changed-files:
- any-glob-to-any-file:
- 'pkg/fancontroller/**'
- 'pkg/hal/**'
- 'pkg/ledengine/**'
# Utilities
util:
- changed-files:
- any-glob-to-any-file:
- 'pkg/util/**'
- 'pkg/log/**'
# Documentation (Markdown files or docs folders — optional here, kept for completeness)
documentation:
- changed-files:
- any-glob-to-any-file: '**/*.md'
# Build system files
build:
- changed-files:
- any-glob-to-any-file:
- '.github/**'
- 'Makefile'
# Add 'enhancement' label to any PR where the head branch name starts with `feature`
enhancement:
- head-branch:
- '^feature'
# Add 'bug' label to any PR where the head branch name starts with `feature`
bug:
- head-branch:
- '^bug'
- '^fix'

View File

@@ -1,36 +1,180 @@
name: ci
name: Go Build - CI
on:
workflow_dispatch:
push:
tags-ignore:
- "v*"
branches-ignore:
- main
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- main
jobs:
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
# Checkout code
- name: Checkout repository
uses: actions/checkout@v6
# Set up Go environment
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
cache: true
- name: GolangCI Lint
uses: golangci/golangci-lint-action@v9
with:
version: latest
- name: Run format-check
run: |
UNFORMATTED=$(gofmt -l .)
if [ -n "$UNFORMATTED" ]; then
echo "The following files are not formatted according to gofmt:"
echo "$UNFORMATTED"
exit 1
fi
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
# Checkout code (full history)
- name: Checkout
uses: actions/checkout@v4
# Checkout code
- name: Checkout repository
uses: actions/checkout@v6
# Setup golang with caching
- name: Setup Golang
uses: actions/setup-go@v4
# Set up Go environment
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: stable
- id: go-cache-paths
run: |
echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
- name: Go Build Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
go-version-file: "go.mod"
cache: true
cache-dependency-path: "**/*.sum"
# Run tests
- name: Run tests
run: make test
run: go test -cover -coverprofile=coverage.txt ./...
- name: Archive code coverage results
uses: actions/upload-artifact@v7
with:
name: code-coverage
path: "coverage.txt"
if-no-files-found: error
code_coverage:
name: "Code coverage report"
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs:
- test
permissions:
contents: read
actions: read # to download code coverage results from "test" job
pull-requests: write # write permission needed to comment on PR
steps:
- uses: fgrosse/go-coverage-report@v1.3.0
with:
coverage-artifact-name: "code-coverage"
coverage-file-name: "coverage.txt"
github-baseline-workflow-ref: "release.yaml"
tinygo:
runs-on: ubuntu-latest
needs:
- test
- quality
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
cache: true
# Setup tinygo
- uses: acifani/setup-tinygo@v2
with:
tinygo-version: "0.37.0"
# Build fanunit firmware
- name: Build FanUnit Firmware
run: make build-fanunit
- name: Archive FanUnit Firmware
uses: actions/upload-artifact@v7
with:
name: fanunit.uf2
path: "fanunit.uf2"
goreleaser:
runs-on: ubuntu-latest
needs:
- tinygo
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
# Install cosign
- name: Install Cosign
uses: sigstore/cosign-installer@v3
# Install GoLang
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
cache: true
# Setup docker buildx
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Download FanUnit Firmware
- name: Download fanunit firmware
uses: actions/download-artifact@v8
with:
pattern: fanunit.uf2
# Run goreleaser
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
version: latest
args: release --snapshot --clean --skip sign
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: compute-blade-agent
path: dist/*-SNAPSHOT-*
if-no-files-found: error

View File

@@ -1,19 +0,0 @@
name: Enforce conventional pr title
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
jobs:
lint:
runs-on: ubuntu-latest
permissions:
statuses: write
steps:
- uses: aslafy-z/conventional-pr-title-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

77
.github/workflows/pr-hygiene.yaml vendored Normal file
View File

@@ -0,0 +1,77 @@
name: GitHub Pull Request Hygiene
on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
jobs:
pr_title:
name: "Validate PR Title"
runs-on: ubuntu-latest
permissions:
statuses: write
pull-requests: write
contents: read
steps:
- uses: amannn/action-semantic-pull-request@v6
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@v3
# When the previous steps fail, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
```
${{ steps.lint_pr_title.outputs.error_message }}
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v3
with:
header: pr-title-lint-error
delete: true
labeler:
name: "Add Labels to PR"
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@v6
id: labeler
with:
sync-labels: true
dot: true
- shell: bash
name: Write step-summary
run: |
echo "All Labels: ${{ steps.labeler.outputs.all-labels }}" >> "$GITHUB_STEP_SUMMARY"
echo "New Labels for this iteration: ${{ steps.labeler.outputs.new-labels }}" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1,90 +1,147 @@
name: release
name: Go Release - Publish
on:
push:
branches: [ main ]
branches:
- main
permissions: write-all
jobs:
# Release-please for auto-updated PRs
release-please:
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
# Checkout code
- name: Checkout repository
uses: actions/checkout@v6
# Set up Go environment
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: true
cache-dependency-path: "**/*.sum"
- name: Run tests
run: go test -cover -coverprofile=coverage.txt ./...
- name: Archive code coverage results
uses: actions/upload-artifact@v7
with:
name: code-coverage
path: "coverage.txt"
if-no-files-found: error
# Release-please for auto-updated PRs
release-please:
name: Release Please
runs-on: ubuntu-latest
needs:
- test
steps:
- uses: googleapis/release-please-action@v3
id: release-please
with:
release-type: simple # actual releasing is handled by goreleaser
package-name: computeblade-agent
release-type: simple # actual releasing is handled by goreleaser
package-name: compute-blade-agent
bump-minor-pre-major: true
bump-patch-for-minor-pre-major: true
outputs:
release_created: ${{ steps.release-please.outputs.release_created }}
# Goreleaser for binary releases / GH release
goreleaser:
tinygo:
name: Build FanUnit Firmware
runs-on: ubuntu-latest
needs:
- release-please
- release-please
if: needs.release-please.outputs.release_created
steps:
# Checkout code (full history)
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
# Setup golang with caching
- name: Setup Golang
uses: actions/setup-go@v4
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: stable
- id: go-cache-paths
run: |
echo "go-build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
echo "go-mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
- name: Go Build Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.go-build }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
cache: true
# Setup tinygo
- uses: acifani/setup-tinygo@v2
with:
tinygo-version: '0.32.0'
# Setup docker buildx
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 'Login to GitHub Container Registry'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{github.actor}}
password: ${{secrets.GITHUB_TOKEN}}
# Install cosign
- name: Install Cosign
uses: sigstore/cosign-installer@v3
tinygo-version: "0.37.0"
# Build fanunit firmware
- name: Build FanUnit Firmware
run: make build-fanunit
# Run goreleaser
- name: Run Goreleaser
uses: goreleaser/goreleaser-action@v5
- name: Archive FanUnit Firmware
uses: actions/upload-artifact@v7
with:
version: latest
name: fanunit.uf2
path: "fanunit.uf2"
# Goreleaser for binary releases / GH release
goreleaser:
runs-on: ubuntu-latest
needs:
- release-please
- tinygo
if: needs.release-please.outputs.release_created
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
# Install cosign
- name: Install Cosign
uses: sigstore/cosign-installer@v3
# Install GoLang
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache-dependency-path: "**/*.sum"
cache: true
# Setup docker buildx
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{github.actor}}
password: ${{secrets.GITHUB_TOKEN}}
# Download FanUnit Firmware
- name: Download fanunit firmware
uses: actions/download-artifact@v8
with:
pattern: fanunit.uf2
# Run goreleaser
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
with:
version: "~> v2"
args: release --clean
env:
COSIGN_YES: "true"
KO_DOCKER_REPO: ghcr.io/${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

190
.gitignore vendored
View File

@@ -1,7 +1,193 @@
bin/
dist/
*.test
*.out
cover.cov
fanunit.uf2
.idea
# Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
# Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode,goland+all,macos,linux
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### GoLand+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### GoLand+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,goland+all,macos,linux
# Build artifacts
agent
bladectl
compute-blade-agent

View File

@@ -1,22 +1,52 @@
project_name: computeblade-agent
version: 2
project_name: compute-blade-agent
before:
hooks:
- go mod tidy
builds:
- env: &env
- CGO_ENABLED=0
- id: agent
env: &env
- CGO_ENABLED=0
goos: &goos
- linux
- linux
goarch: &goarch
- arm64
binary: computeblade-agent
id: agent
- arm64
binary: compute-blade-agent
dir: ./cmd/agent/
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags: &ldflags
- -X=main.Version={{.Version}}
- -X=main.Commit={{.Commit}}
- -X=main.Date={{ .CommitTimestamp }}
- env: *env
- id: bladectl
env: *env
goos: *goos
goarch: *goarch
binary: bladectl
id: bladectl
dir: ./cmd/bladectl/
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags: *ldflags
- id: bladectl_other
env: *env
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
ignore:
- goos: linux
goarch: arm64
binary: bladectl
dir: ./cmd/bladectl/
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags: *ldflags
# Docker image including both agent and bladectl
dockers:
@@ -25,12 +55,12 @@ dockers:
goos: linux
goarch: arm64
ids:
- agent
- bladectl
- agent
- bladectl
image_templates:
- ghcr.io/github.com/uptime-industries/compute-blade-agent:latest
- ghcr.io/github.com/uptime-industries/compute-blade-agent:{{ .Tag }}
- ghcr.io/github.com/uptime-industries/compute-blade-agent:v{{ .Major }}
- ghcr.io/compute-blade-community/compute-blade-agent:latest
- ghcr.io/compute-blade-community/compute-blade-agent:{{ .Tag }}
- ghcr.io/compute-blade-community/compute-blade-agent:v{{ .Major }}
build_flag_templates:
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
@@ -66,10 +96,15 @@ signs:
# Regular OS packages (for now only systemd based OSes)
nfpms:
- id: computeblade-agent
- id: compute-blade-agent
package_name: compute-blade-agent
ids:
- bladectl
- agent
maintainer: Matthias Riegler <me@xvzf.tech>
description: Computeblade Agent
homepage: https://github.com/github.com/uptime-industries/compute-blade-agent
description: compute-blade Agent
homepage: https://github.com/compute-blade-community/compute-blade-agent
vendor: Uptime Industries Inc.
license: Apache 2.0
formats:
- deb
@@ -77,13 +112,42 @@ nfpms:
- archlinux
bindir: /usr/bin
contents:
- src: ./hack/systemd/computeblade-agent.service
dst: /etc/systemd/system/computeblade-agent.service
- src: ./hack/systemd/compute-blade-agent.service
dst: /etc/systemd/system/compute-blade-agent.service
- src: ./cmd/agent/default-config.yaml
dst: /etc/computeblade-agent/config.yaml
dst: /etc/compute-blade-agent/config.yaml
type: config
- src: ./fanunit.uf2
dst: /usr/share/computeblade-agent/fanunit.uf2
dst: /usr/share/compute-blade-agent/fanunit.uf2
- id: bladectl
package_name: bladectl
ids:
- bladectl
- bladectl_other
maintainer: Matthias Riegler <me@xvzf.tech>
description: bladectl
homepage: https://github.com/compute-blade-community/compute-blade-agent
vendor: Uptime Industries Inc.
license: Apache 2.0
formats:
- deb
- rpm
- archlinux
bindir: /usr/bin
archives:
- id: compute-blade-agent
ids:
- agent
- bladectl
name_template: 'compute-blade-agent_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: bladectl
ids:
- bladectl
- bladectl_other
name_template: 'bladectl_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
release:
extra_files:

View File

@@ -1,65 +1,172 @@
# Changelog
## [0.6.3](https://github.com/uptime-industries/compute-blade-agent/compare/v0.6.2...v0.6.3) (2024-08-05)
### Bug Fixes
* oci reg typo ([3cbf7a8](https://github.com/uptime-industries/compute-blade-agent/commit/3cbf7a8733dedde834f7392de0851c971a6e3a05))
## [0.6.2](https://github.com/uptime-industries/compute-blade-agent/compare/v0.6.1...v0.6.2) (2024-08-05)
### Bug Fixes
* cleanup uf2 files ([d088a1b](https://github.com/uptime-industries/compute-blade-agent/commit/d088a1ba0a1adba7694a7d2d3b7d49bb9c72fe0c))
## [0.6.1](https://github.com/uptime-industries/compute-blade-agent/compare/v0.6.0...v0.6.1) (2024-08-05)
### Bug Fixes
* bump tinygo release ([#39](https://github.com/uptime-industries/compute-blade-agent/issues/39)) ([3278678](https://github.com/uptime-industries/compute-blade-agent/commit/32786787683e2a0cd42b63b92fe7dd2c41bb6e8f))
## [0.6.0](https://github.com/uptime-industries/compute-blade-agent/compare/v0.5.0...v0.6.0) (2024-08-05)
## [0.11.2](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.11.1...v0.11.2) (2026-03-04)
### Features
* migrate to uptime-industries gh org ([#37](https://github.com/uptime-industries/compute-blade-agent/issues/37)) ([6421521](https://github.com/uptime-industries/compute-blade-agent/commit/6421521bfc94a6211ed084bf8913f413e27e5b14))
* **hal:** add BCM2712 (CM5/Pi 5) HAL support ([#154](https://github.com/compute-blade-community/compute-blade-agent/issues/154)) ([9477cc7](https://github.com/compute-blade-community/compute-blade-agent/commit/9477cc71c200cf193e467a8d543d8970e733a74f))
* **hal:** add RK3588 (Radxa CM5) HAL with sysfs fan control ([#155](https://github.com/compute-blade-community/compute-blade-agent/issues/155)) ([03541fe](https://github.com/compute-blade-community/compute-blade-agent/commit/03541febb2c02ee5cf3d432128e756de4e68dfd4))
## [0.5.0](https://github.com/github.com/uptime-induestries/compute-blade-agent/compare/v0.4.1...v0.5.0) (2023-11-25)
### Bug Fixes
* **fancontroller:** support more than 2 steps in fan curve ([#156](https://github.com/compute-blade-community/compute-blade-agent/issues/156)) ([8434608](https://github.com/compute-blade-community/compute-blade-agent/commit/84346089caf7cb1b163e9279bdf9fe4cf0519651))
## [0.11.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.11.0...v0.11.1) (2025-07-23)
### Bug Fixes
* **cmd_root.go:** change alias for --all flag from lowercase 'a' to uppercase 'A' for consistency with other flags ([#108](https://github.com/compute-blade-community/compute-blade-agent/issues/108)) ([ddee9b2](https://github.com/compute-blade-community/compute-blade-agent/commit/ddee9b2c14cf4c1855486c4683b6c241a8e47350))
## [0.11.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.10.0...v0.11.0) (2025-06-06)
### Features
* add smart fan unit support ([#29](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/29)) ([9992037](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/99920370fba8176dc34243d28281aa343f437fc5))
* **bladectl:** add server version information to output ([#100](https://github.com/compute-blade-community/compute-blade-agent/issues/100)) ([062e36e](https://github.com/compute-blade-community/compute-blade-agent/commit/062e36e33ad479677affa4773e620ca53be7e9fa))
## [0.10.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.9.1...v0.10.0) (2025-06-06)
### Bug Fixes
* smart fan unit improvements ([#31](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/31)) ([a8d470d](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/a8d470d4f9ec2749e1067474805f67639cd24c09))
## [0.4.1](https://github.com/github.com/uptime-induestries/compute-blade-agent/compare/v0.4.0...v0.4.1) (2023-10-05)
### Bug Fixes
* ${ -&gt; ${{ ... ([#27](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/27)) ([f2cd029](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/f2cd029d83329085354acb7ed68da390dfe9aee4))
* add debug statement ([#25](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/25)) ([21d9942](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/21d99426293b724f53f0de594fce21e5c49724f8))
* debug statement ([#26](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/26)) ([780455e](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/780455e749a6acd896ce862ac565f1d1f5467c20))
* if statement? ([#23](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/23)) ([4691e2b](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/4691e2b3d71b9c28ebbed31b564c5356713b91f9))
* rename release-please -&gt; release workflow ([#28](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/28)) ([e86b221](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/e86b221aa886f11d6303521787ca4c755b114a6e))
## [0.4.0](https://github.com/github.com/uptime-induestries/compute-blade-agent/compare/v0.3.4...v0.4.0) (2023-10-05)
### ⚠ BREAKING CHANGES
* **bladectl:** add more bladectl commands ([#91](https://github.com/compute-blade-community/compute-blade-agent/issues/91))
### Features
* switch to release-please ([#19](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/19)) ([33dd6e5](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/33dd6e5adf45d2b59c1af061c7e78c9426329f15))
* **bladectl:** add more bladectl commands ([#91](https://github.com/compute-blade-community/compute-blade-agent/issues/91)) ([781ded8](https://github.com/compute-blade-community/compute-blade-agent/commit/781ded8e43d7115b334580a6ff18c2ab054e22cc))
* **OpenTelemetry:** Integrate OpenTelemetry into agent ([#90](https://github.com/compute-blade-community/compute-blade-agent/issues/90)) ([7ec49ce](https://github.com/compute-blade-community/compute-blade-agent/commit/7ec49ce05ce4d428e5ee94858c01004cc1a2e40d))
## [0.9.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.9.0...v0.9.1) (2025-06-06)
### Bug Fixes
* explicitly check for true before running goreleaser ([#21](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/21)) ([9c82b60](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/9c82b60fd88718ad90a9a0aa774ffc4bcdd18d3f))
* if condition ([#22](https://github.com/github.com/uptime-induestries/compute-blade-agent/issues/22)) ([cee6912](https://github.com/github.com/uptime-induestries/compute-blade-agent/commit/cee6912f5768a310c2758c8755b9ed1985b10d23))
* **systemd:** Add User to systemd service ([b022133](https://github.com/compute-blade-community/compute-blade-agent/commit/b02213386036ac29a0f1a733395c44a87b3c00e2))
## [0.9.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.8.2...v0.9.0) (2025-06-06)
Re-Release of [v0.7.0](#070-2025-05-11), [v0.8.0](#080-2025-05-24), [v0.8.1](#081-2025-05-24), and , [v0.8.2](#082-2025-05-24) in the [compute-blade-community](https://github.com/compute-blade-community) GitHub Org
### ⚠ BREAKING CHANGES
* **docker:** Docker Images are now available & published
### Documentation
* **release:** document release process ([f6a70fa](https://github.com/compute-blade-community/compute-blade-agent/commit/f6a70fa6a389d31a82dac9e340c1704053b198c0))
## [0.8.2](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.8.1...v0.8.2) (2025-05-24)
### Bug Fixes
* auth to ghcr.io ([#63](https://github.com/compute-blade-community/compute-blade-agent/issues/63)) ([e600d32](https://github.com/compute-blade-community/compute-blade-agent/commit/e600d3245317eafe7df0090e7bc6f1dff45a5693))
## [0.8.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.8.0...v0.8.1) (2025-05-24)
### Bug Fixes
* set goreleaser version to v2.x ([#61](https://github.com/compute-blade-community/compute-blade-agent/issues/61)) ([08a4e9b](https://github.com/compute-blade-community/compute-blade-agent/commit/08a4e9bca67f53e69fec3ce4cdf93344f2cf1327))
## [0.8.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.7.0...v0.8.0) (2025-05-24)
### ⚠ BREAKING CHANGES
* **go version:** Bump go version to 1.24 ([#58](https://github.com/compute-blade-community/compute-blade-agent/issues/58))
### Miscellaneous Chores
* **go version:** Bump go version to 1.24 ([#58](https://github.com/compute-blade-community/compute-blade-agent/issues/58)) ([bb7b8cd](https://github.com/compute-blade-community/compute-blade-agent/commit/bb7b8cd55d88954bb2632606e12b2c9eb057690a))
## [0.7.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.6...v0.7.0) (2025-05-11)
### ⚠ BREAKING CHANGES
* **agent:** add support for mTLS authentication in gRPC server ([#54](https://github.com/compute-blade-community/compute-blade-agent/issues/54))
### Features
* **agent:** add support for mTLS authentication in gRPC server ([#54](https://github.com/compute-blade-community/compute-blade-agent/issues/54)) ([70541d8](https://github.com/compute-blade-community/compute-blade-agent/commit/70541d86bad675a153daf8b5c80a92de204502ab))
* **agent:** expose version, commit, and date information in logs for better tracking ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
* **bladectl:** implement command structure for managing compute-blade features ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
* **goreleaser:** add versioning information to builds for better traceability ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
### Bug Fixes
* **.gitignore:** add .idea directory to ignore list to prevent IDE files from being tracked ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
* **bladectl:** improve error handling in identify command for better user feedback ([ec6229a](https://github.com/compute-blade-community/compute-blade-agent/commit/ec6229ad86b4eff06e40c805f8e4f216fe844c18))
## [0.6.6](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.5...v0.6.6) (2025-01-14)
### Bug Fixes
* correct package name from computeblade-agent to compute-blade-agent ([#47](https://github.com/compute-blade-community/compute-blade-agent/issues/47)) ([67b3411](https://github.com/compute-blade-community/compute-blade-agent/commit/67b3411e32df10673c5f3bab8b76f31f366cf3ab))
## [0.6.5](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.4...v0.6.5) (2024-08-31)
### Bug Fixes
* pin golang/tinygo versions ([ca690d4](https://github.com/compute-blade-community/compute-blade-agent/commit/ca690d418f099881b6aafdb2ca4be3cee6ac73fc))
## [0.6.4](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.3...v0.6.4) (2024-08-31)
### Bug Fixes
* finalize renaming ([158e7fc](https://github.com/compute-blade-community/compute-blade-agent/commit/158e7fc1bde46e66327d70f87743df39070c2753))
## [0.6.3](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.2...v0.6.3) (2024-08-05)
### Bug Fixes
* oci reg typo ([3cbf7a8](https://github.com/compute-blade-community/compute-blade-agent/commit/3cbf7a8733dedde834f7392de0851c971a6e3a05))
## [0.6.2](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.1...v0.6.2) (2024-08-05)
### Bug Fixes
* cleanup uf2 files ([d088a1b](https://github.com/compute-blade-community/compute-blade-agent/commit/d088a1ba0a1adba7694a7d2d3b7d49bb9c72fe0c))
## [0.6.1](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.6.0...v0.6.1) (2024-08-05)
### Bug Fixes
* bump tinygo release ([#39](https://github.com/compute-blade-community/compute-blade-agent/issues/39)) ([3278678](https://github.com/compute-blade-community/compute-blade-agent/commit/32786787683e2a0cd42b63b92fe7dd2c41bb6e8f))
## [0.6.0](https://github.com/compute-blade-community/compute-blade-agent/compare/v0.5.0...v0.6.0) (2024-08-05)
### Features
* migrate to compute-blade-community gh org ([#37](https://github.com/compute-blade-community/compute-blade-agent/issues/37)) ([6421521](https://github.com/compute-blade-community/compute-blade-agent/commit/6421521bfc94a6211ed084bf8913f413e27e5b14))
## [0.5.0](https://github.com/github.com/compute-blade-community/compute-blade-agent/compare/v0.4.1...v0.5.0) (2023-11-25)
### Features
* add smart fan unit support ([#29](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/29)) ([9992037](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/99920370fba8176dc34243d28281aa343f437fc5))
### Bug Fixes
* smart fan unit improvements ([#31](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/31)) ([a8d470d](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/a8d470d4f9ec2749e1067474805f67639cd24c09))
## [0.4.1](https://github.com/github.com/compute-blade-community/compute-blade-agent/compare/v0.4.0...v0.4.1) (2023-10-05)
### Bug Fixes
* ${ -&gt; ${{ ... ([#27](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/27)) ([f2cd029](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/f2cd029d83329085354acb7ed68da390dfe9aee4))
* add debug statement ([#25](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/25)) ([21d9942](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/21d99426293b724f53f0de594fce21e5c49724f8))
* debug statement ([#26](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/26)) ([780455e](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/780455e749a6acd896ce862ac565f1d1f5467c20))
* if statement? ([#23](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/23)) ([4691e2b](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/4691e2b3d71b9c28ebbed31b564c5356713b91f9))
* rename release-please -&gt; release workflow ([#28](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/28)) ([e86b221](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/e86b221aa886f11d6303521787ca4c755b114a6e))
## [0.4.0](https://github.com/github.com/compute-blade-community/compute-blade-agent/compare/v0.3.4...v0.4.0) (2023-10-05)
### Features
* switch to release-please ([#19](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/19)) ([33dd6e5](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/33dd6e5adf45d2b59c1af061c7e78c9426329f15))
### Bug Fixes
* explicitly check for true before running goreleaser ([#21](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/21)) ([9c82b60](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/9c82b60fd88718ad90a9a0aa774ffc4bcdd18d3f))
* if condition ([#22](https://github.com/github.com/compute-blade-community/compute-blade-agent/issues/22)) ([cee6912](https://github.com/github.com/compute-blade-community/compute-blade-agent/commit/cee6912f5768a310c2758c8755b9ed1985b10d23))

2
CODEOWNERS Normal file
View File

@@ -0,0 +1,2 @@
@xvzf
@cedi

52
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,52 @@
# Contributing
## Releases
This project uses [release-please](https://github.com/googleapis/release-please) and [goreleaser](https://goreleaser.com/) to automate releases based on conventional commits.
Releases are **semi-automated** and follow this flow:
### 1. Merge Code to `main`
All new features, fixes, and changes are merged into the `main` branch via pull requests using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
Examples:
- `feat: add new API endpoint`
- `fix: correct off-by-one error`
- `chore: update dependencies`
### 2. Release PR is Auto-Created
Once a commit is merged into `main`, a GitHub Action runs `release-please`, which:
- Calculates the next version (e.g., `v0.9.1`)
- Creates a pull request (e.g., `chore: release v0.9.1`)
- Includes a generated changelog in `CHANGELOG.md`
> 📌 Note:
> This PR should **not be edited manually**. If something is wrong, fix the commit messages instead.
### 3. Merge the Release PR
Once the release PR is approved and merged:
- The changelog and version bump are committed to `main`
- `release-please` pushes a new tag with the version-number the merged commit on `main`
### 4. Tag Triggers `goreleaser`
A GitHub Action watches for `v*` tags and runs `goreleaser`, which:
- Builds all binaries and artifacts
- Publishes them to GitHub Releases
- Optionally signs and pushes container images (if configured)
- Attaches additional files (e.g., firmware, config) as release assets
Once complete, the new GitHub Release is available at: [github.com/compute-blade-community/compute-blade-agent/releases](https://github.com/compute-blade-community/compute-blade-agent/releases)
## Notes
- Never push tags manually.
- Only edit the changelog through conventional commits and `release-please`.
- You can retry failed releases by deleting the failed tag and re-merging the release PR or re-running the workflow.

View File

@@ -1,7 +1,7 @@
FROM cgr.dev/chainguard/wolfi-base
LABEL org.opencontainers.image.source https://github.com/github.com/uptime-induestries/compute-blade-agent
LABEL org.opencontainers.image.source="https://github.com/compute-blade-community/compute-blade-agent"
# Copy binaries generated by goreleaser
COPY computeblade-agent bladectl /bin
COPY compute-blade-agent bladectl /bin/
ENTRYPOINT ["/bin/computeblade-agent"]
ENTRYPOINT ["/bin/compute-blade-agent"]

138
README.md
View File

@@ -1,53 +1,133 @@
# computeblade-agent
# compute-blade-agent
> :warning: **Beta Release**: This software is currently in beta, and both configurations and APIs may undergo breaking changes. It is not yet 100% feature complete, but it functions as intended.
The `computeblade-agent` serves as an operating system agent interfacing with [ComputeBlade](http://computeblade.com) hardware. It takes charge of fan speed, LEDs, and manages common events, such as identifying or locating an individual blade in a server rack. Additionally, it exposes hardware- and agent-related metrics on a [Prometheus](http://prometheus.io) endpoint.
## Quick Start
Install the agent with the one-liner below:
**Quick Setup with TL;DR**:
```bash
curl -L -o /tmp/computeblade-agent-installer.sh https://raw.githubusercontent.com/github.com/uptime-induestries/compute-blade-agent/main/hack/autoinstall.sh
chmod +x /tmp/computeblade-agent-installer.sh
/tmp/computeblade-agent-installer.sh
curl -L -o /tmp/compute-blade-agent-installer.sh https://raw.githubusercontent.com/compute-blade-community/compute-blade-agent/main/hack/autoinstall.sh
chmod +x /tmp/compute-blade-agent-installer.sh
/tmp/compute-blade-agent-installer.sh
```
## Components
### computeblade-agent
This event-loop handler responds to system events, such as button presses and temperature changes. It offers a Prometheus endpoint for monitoring core metrics, including Power over Ethernet (PoE) status.
### `compute-blade-agent`: Hardware Interaction & Monitoring
In normal operation mode, the agent maintains static LEDs and fan speed based on the configuration. If the System on Chip (SoC) temperature exceeds a predefined level, the critical mode is activated, setting the fan speed to 100% and changing the LED color to red. The _identify_ action, independent of the mode, makes the edge LED blink. This can be toggled using `bladectl` on the blade (`bladectl identify`) or by pressing the edge button (or smart fan unit button).
The agent runs as a system service and monitors various hardware states and events:
### Smart Fan Unit Firmware
This firmware controls fan speed and LEDs on the fan unit using a UART-based protocol with agents running on the blades. It reports metrics (fan RPM and airflow temperature) regularly to the blades and forwards button presses (1x -> left blade, 2x -> right blade). The fan unit determines the highest requested fan speed, configuring the fan control chip on the board. Advanced functionalities, such as airflow-based fan curve control, are possible with the EMC2101 chip on the smart fan unit, currently implemented in software on the agent side.
- Reacts to button presses and SoC temperature.
- Automatically enters **critical mode** (fan 100%, red LED) when overheating.
- Exposes system metrics via a Prometheus endpoint (`/metrics`).
### bladectl - interacting with the agent
`bladectl` interacts with the blade-local API exposed by the computeblade-agent. For instance, you can identify the blade in a rack using `bladectl identify --wait`, which blocks and makes the edge LED blink until the button is pressed.
The _identify_ function can be triggered via `bladectl` or a physical button press. It makes the edge LED blink to assist locating a blade in a rack.
## Installation Options
### `bladectl`: User Command-Line Tool
The agent and `bladectl` are available as packages for Debian, RPM, and ArchLinux or as an OCI image to run within Docker/Kubernetes. Packages include a systemd unit, which can be enabled using `systemd enable computeblade-agent.service --now`.
`bladectl` is a CLI utility for remote or local interaction with the running agent. Example use cases:
For global access, `bladectl` requires root privileges since the socket (default `/tmp/computeblade-agent.sock`) does not have user/group access due to privileged access to critical resources.
```bash
bladectl set identify --wait # Blink LED until button is pressed
bladectl set identify --confirm # Cancel identification
bladectl unset identify # Cancel identification (alternative)
```
<!-- WIP
**Kubernetes Deployment**:
A Kustomize environment can be found in `hack/deploy`. Use `kubectl -k hack/deploy` or employ a GitOps tool like FluxCD.
-->
### `fanunit.uf2`: Smart Fan Unit Firmware
This firmware runs on the fan unit microcontroller and:
- Controls fan speed via UART commands from blade agents.
- Reports RPM and airflow temperature back to the blade.
- Forwards button events (1x = left blade, 2x = right blade).
- Uses EMC2101 for optional advanced features like airflow-based fan control.
To install it, [download the `fanunit.uf2`](https://github.com/compute-blade-community/compute-blade-agent/releases/latest), and follow the firmware upgrade instructions [here](https://docs.computeblade.com/fan-unit/uart#update-firmware).
## Installation
Install the agent with the one-liner below:
```bash
curl -L -o /tmp/compute-blade-agent-installer.sh https://raw.githubusercontent.com/compute-blade-community/compute-blade-agent/main/hack/autoinstall.sh
chmod +x /tmp/compute-blade-agent-installer.sh
/tmp/compute-blade-agent-installer.sh
```
> Note: `bladectl` requires root privileges when used locally, due to restricted access to the Unix socket (`/tmp/compute-blade-agent.sock`).
## Configuration
Configuration can be driven by a config file or environment variables. Linux packages ship with the default configuration in `/etc/computeblade-agent/config.yaml`. Alternatively, especially for Kubernetes, all parameters in the YAML configuration can be overwritten using environment variables prefixed with `BLADE_`.
For example, changing the metric address defined in YAML:
The default configuration file is located at:
```bash
/etc/compute-blade-agent/config.yaml
```
You can also override any config option via environment variables using the `BLADE_` prefix.
### Examples
#### YAML:
```yaml
# Listen configuration
listen:
metrics: ":9666"
```
can be achieved with the environment variable `BLADE_LISTEN_METRICS=":1234"`.
Some useful parameters:
- `BLADE_STEALTH_MODE=false`: Enables/disables stealth mode.
- `BLADE_FAN_SPEED_PERCENT=80`: Sets static fan speed (by default, there's a linear fan curve of 40-80%).
- `BLADE_CRITICAL_TEMPERATURE_THRESHOLD=60`: Configures the critical temperature threshold of the agent.
- `BLADE_HAL_RPM_REPORTING_STANDARD_FAN_UNIT=false`: Enables/disables fan speed measurement (disabling it reduces CPU load of the agent).
#### Environment variable override:
```bash
BLADE_LISTEN_METRICS=":1234"
```
### Common Overrides
| Variable | Description |
|---------------------------------------------------|------------------------------------------|
| `BLADE_STEALTH_MODE=false` | Enable/disable stealth mode |
| `BLADE_FAN_SPEED_PERCENT=80` | Set static fan speed |
| `BLADE_CRITICAL_TEMPERATURE_THRESHOLD=60` | Set critical temp threshold (°C) |
| `BLADE_HAL_RPM_REPORTING_STANDARD_FAN_UNIT=false` | Disable RPM monitoring for lower CPU use |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Endpoint for the OTLP exporter |
## Exposing the gRPC API for Remote Access
To allow secure remote use of `bladectl` over the network:
### 1. Update your config (`/etc/compute-blade-agent/config.yaml`):
```yaml
listen:
metrics: ":9666"
grpc: ":8081"
authenticated: true
mode: tcp
```
### 2. Restart the agent:
```bash
systemctl restart compute-blade-agent
```
This will:
- Generate new mTLS server and client certificates in `/etc/compute-blade-agent/*.pem`
- Write a new bladectl config to: `~/.config/bladectl/config.yaml` with the client certificates in place
## Using `bladectl` from your local machine
1. Copy the config from the blade:
```bash
scp root@blade-pi1:~/.config/bladectl/config.yaml ~/.config/bladectl/config.yaml
```
2. Fix the server address to point to the blade:
```bash
yq e '.blades[] | select(.name == "blade-pi1") .blade.server = "blade-pi1.local:8081"' -i ~/.config/bladectl/config.yaml
```
Your `bladectl` tool can now securely talk to the remote agent via gRPC over mTLS.

View File

@@ -309,23 +309,146 @@ func (x *EmitEventRequest) GetEvent() Event {
return Event_IDENTIFY
}
type FanCurveStep struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Temperature int64 `protobuf:"varint,1,opt,name=temperature,proto3" json:"temperature,omitempty"`
Percent uint32 `protobuf:"varint,2,opt,name=percent,proto3" json:"percent,omitempty"`
}
func (x *FanCurveStep) Reset() {
*x = FanCurveStep{}
if protoimpl.UnsafeEnabled {
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *FanCurveStep) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*FanCurveStep) ProtoMessage() {}
func (x *FanCurveStep) ProtoReflect() protoreflect.Message {
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use FanCurveStep.ProtoReflect.Descriptor instead.
func (*FanCurveStep) Descriptor() ([]byte, []int) {
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{3}
}
func (x *FanCurveStep) GetTemperature() int64 {
if x != nil {
return x.Temperature
}
return 0
}
func (x *FanCurveStep) GetPercent() uint32 {
if x != nil {
return x.Percent
}
return 0
}
type VersionInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
Commit string `protobuf:"bytes,2,opt,name=commit,proto3" json:"commit,omitempty"`
Date int64 `protobuf:"varint,3,opt,name=date,proto3" json:"date,omitempty"`
}
func (x *VersionInfo) Reset() {
*x = VersionInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *VersionInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*VersionInfo) ProtoMessage() {}
func (x *VersionInfo) ProtoReflect() protoreflect.Message {
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use VersionInfo.ProtoReflect.Descriptor instead.
func (*VersionInfo) Descriptor() ([]byte, []int) {
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{4}
}
func (x *VersionInfo) GetVersion() string {
if x != nil {
return x.Version
}
return ""
}
func (x *VersionInfo) GetCommit() string {
if x != nil {
return x.Commit
}
return ""
}
func (x *VersionInfo) GetDate() int64 {
if x != nil {
return x.Date
}
return 0
}
type StatusResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
StealthMode bool `protobuf:"varint,1,opt,name=stealth_mode,json=stealthMode,proto3" json:"stealth_mode,omitempty"`
IdentifyActive bool `protobuf:"varint,2,opt,name=identify_active,json=identifyActive,proto3" json:"identify_active,omitempty"`
CriticalActive bool `protobuf:"varint,3,opt,name=critical_active,json=criticalActive,proto3" json:"critical_active,omitempty"`
Temperature int64 `protobuf:"varint,4,opt,name=temperature,proto3" json:"temperature,omitempty"`
FanRpm int64 `protobuf:"varint,5,opt,name=fan_rpm,json=fanRpm,proto3" json:"fan_rpm,omitempty"`
PowerStatus PowerStatus `protobuf:"varint,6,opt,name=power_status,json=powerStatus,proto3,enum=api.bladeapi.v1alpha1.PowerStatus" json:"power_status,omitempty"`
StealthMode bool `protobuf:"varint,1,opt,name=stealth_mode,json=stealthMode,proto3" json:"stealth_mode,omitempty"`
IdentifyActive bool `protobuf:"varint,2,opt,name=identify_active,json=identifyActive,proto3" json:"identify_active,omitempty"`
CriticalActive bool `protobuf:"varint,3,opt,name=critical_active,json=criticalActive,proto3" json:"critical_active,omitempty"`
Temperature int64 `protobuf:"varint,4,opt,name=temperature,proto3" json:"temperature,omitempty"`
FanRpm int64 `protobuf:"varint,5,opt,name=fan_rpm,json=fanRpm,proto3" json:"fan_rpm,omitempty"`
PowerStatus PowerStatus `protobuf:"varint,6,opt,name=power_status,json=powerStatus,proto3,enum=api.bladeapi.v1alpha1.PowerStatus" json:"power_status,omitempty"`
FanPercent uint32 `protobuf:"varint,7,opt,name=fan_percent,json=fanPercent,proto3" json:"fan_percent,omitempty"`
FanSpeedAutomatic bool `protobuf:"varint,8,opt,name=fan_speed_automatic,json=fanSpeedAutomatic,proto3" json:"fan_speed_automatic,omitempty"`
CriticalTemperatureThreshold int64 `protobuf:"varint,9,opt,name=critical_temperature_threshold,json=criticalTemperatureThreshold,proto3" json:"critical_temperature_threshold,omitempty"`
FanCurveSteps []*FanCurveStep `protobuf:"bytes,10,rep,name=fan_curve_steps,json=fanCurveSteps,proto3" json:"fan_curve_steps,omitempty"`
Version *VersionInfo `protobuf:"bytes,11,opt,name=version,proto3" json:"version,omitempty"`
}
func (x *StatusResponse) Reset() {
*x = StatusResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -338,7 +461,7 @@ func (x *StatusResponse) String() string {
func (*StatusResponse) ProtoMessage() {}
func (x *StatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3]
mi := &file_api_bladeapi_v1alpha1_blade_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -351,7 +474,7 @@ func (x *StatusResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use StatusResponse.ProtoReflect.Descriptor instead.
func (*StatusResponse) Descriptor() ([]byte, []int) {
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{3}
return file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP(), []int{5}
}
func (x *StatusResponse) GetStealthMode() bool {
@@ -396,6 +519,41 @@ func (x *StatusResponse) GetPowerStatus() PowerStatus {
return PowerStatus_POE_OR_USBC
}
func (x *StatusResponse) GetFanPercent() uint32 {
if x != nil {
return x.FanPercent
}
return 0
}
func (x *StatusResponse) GetFanSpeedAutomatic() bool {
if x != nil {
return x.FanSpeedAutomatic
}
return false
}
func (x *StatusResponse) GetCriticalTemperatureThreshold() int64 {
if x != nil {
return x.CriticalTemperatureThreshold
}
return 0
}
func (x *StatusResponse) GetFanCurveSteps() []*FanCurveStep {
if x != nil {
return x.FanCurveSteps
}
return nil
}
func (x *StatusResponse) GetVersion() *VersionInfo {
if x != nil {
return x.Version
}
return nil
}
var File_api_bladeapi_v1alpha1_blade_proto protoreflect.FileDescriptor
var file_api_bladeapi_v1alpha1_blade_proto_rawDesc = []byte{
@@ -414,66 +572,99 @@ var file_api_bladeapi_v1alpha1_blade_proto_rawDesc = []byte{
0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62,
0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x87, 0x02,
0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x6d, 0x6f, 0x64, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d,
0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x5f,
0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x64,
0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x27, 0x0a, 0x0f,
0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x41,
0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61,
0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x65, 0x6d, 0x70,
0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x61, 0x6e, 0x5f, 0x72,
0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x61, 0x6e, 0x52, 0x70, 0x6d,
0x12, 0x45, 0x0a, 0x0c, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61,
0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x50,
0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0b, 0x70, 0x6f, 0x77, 0x65,
0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0x4d, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x12, 0x0c, 0x0a, 0x08, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x10, 0x00, 0x12, 0x14,
0x0a, 0x10, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49,
0x52, 0x4d, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c,
0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x5f, 0x52,
0x45, 0x53, 0x45, 0x54, 0x10, 0x03, 0x2a, 0x21, 0x0a, 0x07, 0x46, 0x61, 0x6e, 0x55, 0x6e, 0x69,
0x74, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x09,
0x0a, 0x05, 0x53, 0x4d, 0x41, 0x52, 0x54, 0x10, 0x01, 0x2a, 0x2e, 0x0a, 0x0b, 0x50, 0x6f, 0x77,
0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x4f, 0x45, 0x5f,
0x4f, 0x52, 0x5f, 0x55, 0x53, 0x42, 0x43, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x50, 0x4f, 0x45,
0x5f, 0x38, 0x30, 0x32, 0x5f, 0x41, 0x54, 0x10, 0x01, 0x32, 0xa8, 0x03, 0x0a, 0x11, 0x42, 0x6c,
0x61, 0x64, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
0x4e, 0x0a, 0x09, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c,
0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12,
0x4a, 0x0a, 0x16, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69,
0x66, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x52, 0x0a, 0x0b, 0x53,
0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x29, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
0x61, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12,
0x55, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d, 0x6f, 0x64,
0x65, 0x12, 0x29, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69,
0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x74,
0x68, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x4a, 0x0a,
0x0c, 0x46, 0x61, 0x6e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x12, 0x20, 0x0a,
0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12,
0x18, 0x0a, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
0x52, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x22, 0x53, 0x0a, 0x0b, 0x56, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61,
0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x22, 0xa9,
0x04, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x6d, 0x6f, 0x64,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68,
0x4d, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79,
0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69,
0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x79, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x27, 0x0a,
0x0f, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65,
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c,
0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72,
0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x65, 0x6d,
0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x61, 0x6e, 0x5f,
0x72, 0x70, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x61, 0x6e, 0x52, 0x70,
0x6d, 0x12, 0x45, 0x0a, 0x0c, 0x70, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75,
0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c,
0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
0x50, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0b, 0x70, 0x6f, 0x77,
0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x61, 0x6e, 0x5f,
0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x66,
0x61, 0x6e, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x61, 0x6e,
0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63,
0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x66, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64,
0x41, 0x75, 0x74, 0x6f, 0x6d, 0x61, 0x74, 0x69, 0x63, 0x12, 0x44, 0x0a, 0x1e, 0x63, 0x72, 0x69,
0x74, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x65, 0x72, 0x61, 0x74, 0x75, 0x72,
0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28,
0x03, 0x52, 0x1c, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x65, 0x6d, 0x70, 0x65,
0x72, 0x61, 0x74, 0x75, 0x72, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12,
0x4b, 0x0a, 0x0f, 0x66, 0x61, 0x6e, 0x5f, 0x63, 0x75, 0x72, 0x76, 0x65, 0x5f, 0x73, 0x74, 0x65,
0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62,
0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
0x2e, 0x46, 0x61, 0x6e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x52, 0x0d, 0x66,
0x61, 0x6e, 0x43, 0x75, 0x72, 0x76, 0x65, 0x53, 0x74, 0x65, 0x70, 0x73, 0x12, 0x3c, 0x0a, 0x07,
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61,
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66,
0x6f, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2a, 0x4d, 0x0a, 0x05, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x12, 0x0c, 0x0a, 0x08, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x10,
0x00, 0x12, 0x14, 0x0a, 0x10, 0x49, 0x44, 0x45, 0x4e, 0x54, 0x49, 0x46, 0x59, 0x5f, 0x43, 0x4f,
0x4e, 0x46, 0x49, 0x52, 0x4d, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x52, 0x49, 0x54, 0x49,
0x43, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41,
0x4c, 0x5f, 0x52, 0x45, 0x53, 0x45, 0x54, 0x10, 0x03, 0x2a, 0x21, 0x0a, 0x07, 0x46, 0x61, 0x6e,
0x55, 0x6e, 0x69, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10,
0x00, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x4d, 0x41, 0x52, 0x54, 0x10, 0x01, 0x2a, 0x2e, 0x0a, 0x0b,
0x50, 0x6f, 0x77, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0f, 0x0a, 0x0b, 0x50,
0x4f, 0x45, 0x5f, 0x4f, 0x52, 0x5f, 0x55, 0x53, 0x42, 0x43, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a,
0x50, 0x4f, 0x45, 0x5f, 0x38, 0x30, 0x32, 0x5f, 0x41, 0x54, 0x10, 0x01, 0x32, 0xed, 0x03, 0x0a,
0x11, 0x42, 0x6c, 0x61, 0x64, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x4e, 0x0a, 0x09, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12,
0x27, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76,
0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6d, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e,
0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x22, 0x00, 0x12, 0x4a, 0x0a, 0x16, 0x57, 0x61, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x49, 0x64, 0x65,
0x6e, 0x74, 0x69, 0x66, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x12, 0x16, 0x2e, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x25, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70,
0x68, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x78, 0x76, 0x7a, 0x66, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x62,
0x6c, 0x61, 0x64, 0x65, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x62,
0x6c, 0x61, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b, 0x62, 0x6c,
0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x52,
0x0a, 0x0b, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x29, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61,
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65,
0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x22, 0x00, 0x12, 0x43, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x46, 0x61, 0x6e, 0x53, 0x70, 0x65, 0x65,
0x64, 0x41, 0x75, 0x74, 0x6f, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x55, 0x0a, 0x0e, 0x53, 0x65, 0x74, 0x53, 0x74,
0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x29, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
0x31, 0x2e, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x4c,
0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x25, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61,
0x70, 0x69, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74,
0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x57, 0x5a, 0x55,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x70, 0x74, 0x69, 0x6d,
0x65, 0x2d, 0x69, 0x6e, 0x64, 0x75, 0x65, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x2f, 0x63, 0x6f,
0x6d, 0x70, 0x75, 0x74, 0x65, 0x2d, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x2d, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x2f, 0x76, 0x31, 0x61, 0x6c,
0x70, 0x68, 0x61, 0x31, 0x3b, 0x62, 0x6c, 0x61, 0x64, 0x65, 0x61, 0x70, 0x69, 0x76, 0x31, 0x61,
0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -489,7 +680,7 @@ func file_api_bladeapi_v1alpha1_blade_proto_rawDescGZIP() []byte {
}
var file_api_bladeapi_v1alpha1_blade_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_api_bladeapi_v1alpha1_blade_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_api_bladeapi_v1alpha1_blade_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_api_bladeapi_v1alpha1_blade_proto_goTypes = []interface{}{
(Event)(0), // 0: api.bladeapi.v1alpha1.Event
(FanUnit)(0), // 1: api.bladeapi.v1alpha1.FanUnit
@@ -497,27 +688,33 @@ var file_api_bladeapi_v1alpha1_blade_proto_goTypes = []interface{}{
(*StealthModeRequest)(nil), // 3: api.bladeapi.v1alpha1.StealthModeRequest
(*SetFanSpeedRequest)(nil), // 4: api.bladeapi.v1alpha1.SetFanSpeedRequest
(*EmitEventRequest)(nil), // 5: api.bladeapi.v1alpha1.EmitEventRequest
(*StatusResponse)(nil), // 6: api.bladeapi.v1alpha1.StatusResponse
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
(*FanCurveStep)(nil), // 6: api.bladeapi.v1alpha1.FanCurveStep
(*VersionInfo)(nil), // 7: api.bladeapi.v1alpha1.VersionInfo
(*StatusResponse)(nil), // 8: api.bladeapi.v1alpha1.StatusResponse
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
}
var file_api_bladeapi_v1alpha1_blade_proto_depIdxs = []int32{
0, // 0: api.bladeapi.v1alpha1.EmitEventRequest.event:type_name -> api.bladeapi.v1alpha1.Event
2, // 1: api.bladeapi.v1alpha1.StatusResponse.power_status:type_name -> api.bladeapi.v1alpha1.PowerStatus
5, // 2: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:input_type -> api.bladeapi.v1alpha1.EmitEventRequest
7, // 3: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:input_type -> google.protobuf.Empty
4, // 4: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:input_type -> api.bladeapi.v1alpha1.SetFanSpeedRequest
3, // 5: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:input_type -> api.bladeapi.v1alpha1.StealthModeRequest
7, // 6: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:input_type -> google.protobuf.Empty
7, // 7: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:output_type -> google.protobuf.Empty
7, // 8: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:output_type -> google.protobuf.Empty
7, // 9: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:output_type -> google.protobuf.Empty
7, // 10: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:output_type -> google.protobuf.Empty
6, // 11: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:output_type -> api.bladeapi.v1alpha1.StatusResponse
7, // [7:12] is the sub-list for method output_type
2, // [2:7] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
0, // 0: api.bladeapi.v1alpha1.EmitEventRequest.event:type_name -> api.bladeapi.v1alpha1.Event
2, // 1: api.bladeapi.v1alpha1.StatusResponse.power_status:type_name -> api.bladeapi.v1alpha1.PowerStatus
6, // 2: api.bladeapi.v1alpha1.StatusResponse.fan_curve_steps:type_name -> api.bladeapi.v1alpha1.FanCurveStep
7, // 3: api.bladeapi.v1alpha1.StatusResponse.version:type_name -> api.bladeapi.v1alpha1.VersionInfo
5, // 4: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:input_type -> api.bladeapi.v1alpha1.EmitEventRequest
9, // 5: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:input_type -> google.protobuf.Empty
4, // 6: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:input_type -> api.bladeapi.v1alpha1.SetFanSpeedRequest
9, // 7: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeedAuto:input_type -> google.protobuf.Empty
3, // 8: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:input_type -> api.bladeapi.v1alpha1.StealthModeRequest
9, // 9: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:input_type -> google.protobuf.Empty
9, // 10: api.bladeapi.v1alpha1.BladeAgentService.EmitEvent:output_type -> google.protobuf.Empty
9, // 11: api.bladeapi.v1alpha1.BladeAgentService.WaitForIdentifyConfirm:output_type -> google.protobuf.Empty
9, // 12: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeed:output_type -> google.protobuf.Empty
9, // 13: api.bladeapi.v1alpha1.BladeAgentService.SetFanSpeedAuto:output_type -> google.protobuf.Empty
9, // 14: api.bladeapi.v1alpha1.BladeAgentService.SetStealthMode:output_type -> google.protobuf.Empty
8, // 15: api.bladeapi.v1alpha1.BladeAgentService.GetStatus:output_type -> api.bladeapi.v1alpha1.StatusResponse
10, // [10:16] is the sub-list for method output_type
4, // [4:10] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_api_bladeapi_v1alpha1_blade_proto_init() }
@@ -563,6 +760,30 @@ func file_api_bladeapi_v1alpha1_blade_proto_init() {
}
}
file_api_bladeapi_v1alpha1_blade_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*FanCurveStep); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_bladeapi_v1alpha1_blade_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*VersionInfo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_bladeapi_v1alpha1_blade_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StatusResponse); i {
case 0:
return &v.state
@@ -581,7 +802,7 @@ func file_api_bladeapi_v1alpha1_blade_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_bladeapi_v1alpha1_blade_proto_rawDesc,
NumEnums: 3,
NumMessages: 4,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -1,4 +1,4 @@
syntax = "proto4";
syntax = "proto3";
import "google/protobuf/empty.proto";
package api.bladeapi.v1alpha1;
@@ -37,6 +37,17 @@ message EmitEventRequest {
Event event = 1;
}
message FanCurveStep {
int64 temperature = 1;
uint32 percent = 2;
}
message VersionInfo {
string version = 1;
string commit = 2;
int64 date = 3;
}
message StatusResponse {
bool stealth_mode = 1;
bool identify_active = 2;
@@ -44,6 +55,11 @@ message StatusResponse {
int64 temperature = 4;
int64 fan_rpm = 5;
PowerStatus power_status = 6;
uint32 fan_percent = 7;
bool fan_speed_automatic = 8;
int64 critical_temperature_threshold = 9;
repeated FanCurveStep fan_curve_steps = 10;
VersionInfo version = 11;
}
service BladeAgentService {
@@ -53,9 +69,17 @@ service BladeAgentService {
// WaitForIdentifyConfirm blocks until the blades button is pressed
rpc WaitForIdentifyConfirm(google.protobuf.Empty) returns (google.protobuf.Empty) {}
// Sets the fan speed to a specific value.
rpc SetFanSpeed(SetFanSpeedRequest) returns (google.protobuf.Empty) {}
// Sets the fan speed to automatic mode.
//
// Internally, this is equivalent to calling SetFanSpeed with a nil/empty value.
rpc SetFanSpeedAuto(google.protobuf.Empty) returns (google.protobuf.Empty) {}
// Sets the blade to stealth mode (disables all LEDs)
rpc SetStealthMode(StealthModeRequest) returns (google.protobuf.Empty) {}
// Gets the current status of the blade
rpc GetStatus(google.protobuf.Empty) returns (StatusResponse) {}
}

View File

@@ -23,6 +23,7 @@ const (
BladeAgentService_EmitEvent_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/EmitEvent"
BladeAgentService_WaitForIdentifyConfirm_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/WaitForIdentifyConfirm"
BladeAgentService_SetFanSpeed_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/SetFanSpeed"
BladeAgentService_SetFanSpeedAuto_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/SetFanSpeedAuto"
BladeAgentService_SetStealthMode_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/SetStealthMode"
BladeAgentService_GetStatus_FullMethodName = "/api.bladeapi.v1alpha1.BladeAgentService/GetStatus"
)
@@ -35,8 +36,15 @@ type BladeAgentServiceClient interface {
EmitEvent(ctx context.Context, in *EmitEventRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// WaitForIdentifyConfirm blocks until the blades button is pressed
WaitForIdentifyConfirm(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Sets the fan speed to a specific value.
SetFanSpeed(ctx context.Context, in *SetFanSpeedRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Sets the fan speed to automatic mode.
//
// Internally, this is equivalent to calling SetFanSpeed with a nil/empty value.
SetFanSpeedAuto(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Sets the blade to stealth mode (disables all LEDs)
SetStealthMode(ctx context.Context, in *StealthModeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// Gets the current status of the blade
GetStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StatusResponse, error)
}
@@ -75,6 +83,15 @@ func (c *bladeAgentServiceClient) SetFanSpeed(ctx context.Context, in *SetFanSpe
return out, nil
}
func (c *bladeAgentServiceClient) SetFanSpeedAuto(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, BladeAgentService_SetFanSpeedAuto_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *bladeAgentServiceClient) SetStealthMode(ctx context.Context, in *StealthModeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, BladeAgentService_SetStealthMode_FullMethodName, in, out, opts...)
@@ -101,8 +118,15 @@ type BladeAgentServiceServer interface {
EmitEvent(context.Context, *EmitEventRequest) (*emptypb.Empty, error)
// WaitForIdentifyConfirm blocks until the blades button is pressed
WaitForIdentifyConfirm(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
// Sets the fan speed to a specific value.
SetFanSpeed(context.Context, *SetFanSpeedRequest) (*emptypb.Empty, error)
// Sets the fan speed to automatic mode.
//
// Internally, this is equivalent to calling SetFanSpeed with a nil/empty value.
SetFanSpeedAuto(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
// Sets the blade to stealth mode (disables all LEDs)
SetStealthMode(context.Context, *StealthModeRequest) (*emptypb.Empty, error)
// Gets the current status of the blade
GetStatus(context.Context, *emptypb.Empty) (*StatusResponse, error)
mustEmbedUnimplementedBladeAgentServiceServer()
}
@@ -120,6 +144,9 @@ func (UnimplementedBladeAgentServiceServer) WaitForIdentifyConfirm(context.Conte
func (UnimplementedBladeAgentServiceServer) SetFanSpeed(context.Context, *SetFanSpeedRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetFanSpeed not implemented")
}
func (UnimplementedBladeAgentServiceServer) SetFanSpeedAuto(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetFanSpeedAuto not implemented")
}
func (UnimplementedBladeAgentServiceServer) SetStealthMode(context.Context, *StealthModeRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetStealthMode not implemented")
}
@@ -193,6 +220,24 @@ func _BladeAgentService_SetFanSpeed_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler)
}
func _BladeAgentService_SetFanSpeedAuto_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BladeAgentServiceServer).SetFanSpeedAuto(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: BladeAgentService_SetFanSpeedAuto_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BladeAgentServiceServer).SetFanSpeedAuto(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _BladeAgentService_SetStealthMode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StealthModeRequest)
if err := dec(in); err != nil {
@@ -248,6 +293,10 @@ var BladeAgentService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetFanSpeed",
Handler: _BladeAgentService_SetFanSpeed_Handler,
},
{
MethodName: "SetFanSpeedAuto",
Handler: _BladeAgentService_SetFanSpeedAuto_Handler,
},
{
MethodName: "SetStealthMode",
Handler: _BladeAgentService_SetStealthMode_Handler,

View File

@@ -1,16 +1,15 @@
# Default configuration for the computeblade-agent
log:
mode: production # production, development
# Default configuration for the compute-blade-agent
# Listen configuration
listen:
metrics: ":9666"
grpc: /tmp/computeblade-agent.sock
grpc: /tmp/compute-blade-agent.sock
authenticated: false
mode: unix # tcp or unix
# Hardware abstraction layer configuration
hal:
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU laod.
# For the default fan unit, fanspeed measurement is causing a tiny bit of CPU load.
# Sometimes it might not be desired
rpm_reporting_standard_fan_unit: true
@@ -35,14 +34,13 @@ criticalLedColor:
# Enable/disable stealth mode; turns off all LEDs on the blade
stealth_mode: false
# Simple fan-speed controls based on the SoC temperature
fan_controller:
# For now, this is only supporting a two-step configuration.
steps:
- temperature: 45
percent: 40
- temperature: 55
percent: 80
# Critical temperature threshold
critical_temperature_threshold: 60

View File

@@ -2,39 +2,49 @@ package main
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
internal_agent "github.com/compute-blade-community/compute-blade-agent/internal/agent"
"github.com/compute-blade-community/compute-blade-agent/pkg/agent"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spechtlabs/go-otel-utils/otelprovider"
"github.com/spechtlabs/go-otel-utils/otelzap"
"github.com/spf13/pflag"
"github.com/spf13/viper"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/uptime-induestries/compute-blade-agent/internal/agent"
"github.com/uptime-induestries/compute-blade-agent/pkg/log"
"go.uber.org/zap"
"google.golang.org/grpc"
)
var (
Version string
Commit string
// Date is the CommitTimestamp when the build was done as UNIX Timestamp
Date string
BuildTime time.Time
)
var debug = pflag.BoolP("debug", "v", false, "enable verbose logging")
func main() {
var wg sync.WaitGroup
pflag.Parse()
// Setup configuration
viper.SetConfigType("yaml")
// auto-bind environment variables
viper.SetEnvPrefix("BLADE")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/computeblade-agent")
viper.AddConfigPath("/etc/compute-blade-agent")
// Load potential file configs
if err := viper.ReadInConfig(); err != nil {
@@ -43,95 +53,187 @@ func main() {
// setup logger
var baseLogger *zap.Logger
switch logMode := viper.GetString("log.mode"); logMode {
case "development":
if debug != nil && *debug {
baseLogger = zap.Must(zap.NewDevelopment())
case "production":
} else {
baseLogger = zap.Must(zap.NewProduction())
default:
panic(fmt.Errorf("invalid log.mode: %s", logMode))
}
zapLogger := baseLogger.With(zap.String("app", "computeblade-agent"))
zapLogger := baseLogger.With(
zap.String("app", "compute-blade-agent"),
zap.String("version", Version),
)
defer func() {
_ = zapLogger.Sync()
}()
_ = zap.ReplaceGlobals(zapLogger.With(zap.String("scope", "global")))
baseCtx := log.IntoContext(context.Background(), zapLogger)
// Replace zap global
undoZapGlobals := zap.ReplaceGlobals(zapLogger)
// Redirect stdlib log to zap
undoStdLogRedirect := zap.RedirectStdLog(zapLogger)
// Create OpenTelemetry Log and Trace provider
logProvider := otelprovider.NewLogger(
otelprovider.WithLogAutomaticEnv(),
)
traceProvider := otelprovider.NewTracer(
otelprovider.WithTraceAutomaticEnv(),
)
// Create otelLogger
otelZapLogger := otelzap.New(zapLogger,
otelzap.WithCaller(true),
otelzap.WithMinLevel(zap.InfoLevel),
otelzap.WithAnnotateLevel(zap.WarnLevel),
otelzap.WithErrorStatusLevel(zap.ErrorLevel),
otelzap.WithStackTrace(false),
otelzap.WithLoggerProvider(logProvider),
)
// Replace global otelZap logger
undoOtelZapGlobals := otelzap.ReplaceGlobals(otelZapLogger)
defer undoOtelZapGlobals()
// Cleanup Logging and Tracing
defer func() {
if err := traceProvider.ForceFlush(context.Background()); err != nil {
otelzap.L().Warn("failed to flush traces")
}
if err := logProvider.ForceFlush(context.Background()); err != nil {
otelzap.L().Warn("failed to flush logs")
}
if err := traceProvider.Shutdown(context.Background()); err != nil {
panic(err)
}
if err := logProvider.Shutdown(context.Background()); err != nil {
panic(err)
}
undoStdLogRedirect()
undoZapGlobals()
}()
// Setup context
baseCtx := log.IntoContext(context.Background(), otelZapLogger)
ctx, cancelCtx := context.WithCancelCause(baseCtx)
defer cancelCtx(context.Canceled)
// load configuration
var cbAgentConfig agent.ComputeBladeAgentConfig
if err := viper.Unmarshal(&cbAgentConfig); err != nil {
log.FromContext(ctx).Error("Failed to load configuration", zap.Error(err))
cancelCtx(err)
log.FromContext(ctx).WithError(err).Fatal("Failed to load configuration")
}
// setup stop signal handlers
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
wg.Add(1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
defer wg.Done()
// Wait for context cancel or signal
select {
// Wait for context cancel
case <-ctx.Done():
// Wait for signal
case sig := <-sigs:
// On signal, cancel context
cancelCtx(fmt.Errorf("signal %s received", sig))
switch sig {
case syscall.SIGTERM:
fallthrough
case syscall.SIGINT:
fallthrough
case syscall.SIGQUIT:
// On terminate signal, cancel context causing the program to terminate
cancelCtx(fmt.Errorf("signal %s received", sig))
default:
log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String()))
}
}
}()
log.FromContext(ctx).Info("Bootstrapping computeblade-agent", zap.String("version", viper.GetString("version")))
computebladeAgent, err := agent.NewComputeBladeAgent(ctx, cbAgentConfig)
if Date != "" {
if unixTimestamp, err := strconv.ParseInt(Date, 10, 64); err == nil {
BuildTime = time.Unix(unixTimestamp, 0)
} else {
BuildTime = time.Unix(0, 0)
log.FromContext(context.Background()).WithError(err).Warn("Failed to parse build timestamp")
}
}
log.FromContext(ctx).Info("Bootstrapping compute-blade-agent",
zap.String("version", Version),
zap.String("commit", Commit),
zap.String("date", BuildTime.Format(time.RFC3339)),
)
cbAgentInfo := agent.ComputeBladeAgentInfo{
Version: Version,
Commit: Commit,
BuildTime: BuildTime,
}
computebladeAgent, err := internal_agent.NewComputeBladeAgent(ctx, cbAgentConfig, cbAgentInfo)
if err != nil {
log.FromContext(ctx).Error("Failed to create agent", zap.Error(err))
cancelCtx(err)
os.Exit(1)
log.FromContext(ctx).WithError(err).Fatal("Failed to create agent")
}
// Run agent
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting agent")
err := computebladeAgent.Run(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("Failed to run agent", zap.Error(err))
cancelCtx(err)
}
}()
// Setup GRPC server
// FIXME add logging middleware
grpcServer := grpc.NewServer()
bladeapiv1alpha1.RegisterBladeAgentServiceServer(grpcServer, agent.NewGrpcServiceFor(computebladeAgent))
wg.Add(1)
go func() {
defer wg.Done()
grpcListen, err := net.Listen("unix", viper.GetString("listen.grpc"))
if err != nil {
log.FromContext(ctx).Error("Failed to create grpc listener", zap.Error(err))
cancelCtx(err)
return
}
log.FromContext(ctx).Info("Starting grpc server", zap.String("address", viper.GetString("listen.grpc")))
if err := grpcServer.Serve(grpcListen); err != nil && err != grpc.ErrServerStopped {
log.FromContext(ctx).Error("Failed to start grpc server", zap.Error(err))
cancelCtx(err)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
log.FromContext(ctx).Info("Shutting down grpc server")
grpcServer.GracefulStop()
}()
computebladeAgent.RunAsync(ctx, cancelCtx)
// setup prometheus endpoint
promServer := runPrometheusEndpoint(ctx, cancelCtx, &cbAgentConfig.Listen)
// Wait for done
<-ctx.Done()
// Since ctx is now done, we can no longer use it to get `log.FromContext(ctx)`
// but we must use otelzap.L() to get a logger
// Shut down gRPC and Prom Servers async
var wg sync.WaitGroup
// Shut-Down GRPC Server
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Shutting down compute blade agent...")
if err := computebladeAgent.GracefulStop(ctx); err != nil {
log.FromContext(ctx).WithError(err).Error("Failed to close compute blade agent")
}
}()
// Shut-Down Prometheus Endpoint
wg.Add(1)
go func() {
defer wg.Done()
shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCtxCancel()
otelzap.L().Info("Shutting down prometheus/pprof server")
if err := promServer.Shutdown(shutdownCtx); err != nil {
otelzap.L().WithError(err).Error("Failed to shutdown prometheus/pprof server")
}
}()
wg.Wait()
// Terminate accordingly
if err := ctx.Err(); !errors.Is(err, context.Canceled) {
otelzap.L().WithError(err).Fatal("Exiting")
} else {
otelzap.L().Info("Exiting")
}
}
func runPrometheusEndpoint(ctx context.Context, cancel context.CancelCauseFunc, apiConfig *agent.ApiConfig) *http.Server {
instrumentationHandler := http.NewServeMux()
instrumentationHandler.Handle("/metrics", promhttp.Handler())
instrumentationHandler.HandleFunc("/debug/pprof/", pprof.Index)
@@ -139,33 +241,17 @@ func main() {
instrumentationHandler.HandleFunc("/debug/pprof/profile", pprof.Profile)
instrumentationHandler.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
instrumentationHandler.HandleFunc("/debug/pprof/trace", pprof.Trace)
server := &http.Server{Addr: ":9666", Handler: instrumentationHandler}
wg.Add(1)
server := &http.Server{Addr: apiConfig.Metrics, Handler: instrumentationHandler}
// Run Prometheus Endpoint
go func() {
defer wg.Done()
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.FromContext(ctx).Error("Failed to start prometheus/pprof server", zap.Error(err))
cancelCtx(err)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := server.Shutdown(shutdownCtx)
if err != nil {
log.FromContext(ctx).Error("Failed to shutdown prometheus/pprof server", zap.Error(err))
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.FromContext(ctx).WithError(err).Error("Failed to start prometheus/pprof server")
cancel(err)
}
}()
// Wait for context cancel
wg.Wait()
if err := ctx.Err(); err != nil && err != context.Canceled {
log.FromContext(ctx).Fatal("Exiting", zap.Error(err))
} else {
log.FromContext(ctx).Info("Exiting")
}
return server
}

View File

@@ -1,45 +1,209 @@
package main
import (
"strconv"
"fmt"
"os"
"sort"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"google.golang.org/protobuf/types/known/emptypb"
)
var (
percent int
auto bool
)
func init() {
cmdFan.AddCommand(cmdFanSetPercent)
rootCmd.AddCommand(cmdFan)
cmdSetFan.Flags().IntVarP(&percent, "percent", "p", 40, "Fan speed in percent (Default: 40).")
cmdSetFan.Flags().BoolVarP(&auto, "auto", "a", false, "Set fan speed to automatic mode.")
cmdSet.AddCommand(cmdSetFan)
cmdGet.AddCommand(cmdGetFan)
cmdRemove.AddCommand(cmdRmFan)
cmdDescribe.AddCommand(cmdDescribeFan)
}
var (
cmdFan = &cobra.Command{
Use: "fan",
Short: "Fan-related commands for the compute blade",
}
fanAliases = []string{"fan_speed", "rpm"}
cmdFanSetPercent = &cobra.Command{
Use: "set-percent <percent>",
Example: "bladectl fan set-percent 50",
Short: "Set the fan speed in percent",
Args: cobra.ExactArgs(1),
cmdSetFan = &cobra.Command{
Use: "fan",
Aliases: fanAliases,
Short: "Control the fan behavior of the compute-blade",
Example: "bladectl set fan --percent 50",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
var err error
autoSet := cmd.Flags().Changed("auto")
percentSet := cmd.Flags().Changed("percent")
ctx := cmd.Context()
client := clientFromContext(ctx)
// convert string to int
percent, err := strconv.Atoi(args[0])
if err != nil {
return err
if autoSet && percentSet {
return fmt.Errorf("only one of --auto or --percent can be specified")
}
_, err = client.SetFanSpeed(ctx, &bladeapiv1alpha1.SetFanSpeedRequest{
Percent: int64(percent),
})
if !autoSet && !percentSet {
return fmt.Errorf("you must specify either --auto or --percent")
}
return err
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for _, client := range clients {
var err error
if auto {
_, err = client.SetFanSpeedAuto(ctx, &emptypb.Empty{})
} else {
_, err = client.SetFanSpeed(ctx, &bladeapiv1alpha1.SetFanSpeedRequest{
Percent: int64(percent),
})
}
if err != nil {
return err
}
}
return nil
},
}
cmdRmFan = &cobra.Command{
Use: "fan",
Aliases: fanAliases,
Short: "Remove the fan speed override of the compute-blade",
Example: "bladectl unset fan",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for _, client := range clients {
if _, err := client.SetFanSpeedAuto(ctx, &emptypb.Empty{}); err != nil {
return err
}
}
return nil
},
}
cmdGetFan = &cobra.Command{
Use: "fan",
Aliases: fanAliases,
Short: "Get the fan speed of the compute-blade",
Example: "bladectl get fan",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for idx, client := range clients {
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
return err
}
rpm := bladeStatus.FanRpm
percent := bladeStatus.FanPercent
rowPrefix := bladeNames[idx]
if len(bladeNames) > 1 {
rowPrefix += ": "
} else {
rowPrefix = ""
}
fmt.Println(rpmStyle(rpm).Render(fmt.Sprint(rowPrefix + rpmLabel(rpm) + " (" + percentLabel(percent) + ")")))
}
return nil
},
}
cmdDescribeFan = &cobra.Command{
Use: "fan",
Aliases: fanAliases,
Short: "Get the fan speed curve of the compute-blade",
Example: "bladectl describe fan",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
bladeFanCurves := make([][]*bladeapiv1alpha1.FanCurveStep, len(clients))
criticalTemps := make([]int64, len(clients))
for idx, client := range clients {
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
return err
}
bladeFanCurves[idx] = bladeStatus.FanCurveSteps
criticalTemps[idx] = bladeStatus.CriticalTemperatureThreshold
}
printFanCurveTable(bladeFanCurves, criticalTemps)
return nil
},
}
)
func printFanCurveTable(bladeValues [][]*bladeapiv1alpha1.FanCurveStep, criticalTemps []int64) {
bladeCount := len(bladeValues)
// Map blade index -> temperature -> step
bladeTempMap := make([]map[int64]*bladeapiv1alpha1.FanCurveStep, bladeCount)
allTempsSet := make(map[int64]struct{})
for bladeIdx, steps := range bladeValues {
bladeTempMap[bladeIdx] = make(map[int64]*bladeapiv1alpha1.FanCurveStep)
for _, step := range steps {
temp := step.Temperature
bladeTempMap[bladeIdx][temp] = step
allTempsSet[temp] = struct{}{}
}
}
// Sorted temperature list
var allTemps []int64
for t := range allTempsSet {
allTemps = append(allTemps, t)
}
sort.Slice(allTemps, func(i, j int) bool {
return allTemps[i] < allTemps[j]
})
// Header: Blade | Temp1 | Temp2 | ...
header := []string{"Blade"}
for _, t := range allTemps {
header = append(header, tempLabel(t))
}
// Table writer setup
tbl := tablewriter.NewTable(os.Stdout,
tablewriter.WithHeader(header),
tablewriter.WithHeaderAlignment(tw.AlignLeft),
tablewriter.WithHeaderAutoFormat(tw.Off),
)
// Rows: one per blade
for bladeIdx, tempMap := range bladeTempMap {
row := []string{bladeNames[bladeIdx]}
for _, t := range allTemps {
if step, ok := tempMap[t]; ok {
style := tempStyle(step.Temperature, criticalTemps[bladeIdx])
colored := style.Render(percentLabel(step.Percent))
row = append(row, colored)
} else {
row = append(row, "")
}
}
_ = tbl.Append(row)
}
_ = tbl.Render()
}

View File

@@ -0,0 +1,105 @@
package main
import (
"fmt"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
func init() {
cmdGet.AddCommand(cmdGetTemp)
cmdGet.AddCommand(cmdGetCritical)
cmdGet.AddCommand(cmdGetPowerStatus)
}
var (
cmdGetTemp = &cobra.Command{
Use: "temp",
Aliases: []string{"temperature"},
Short: "Get the temperature of the compute-blade",
Example: "bladectl get temp",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for idx, client := range clients {
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
return err
}
temp := bladeStatus.Temperature
rowPrefix := bladeNames[idx]
if len(bladeNames) > 1 {
rowPrefix += ": "
} else {
rowPrefix = ""
}
fmt.Println(tempStyle(temp, bladeStatus.CriticalTemperatureThreshold).Render(rowPrefix + tempLabel(temp)))
}
return nil
},
}
cmdGetCritical = &cobra.Command{
Use: "critical",
Short: "Get the critical of the compute-blade",
Example: "bladectl get critical",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for idx, client := range clients {
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
return err
}
rowPrefix := bladeNames[idx]
if len(bladeNames) > 1 {
rowPrefix += ": "
} else {
rowPrefix = ""
}
fmt.Println(activeStyle(bladeStatus.CriticalActive).Render(rowPrefix + activeLabel(bladeStatus.CriticalActive)))
}
return nil
},
}
cmdGetPowerStatus = &cobra.Command{
Use: "power_status",
Aliases: []string{"powerstatus", "power"},
Short: "Get the power status of the compute-blade",
Example: "bladectl get power",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for idx, client := range clients {
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
return err
}
rowPrefix := bladeNames[idx]
if len(bladeNames) > 1 {
rowPrefix += ": "
} else {
rowPrefix = ""
}
fmt.Println(rowPrefix + hal.PowerStatus(bladeStatus.PowerStatus).String())
}
return nil
},
}
)

View File

@@ -1,58 +1,124 @@
package main
import (
"errors"
"fmt"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/sierrasoftworks/humane-errors-go"
"github.com/spf13/cobra"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"google.golang.org/protobuf/types/known/emptypb"
)
var (
confirm bool
wait bool
)
func init() {
cmdIdentify.Flags().Bool("confirm", false, "confirm the identify state")
cmdIdentify.Flags().Bool("wait", false, "Wait for the identify state to be confirmed (e.g. by a physical button press)")
rootCmd.AddCommand(cmdIdentify)
cmdSetIdentify.Flags().BoolVarP(&confirm, "confirm", "c", false, "confirm the identify state")
cmdSetIdentify.Flags().BoolVarP(&wait, "wait", "w", false, "Wait for the identify state to be confirmed (e.g. by a physical button press)")
cmdSet.AddCommand(cmdSetIdentify)
cmdRemove.AddCommand(cmdRmIdentify)
cmdGet.AddCommand(cmdGetIdentify)
}
var cmdIdentify = &cobra.Command{
Use: "identify",
Short: "interact with the compute-blade identity LED",
RunE: runIdentity,
}
var (
cmdSetIdentify = &cobra.Command{
Use: "identify",
Example: "bladectl set identify --wait",
Short: "interact with the compute-blade identity LED",
RunE: func(cmd *cobra.Command, _ []string) error {
if len(bladeNames) > 1 && wait {
return fmt.Errorf("cannot enable identify on multiple compute-blades at the same with the --wait flag")
}
func runIdentity(cmd *cobra.Command, _ []string) error {
var err error
ctx := cmd.Context()
clients := clientsFromContext(ctx)
ctx := cmd.Context()
client := clientFromContext(ctx)
for _, client := range clients {
// Check if we should wait for the identify state to be confirmed
event := bladeapiv1alpha1.Event_IDENTIFY
if confirm {
event = bladeapiv1alpha1.Event_IDENTIFY_CONFIRM
}
// Get flags
confirm, err := cmd.Flags().GetBool("confirm")
if err != nil {
return err
}
wait, err := cmd.Flags().GetBool("wait")
if err != nil {
return err
// Emit the event to the compute-blade-agent
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
if err != nil {
return errors.New(humane.Wrap(err,
"failed to emit event",
"ensure the compute-blade agent is running and responsive to requests",
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
).Display())
}
// Check if we should wait for the identify state to be confirmed
if wait {
if _, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{}); err != nil {
return errors.New(
humane.Wrap(err, "unable to wait for confirmation",
"ensure the compute-blade agent is running and responsive to requests",
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
).Display())
}
}
}
return nil
},
}
// Check if we should wait for the identify state to be confirmed
event := bladeapiv1alpha1.Event_IDENTIFY
if confirm {
event = bladeapiv1alpha1.Event_IDENTIFY_CONFIRM
cmdRmIdentify = &cobra.Command{
Use: "identify",
Example: "bladectl unset identify",
Short: "remove the identify state with the compute-blade identity LED",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for _, client := range clients {
// Emit the event to the compute-blade-agent
_, err := client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: bladeapiv1alpha1.Event_IDENTIFY_CONFIRM})
if err != nil {
return errors.New(humane.Wrap(err,
"failed to emit event",
"ensure the compute-blade agent is running and responsive to requests",
"check the compute-blade agent logs for more information using 'journalctl -u compute-blade-agent.service'",
).Display())
}
}
return nil
},
}
// Emit the event to the computeblade-agent
_, err = client.EmitEvent(ctx, &bladeapiv1alpha1.EmitEventRequest{Event: event})
if err != nil {
return err
}
cmdGetIdentify = &cobra.Command{
Use: "identify",
Example: "bladectl get identify",
Short: "get the identify state of the compute-blade identity LED",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
// Check if we should wait for the identify state to be confirmed
if wait {
_, err := client.WaitForIdentifyConfirm(ctx, &emptypb.Empty{})
if err != nil {
return err
}
}
for idx, client := range clients {
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
return err
}
return nil
}
rowPrefix := bladeNames[idx]
if len(bladeNames) > 1 {
rowPrefix += ": "
} else {
rowPrefix = ""
}
fmt.Println(activeStyle(bladeStatus.IdentifyActive).Render(rowPrefix, activeLabel(bladeStatus.IdentifyActive)))
}
return nil
},
}
)

169
cmd/bladectl/cmd_monitor.go Normal file
View File

@@ -0,0 +1,169 @@
package main
import (
"context"
"errors"
"fmt"
"math"
"time"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
func init() {
rootCmd.AddCommand(cmdMonitor)
}
var cmdMonitor = &cobra.Command{
Use: "monitor",
Aliases: fanAliases,
Short: "Render a line-chart of the fan speed and temperature of the compute-blade",
Example: "bladectl chart status",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
if len(bladeNames) > 1 {
return fmt.Errorf("cannot monitor multiple blades at once, please specify a single blade with --blade")
}
ctx := cmd.Context()
client := clientFromContext(ctx)
if err := ui.Init(); err != nil {
return fmt.Errorf("failed to initialize UI: %w", err)
}
defer ui.Close()
events := ui.PollEvents()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
labelBox := widgets.NewParagraph()
labelBox.Title = fmt.Sprintf(" %s: Blade Status ", bladeNames[0])
labelBox.Border = true
labelBox.TextStyle = ui.NewStyle(ui.ColorWhite)
fanPlot := newPlot(fmt.Sprintf(" %s: Fan Speed (RPM) ", bladeNames[0]), ui.ColorGreen)
tempPlot := newPlot(fmt.Sprintf(" %s: SoC Temperature (\u00b0C) ", bladeNames[0]), ui.ColorCyan)
fanData := []float64{math.NaN(), math.NaN()}
tempData := []float64{math.NaN(), math.NaN()}
for {
select {
case <-ctx.Done():
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return ctx.Err()
case e := <-events:
switch e.ID {
case "q", "<C-c>":
return nil
case "<Resize>":
renderCharts(nil, fanPlot, tempPlot, labelBox)
ui.Clear()
ui.Render(labelBox, fanPlot, tempPlot)
}
case <-ticker.C:
status, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
labelBox.Text = "Error retrieving blade status: " + err.Error()
ui.Render(labelBox)
continue
}
fanData = append(fanData, float64(status.FanRpm))
tempData = append(tempData, float64(status.Temperature))
fanPlot.Data[0] = reversedFloats(fanData)
tempPlot.Data[0] = reversedFloats(tempData)
renderCharts(status, fanPlot, tempPlot, labelBox)
ui.Render(labelBox, fanPlot, tempPlot)
}
}
},
}
func reversedFloats(s []float64) []float64 {
r := make([]float64, len(s))
for i := range s {
r[len(s)-1-i] = s[i]
}
return r
}
func newPlot(title string, color ui.Color) *widgets.Plot {
plot := widgets.NewPlot()
plot.Title = title
plot.Data = [][]float64{{}}
plot.LineColors = []ui.Color{color}
plot.AxesColor = ui.ColorWhite
plot.DrawDirection = widgets.DrawLeft
plot.HorizontalScale = 2
return plot
}
func renderCharts(status *bladeapiv1alpha1.StatusResponse, fanPlot, tempPlot *widgets.Plot, labelBox *widgets.Paragraph) {
width, height := ui.TerminalDimensions()
labelHeight := 4
if status != nil {
if status.CriticalActive {
labelBox.Text = fmt.Sprintf(
"Critical: %s | %s",
activeLabel(status.CriticalActive),
labelBox.Text,
)
}
labelBox.Text = fmt.Sprintf(
"Temp: %d°C | Fan: %d RPM (%d%%)",
status.Temperature,
status.FanRpm,
status.FanPercent,
)
if !status.FanSpeedAutomatic {
labelBox.Text = fmt.Sprintf(
"%s | Fan Override: %s",
labelBox.Text,
fanSpeedOverrideLabel(status.FanSpeedAutomatic, status.FanPercent),
)
}
if status.StealthMode {
labelBox.Text = fmt.Sprintf(
"%s | Stealth: %s",
labelBox.Text,
activeLabel(status.StealthMode),
)
}
labelBox.Text = fmt.Sprintf(
"%s | Identify: %s | Power: %s",
labelBox.Text,
activeLabel(status.IdentifyActive),
hal.PowerStatus(status.PowerStatus).String(),
)
}
labelBox.SetRect(0, 0, width, labelHeight)
if width >= 140 {
fanPlot.SetRect(0, labelHeight, width/2, height)
tempPlot.SetRect(width/2, labelHeight, width, height)
} else {
midY := (height-labelHeight)/2 + labelHeight
fanPlot.SetRect(0, labelHeight, width, midY)
tempPlot.SetRect(0, midY, width, height)
}
}

187
cmd/bladectl/cmd_root.go Normal file
View File

@@ -0,0 +1,187 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/compute-blade-community/compute-blade-agent/cmd/bladectl/config"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/sierrasoftworks/humane-errors-go"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
)
var (
allBlades bool
bladeNames []string
timeout time.Duration
)
func init() {
rootCmd.PersistentFlags().BoolVarP(&allBlades, "all", "A", false, "control all compute-blades at the same time")
rootCmd.PersistentFlags().StringArrayVar(&bladeNames, "blade", []string{""}, "Name of the compute-blade to control. If not provided, the compute-blade specified in `current-blade` will be used.")
rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", time.Minute, "timeout for gRPC requests")
}
var rootCmd = &cobra.Command{
Use: "bladectl",
Short: "bladectl interacts with the compute-blade-agent and allows you to manage hardware-features of your compute blade(s)",
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
ctx, cancelCtx := context.WithCancelCause(cmd.Context())
// load configuration
var bladectlCfg config.BladectlConfig
if err := viper.ReadInConfig(); err != nil {
cancelCtx(err)
return err
}
if err := viper.Unmarshal(&bladectlCfg); err != nil {
cancelCtx(err)
return err
}
// setup signal handlers for SIGINT and SIGTERM
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
select {
// Wait for context cancel
case <-ctx.Done():
// Wait for signal
case sig := <-sigs:
fmt.Println("Received signal", sig.String())
switch sig {
case syscall.SIGTERM:
fallthrough
case syscall.SIGINT:
fallthrough
case syscall.SIGQUIT:
// On terminate signal, cancel context causing the program to terminate
cancelCtx(context.Canceled)
default:
log.FromContext(ctx).Warn("Received unknown signal", zap.String("signal", sig.String()))
}
}
}()
// Allow to easily select all blades
if allBlades {
bladeNames = make([]string, len(bladectlCfg.Blades))
for idx, blade := range bladectlCfg.Blades {
bladeNames[idx] = blade.Name
}
}
clients := make([]bladeapiv1alpha1.BladeAgentServiceClient, len(bladeNames))
for idx, bladeName := range bladeNames {
namedBlade, herr := bladectlCfg.FindBlade(bladeName)
if herr != nil {
cancelCtx(herr)
return errors.New(herr.Display())
}
bladeNames[idx] = namedBlade.Name
client, herr := buildClient(&namedBlade.Blade)
if herr != nil {
cancelCtx(herr)
return errors.New(herr.Display())
}
clients[idx] = client
}
ctx = clientIntoContext(ctx, clients[0]) // Add the default client
ctx = clientsIntoContext(ctx, clients) // Add all clients
cmd.SetContext(ctx)
return nil
},
}
func loadTlsCredentials(server string, certData config.Certificate) (credentials.TransportCredentials, humane.Error) {
// Decode base64 certificate, key, and CA
certPEM, err := base64.StdEncoding.DecodeString(certData.ClientCertificateData)
if err != nil {
return nil, humane.Wrap(err, "invalid base64 client cert")
}
keyPEM, err := base64.StdEncoding.DecodeString(certData.ClientKeyData)
if err != nil {
return nil, humane.Wrap(err, "invalid base64 client key")
}
caPEM, err := base64.StdEncoding.DecodeString(certData.CertificateAuthorityData)
if err != nil {
return nil, humane.Wrap(err, "invalid base64 CA cert")
}
// Load client cert/key pair
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, humane.Wrap(err, "failed to parse client cert/key pair")
}
// Load CA into CertPool
caPool := x509.NewCertPool()
if !caPool.AppendCertsFromPEM(caPEM) {
return nil, humane.Wrap(err, "failed to append CA certificate")
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{tlsCert},
RootCAs: caPool,
ServerName: server,
}
return credentials.NewTLS(tlsConfig), nil
}
func buildClient(blade *config.Blade) (bladeapiv1alpha1.BladeAgentServiceClient, humane.Error) {
// Create our gRPC Transport Credentials
creds := insecure.NewCredentials()
certData := blade.Certificate
// If we're presented with certificate data in the config, we try to create a mTLS connection
if len(certData.ClientCertificateData) > 0 && len(certData.ClientKeyData) > 0 && len(certData.CertificateAuthorityData) > 0 {
serverName := blade.Server
if strings.Contains(serverName, ":") {
var err error
if serverName, _, err = net.SplitHostPort(blade.Server); err != nil {
return nil, humane.Wrap(err, "failed to parse server address")
}
}
var err humane.Error
if creds, err = loadTlsCredentials(serverName, certData); err != nil {
return nil, err
}
}
conn, err := grpc.NewClient(blade.Server, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, humane.Wrap(err,
"failed to dial grpc server",
"ensure the gRPC server you are trying to connect to is running and the address is correct",
)
}
return bladeapiv1alpha1.NewBladeAgentServiceClient(conn), nil
}

View File

@@ -0,0 +1,77 @@
package main
import (
"os"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
func init() {
cmdGet.AddCommand(cmdGetStatus)
}
var cmdGetStatus = &cobra.Command{
Use: "status",
Short: "Get in-depth information about the current state of the compute-blade",
Example: "bladectl get status",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
bladeStatus := make([]*bladeapiv1alpha1.StatusResponse, len(clients))
for idx, client := range clients {
var err error
if bladeStatus[idx], err = client.GetStatus(ctx, &emptypb.Empty{}); err != nil {
return err
}
}
printStatusTable(bladeStatus)
return nil
},
}
func printStatusTable(bladeStatus []*bladeapiv1alpha1.StatusResponse) {
// Header: Blade | Stat1 | Stat2 | ...
header := []string{
"Blade",
"Temperature",
"Fan Speed Override",
"Fan Speed",
"Stealth Mode",
"Identify",
"Critical Mode",
"Power Status",
}
// Table writer setup
tbl := tablewriter.NewTable(os.Stdout,
tablewriter.WithHeader(header),
tablewriter.WithHeaderAlignment(tw.AlignLeft),
tablewriter.WithHeaderAutoFormat(tw.Off),
)
// Rows: one per blade
for bladeIdx, status := range bladeStatus {
row := []string{
bladeNames[bladeIdx],
tempStyle(status.Temperature, status.CriticalTemperatureThreshold).Render(tempLabel(status.Temperature)),
speedOverrideStyle(status.FanSpeedAutomatic).Render(fanSpeedOverrideLabel(status.FanSpeedAutomatic, status.FanPercent)),
rpmStyle(status.FanRpm).Render(rpmLabel(status.FanRpm) + " (" + percentLabel(status.FanPercent) + ")"),
activeStyle(status.StealthMode).Render(activeLabel(status.StealthMode)),
activeStyle(status.IdentifyActive).Render(activeLabel(status.IdentifyActive)),
activeStyle(status.CriticalActive).Render(activeLabel(status.CriticalActive)),
okStyle().Render(hal.PowerStatus(status.PowerStatus).String()),
}
_ = tbl.Append(row)
}
_ = tbl.Render()
}

View File

@@ -0,0 +1,88 @@
package main
import (
"fmt"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
var disable bool
func init() {
cmdSetStealth.Flags().BoolVarP(&disable, "disable", "e", false, "disable stealth mode")
cmdSet.AddCommand(cmdSetStealth)
cmdRemove.AddCommand(cmdRmStealth)
cmdGet.AddCommand(cmdGetStealth)
}
var (
cmdSetStealth = &cobra.Command{
Use: "stealth",
Short: "Enable or disable stealth mode on the compute-blade",
Example: "bladectl set stealth --disable",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for _, client := range clients {
_, err := client.SetStealthMode(ctx, &bladeapiv1alpha1.StealthModeRequest{Enable: !disable})
if err != nil {
return err
}
}
return nil
},
}
cmdRmStealth = &cobra.Command{
Use: "stealth",
Short: "Disable stealth mode on the compute-blade",
Example: "bladectl remove stealth",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for _, client := range clients {
_, err := client.SetStealthMode(ctx, &bladeapiv1alpha1.StealthModeRequest{Enable: false})
if err != nil {
return err
}
}
return nil
},
}
cmdGetStealth = &cobra.Command{
Use: "stealth",
Short: "Get the stealth mode status of the compute-blade",
Example: "bladectl get stealth",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
for idx, client := range clients {
bladeStatus, err := client.GetStatus(ctx, &emptypb.Empty{})
if err != nil {
return err
}
rowPrefix := bladeNames[idx]
if len(bladeNames) > 1 {
rowPrefix += ": "
} else {
rowPrefix = ""
}
fmt.Println(activeStyle(bladeStatus.StealthMode).Render(rowPrefix, activeLabel(bladeStatus.StealthMode)))
}
return nil
},
}
)

39
cmd/bladectl/cmd_verbs.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import (
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(cmdGet)
rootCmd.AddCommand(cmdSet)
rootCmd.AddCommand(cmdRemove)
rootCmd.AddCommand(cmdDescribe)
}
var (
cmdGet = &cobra.Command{
Use: "get",
Short: "Display compute-blade related information",
Long: "Prints information about compute-blade related information, e.g. fan speed, temperature, etc.",
}
cmdDescribe = &cobra.Command{
Use: "describe",
Short: "Display compute-blade related information",
Long: "Prints information about compute-blade related information, e.g. fan speed curve steps, etc.",
}
cmdSet = &cobra.Command{
Use: "set",
Short: "Configure compute-blade",
Long: "These commands allow you make changes to compute-blade related information.",
}
cmdRemove = &cobra.Command{
Use: "remove",
Aliases: []string{"rm", "delete", "del", "unset"},
Short: "Configure compute-blade",
Long: "These commands allow you make changes to compute-blade related information.",
}
)

View File

@@ -0,0 +1,80 @@
package main
import (
"fmt"
"log"
"os"
"sync"
"time"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
func init() {
rootCmd.AddCommand(cmdVersion)
}
var cmdVersion = &cobra.Command{
Use: "version",
Short: "Shows version information",
Example: "bladectl version",
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
clients := clientsFromContext(ctx)
header := []string{
"Component",
"Version",
"Commit",
"Build Time",
}
// Table writer setup
tbl := tablewriter.NewTable(os.Stdout,
tablewriter.WithHeader(header),
tablewriter.WithHeaderAlignment(tw.AlignLeft),
tablewriter.WithHeaderAutoFormat(tw.Off),
)
commit := Commit
if len(commit) > 7 {
commit = commit[:7]
}
_ = tbl.Append([]string{"bladectl", Version, commit, BuildTime.Format(time.RFC3339)})
var wg sync.WaitGroup
for idx, client := range clients {
wg.Add(1)
go func() {
defer wg.Done()
if status, err := client.GetStatus(ctx, &emptypb.Empty{}); err == nil && status.Version != nil {
commit := status.Version.Commit
if len(commit) > 7 {
commit = commit[:7]
}
_ = tbl.Append([]string{
fmt.Sprintf("api: %s", bladeNames[idx]),
status.Version.Version,
commit,
time.Unix(status.Version.Date, 0).Format(time.RFC3339),
})
} else {
log.Printf("Error (%s) getting status: %v", bladeNames[idx], err)
}
}()
}
wg.Wait()
_ = tbl.Render()
return nil
},
}

View File

@@ -0,0 +1,94 @@
package config
import (
"encoding/base64"
"os"
"path/filepath"
"github.com/sierrasoftworks/humane-errors-go"
"github.com/spechtlabs/go-otel-utils/otelzap"
)
type BladectlConfig struct {
Blades []NamedBlade `yaml:"blades" mapstructure:"blades"`
CurrentBlade string `yaml:"current-blade" mapstructure:"current-blade"`
}
type NamedBlade struct {
Name string `yaml:"name" mapstructure:"name"`
Blade Blade `yaml:"blade" mapstructure:"blade"`
}
type Blade struct {
Server string `yaml:"server" mapstructure:"server"`
Certificate Certificate `yaml:"cert,omitempty" mapstructure:"cert,omitempty"`
}
type Certificate struct {
CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty" mapstructure:"certificate-authority-data,omitempty"`
ClientCertificateData string `yaml:"client-certificate-data,omitempty" mapstructure:"client-certificate-data,omitempty"`
ClientKeyData string `yaml:"client-key-data,omitempty" mapstructure:"client-key-data,omitempty"`
}
func (c *BladectlConfig) FindBlade(name string) (*NamedBlade, humane.Error) {
if len(name) == 0 {
name = c.CurrentBlade
}
for _, blade := range c.Blades {
if blade.Name == name {
return &blade, nil
}
}
return nil, humane.New("current blade not found in configuration",
"ensure you have a current-blade set in your configuration file, or use the --current-blade flag to specify one",
"make sure you have a blade with the name you specified in the blades configuration",
)
}
func NewAuthenticatedBladectlConfig(server string, caPEM []byte, clientCertDER []byte, clientKeyDER []byte) *BladectlConfig {
cfg := NewBladectlConfig(server)
cfg.Blades[0].Blade.Certificate.CertificateAuthorityData = base64.StdEncoding.EncodeToString(caPEM)
cfg.Blades[0].Blade.Certificate.ClientCertificateData = base64.StdEncoding.EncodeToString(clientCertDER)
cfg.Blades[0].Blade.Certificate.ClientKeyData = base64.StdEncoding.EncodeToString(clientKeyDER)
return cfg
}
func NewBladectlConfig(server string) *BladectlConfig {
hostname, err := os.Hostname()
if err != nil {
otelzap.L().WithError(err).Fatal("Failed to extract hostname")
}
return &BladectlConfig{
Blades: []NamedBlade{
{
Name: hostname,
Blade: Blade{
Server: server,
},
},
},
CurrentBlade: hostname,
}
}
func EnsureBladectlConfigHome() (string, humane.Error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", humane.Wrap(err, "Failed to extract home directory",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
configDir := filepath.Join(homeDir, ".config", "bladectl")
if err := os.MkdirAll(configDir, 0700); err != nil {
return "", humane.Wrap(err, "Failed to create config directory",
"ensure the home-directory is writable by the agent user",
)
}
return configDir, nil
}

View File

@@ -2,41 +2,37 @@ package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/spf13/viper"
)
type grpcClientContextKey int
const (
defaultGrpcClientContextKey grpcClientContextKey = 0
defaultGrpcClientConnContextKey grpcClientContextKey = 1
defaultGrpcClientContextKey grpcClientContextKey = 0
defaultGrpcClientsContextKey grpcClientContextKey = 1
)
var (
grpcAddr string
timeout time.Duration
Version string
Commit string
Date string
BuildTime time.Time
)
func init() {
rootCmd.PersistentFlags().
StringVar(&grpcAddr, "addr", "unix:///tmp/computeblade-agent.sock", "address of the computeblade-agent gRPC server")
rootCmd.PersistentFlags().DurationVar(&timeout, "timeout", time.Minute, "timeout for gRPC requests")
}
func clientIntoContext(ctx context.Context, client bladeapiv1alpha1.BladeAgentServiceClient) context.Context {
return context.WithValue(ctx, defaultGrpcClientContextKey, client)
}
func clientsIntoContext(ctx context.Context, clients []bladeapiv1alpha1.BladeAgentServiceClient) context.Context {
return context.WithValue(ctx, defaultGrpcClientsContextKey, clients)
}
func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceClient {
client, ok := ctx.Value(defaultGrpcClientContextKey).(bladeapiv1alpha1.BladeAgentServiceClient)
if !ok {
@@ -45,40 +41,30 @@ func clientFromContext(ctx context.Context) bladeapiv1alpha1.BladeAgentServiceCl
return client
}
var rootCmd = &cobra.Command{
Use: "bladectl",
Short: "bladectl interacts with the computeblade-agent and allows you to manage hardware-features of your compute blade(s)",
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
origCtx := cmd.Context()
// setup signal handlers for SIGINT and SIGTERM
ctx, cancelCtx := context.WithTimeout(origCtx, timeout)
// setup signal handler channels
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
// Wait for context cancel or signal
select {
case <-ctx.Done():
case <-sigs:
// On signal, cancel context
cancelCtx()
}
}()
conn, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return fmt.Errorf("failed to dial grpc server: %w", err)
}
client := bladeapiv1alpha1.NewBladeAgentServiceClient(conn)
cmd.SetContext(clientIntoContext(ctx, client))
return nil
},
func clientsFromContext(ctx context.Context) []bladeapiv1alpha1.BladeAgentServiceClient {
clients, ok := ctx.Value(defaultGrpcClientsContextKey).([]bladeapiv1alpha1.BladeAgentServiceClient)
if !ok {
panic("grpc client not found in context")
}
return clients
}
func main() {
if Date != "" {
if unixTimestamp, err := strconv.ParseInt(Date, 10, 64); err == nil {
BuildTime = time.Unix(unixTimestamp, 0)
} else {
BuildTime = time.Unix(0, 0)
}
}
// Setup configuration
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("$HOME/.config/bladectl")
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}

83
cmd/bladectl/util.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import (
"fmt"
"github.com/charmbracelet/lipgloss"
)
const (
ColorCritical = lipgloss.Color("#cc0000")
ColorWarning = lipgloss.Color("#e69138")
ColorOk = lipgloss.Color("#04B575")
)
func fanSpeedOverrideLabel(automatic bool, percent uint32) string {
if automatic {
return "Not set"
}
return fmt.Sprintf("%d%%", percent)
}
func tempLabel(temp int64) string {
return fmt.Sprintf("%d°C", temp)
}
func percentLabel(percent uint32) string {
return fmt.Sprintf("%d%%", percent)
}
func rpmLabel(rpm int64) string {
return fmt.Sprintf("%d RPM", rpm)
}
func activeLabel(b bool) string {
if b {
return "Active"
}
return "Off"
}
func speedOverrideStyle(automaticMode bool) lipgloss.Style {
if automaticMode {
return lipgloss.NewStyle().Foreground(ColorOk)
}
return lipgloss.NewStyle().Foreground(ColorCritical)
}
func activeStyle(active bool) lipgloss.Style {
if active {
return lipgloss.NewStyle().Foreground(ColorCritical)
}
return lipgloss.NewStyle().Foreground(ColorOk)
}
func tempStyle(temp int64, criticalTemp int64) lipgloss.Style {
color := ColorOk
if temp >= criticalTemp {
color = ColorCritical
} else if temp >= criticalTemp-10 {
color = ColorWarning
}
return lipgloss.NewStyle().Foreground(color)
}
func rpmStyle(rpm int64) lipgloss.Style {
color := ColorOk
if rpm > 6000 {
color = ColorCritical
} else if rpm > 5250 {
color = ColorWarning
}
return lipgloss.NewStyle().Foreground(color)
}
func okStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(ColorOk)
}

View File

@@ -4,14 +4,15 @@ package main
import (
"context"
"machine"
"time"
"github.com/uptime-induestries/compute-blade-agent/pkg/eventbus"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/emc2101"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
"machine"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/emc2101"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
"golang.org/x/sync/errgroup"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/ws2812"
@@ -33,7 +34,7 @@ type Controller struct {
LeftUART drivers.UART
RightUART drivers.UART
eb eventbus.EventBus
eb events.EventBus
leftLed led.Color
rightLed led.Color
leftReqFanSpeed uint8
@@ -43,7 +44,7 @@ type Controller struct {
}
func (c *Controller) Run(parentCtx context.Context) error {
c.eb = eventbus.New()
c.eb = events.New()
c.FanController.Init()
c.FanController.SetFanPercent(c.DefaultFanSpeed)
@@ -80,7 +81,7 @@ func (c *Controller) Run(parentCtx context.Context) error {
})
// right blade events
println("[+] Starting event listener (righ)")
println("[+] Starting event listener (right)")
group.Go(func() error {
return c.listenEvents(ctx, c.RightUART, rightBladeTopicIn)
})
@@ -123,7 +124,7 @@ func (c *Controller) Run(parentCtx context.Context) error {
return group.Wait()
}
// listenEvents reads events from the UART interface and dispatches them to the eventbus
// listenEvents reads events from the UART interface and dispatches them to the events
func (c *Controller) listenEvents(ctx context.Context, uart drivers.UART, targetTopic string) error {
for {
// Read packet from UART; blocks until packet is received
@@ -137,9 +138,9 @@ func (c *Controller) listenEvents(ctx context.Context, uart drivers.UART, target
}
}
// dispatchEvents reads events from the eventbus and writes them to the UART interface
// dispatchEvents reads events from the events and writes them to the UART interface
func (c *Controller) dispatchEvents(ctx context.Context, uart drivers.UART, sourceTopic string) error {
sub := c.eb.Subscribe(sourceTopic, 4, eventbus.MatchAll)
sub := c.eb.Subscribe(sourceTopic, 4, events.MatchAll)
defer sub.Unsubscribe()
for {
select {
@@ -201,9 +202,9 @@ func (c *Controller) metricReporter(ctx context.Context) error {
func (c *Controller) updateFanSpeed(ctx context.Context) error {
var pkt smartfanunit.SetFanSpeedPercentPacket
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, eventbus.MatchAll)
subLeft := c.eb.Subscribe(leftBladeTopicIn, 1, events.MatchAll)
defer subLeft.Unsubscribe()
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, eventbus.MatchAll)
subRight := c.eb.Subscribe(rightBladeTopicIn, 1, events.MatchAll)
defer subRight.Unsubscribe()
for {

View File

@@ -4,11 +4,12 @@ package main
import (
"context"
"machine"
"time"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/emc2101"
"machine"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/emc2101"
"tinygo.org/x/drivers/ws2812"
)
@@ -26,15 +27,15 @@ func main() {
err = machine.UART0.Configure(machine.UARTConfig{TX: machine.UART0_TX_PIN, RX: machine.UART0_RX_PIN})
if err != nil {
println("[!] Failed to initialize UART0:", err.Error())
goto errprint
goto errPrint
}
machine.UART0.SetBaudRate(smartfanunit.Baudrate)
machine.UART0.SetBaudRate(smartfanunit.BaudRate)
err = machine.UART1.Configure(machine.UARTConfig{TX: machine.UART1_TX_PIN, RX: machine.UART1_RX_PIN})
if err != nil {
println("[!] Failed to initialize UART1:", err.Error())
goto errprint
goto errPrint
}
machine.UART1.SetBaudRate(smartfanunit.Baudrate)
machine.UART1.SetBaudRate(smartfanunit.BaudRate)
// Enables fan, DO NOT CHANGE
machine.GP16.Configure(machine.PinConfig{Mode: machine.PinOutput})
@@ -57,7 +58,7 @@ func main() {
err = emc.Init()
if err != nil {
println("[!] Failed to initialize emc2101:", err.Error())
goto errprint
goto errPrint
}
println("[+] IO initialized, starting controller...")
@@ -75,7 +76,7 @@ func main() {
err = controller.Run(context.Background())
// Blinking -> something went wrong
errprint:
errPrint:
ledState := false
for {
ledState = !ledState

123
go.mod
View File

@@ -1,53 +1,98 @@
module github.com/uptime-induestries/compute-blade-agent
module github.com/compute-blade-community/compute-blade-agent
go 1.22
toolchain go1.22.5
go 1.25.0
require (
github.com/prometheus/client_golang v1.16.0
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.3
github.com/charmbracelet/lipgloss v1.1.0
github.com/gizak/termui/v3 v3.1.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
github.com/olekukonko/tablewriter v1.1.4
github.com/prometheus/client_golang v1.23.2
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/warthog618/gpiod v0.8.1
go.bug.st/serial v1.6.1
go.uber.org/zap v1.24.0
golang.org/x/sync v0.2.0
google.golang.org/grpc v1.56.2
google.golang.org/protobuf v1.31.0
tinygo.org/x/drivers v0.26.0
go.bug.st/serial v1.6.4
go.uber.org/zap v1.27.1
golang.org/x/sync v0.20.0
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
tinygo.org/x/drivers v0.34.0
)
require (
github.com/aws/smithy-go v1.22.5 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/creack/goselect v0.1.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
go.uber.org/atomic v1.11.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
go.opentelemetry.io/otel/log v0.13.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
)

958
go.sum
View File

@@ -1,548 +1,462 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/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/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/clipperhouse/displaywidth v0.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk=
github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM=
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
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/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=
github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/olekukonko/tablewriter v1.1.1 h1:b3reP6GCfrHwmKkYwNRFh2rxidGHcT6cgxj/sHiDDx0=
github.com/olekukonko/tablewriter v1.1.1/go.mod h1:De/bIcTF+gpBDB3Alv3fEsZA+9unTsSzAg/ZGADCtn4=
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
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/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16 h1:9vtY3febGroV+aPR5OlI3fekkesi+lMVsVWyxBp/rfk=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250507223502-4bb667dc1e16/go.mod h1:CbJLj9L1qHdzLg4YRh2Lzr0noe9pR6QrVEqfLbITRKw=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250811205537-5f14a04ebff5 h1:nlfxPheTxwOE5hEq9iVurbo83/Wie52V5lKIhi73mRw=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20250811205537-5f14a04ebff5/go.mod h1:CbJLj9L1qHdzLg4YRh2Lzr0noe9pR6QrVEqfLbITRKw=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20251121131909-6b4ca9dd07a7 h1:mD7GjWbIAdxIvDwTXs2uidulnIAfi06h7SZBQvDZga8=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20251121131909-6b4ca9dd07a7/go.mod h1:AyTGN4SGjbmEHvVAnBy8SpCnG3hskAfZ7TJW1AzMXgk=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32 h1:6qDjIFnVHw6zdtW0t3RuN1PWcVSL6kXZiiDYR/P7id8=
github.com/sierrasoftworks/humane-errors-go v0.0.0-20260226124905-a7be4ffe4f32/go.mod h1:AyTGN4SGjbmEHvVAnBy8SpCnG3hskAfZ7TJW1AzMXgk=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10 h1:Q5p+5KGA587GfzR6FdXGje4XBfxhi1u4NSu6lSnWCGA=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.10/go.mod h1:sFuJXEBbNq/pQx9pP5OnVtx9yGJnH4fXi7x3qihW0ak=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.13 h1:DyTnvzLrhGowglrmDO8GFzbC/ZnzHRkMlFCk7p/pEmc=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.13/go.mod h1:63JphMzIY2vGTZ9OMjntS/XUM2sVwLCydSsjVE+M5QU=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15 h1:LRx9EFzD4bI+kH3NNn6GtqwRrGbJgmyau4IOJcfBTYE=
github.com/spechtlabs/go-otel-utils/otelprovider v0.0.15/go.mod h1:ACquqruOxYFRL5H7TCxXKXIJTVj+6eqlEJR6WQAEBeE=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.10 h1:RR/WS4b+ABxNL7xzlK4FTvnuXRbGk3yyggvvLnQ+FeM=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.10/go.mod h1:IhsBuW+sZwLxX1Ww5LmTlIonBP8GiyhsiZkIRq+ySE0=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.11 h1:D3jzku3MMLMt/CgI++Zk3TE/mnQWz9dI9y/sCBb60OA=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.11/go.mod h1:Bk/uRNkU1/MsMc2J1NUxF+0lRNDbUUIulioJevLYM74=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.13 h1:eKrf3Jd4WurABA+iw4TAZJRF2/Ke1cIzsX1wSUOZZT0=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.13/go.mod h1:Bk/uRNkU1/MsMc2J1NUxF+0lRNDbUUIulioJevLYM74=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15 h1:LPo3vmPRVTQDG/ic2r4q1t9irptnffRRTEpaLYjbguo=
github.com/spechtlabs/go-otel-utils/otelzap v0.0.15/go.mod h1:0LC7Tzo53EdZ0FY6dUMDDkDmimphYDbjqIg+BY5MJjk=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/warthog618/gpiod v0.8.1 h1:+8iHpHd3fljAd6l4AT8jPbMDQNKdvBIpW/hmLgAcHiM=
github.com/warthog618/gpiod v0.8.1/go.mod h1:A7v1hGR2eTsnkN+e9RoAPYgJG9bLJWtwyIIK+pgqC7s=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.bug.st/serial v1.6.1 h1:VSSWmUxlj1T/YlRo2J104Zv3wJFrjHIl/T3NeruWAHY=
go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
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/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
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 v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0 h1:z6lNIajgEBVtQZHjfw2hAccPEBDs+nx58VemmXWa2ec=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.13.0/go.mod h1:+kyc3bRx/Qkq05P6OCu3mTEIOxYRYzoIg+JsUp5X+PM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0 h1:zUfYw8cscHHLwaY8Xz3fiJu+R59xBnkgq2Zr1lwmK/0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.13.0/go.mod h1:514JLMCcFLQFS8cnTepOk6I09cKWJ5nGHBxHrMJ8Yfg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
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.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
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/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
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/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
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 v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c=
go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY=
go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ=
go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
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/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
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.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
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=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo=
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY=
google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA=
google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
tinygo.org/x/drivers v0.26.0 h1:7KSIYssX0ki0dd7yBYkVZWSG0kt8vrZNS0It73TymcA=
tinygo.org/x/drivers v0.26.0/go.mod h1:X7utcg3yfFUFuKLOMTZD56eztXMjpkcf8OHldfTBsjw=
tinygo.org/x/drivers v0.31.0 h1:Q2RpvTRMtdmjHD2Xyn4e8WXsJZKpIny3Lg4hzG1dLu4=
tinygo.org/x/drivers v0.31.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
tinygo.org/x/drivers v0.32.0 h1:qz7MRR1ZBIUhWC6kc4XuVNPr2+mUT8m7QJwA+Jji4IU=
tinygo.org/x/drivers v0.32.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
tinygo.org/x/drivers v0.33.0 h1:5r8Ab0IxjWQi7LzYLNWpya6U4nedo9ZtxeMaAzrJTG8=
tinygo.org/x/drivers v0.33.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=
tinygo.org/x/drivers v0.34.0 h1:lw8ePJeUSn9oICKBvQXHC9TIE+J00OfXfkGTrpXM9Iw=
tinygo.org/x/drivers v0.34.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk=

View File

@@ -5,7 +5,7 @@ set -eu
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
# Function to detect the Linux package manager
# Function to detect the package suffix based on the available package manager
detect_package_suffix() {
if [ -x "$(command -v dpkg)" ]; then
echo "deb"
@@ -25,13 +25,16 @@ get_latest_release() {
curl -s "https://api.github.com/repos/$repo/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
}
github_repo="github.com/uptime-induestries/compute-blade-agent"
github_repo="compute-blade-community/compute-blade-agent"
package_suffix=$(detect_package_suffix)
latest_release=$(get_latest_release "$github_repo")
# Create a temporary directory for the download
tmp_dir=$(mktemp -d)
# Construct the download URL and filename based on the detected package manager and latest release
download_url="https://github.com/$github_repo/releases/download/$latest_release/${github_repo##*/}_${latest_release#v}_linux_arm64.$package_suffix"
target_file="$tmp_dir/computeblade-agent.$package_suffix"
download_url="https://github.com/$github_repo/releases/download/$latest_release/compute-blade-agent_${latest_release#v}_linux_arm64.$package_suffix"
target_file="$tmp_dir/compute-blade-agent.$package_suffix"
# Download the package
echo "Downloading $download_url"
@@ -49,8 +52,12 @@ case "$package_suffix" in
pkg.tar.zst)
sudo pacman -U --noconfirm "$target_file"
;;
*)
echo "Unsupported package format" >> /dev/stderr
exit 1
;;
esac
# Enable and start the service
echo "Enabling and starting computeblade-agent"
sudo systemctl enable computeblade-agent --now
echo "Enabling and starting compute-blade-agent"
sudo systemctl enable compute-blade-agent --now

View File

@@ -1,11 +1,12 @@
[Unit]
Description=ComputeBlade Agent
Documentation=https://github.com/uptime-induestries/compute-blade-agent
Documentation=https://github.com/compute-blade-community/compute-blade-agent
After=network.target
[Service]
User=root
Restart=on-failure
ExecStart=/usr/bin/computeblade-agent
ExecStart=/usr/bin/compute-blade-agent
TimeoutStopSec=20s
[Install]

View File

@@ -1,19 +1,25 @@
package agent
package internal_agent
import (
"context"
"errors"
"sync"
"fmt"
"net"
"time"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/compute-blade-community/compute-blade-agent/pkg/agent"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/ledengine"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/uptime-induestries/compute-blade-agent/pkg/fancontroller"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/ledengine"
"github.com/uptime-induestries/compute-blade-agent/pkg/log"
"github.com/sierrasoftworks/humane-errors-go"
"go.uber.org/zap"
"google.golang.org/grpc"
)
var (
@@ -21,429 +27,277 @@ var (
eventCounter = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "computeblade_agent",
Name: "events_count",
Help: "ComputeBlade Agent internal event handler statistics (handled events)",
Help: "ComputeBlade agent internal event handler statistics (handled events)",
}, []string{"type"})
// droppedEventCounter is a prometheus counter that counts the number of events dropped by the agent
droppedEventCounter = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "computeblade_agent",
Name: "events_dropped_count",
Help: "ComputeBlade Agent internal event handler statistics (dropped events)",
Help: "ComputeBlade agent internal event handler statistics (dropped events)",
}, []string{"type"})
)
type Event int
const (
NoopEvent = iota
IdentifyEvent
IdentifyConfirmEvent
CriticalEvent
CriticalResetEvent
EdgeButtonEvent
)
func (e Event) String() string {
switch e {
case NoopEvent:
return "noop"
case IdentifyEvent:
return "identify"
case IdentifyConfirmEvent:
return "identify_confirm"
case CriticalEvent:
return "critical"
case CriticalResetEvent:
return "critical_reset"
case EdgeButtonEvent:
return "edge_button"
default:
return "unknown"
}
}
type ComputeBladeAgentConfig struct {
// IdleLedColor is the color of the edge LED when the blade is idle mode
IdleLedColor led.Color `mapstructure:"idle_led_color"`
// IdentifyLedColor is the color of the edge LED when the blade is in identify mode
IdentifyLedColor led.Color `mapstructure:"identify_led_color"`
// CriticalLedColor is the color of the top(!) LED when the blade is in critical mode.
// In the circumstance when >1 blades are in critical mode, the identidy function can be used to find the right blade
CriticalLedColor led.Color `mapstructure:"critical_led_color"`
// StealthModeEnabled indicates whether stealth mode is enabled
StealthModeEnabled bool `mapstructure:"stealth_mode"`
// Critical temperature of the compute blade (used to trigger critical mode)
CriticalTemperatureThreshold uint `mapstructure:"critical_temperature_threshold"`
// FanSpeed allows to set a fixed fan speed (in percent)
FanSpeed *fancontroller.FanOverrideOpts `mapstructure:"fan_speed"`
// FanControllerConfig is the configuration of the fan controller
FanControllerConfig fancontroller.FanControllerConfig `mapstructure:"fan_controller"`
ComputeBladeHalOpts hal.ComputeBladeHalOpts `mapstructure:"hal"`
}
// ComputeBladeAgent implements the core-logic of the agent. It is responsible for handling events and interfacing with the hardware.
type ComputeBladeAgent interface {
// Run dispatches the agent and blocks until the context is canceled or an error occurs
Run(ctx context.Context) error
// EmitEvent emits an event to the agent
EmitEvent(ctx context.Context, event Event) error
// SetFanSpeed sets the fan speed in percent
SetFanSpeed(_ context.Context, speed uint8) error
// SetStealthMode sets the stealth mode
SetStealthMode(_ context.Context, enabled bool) error
// WaitForIdentifyConfirm blocks until the user confirms the identify mode
WaitForIdentifyConfirm(ctx context.Context) error
}
// computeBladeAgentImpl is the implementation of the ComputeBladeAgent interface
type computeBladeAgentImpl struct {
opts ComputeBladeAgentConfig
// computeBladeAgent manages the operation and coordination of hardware components and services for a compute blade agent.
type computeBladeAgent struct {
bladeapiv1alpha1.UnimplementedBladeAgentServiceServer
config agent.ComputeBladeAgentConfig
blade hal.ComputeBladeHal
state ComputebladeState
state agent.ComputebladeState
edgeLedEngine ledengine.LedEngine
topLedEngine ledengine.LedEngine
fanController fancontroller.FanController
eventChan chan Event
eventChan chan events.Event
server *grpc.Server
agentInfo agent.ComputeBladeAgentInfo
}
func NewComputeBladeAgent(ctx context.Context, opts ComputeBladeAgentConfig) (ComputeBladeAgent, error) {
var err error
// blade, err := hal.NewCm4Hal(hal.ComputeBladeHalOpts{
blade, err := hal.NewCm4Hal(ctx, opts.ComputeBladeHalOpts)
// NewComputeBladeAgent creates and initializes a new ComputeBladeAgent, including gRPC server setup and hardware interfaces.
func NewComputeBladeAgent(ctx context.Context, config agent.ComputeBladeAgentConfig, agentInfo agent.ComputeBladeAgentInfo) (agent.ComputeBladeAgent, error) {
blade, err := hal.NewHal(ctx, config.ComputeBladeHalOpts)
if err != nil {
return nil, err
}
edgeLedEngine := ledengine.NewLedEngine(ledengine.LedEngineOpts{
LedIdx: hal.LedEdge,
Hal: blade,
})
fanController, err := fancontroller.NewLinearFanController(config.FanControllerConfig)
if err != nil {
return nil, err
}
topLedEngine := ledengine.NewLedEngine(ledengine.LedEngineOpts{
LedIdx: hal.LedTop,
Hal: blade,
})
if err != nil {
return nil, err
}
fanController, err := fancontroller.NewLinearFanController(opts.FanControllerConfig)
if err != nil {
return nil, err
}
return &computeBladeAgentImpl{
opts: opts,
a := &computeBladeAgent{
config: config,
blade: blade,
edgeLedEngine: edgeLedEngine,
topLedEngine: topLedEngine,
edgeLedEngine: ledengine.New(blade, hal.LedEdge),
topLedEngine: ledengine.New(blade, hal.LedTop),
fanController: fanController,
state: NewComputeBladeState(),
eventChan: make(
chan Event,
10,
), // backlog of 10 events. They should process fast but we e.g. don't want to miss button presses
}, nil
state: agent.NewComputeBladeState(),
eventChan: make(chan events.Event, 10),
agentInfo: agentInfo,
}
if err := a.setupGrpcServer(ctx); err != nil {
return nil, err
}
bladeapiv1alpha1.RegisterBladeAgentServiceServer(a.server, a)
return a, nil
}
func (a *computeBladeAgentImpl) Run(origCtx context.Context) error {
var wg sync.WaitGroup
// RunAsync starts the agent in a separate goroutine and handles errors, allowing cancellation through the provided context.
func (a *computeBladeAgent) RunAsync(ctx context.Context, cancel context.CancelCauseFunc) {
go func() {
log.FromContext(ctx).Info("Starting agent")
err := a.Run(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Failed to run agent")
cancel(err)
}
}()
}
// Run initializes and starts the compute blade agent, setting up necessary components and processes, and waits for termination.
func (a *computeBladeAgent) Run(origCtx context.Context) error {
ctx, cancelCtx := context.WithCancelCause(origCtx)
defer a.cleanup(ctx)
defer cancelCtx(fmt.Errorf("cancel"))
log.FromContext(ctx).Info("Starting ComputeBlade agent")
// Ingest noop event to initialise metrics
a.state.RegisterEvent(NoopEvent)
a.state.RegisterEvent(events.NoopEvent)
// Set defaults
if err := a.blade.SetStealthMode(a.opts.StealthModeEnabled); err != nil {
if err := a.blade.SetStealthMode(a.config.StealthModeEnabled); err != nil {
return err
}
// Run HAL
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting HAL")
if err := a.blade.Run(ctx); err != nil && err != context.Canceled {
log.FromContext(ctx).Error("HAL failed", zap.Error(err))
cancelCtx(err)
}
}()
go a.runHal(ctx, cancelCtx)
// Start edge button event handler
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting edge button event handler")
for {
err := a.blade.WaitForEdgeButtonPress(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("Edge button event handler failed", zap.Error(err))
cancelCtx(err)
} else if err != nil {
return
}
select {
case a.eventChan <- Event(EdgeButtonEvent):
default:
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
droppedEventCounter.WithLabelValues(Event(EdgeButtonEvent).String()).Inc()
}
}
}()
go a.runEdgeButtonHandler(ctx, cancelCtx)
// Start top LED engine
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting top LED engine")
err := a.runTopLedEngine(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("Top LED engine failed", zap.Error(err))
cancelCtx(err)
}
}()
go a.runTopLedEngine(ctx, cancelCtx)
// Start edge LED engine
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting edge LED engine")
err := a.runEdgeLedEngine(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("Edge LED engine failed", zap.Error(err))
cancelCtx(err)
}
}()
go a.runEdgeLedEngine(ctx, cancelCtx)
// Start fan controller
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting fan controller")
err := a.runFanController(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("Fan Controller Failed", zap.Error(err))
cancelCtx(err)
}
}()
go a.runFanController(ctx, cancelCtx)
// Start event handler
wg.Add(1)
go func() {
defer wg.Done()
log.FromContext(ctx).Info("Starting event handler")
for {
select {
case <-ctx.Done():
return
case event := <-a.eventChan:
err := a.handleEvent(ctx, event)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("Event handler failed", zap.Error(err))
cancelCtx(err)
}
}
}
}()
go a.runEventHandler(ctx, cancelCtx)
// Start gRPC API
go a.runGRpcApi(ctx, cancelCtx)
// wait till we're done
<-ctx.Done()
wg.Wait()
return ctx.Err()
}
// cleanup restores sane defaults before exiting. Ignores canceled context!
func (a *computeBladeAgentImpl) cleanup(ctx context.Context) {
// GracefulStop gracefully stops the gRPC server, ensuring all in-progress RPCs are completed before shutting down.
func (a *computeBladeAgent) GracefulStop(ctx context.Context) error {
a.server.GracefulStop()
log.FromContext(ctx).Info("Exiting, restoring safe settings")
if err := a.blade.SetFanSpeed(100); err != nil {
log.FromContext(ctx).Error("Failed to set fan speed to 100%", zap.Error(err))
log.FromContext(ctx).WithError(err).Error("Failed to set fan speed to 100%")
}
if err := a.blade.SetLed(hal.LedEdge, led.Color{}); err != nil {
log.FromContext(ctx).Error("Failed to set edge LED to off", zap.Error(err))
log.FromContext(ctx).WithError(err).Error("Failed to set edge LED to off")
}
if err := a.blade.SetLed(hal.LedTop, led.Color{}); err != nil {
log.FromContext(ctx).Error("Failed to set edge LED to off", zap.Error(err))
}
if err := a.Close(); err != nil {
log.FromContext(ctx).Error("Failed to close blade", zap.Error(err))
}
}
func (a *computeBladeAgentImpl) handleEvent(ctx context.Context, event Event) error {
log.FromContext(ctx).Info("Handling event", zap.String("event", event.String()))
eventCounter.WithLabelValues(event.String()).Inc()
// register event in state
a.state.RegisterEvent(event)
// Dispatch incoming events to the right handler(s)
switch event {
case CriticalEvent:
// Handle critical event
return a.handleCriticalActive(ctx)
case CriticalResetEvent:
// Handle critical event
return a.handleCriticalReset(ctx)
case IdentifyEvent:
// Handle identify event
return a.handleIdentifyActive(ctx)
case IdentifyConfirmEvent:
// Handle identify event
return a.handleIdentifyConfirm(ctx)
case EdgeButtonEvent:
// Handle edge button press to toggle identify mode
event := Event(IdentifyEvent)
if a.state.IdentifyActive() {
event = Event(IdentifyConfirmEvent)
}
select {
case a.eventChan <- Event(event):
default:
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
droppedEventCounter.WithLabelValues(event.String()).Inc()
}
log.FromContext(ctx).WithError(err).Error("Failed to set edge LED to off")
}
return nil
return a.blade.Close()
}
func (a *computeBladeAgentImpl) handleIdentifyActive(ctx context.Context) error {
log.FromContext(ctx).Info("Identify active")
return a.edgeLedEngine.SetPattern(ledengine.NewBurstPattern(led.Color{}, a.opts.IdentifyLedColor))
}
func (a *computeBladeAgentImpl) handleIdentifyConfirm(ctx context.Context) error {
log.FromContext(ctx).Info("Identify confirmed/cleared")
return a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.opts.IdleLedColor))
}
func (a *computeBladeAgentImpl) handleCriticalActive(ctx context.Context) error {
log.FromContext(ctx).Warn("Blade in critical state, setting fan speed to 100% and turning on LEDs")
// Set fan speed to 100%
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: 100})
// Disable stealth mode (turn on LEDs)
setStealthModeError := a.blade.SetStealthMode(false)
// Set critical pattern for top LED
setPatternTopLedErr := a.topLedEngine.SetPattern(
ledengine.NewSlowBlinkPattern(led.Color{}, a.opts.CriticalLedColor),
)
// Combine errors, but don't stop execution flow for now
return errors.Join(setStealthModeError, setPatternTopLedErr)
}
func (a *computeBladeAgentImpl) handleCriticalReset(ctx context.Context) error {
log.FromContext(ctx).Info("Critical state cleared, setting fan speed to default and restoring LEDs to default state")
// Reset fan controller overrides
a.fanController.Override(nil)
// Reset stealth mode
if err := a.blade.SetStealthMode(a.opts.StealthModeEnabled); err != nil {
return err
// runHal initializes and starts the HAL service within the given context, handling errors and supporting graceful cancellation.
func (a *computeBladeAgent) runHal(ctx context.Context, cancel context.CancelCauseFunc) {
log.FromContext(ctx).Info("Starting HAL")
if err := a.blade.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("HAL failed")
cancel(err)
}
// Set top LED off
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{})); err != nil {
return err
}
return nil
}
func (a *computeBladeAgentImpl) Close() error {
return errors.Join(a.blade.Close())
}
// runTopLedEngine runs the top LED engine
func (a *computeBladeAgentImpl) runTopLedEngine(ctx context.Context) error {
// FIXME the top LED is only used to indicate emergency situations
err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{}))
if err != nil {
return err
// FIXME the top LED is only used to indicate emergency situations
func (a *computeBladeAgent) runTopLedEngine(ctx context.Context, cancel context.CancelCauseFunc) {
log.FromContext(ctx).Info("Starting top LED engine")
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{})); err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Top LED engine failed")
cancel(err)
}
if err := a.topLedEngine.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Top LED engine failed")
cancel(err)
}
return a.topLedEngine.Run(ctx)
}
// runEdgeLedEngine runs the edge LED engine
func (a *computeBladeAgentImpl) runEdgeLedEngine(ctx context.Context) error {
err := a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.opts.IdleLedColor))
if err != nil {
return err
func (a *computeBladeAgent) runEdgeLedEngine(ctx context.Context, cancel context.CancelCauseFunc) {
log.FromContext(ctx).Info("Starting edge LED engine")
if err := a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.config.IdleLedColor)); err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Edge LED engine failed")
cancel(err)
}
if err := a.edgeLedEngine.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Edge LED engine failed")
cancel(err)
}
return a.edgeLedEngine.Run(ctx)
}
func (a *computeBladeAgentImpl) runFanController(ctx context.Context) error {
// runFanController initializes and manages a periodic task to control fan speed based on temperature readings.
// The method uses a ticker to execute fan speed adjustments and handles context cancellation for cleanup.
// If obtaining temperature or setting fan speed fails, appropriate error logs are recorded.
func (a *computeBladeAgent) runFanController(ctx context.Context, cancel context.CancelCauseFunc) {
log.FromContext(ctx).Info("Starting fan controller")
// Update fan speed periodically
ticker := time.NewTicker(5 * time.Second)
for {
// Wait for the next tick
select {
case <-ctx.Done():
ticker.Stop()
return ctx.Err()
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Fan Controller Failed")
cancel(err)
}
return
case <-ticker.C:
}
// Get temperature
temp, err := a.blade.GetTemperature()
if err != nil {
log.FromContext(ctx).Error("Failed to get temperature", zap.Error(err))
log.FromContext(ctx).WithError(err).Error("Failed to get temperature")
temp = 100 // set to a high value to trigger the maximum speed defined by the fan curve
}
// Derive fan speed from temperature
speed := a.fanController.GetFanSpeed(temp)
speed := a.fanController.GetFanSpeedPercent(temp)
// Set fan speed
if err := a.blade.SetFanSpeed(speed); err != nil {
log.FromContext(ctx).Error("Failed to set fan speed", zap.Error(err))
log.FromContext(ctx).WithError(err).Error("Failed to set fan speed")
}
}
}
// EmitEvent dispatches an event to the event handler
func (a *computeBladeAgentImpl) EmitEvent(ctx context.Context, event Event) error {
select {
case a.eventChan <- event:
return nil
case <-ctx.Done():
return ctx.Err()
// runEdgeButtonHandler initializes and handles edge button press events in a loop until the context is canceled.
// It waits for edge button presses and sends corresponding events to the event channel, logging errors and warnings.
// If an unrecoverable error occurs, the cancel function is triggered to terminate the operation.
func (a *computeBladeAgent) runEdgeButtonHandler(ctx context.Context, cancel context.CancelCauseFunc) {
log.FromContext(ctx).Info("Starting edge button event handler")
for {
if err := a.blade.WaitForEdgeButtonPress(ctx); err != nil {
if !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Edge button event handler failed")
cancel(err)
}
return
}
select {
case a.eventChan <- events.Event(events.EdgeButtonEvent):
default:
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
droppedEventCounter.WithLabelValues(events.Event(events.EdgeButtonEvent).String()).Inc()
}
}
}
// SetFanSpeed sets the fan speed
func (a *computeBladeAgentImpl) SetFanSpeed(_ context.Context, speed uint8) error {
if a.state.CriticalActive() {
return errors.New("cannot set fan speed while the blade is in a critical state")
// runEventHandler processes events from the agent's event channel, handles them, and cancels on critical failure or context cancellation.
func (a *computeBladeAgent) runEventHandler(ctx context.Context, cancel context.CancelCauseFunc) {
log.FromContext(ctx).Info("Starting event handler")
for {
select {
case <-ctx.Done():
return
case event := <-a.eventChan:
err := a.handleEvent(ctx, event)
if err != nil && !errors.Is(err, context.Canceled) {
log.FromContext(ctx).WithError(err).Error("Event handler failed")
cancel(err)
}
}
}
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: speed})
return nil
}
// SetStealthMode enables/disables the stealth mode
func (a *computeBladeAgentImpl) SetStealthMode(_ context.Context, enabled bool) error {
if a.state.CriticalActive() {
return errors.New("cannot set stealth mode while the blade is in a critical state")
// runGRpcApi starts the gRPC server for the agent based on the configuration and gracefully handles errors or cancellation.
func (a *computeBladeAgent) runGRpcApi(ctx context.Context, cancel context.CancelCauseFunc) {
if len(a.config.Listen.Grpc) == 0 {
err := humane.New("no listen address provided",
"ensure you are passing a valid listen config to the grpc server",
)
log.FromContext(ctx).Error("no listen address provided, not starting gRPC server", humane.Zap(err)...)
cancel(err)
}
return a.blade.SetStealthMode(enabled)
}
// WaitForIdentifyConfirm waits for the identify confirm event
func (a *computeBladeAgentImpl) WaitForIdentifyConfirm(ctx context.Context) error {
return a.state.WaitForIdentifyConfirm(ctx)
grpcListen, err := net.Listen(a.config.Listen.GrpcListenMode, a.config.Listen.Grpc)
if err != nil {
err := humane.Wrap(err, "failed to create grpc listener",
"ensure the gRPC server you are trying to serve to is not already running and the address is not bound by another process",
)
log.FromContext(ctx).Error("failed to create grpc listener, not starting gRPC server", humane.Zap(err)...)
cancel(err)
}
log.FromContext(ctx).Info("Starting grpc server", zap.String("address", a.config.Listen.Grpc))
if err := a.server.Serve(grpcListen); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
log.FromContext(ctx).Error("failed to start grpc server", humane.Zap(err)...)
cancel(err)
}
}

View File

@@ -1,65 +1,156 @@
package agent
package internal_agent
import (
"context"
"crypto/tls"
bladeapiv1alpha1 "github.com/uptime-induestries/compute-blade-agent/api/bladeapi/v1alpha1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
grpczap "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
"github.com/sierrasoftworks/humane-errors-go"
"github.com/spechtlabs/go-otel-utils/otelzap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/types/known/emptypb"
)
// ComputeBladeAgent implementing the BladeAgentServiceServer
type agentGrpcService struct {
bladeapiv1alpha1.UnimplementedBladeAgentServiceServer
// setupGrpcServer initializes and configures the gRPC server with authentication, logging, and server options.
func (a *computeBladeAgent) setupGrpcServer(ctx context.Context) error {
listenMode, err := ListenModeFromString(a.config.Listen.GrpcListenMode)
if err != nil {
return err
}
Agent ComputeBladeAgent
var grpcOpts []grpc.ServerOption
if listenMode == ModeTcp && a.config.Listen.GrpcAuthenticated {
tlsCfg, err := createServerTLSConfig(ctx)
if err != nil {
return err
}
grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(tlsCfg)))
if err := EnsureAuthenticatedBladectlConfig(ctx, a.config.Listen.Grpc, listenMode); err != nil {
return err
}
} else {
if err := EnsureUnauthenticatedBladectlConfig(ctx, a.config.Listen.Grpc, listenMode); err != nil {
return err
}
}
logger := log.InterceptorLogger(otelzap.L())
grpcOpts = append(grpcOpts,
grpc.ChainUnaryInterceptor(grpczap.UnaryServerInterceptor(logger)),
grpc.ChainStreamInterceptor(grpczap.StreamServerInterceptor(logger)),
)
a.server = grpc.NewServer(grpcOpts...)
return nil
}
// NewGrpcServiceFor creates a new gRPC service for a given agent
func NewGrpcServiceFor(agent ComputeBladeAgent) *agentGrpcService {
return &agentGrpcService{
Agent: agent,
// createServerTLSConfig creates and returns a TLS configuration for a server, enforcing client authentication.
// It generates or loads the necessary certificates and certificate pools, logging fatal errors if certificate loading fails.
func createServerTLSConfig(ctx context.Context) (*tls.Config, error) {
cert, certPool, err := EnsureServerCertificate(ctx)
if err != nil {
log.FromContext(ctx).WithError(err).Fatal("failed to load server key pair")
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
}, nil
}
// EmitEvent dispatches an event to the event handler
func (a *computeBladeAgent) EmitEvent(ctx context.Context, req *bladeapiv1alpha1.EmitEventRequest) (*emptypb.Empty, error) {
event, err := fromProto(req.GetEvent())
if err != nil {
return nil, err
}
select {
case a.eventChan <- event:
return &emptypb.Empty{}, nil
case <-ctx.Done():
return &emptypb.Empty{}, ctx.Err()
}
}
// EmitEvent emits an event to the agent runtime
func (service *agentGrpcService) EmitEvent(
ctx context.Context,
req *bladeapiv1alpha1.EmitEventRequest,
) (*emptypb.Empty, error) {
switch req.GetEvent() {
case bladeapiv1alpha1.Event_IDENTIFY:
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, IdentifyEvent)
case bladeapiv1alpha1.Event_IDENTIFY_CONFIRM:
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, IdentifyConfirmEvent)
case bladeapiv1alpha1.Event_CRITICAL:
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, CriticalEvent)
case bladeapiv1alpha1.Event_CRITICAL_RESET:
return &emptypb.Empty{}, service.Agent.EmitEvent(ctx, CriticalResetEvent)
default:
return &emptypb.Empty{}, status.Errorf(codes.InvalidArgument, "invalid event type")
// SetFanSpeed sets the fan speed
func (a *computeBladeAgent) SetFanSpeed(_ context.Context, req *bladeapiv1alpha1.SetFanSpeedRequest) (*emptypb.Empty, error) {
if a.state.CriticalActive() {
return &emptypb.Empty{}, humane.New("cannot set fan speed while the blade is in a critical state", "improve cooling on your blade before attempting to overwrite the fan speed")
}
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: uint8(req.GetPercent())})
return &emptypb.Empty{}, nil
}
func (service *agentGrpcService) WaitForIdentifyConfirm(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
return &emptypb.Empty{}, service.Agent.WaitForIdentifyConfirm(ctx)
// SetFanSpeedAuto sets the fan speed to automatic mode
func (a *computeBladeAgent) SetFanSpeedAuto(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
a.fanController.Override(nil)
return &emptypb.Empty{}, nil
}
// SetFanSpeed sets the fan speed of the blade
func (service *agentGrpcService) SetFanSpeed(
ctx context.Context,
req *bladeapiv1alpha1.SetFanSpeedRequest,
) (*emptypb.Empty, error) {
return &emptypb.Empty{}, service.Agent.SetFanSpeed(ctx, uint8(req.GetPercent()))
}
// SetStealthMode enables/disables stealth mode on the blade
func (service *agentGrpcService) SetStealthMode(ctx context.Context, req *bladeapiv1alpha1.StealthModeRequest) (*emptypb.Empty, error) {
return &emptypb.Empty{}, service.Agent.SetStealthMode(ctx, req.GetEnable())
// SetStealthMode enables/disables the stealth mode
func (a *computeBladeAgent) SetStealthMode(_ context.Context, req *bladeapiv1alpha1.StealthModeRequest) (*emptypb.Empty, error) {
if a.state.CriticalActive() {
return &emptypb.Empty{}, humane.New("cannot set stealth mode while the blade is in a critical state", "improve cooling on your blade before attempting to enable stealth mode again")
}
return &emptypb.Empty{}, a.blade.SetStealthMode(req.GetEnable())
}
// GetStatus aggregates the status of the blade
func (service *agentGrpcService) GetStatus(context.Context, *emptypb.Empty) (*bladeapiv1alpha1.StatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStatus not implemented")
func (a *computeBladeAgent) GetStatus(_ context.Context, _ *emptypb.Empty) (*bladeapiv1alpha1.StatusResponse, error) {
rpm, err := a.blade.GetFanRPM()
if err != nil {
return nil, err
}
temp, err := a.blade.GetTemperature()
if err != nil {
return nil, err
}
powerStatus, err := a.blade.GetPowerStatus()
if err != nil {
return nil, err
}
steps := a.fanController.Steps()
fanCurveSteps := make([]*bladeapiv1alpha1.FanCurveStep, len(steps))
for idx, step := range steps {
fanCurveSteps[idx] = &bladeapiv1alpha1.FanCurveStep{
Temperature: int64(step.Temperature),
Percent: uint32(step.Percent),
}
}
versionInfo := &bladeapiv1alpha1.VersionInfo{
Version: a.agentInfo.Version,
Commit: a.agentInfo.Commit,
Date: a.agentInfo.BuildTime.Unix(),
}
return &bladeapiv1alpha1.StatusResponse{
StealthMode: a.blade.StealthModeActive(),
IdentifyActive: a.state.IdentifyActive(),
CriticalActive: a.state.CriticalActive(),
Temperature: int64(temp),
FanRpm: int64(rpm),
FanPercent: uint32(a.fanController.GetFanSpeedPercent(temp)),
FanSpeedAutomatic: a.fanController.IsAutomaticSpeed(),
PowerStatus: bladeapiv1alpha1.PowerStatus(powerStatus),
FanCurveSteps: fanCurveSteps,
CriticalTemperatureThreshold: int64(a.config.CriticalTemperatureThreshold),
Version: versionInfo,
}, nil
}
// WaitForIdentifyConfirm blocks until the identify confirmation process is completed or an error occurs.
func (a *computeBladeAgent) WaitForIdentifyConfirm(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
return &emptypb.Empty{}, a.state.WaitForIdentifyConfirm(ctx)
}

View File

@@ -0,0 +1,348 @@
package internal_agent
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"net"
"os"
"path/filepath"
"time"
"github.com/compute-blade-community/compute-blade-agent/cmd/bladectl/config"
"github.com/compute-blade-community/compute-blade-agent/pkg/certificate"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
"github.com/sierrasoftworks/humane-errors-go"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)
const certDir = "/etc/compute-blade-agent"
var (
caPath = filepath.Join(certDir, "ca.pem")
caKeyPath = filepath.Join(certDir, "ca-key.pem")
serverCertPath = filepath.Join(certDir, "server.pem")
serverKeyPath = filepath.Join(certDir, "server-key.pem")
)
// GenerateClientCert creates a client certificate signed by a CA with the specified common name.
// It validates the CA certificate and private key before generating the client certificate.
// Returns CA certificate, client certificate, private key in PEM format, and any error encountered.
func GenerateClientCert(commonName string) (caPEM, certPEM, keyPEM []byte, herr humane.Error) {
caCert, caKey, herr := certificate.LoadAndValidateCertificate(caPath, caKeyPath)
if herr != nil {
return nil, nil, nil, humane.Wrap(herr, "No valid CA found to sign the client certificate")
}
certDER, keyDER, herr := certificate.GenerateCertificate(
commonName,
certificate.WithClientUsage(),
certificate.WithCaCert(caCert),
certificate.WithCaKey(caKey),
)
if herr != nil {
return nil, nil, nil, humane.Wrap(herr, "failed to generate client certificate")
}
// Load CA PEM
caPEM, err := os.ReadFile(caPath)
if err != nil {
return nil, nil, nil, humane.Wrap(err, "failed to read CA",
fmt.Sprintf("ensure the certificate file %s exists and is readable by the agent user", caPath),
)
}
// Convert DER to PEM
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
return caPEM, certPEM, keyPEM, nil
}
func EnsureAuthenticatedBladectlConfig(ctx context.Context, serverAddr string, serverMode ListenMode) humane.Error {
configDir, herr := config.EnsureBladectlConfigHome()
if herr != nil {
return herr
}
configPath := filepath.Join(configDir, "config.yaml")
if util.FileExists(configPath) {
// Load and decode bladectl config
configBytes, err := os.ReadFile(configPath)
if err != nil {
return humane.Wrap(err, "failed to read bladectl config",
fmt.Sprintf("ensure the config file %s exists and is readable by the agent user", configPath),
)
}
var bladectlConfig config.BladectlConfig
if err := yaml.Unmarshal(configBytes, &bladectlConfig); err != nil {
return humane.Wrap(err, "failed to parse bladectl config",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
"ensure your config file is valid YAML",
)
}
blade, herr := bladectlConfig.FindBlade("")
if herr != nil {
return herr
}
certPEM, err := base64.StdEncoding.DecodeString(blade.Blade.Certificate.ClientCertificateData)
if err != nil {
return humane.Wrap(err, fmt.Sprintf("failed to decode client certificate data for blade %s", blade.Name),
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
"ensure your config file is valid YAML",
)
}
keyPEM, err := base64.StdEncoding.DecodeString(blade.Blade.Certificate.ClientKeyData)
if err != nil {
return humane.Wrap(err, fmt.Sprintf("failed to decode client certificate key data for blade %s", blade.Name),
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
"ensure your config file is valid YAML",
)
}
if _, _, err := certificate.ValidateCertificate(certPEM, keyPEM); err != nil {
return err
}
return nil
}
// Generate localhost keys
log.FromContext(ctx).Debug("Generating new local client certificate...")
caPEM, clientCertDER, clientKeyDER, herr := GenerateClientCert("localhost")
if herr != nil {
return herr
}
if serverMode == ModeTcp {
_, grpcApiPort, err := net.SplitHostPort(serverAddr)
if err != nil {
return humane.Wrap(err, "failed to extract port from gRPC address",
"check your gRPC address is correct in your agent config",
)
}
serverAddr = fmt.Sprintf("localhost:%s", grpcApiPort)
}
bladectlConfig := config.NewAuthenticatedBladectlConfig(serverAddr, caPEM, clientCertDER, clientKeyDER)
data, err := yaml.Marshal(&bladectlConfig)
if err != nil {
return humane.Wrap(err, "Failed to marshal YAML config",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
if err := os.WriteFile(configPath, data, 0600); err != nil {
return humane.Wrap(err, "Failed to write bladectl config file",
"ensure the home-directory is writable by the agent user",
)
}
log.FromContext(ctx).Info("Generated new local bladectl config",
zap.String("path", configPath),
zap.String("server", serverAddr),
zap.Bool("authenticated", true),
)
return nil
}
func EnsureUnauthenticatedBladectlConfig(ctx context.Context, serverAddr string, serverMode ListenMode) humane.Error {
configDir, herr := config.EnsureBladectlConfigHome()
if herr != nil {
return herr
}
configPath := filepath.Join(configDir, "config.yaml")
if util.FileExists(configPath) {
return nil
}
// Generate localhost keys
log.FromContext(ctx).Debug("Generating new local bladectl config...")
if serverMode == ModeTcp {
_, grpcApiPort, err := net.SplitHostPort(serverAddr)
if err != nil {
return humane.Wrap(err, "failed to extract port from gRPC address",
"check your gRPC address is correct in your agent config",
)
}
serverAddr = fmt.Sprintf("localhost:%s", grpcApiPort)
}
bladectlConfig := config.NewBladectlConfig(serverAddr)
data, err := yaml.Marshal(&bladectlConfig)
if err != nil {
return humane.Wrap(err, "Failed to marshal YAML config",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
if err := os.WriteFile(configPath, data, 0600); err != nil {
return humane.Wrap(err, "Failed to write bladectl config file",
"ensure the home-directory is writable by the agent user",
)
}
log.FromContext(ctx).Info("Generated new local bladectl config",
zap.String("path", configPath),
zap.String("server", serverAddr),
zap.Bool("authenticated", false),
)
return nil
}
// EnsureServerCertificate ensures the presence of a valid server certificate and CA, generating them if necessary.
func EnsureServerCertificate(ctx context.Context) (tls.Certificate, *x509.CertPool, humane.Error) {
// If Keys already exist, there is nothing to do :)
if util.FileExists(caPath) && util.FileExists(caKeyPath) && util.FileExists(serverCertPath) && util.FileExists(serverKeyPath) {
if _, _, err := certificate.LoadAndValidateCertificate(caPath, caKeyPath); err != nil {
return tls.Certificate{}, nil, err
}
cert, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
if err != nil {
return tls.Certificate{}, nil, humane.Wrap(err, "failed to load existing server cert",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
pool, herr := certificate.GetCertPoolFrom(caPath)
if herr != nil {
return tls.Certificate{}, nil, herr
}
return cert, pool, nil
}
// We need a CA
if err := ensureCA(ctx); err != nil {
return tls.Certificate{}, nil, err
}
// But more importantly: a valid CA
caCert, caKey, herr := certificate.LoadAndValidateCertificate(caPath, caKeyPath)
if herr != nil {
return tls.Certificate{}, nil, herr
}
// Generate Server Keys
log.FromContext(ctx).Debug("Generating new server certificate...")
serverCertDER, serverKeyDER, herr := certificate.GenerateCertificate(
"Compute Blade Agent",
certificate.WithServerUsage(),
certificate.WithCaCert(caCert),
certificate.WithCaKey(caKey),
)
if herr != nil {
return tls.Certificate{}, nil, herr
}
if err := certificate.WriteCertificate(serverCertPath, serverKeyPath, serverCertDER, serverKeyDER); err != nil {
return tls.Certificate{}, nil, err
}
log.FromContext(ctx).Info("Generated new server certificates",
zap.String("cert", serverCertPath),
zap.String("key", serverKeyPath),
zap.String("ca", caPath),
)
cert, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
if err != nil {
return tls.Certificate{}, nil, humane.Wrap(err, "failed to parse generated server certificate",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
pool, herr := certificate.GetCertPoolFrom(caPath)
if herr != nil {
return tls.Certificate{}, nil, herr
}
return cert, pool, nil
}
// ensureCA ensures that a valid Certificate Authority (CA) certificate and private key exist or generates new ones.
func ensureCA(ctx context.Context) humane.Error {
if util.FileExists(caPath) && util.FileExists(caKeyPath) {
_, _, err := certificate.LoadAndValidateCertificate(caPath, caKeyPath)
if err != nil {
return err
}
return nil
}
log.FromContext(ctx).Info("Generating new CA for compute-blade-agent")
caKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return humane.Wrap(err, "failed to generate CA key")
}
caTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{Organization: []string{"Compute Blade CA"}, CommonName: "Compute Blade Agent Root CA"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
IsCA: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
if err != nil {
return humane.Wrap(err, "failed to create CA certificate",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
caKeyBytes, err := x509.MarshalECPrivateKey(caKey)
if err != nil {
return humane.Wrap(err, "failed to marshal CA private key",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
if err := os.MkdirAll(certDir, 0600); err != nil {
return humane.Wrap(err, "failed to create cert directory",
"ensure the directory you are trying to create exists and is writable by the agent user",
)
}
if err := certificate.WriteCertificate(caPath, caKeyPath, caCertDER, caKeyBytes); err != nil {
return err
}
return nil
}

104
internal/agent/handler.go Normal file
View File

@@ -0,0 +1,104 @@
package internal_agent
import (
"context"
"errors"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/ledengine"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"go.uber.org/zap"
)
// handleEvent processes an incoming event, updates state, and dispatches it to the appropriate handler based on the event type.
func (a *computeBladeAgent) handleEvent(ctx context.Context, event events.Event) error {
log.FromContext(ctx).Info("Handling event", zap.String("event", event.String()))
eventCounter.WithLabelValues(event.String()).Inc()
// register event in state
a.state.RegisterEvent(event)
// Dispatch incoming events to the right handler(s)
switch event {
case events.CriticalEvent:
// Handle critical event
return a.handleCriticalActive(ctx)
case events.CriticalResetEvent:
// Handle critical event
return a.handleCriticalReset(ctx)
case events.IdentifyEvent:
// Handle identify event
return a.handleIdentifyActive(ctx)
case events.IdentifyConfirmEvent:
// Handle identify event
return a.handleIdentifyConfirm(ctx)
case events.EdgeButtonEvent:
// Handle edge button press to toggle identify mode
event := events.Event(events.IdentifyEvent)
if a.state.IdentifyActive() {
event = events.Event(events.IdentifyConfirmEvent)
}
select {
case a.eventChan <- event:
default:
log.FromContext(ctx).Warn("Edge button press event dropped due to backlog")
droppedEventCounter.WithLabelValues(event.String()).Inc()
}
case events.NoopEvent:
}
return nil
}
// handleIdentifyActive is responsible for handling the identify event by setting a burst LED pattern based on the configuration.
func (a *computeBladeAgent) handleIdentifyActive(ctx context.Context) error {
log.FromContext(ctx).Info("Identify active")
return a.edgeLedEngine.SetPattern(ledengine.NewBurstPattern(led.Color{}, a.config.IdentifyLedColor))
}
// handleIdentifyConfirm handles the confirmation of an identify event by updating the LED engine with a static idle pattern.
func (a *computeBladeAgent) handleIdentifyConfirm(ctx context.Context) error {
log.FromContext(ctx).Info("Identify confirmed/cleared")
return a.edgeLedEngine.SetPattern(ledengine.NewStaticPattern(a.config.IdleLedColor))
}
// handleCriticalActive handles the system's response to a critical state by adjusting fan speed and LED indications.
// It sets the fan speed to 100%, disables stealth mode, and applies a critical LED pattern.
// Returns any errors encountered during the process as a combined error.
func (a *computeBladeAgent) handleCriticalActive(ctx context.Context) error {
log.FromContext(ctx).Warn("Blade in critical state, setting fan speed to 100% and turning on LEDs")
// Set fan speed to 100%
a.fanController.Override(&fancontroller.FanOverrideOpts{Percent: 100})
// Disable stealth mode (turn on LEDs)
setStealthModeError := a.blade.SetStealthMode(false)
// Set critical pattern for top LED
setPatternTopLedErr := a.topLedEngine.SetPattern(
ledengine.NewSlowBlinkPattern(led.Color{}, a.config.CriticalLedColor),
)
// Combine errors, but don't stop execution flow for now
return errors.Join(setStealthModeError, setPatternTopLedErr)
}
// handleCriticalReset handles the reset of a critical state by restoring default hardware settings for fans and LEDs.
func (a *computeBladeAgent) handleCriticalReset(ctx context.Context) error {
log.FromContext(ctx).Info("Critical state cleared, setting fan speed to default and restoring LEDs to default state")
// Reset fan controller overrides
a.fanController.Override(nil)
// Reset stealth mode
if err := a.blade.SetStealthMode(a.config.StealthModeEnabled); err != nil {
return err
}
// Set top LED off
if err := a.topLedEngine.SetPattern(ledengine.NewStaticPattern(led.Color{})); err != nil {
return err
}
return nil
}

30
internal/agent/options.go Normal file
View File

@@ -0,0 +1,30 @@
package internal_agent
import (
"github.com/sierrasoftworks/humane-errors-go"
)
type ListenMode string
const (
ModeTcp ListenMode = "tcp"
ModeUnix ListenMode = "unix"
)
func ListenModeFromString(s string) (ListenMode, humane.Error) {
switch s {
case string(ModeTcp):
return ModeTcp, nil
case string(ModeUnix):
return ModeUnix, nil
default:
return "", humane.New("invalid listen mode",
"ensure you are passing a valid listen mode to the grpc server",
"valid modes are: [tcp, unix]",
)
}
}
func (l ListenMode) String() string {
return string(l)
}

25
internal/agent/utils.go Normal file
View File

@@ -0,0 +1,25 @@
package internal_agent
import (
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// fromProto converts a `bladeapiv1alpha1.Event` into a corresponding `events.Event` type.
// Returns an error if the event type is invalid.
func fromProto(event bladeapiv1alpha1.Event) (events.Event, error) {
switch event {
case bladeapiv1alpha1.Event_IDENTIFY:
return events.IdentifyEvent, nil
case bladeapiv1alpha1.Event_IDENTIFY_CONFIRM:
return events.IdentifyConfirmEvent, nil
case bladeapiv1alpha1.Event_CRITICAL:
return events.CriticalEvent, nil
case bladeapiv1alpha1.Event_CRITICAL_RESET:
return events.CriticalResetEvent, nil
default:
return events.NoopEvent, status.Errorf(codes.InvalidArgument, "invalid event type")
}
}

19
pkg/agent/agent.go Normal file
View File

@@ -0,0 +1,19 @@
package agent
import (
"context"
bladeapiv1alpha1 "github.com/compute-blade-community/compute-blade-agent/api/bladeapi/v1alpha1"
)
// ComputeBladeAgent implements the core-logic of the agent. It is responsible for handling events and interfacing with the hardware.
// any ComputeBladeAgent must also be a bladeapiv1alpha1.BladeAgentServiceServer to handle the gRPC API requests.
type ComputeBladeAgent interface {
bladeapiv1alpha1.BladeAgentServiceServer
// RunAsync dispatches the agent until the context is canceled or an error occurs
RunAsync(ctx context.Context, cancel context.CancelCauseFunc)
// Run dispatches the agent and blocks until the context is canceled or an error occurs
Run(ctx context.Context) error
// GracefulStop gracefully stops the gRPC server, ensuring all in-progress RPCs are completed before shutting down.
GracefulStop(ctx context.Context) error
}

62
pkg/agent/config.go Normal file
View File

@@ -0,0 +1,62 @@
package agent
import (
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
)
type LogConfiguration struct {
Mode string `mapstructure:"mode"`
}
type ApiConfig struct {
Metrics string `mapstructure:"metrics"`
Grpc string `mapstructure:"grpc"`
GrpcAuthenticated bool `mapstructure:"authenticated"`
GrpcListenMode string `mapstructure:"mode"`
}
type ComputeBladeAgentConfig struct {
// Log is the logging configuration
Log LogConfiguration `mapstructure:"log"`
// Listen is the listen configuration for the server
Listen ApiConfig `mapstructure:"listen"`
// Hal is the hardware abstraction layer configuration
Hal hal.Config `mapstructure:"hal"`
// IdleLedColor is the color of the edge LED when the blade is idle mode
IdleLedColor led.Color `mapstructure:"idle_led_color"`
// IdentifyLedColor is the color of the edge LED when the blade is in identify mode
IdentifyLedColor led.Color `mapstructure:"identify_led_color"`
// CriticalLedColor is the color of the top(!) LED when the blade is in critical mode.
// In the circumstance when >1 blades are in critical mode, the identify function can be used to find the right blade
CriticalLedColor led.Color `mapstructure:"critical_led_color"`
// StealthModeEnabled indicates whether stealth mode is enabled
StealthModeEnabled bool `mapstructure:"stealth_mode"`
// Critical temperature of the compute blade (used to trigger critical mode)
CriticalTemperatureThreshold uint `mapstructure:"critical_temperature_threshold"`
// FanSpeed allows to set a fixed fan speed (in percent)
FanSpeed *fancontroller.FanOverrideOpts `mapstructure:"fan_speed"`
// FanControllerConfig is the configuration of the fan controller
FanControllerConfig fancontroller.Config `mapstructure:"fan_controller"`
ComputeBladeHalOpts hal.ComputeBladeHalOpts `mapstructure:"hal"`
}
// ComputeBladeAgentInfo represents metadata information about a compute blade agent, including version, commit, and build time.
type ComputeBladeAgentInfo struct {
Version string
Commit string
BuildTime time.Time
}

View File

@@ -4,8 +4,11 @@ import (
"context"
"sync"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/spechtlabs/go-otel-utils/otelzap"
"go.uber.org/zap"
)
var (
@@ -17,7 +20,7 @@ var (
)
type ComputebladeState interface {
RegisterEvent(event Event)
RegisterEvent(event events.Event)
IdentifyActive() bool
WaitForIdentifyConfirm(ctx context.Context) error
CriticalActive() bool
@@ -28,10 +31,10 @@ type computebladeStateImpl struct {
mutex sync.Mutex
// identifyActive indicates whether the blade is currently in identify mode
identifyActive bool
identifyActive bool
identifyConfirmChan chan struct{}
// criticalActive indicates whether the blade is currently in critical mode
criticalActive bool
criticalActive bool
criticalConfirmChan chan struct{}
}
@@ -42,23 +45,26 @@ func NewComputeBladeState() ComputebladeState {
}
}
func (s *computebladeStateImpl) RegisterEvent(event Event) {
func (s *computebladeStateImpl) RegisterEvent(event events.Event) {
s.mutex.Lock()
defer s.mutex.Unlock()
switch event {
case IdentifyEvent:
case events.IdentifyEvent:
s.identifyActive = true
case IdentifyConfirmEvent:
case events.IdentifyConfirmEvent:
s.identifyActive = false
close(s.identifyConfirmChan)
s.identifyConfirmChan = make(chan struct{})
case CriticalEvent:
case events.CriticalEvent:
s.criticalActive = true
s.identifyActive = false
case CriticalResetEvent:
case events.CriticalResetEvent:
s.criticalActive = false
close(s.criticalConfirmChan)
s.criticalConfirmChan = make(chan struct{})
default:
otelzap.L().Warn("Unknown event", zap.String("event", event.String()))
}
// Set identify state metric

View File

@@ -6,8 +6,9 @@ import (
"testing"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/agent"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/stretchr/testify/assert"
"github.com/uptime-induestries/compute-blade-agent/internal/agent"
)
func TestNewComputeBladeState(t *testing.T) {
@@ -23,9 +24,9 @@ func TestComputeBladeState_RegisterEventIdentify(t *testing.T) {
state := agent.NewComputeBladeState()
// Identify event
state.RegisterEvent(agent.IdentifyEvent)
state.RegisterEvent(events.IdentifyEvent)
assert.True(t, state.IdentifyActive())
state.RegisterEvent(agent.IdentifyConfirmEvent)
state.RegisterEvent(events.IdentifyConfirmEvent)
assert.False(t, state.IdentifyActive())
}
@@ -35,9 +36,9 @@ func TestComputeBladeState_RegisterEventCritical(t *testing.T) {
state := agent.NewComputeBladeState()
// critical event
state.RegisterEvent(agent.CriticalEvent)
state.RegisterEvent(events.CriticalEvent)
assert.True(t, state.CriticalActive())
state.RegisterEvent(agent.CriticalResetEvent)
state.RegisterEvent(events.CriticalResetEvent)
assert.False(t, state.CriticalActive())
}
@@ -47,15 +48,15 @@ func TestComputeBladeState_RegisterEventMixed(t *testing.T) {
state := agent.NewComputeBladeState()
// Send a bunch of events
state.RegisterEvent(agent.CriticalEvent)
state.RegisterEvent(agent.CriticalResetEvent)
state.RegisterEvent(agent.NoopEvent)
state.RegisterEvent(agent.CriticalEvent)
state.RegisterEvent(agent.NoopEvent)
state.RegisterEvent(agent.IdentifyEvent)
state.RegisterEvent(agent.IdentifyEvent)
state.RegisterEvent(agent.CriticalResetEvent)
state.RegisterEvent(agent.IdentifyEvent)
state.RegisterEvent(events.CriticalEvent)
state.RegisterEvent(events.CriticalResetEvent)
state.RegisterEvent(events.NoopEvent)
state.RegisterEvent(events.CriticalEvent)
state.RegisterEvent(events.NoopEvent)
state.RegisterEvent(events.IdentifyEvent)
state.RegisterEvent(events.IdentifyEvent)
state.RegisterEvent(events.CriticalResetEvent)
state.RegisterEvent(events.IdentifyEvent)
assert.False(t, state.CriticalActive())
assert.True(t, state.IdentifyActive())
@@ -68,7 +69,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_NoTimeout(t *testing.T) {
// send identify event
t.Log("Setting identify event")
state.RegisterEvent(agent.IdentifyEvent)
state.RegisterEvent(events.IdentifyEvent)
assert.True(t, state.IdentifyActive())
var wg sync.WaitGroup
@@ -87,7 +88,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_NoTimeout(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// confirm event
state.RegisterEvent(agent.IdentifyConfirmEvent)
state.RegisterEvent(events.IdentifyConfirmEvent)
t.Log("Identify event confirmed")
wg.Wait()
@@ -100,7 +101,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_Timeout(t *testing.T) {
// send identify event
t.Log("Setting identify event")
state.RegisterEvent(agent.IdentifyEvent)
state.RegisterEvent(events.IdentifyEvent)
assert.True(t, state.IdentifyActive())
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
@@ -120,7 +121,7 @@ func TestComputeBladeState_WaitForIdentifyConfirm_Timeout(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// confirm event
state.RegisterEvent(agent.IdentifyConfirmEvent)
state.RegisterEvent(events.IdentifyConfirmEvent)
t.Log("Identify event confirmed")
wg.Wait()
@@ -133,7 +134,7 @@ func TestComputeBladeState_WaitForCriticalClear_NoTimeout(t *testing.T) {
// send critical event
t.Log("Setting critical event")
state.RegisterEvent(agent.CriticalEvent)
state.RegisterEvent(events.CriticalEvent)
assert.True(t, state.CriticalActive())
var wg sync.WaitGroup
@@ -152,7 +153,7 @@ func TestComputeBladeState_WaitForCriticalClear_NoTimeout(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// confirm event
state.RegisterEvent(agent.CriticalResetEvent)
state.RegisterEvent(events.CriticalResetEvent)
t.Log("critical event confirmed")
wg.Wait()
@@ -165,7 +166,7 @@ func TestComputeBladeState_WaitForCriticalClear_Timeout(t *testing.T) {
// send critical event
t.Log("Setting critical event")
state.RegisterEvent(agent.CriticalEvent)
state.RegisterEvent(events.CriticalEvent)
assert.True(t, state.CriticalActive())
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
@@ -185,7 +186,7 @@ func TestComputeBladeState_WaitForCriticalClear_Timeout(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// confirm event
state.RegisterEvent(agent.CriticalResetEvent)
state.RegisterEvent(events.CriticalResetEvent)
t.Log("critical event confirmed")
wg.Wait()

View File

@@ -0,0 +1,237 @@
package certificate
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"os"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
"github.com/sierrasoftworks/humane-errors-go"
)
// LoadAndValidateCertificate loads and validates a certificate and its private key from the provided file paths.
// It reads, decodes, and parses the certificate and private key, ensuring the public key matches the private key.
// Returns the parsed X.509 certificate, ECDSA private key, and a humane.Error if any error occurs during processing.
func LoadAndValidateCertificate(certPath, keyPath string) (cert *x509.Certificate, key *ecdsa.PrivateKey, herr humane.Error) {
// Load and decode CA cert
certPEM, err := os.ReadFile(certPath)
if err != nil {
return nil, nil, humane.Wrap(err, "failed to read certificate",
fmt.Sprintf("ensure the certificate file %s exists and is readable by the agent user", certPath),
)
}
// Load and decode CA key
keyPEM, err := os.ReadFile(keyPath)
if err != nil {
return nil, nil, humane.Wrap(err, "failed to read private key",
fmt.Sprintf("ensure the key file %s exists and is readable by the agent user", keyPath),
)
}
return ValidateCertificate(certPEM, keyPEM)
}
// ValidateCertificate validates a PEM-encoded certificate and private key, ensuring the private key matches the certificate.
// Returns a parsed *x509.Certificate, *ecdsa.PrivateKey, or a humane.Error if any issue occurs during validation or parsing.
func ValidateCertificate(certPEM []byte, keyPEM []byte) (cert *x509.Certificate, key *ecdsa.PrivateKey, herr humane.Error) {
certBlock, _ := pem.Decode(certPEM)
if certBlock == nil {
return nil, nil, humane.New("failed to decode certificate",
"Verify if the certificate is valid by run the following command:",
"openssl x509 -in /path/to/certificate.pem -text -noout",
)
}
cert, err := x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return nil, nil, humane.New("failed to parse certificate",
"Verify if the certificate is valid by run the following command:",
"openssl x509 -in /path/to/certificate.pem -text -noout",
)
}
keyBlock, _ := pem.Decode(keyPEM)
if keyBlock == nil {
return nil, nil, humane.New("failed to decode certificate",
"Verify if the key-file is valid by run the following command:",
"openssl ec -in /path/to/keyfile.pem -check",
)
}
key, err = x509.ParseECPrivateKey(keyBlock.Bytes)
if err != nil {
return nil, nil, humane.Wrap(err, "failed to parse private key",
"Verify if the key-file is valid by run the following command:",
"openssl ec -in /path/to/keyfile.pem -check",
)
}
// Compare public keys
certPub, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok || certPub.X.Cmp(key.X) != 0 || certPub.Y.Cmp(key.Y) != 0 {
return nil, nil, humane.New("private key does not match certificate",
"Verify the certificate and private key match.",
"To verify on the CLI, use:",
fmt.Sprintf("cmp <(openssl x509 -in %s -pubkey -noout -outform PEM) <(openssl ec -in %s -pubout -outform PEM) && echo \"✅ Certificate and key match\" || echo \"❌ Mismatch\"",
"/path/to/certificate.pem",
"/path/to/keyfile.pem",
),
)
}
return cert, key, nil
}
// GenerateCertificate generates a certificate and private key based on provided options and outputs them in DER format.
// It supports client and server certificates, returning the certificate, private key, and an error if generation fails.
func GenerateCertificate(commonName string, opts ...Option) (certDER, keyDER []byte, herr humane.Error) {
options := &options{
Usage: UsageClient,
CaCert: nil,
CaKey: nil,
}
for _, opt := range opts {
opt(options)
}
hostname, err := os.Hostname()
if err != nil {
return nil, nil, humane.Wrap(err, "failed to extract hostname",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
var extKeyUsage []x509.ExtKeyUsage
var hostIps []net.IP
// If we generate server certificates
switch options.Usage {
case UsageClient:
extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
case UsageServer:
// make sure to use the correct key-usage
extKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
// And add all the host-ips
if hostIps, err = util.GetHostIPs(); err != nil {
return nil, nil, humane.Wrap(err, "failed to extract server IPs",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
default:
return nil, nil, humane.New(fmt.Sprintf("invalid certificate usage %s", options.Usage.String()),
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: extKeyUsage,
DNSNames: []string{"localhost", hostname, fmt.Sprintf("%s.local", hostname)},
IPAddresses: hostIps,
}
clientKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return nil, nil, humane.Wrap(err, "failed to generate client key",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
// prevent nil pointer exceptions by using the cert key as signing key and generate a
// self-signed certificate, if no CA is provided
signingCert := certTemplate
signingKey := clientKey
if options.CaCert != nil && options.CaKey != nil {
signingCert = options.CaCert
signingKey = options.CaKey
}
certDER, err = x509.CreateCertificate(rand.Reader, certTemplate, signingCert, &clientKey.PublicKey, signingKey)
if err != nil {
return nil, nil, humane.Wrap(err, "failed to create client certificate",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
clientKeyBytes, err := x509.MarshalECPrivateKey(clientKey)
if err != nil {
return nil, nil, humane.Wrap(err, "failed to marshal client private key",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
)
}
return certDER, clientKeyBytes, nil
}
// WriteCertificate writes a certificate and its private key to the specified file paths in PEM format.
// certPath specifies the file path to write the certificate PEM data.
// keyPath specifies the file path to write the private key PEM data.
// certDataDER is the DER-encoded certificate data to be written.
// keyDataDER is the DER-encoded private key data to be written.
// Returns a humane.Error if writing to the files fails.
func WriteCertificate(certPath, keyPath string, certDataDER []byte, keyDataDER []byte) humane.Error {
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDataDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDataDER})
if err := os.WriteFile(certPath, certPEM, 0600); err != nil {
return humane.Wrap(err, "failed to write certificate file",
"ensure the directory you are trying to create exists and is writable by the agent user",
)
}
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
return humane.Wrap(err, "failed to write key file",
"ensure the directory you are trying to create exists and is writable by the agent user",
)
}
return nil
}
// GetCertPoolFrom reads a CA certificate from a given path and initializes a x509.CertPool with its contents.
// Returns the initialized certificate pool or a descriptive error if reading or appending the certificate fails.
func GetCertPoolFrom(caPath string) (pool *x509.CertPool, herr humane.Error) {
caCert, err := os.ReadFile(caPath)
if err != nil {
return nil, humane.Wrap(err, "failed to read CA certificate",
"ensure the directory you are trying to create exists and is writable by the agent user",
)
}
pool = x509.NewCertPool()
if !pool.AppendCertsFromPEM(caCert) {
return nil, humane.New("failed to append CA certificate to pool",
"this should never happen",
"please report this as a bug to https://github.com/compute-blade-community/compute-blade-agent/issues",
"Verify if the CA certificate is valid by run the following command:",
fmt.Sprintf("openssl x509 -in %s -text -noout", caPath),
)
}
return pool, nil
}

View File

@@ -0,0 +1,40 @@
package certificate
import (
"crypto/ecdsa"
"crypto/x509"
)
type options struct {
CaCert *x509.Certificate
CaKey *ecdsa.PrivateKey
Usage Usage
}
type Option func(*options)
func WithUsage(usage Usage) Option {
return func(o *options) {
o.Usage = usage
}
}
func WithClientUsage() Option {
return WithUsage(UsageClient)
}
func WithServerUsage() Option {
return WithUsage(UsageServer)
}
func WithCaCert(cert *x509.Certificate) Option {
return func(o *options) {
o.CaCert = cert
}
}
func WithCaKey(key *ecdsa.PrivateKey) Option {
return func(o *options) {
o.CaKey = key
}
}

45
pkg/certificate/types.go Normal file
View File

@@ -0,0 +1,45 @@
package certificate
import "fmt"
// Usage defines the intended purpose of a certificate, such as client or server usage.
type Usage int
func (c Usage) String() string {
switch c {
case UsageClient:
return "client"
case UsageServer:
return "server"
default:
return fmt.Sprintf("CertificateUsage(%d)", c)
}
}
const (
UsageClient Usage = iota // Certificate is for Client
UsageServer // Certificate is for Server
)
// Format represents the encoding format of a certificate, such as PEM or DER.
type Format int
func (c Format) String() string {
switch c {
case FormatPEM:
return "pem"
case FormatDER:
return "der"
default:
return fmt.Sprintf("CertificateFormat(%d)", c)
}
}
const (
FormatPEM Format = iota // PEM Encoded Certificate
FormatDER // DER Encoded Certificate
)

31
pkg/events/event.go Normal file
View File

@@ -0,0 +1,31 @@
package events
type Event int
const (
NoopEvent = iota
IdentifyEvent
IdentifyConfirmEvent
CriticalEvent
CriticalResetEvent
EdgeButtonEvent
)
func (e Event) String() string {
switch e {
case NoopEvent:
return "noop"
case IdentifyEvent:
return "identify"
case IdentifyConfirmEvent:
return "identify_confirm"
case CriticalEvent:
return "critical"
case CriticalResetEvent:
return "critical_reset"
case EdgeButtonEvent:
return "edge_button"
default:
return "unknown"
}
}

View File

@@ -1,4 +1,4 @@
package eventbus
package events
import (
"sync"
@@ -27,7 +27,7 @@ type subscriber struct {
closed bool
}
func MatchAll(any) bool {
func MatchAll(_ any) bool {
return true
}

View File

@@ -1,17 +1,17 @@
package eventbus_test
package events_test
import (
"testing"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/stretchr/testify/assert"
"github.com/uptime-induestries/compute-blade-agent/pkg/eventbus"
)
func TestEventBusManySubscribers(t *testing.T) {
eb := eventbus.New()
eb := events.New()
// Create a channel and subscribe to a topic without a filter
sub0 := eb.Subscribe("topic0", 2, eventbus.MatchAll)
sub0 := eb.Subscribe("topic0", 2, events.MatchAll)
assert.Equal(t, cap(sub0.C()), 2)
assert.Equal(t, len(sub0.C()), 0)
defer sub0.Unsubscribe()
@@ -25,12 +25,12 @@ func TestEventBusManySubscribers(t *testing.T) {
defer sub1.Unsubscribe()
// Create a channel and subscribe to another topic
sub2 := eb.Subscribe("topic1", 1, eventbus.MatchAll)
sub2 := eb.Subscribe("topic1", 1, events.MatchAll)
assert.Equal(t, cap(sub2.C()), 1)
assert.Equal(t, len(sub2.C()), 0)
defer sub2.Unsubscribe()
sub3 := eb.Subscribe("topic1", 0, eventbus.MatchAll)
sub3 := eb.Subscribe("topic1", 0, events.MatchAll)
assert.Equal(t, cap(sub3.C()), 0)
assert.Equal(t, len(sub3.C()), 0)
defer sub3.Unsubscribe()
@@ -56,10 +56,10 @@ func TestEventBusManySubscribers(t *testing.T) {
}
func TestUnsubscribe(t *testing.T) {
eb := eventbus.New()
eb := events.New()
// Create a channel and subscribe to a topic
sub := eb.Subscribe("topic", 2, eventbus.MatchAll)
sub := eb.Subscribe("topic", 2, events.MatchAll)
// Unsubscribe from the topic
sub.Unsubscribe()

View File

@@ -0,0 +1,18 @@
package fancontroller
type FanOverrideOpts struct {
Percent uint8 `mapstructure:"speed"`
}
type Step struct {
// Temperature is the temperature to react to
Temperature float64 `mapstructure:"temperature"`
// Percent is the fan speed in percent
Percent uint8 `mapstructure:"percent"`
}
// Config configures a fan controller for the computeblade
type Config struct {
// Steps defines the temperature/speed steps for the fan controller
Steps []Step `mapstructure:"steps"`
}

View File

@@ -2,53 +2,64 @@ package fancontroller
import (
"fmt"
"sort"
"sync"
"github.com/sierrasoftworks/humane-errors-go"
)
type FanController interface {
Override(opts *FanOverrideOpts)
GetFanSpeed(temperature float64) uint8
}
// GetFanSpeedPercent returns the fan speed in percent based on the current temperature
GetFanSpeedPercent(temperature float64) uint8
// IsAutomaticSpeed returns true if the FanSpeed is determined by the fan controller logic, or false if determined
// by an FanOverrideOpts
IsAutomaticSpeed() bool
type FanOverrideOpts struct {
Percent uint8 `mapstructure:"speed"`
}
type FanControllerStep struct {
// Temperature is the temperature to react to
Temperature float64 `mapstructure:"temperature"`
// Percent is the fan speed in percent
Percent uint8 `mapstructure:"percent"`
}
// FanController configures a fan controller for the computeblade
type FanControllerConfig struct {
// Steps defines the temperature/speed steps for the fan controller
Steps []FanControllerStep `mapstructure:"steps"`
// Steps returns the list of temperature and fan speed steps configured for the fan controller.
Steps() []Step
}
// FanController is a simple fan controller that reacts to temperature changes with a linear function
type fanControllerLinear struct {
mu sync.Mutex
mu sync.Mutex
overrideOpts *FanOverrideOpts
config FanControllerConfig
config Config
}
// NewFanControllerLinear creates a new FanControllerLinear
func NewLinearFanController(config FanControllerConfig) (FanController, error) {
// NewLinearFanController creates a new FanControllerLinear
func NewLinearFanController(config Config) (FanController, humane.Error) {
steps := config.Steps
// Validate config for a very simple linear fan controller
if len(config.Steps) != 2 {
return nil, fmt.Errorf("exactly two steps must be defined")
// Sort steps by temperature
sort.Slice(steps, func(i, j int) bool {
return steps[i].Temperature < steps[j].Temperature
})
for i := 0; i < len(steps)-1; i++ {
curr := steps[i]
next := steps[i+1]
if curr.Temperature >= next.Temperature {
return nil, humane.New("steps must have strictly increasing temperatures",
"Ensure that the temperatures are in ascending order and the ranges do not overlap",
fmt.Sprintf("Ensure defined temperature stepd %.2f is >= %.2f", curr.Temperature, next.Temperature),
)
}
if curr.Percent > next.Percent {
return nil, humane.New("fan percent must not decrease",
"Ensure that the fan percentages are not decreasing for higher temperatures",
fmt.Sprintf("Temperature %.2f is defined at %d%% and must be >= %d%% defined for temperature %.2f", curr.Temperature, curr.Percent, next.Percent, next.Temperature),
)
}
}
if config.Steps[0].Temperature > config.Steps[1].Temperature {
return nil, fmt.Errorf("step 1 temperature must be lower than step 2 temperature")
}
if config.Steps[0].Percent > config.Steps[1].Percent {
return nil, fmt.Errorf("step 1 speed must be lower than step 2 speed")
}
if config.Steps[0].Percent > 100 || config.Steps[1].Percent > 100 {
return nil, fmt.Errorf("speed must be between 0 and 100")
for _, step := range steps {
if step.Percent > 100 {
return nil, humane.New("fan percent must be between 0 and 100",
fmt.Sprintf("Ensure your fan percentage is 0 < %d < 100", step.Percent),
)
}
}
return &fanControllerLinear{
@@ -56,14 +67,18 @@ func NewLinearFanController(config FanControllerConfig) (FanController, error) {
}, nil
}
func (f *fanControllerLinear) Steps() []Step {
return f.config.Steps
}
func (f *fanControllerLinear) Override(opts *FanOverrideOpts) {
f.mu.Lock()
defer f.mu.Unlock()
f.overrideOpts = opts
}
// GetFanSpeed returns the fan speed in percent based on the current temperature
func (f *fanControllerLinear) GetFanSpeed(temperature float64) uint8 {
// GetFanSpeedPercent returns the fan speed in percent based on the current temperature
func (f *fanControllerLinear) GetFanSpeedPercent(temperature float64) uint8 {
f.mu.Lock()
defer f.mu.Unlock()
@@ -71,18 +86,33 @@ func (f *fanControllerLinear) GetFanSpeed(temperature float64) uint8 {
return f.overrideOpts.Percent
}
if temperature <= f.config.Steps[0].Temperature {
return f.config.Steps[0].Percent
}
if temperature >= f.config.Steps[1].Temperature {
return f.config.Steps[1].Percent
steps := f.config.Steps
// Below minimum temperature: use minimum fan speed
if temperature <= steps[0].Temperature {
return steps[0].Percent
}
// Calculate slope
slope := float64(f.config.Steps[1].Percent-f.config.Steps[0].Percent) / (f.config.Steps[1].Temperature - f.config.Steps[0].Temperature)
// Above maximum temperature: use maximum fan speed
lastIdx := len(steps) - 1
if temperature >= steps[lastIdx].Temperature {
return steps[lastIdx].Percent
}
// Calculate speed
speed := float64(f.config.Steps[0].Percent) + slope*(temperature-f.config.Steps[0].Temperature)
// Find the bracket where steps[i].Temperature <= temperature < steps[i+1].Temperature
for i := 0; i < lastIdx; i++ {
if temperature >= steps[i].Temperature && temperature < steps[i+1].Temperature {
// Linear interpolation between steps[i] and steps[i+1]
slope := float64(steps[i+1].Percent-steps[i].Percent) / (steps[i+1].Temperature - steps[i].Temperature)
speed := float64(steps[i].Percent) + slope*(temperature-steps[i].Temperature)
return uint8(speed)
}
}
return uint8(speed)
// Fallback (should not reach here due to above checks)
return steps[lastIdx].Percent
}
func (f *fanControllerLinear) IsAutomaticSpeed() bool {
return f.overrideOpts == nil
}

View File

@@ -1,17 +1,17 @@
// fancontroller_test.go
package fancontroller_test
import (
"testing"
"github.com/uptime-induestries/compute-blade-agent/pkg/fancontroller"
"github.com/compute-blade-community/compute-blade-agent/pkg/fancontroller"
"github.com/stretchr/testify/assert"
)
func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
t.Parallel()
config := fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
config := fancontroller.Config{
Steps: []fancontroller.Step{
{Temperature: 20, Percent: 30},
{Temperature: 30, Percent: 60},
},
@@ -31,15 +31,65 @@ func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
{35, 60}, // Should use the maximum speed
}
assert.Equal(t, controller.Steps(), config.Steps)
for _, tc := range testCases {
expected := tc.expected
temperature := tc.temperature
t.Run("", func(t *testing.T) {
t.Parallel()
speed := controller.GetFanSpeed(temperature)
if speed != expected {
t.Errorf("For temperature %.2f, expected speed %d but got %d", temperature, expected, speed)
}
speed := controller.GetFanSpeedPercent(temperature)
assert.Equal(t, expected, speed)
assert.True(t, controller.IsAutomaticSpeed(), "Expected fan speed to be automatic, but it was not")
})
}
}
func TestFanControllerLinear_GetFanSpeedMultipleSteps(t *testing.T) {
t.Parallel()
// Typical 5-step fan curve configuration
config := fancontroller.Config{
Steps: []fancontroller.Step{
{Temperature: 40, Percent: 30},
{Temperature: 50, Percent: 50},
{Temperature: 60, Percent: 70},
{Temperature: 70, Percent: 90},
{Temperature: 75, Percent: 100},
},
}
controller, err := fancontroller.NewLinearFanController(config)
if err != nil {
t.Fatalf("Failed to create fan controller: %v", err)
}
testCases := []struct {
name string
temperature float64
expected uint8
}{
{"below minimum", 30, 30}, // Below 40°C: use minimum 30%
{"at step 0", 40, 30}, // At 40°C: 30%
{"between step 0-1", 45, 40}, // Midpoint 40-50°C: 40%
{"at step 1", 50, 50}, // At 50°C: 50%
{"between step 1-2", 55, 60}, // Midpoint 50-60°C: 60%
{"at step 2", 60, 70}, // At 60°C: 70%
{"between step 2-3", 65, 80}, // Midpoint 60-70°C: 80%
{"at step 3", 70, 90}, // At 70°C: 90%
{"between step 3-4", 72, 94}, // 70 + (100-90)*(72-70)/(75-70) = 90 + 4 = 94%
{"at step 4", 75, 100}, // At 75°C: 100%
{"above maximum", 80, 100}, // Above 75°C: use maximum 100%
{"well above maximum", 90, 100}, // Well above: still 100%
}
for _, tc := range testCases {
expected := tc.expected
temperature := tc.temperature
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
speed := controller.GetFanSpeedPercent(temperature)
assert.Equal(t, expected, speed, "Temperature %.1f°C should yield %d%% fan speed", temperature, expected)
})
}
}
@@ -47,8 +97,8 @@ func TestFanControllerLinear_GetFanSpeed(t *testing.T) {
func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
t.Parallel()
config := fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
config := fancontroller.Config{
Steps: []fancontroller.Step{
{Temperature: 20, Percent: 30},
{Temperature: 30, Percent: 60},
},
@@ -76,10 +126,9 @@ func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
temperature := tc.temperature
t.Run("", func(t *testing.T) {
t.Parallel()
speed := controller.GetFanSpeed(temperature)
if speed != expected {
t.Errorf("For temperature %.2f, expected speed %d but got %d", temperature, expected, speed)
}
speed := controller.GetFanSpeedPercent(temperature)
assert.Equal(t, expected, speed)
assert.False(t, controller.IsAutomaticSpeed(), "Expected fan speed to be overridden, but it was not")
})
}
}
@@ -87,47 +136,38 @@ func TestFanControllerLinear_GetFanSpeedWithOverride(t *testing.T) {
func TestFanControllerLinear_ConstructionErrors(t *testing.T) {
testCases := []struct {
name string
config fancontroller.FanControllerConfig
config fancontroller.Config
errMsg string
}{
{
name: "InvalidStepCount",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
name: "Overlapping Step Temperatures",
config: fancontroller.Config{
Steps: []fancontroller.Step{
{Temperature: 20, Percent: 60},
{Temperature: 20, Percent: 30},
},
},
errMsg: "exactly two steps must be defined",
errMsg: "steps must have strictly increasing temperatures",
},
{
name: "InvalidStepTemperatures",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
{Temperature: 30, Percent: 60},
{Temperature: 20, Percent: 30},
},
},
errMsg: "step 1 temperature must be lower than step 2 temperature",
},
{
name: "InvalidStepSpeeds",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
name: "Percentages must not decrease",
config: fancontroller.Config{
Steps: []fancontroller.Step{
{Temperature: 20, Percent: 60},
{Temperature: 30, Percent: 30},
},
},
errMsg: "step 1 speed must be lower than step 2 speed",
errMsg: "fan percent must not decrease",
},
{
name: "InvalidSpeedRange",
config: fancontroller.FanControllerConfig{
Steps: []fancontroller.FanControllerStep{
config: fancontroller.Config{
Steps: []fancontroller.Step{
{Temperature: 20, Percent: 10},
{Temperature: 30, Percent: 200},
},
},
errMsg: "speed must be between 0 and 100",
errMsg: "fan percent must be between 0 and 100",
},
}
@@ -137,11 +177,9 @@ func TestFanControllerLinear_ConstructionErrors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := fancontroller.NewLinearFanController(config)
if err == nil {
t.Errorf("Expected error with message '%s', but got no error", expectedErrMsg)
} else if err.Error() != expectedErrMsg {
t.Errorf("Expected error message '%s', but got '%s'", expectedErrMsg, err.Error())
}
assert.NotNil(t, err, "Expected error with message '%s', but got no error", expectedErrMsg)
assert.EqualError(t, err, expectedErrMsg)
})
}
}

5
pkg/hal/config.go Normal file
View File

@@ -0,0 +1,5 @@
package hal
type Config struct {
RpmReporting bool `mapstructure:"rpm_reporting_standard_fan_unit"`
}

View File

@@ -6,8 +6,8 @@ import (
"context"
"log"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
)
func ExampleNewSmartFanUnit() {
@@ -30,7 +30,7 @@ func ExampleNewSmartFanUnit() {
panic(err)
}
// Set fanspeed to 20%
// Set fan speed to 20%
err = client.SetFanSpeedPercent(ctx, 20)
if err != nil {
panic(err)

View File

@@ -3,7 +3,7 @@ package hal
import (
"context"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
)
type FanUnitKind uint8
@@ -32,8 +32,10 @@ const (
PowerPoe802at
)
type LedIndex uint8
const (
LedTop = iota
LedTop LedIndex = iota
LedEdge
)
@@ -49,23 +51,24 @@ type ComputeBladeHal interface {
Close() error
// SetFanSpeed sets the fan speed in percent
SetFanSpeed(speed uint8) error
// GetFanSpeed returns the current fan speed in percent (based on moving average)
// GetFanRPM returns the current fan speed in percent (based on moving average)
GetFanRPM() (float64, error)
// SetStealthMode enables/disables stealth mode of the blade (turning on/off the LEDs)
SetStealthMode(enabled bool) error
// SetLEDs sets the color of the LEDs
SetLed(idx uint, color led.Color) error
// StealthModeActive returns if stealth mode of the blade is currently active
StealthModeActive() bool
// SetLed sets the color of the LEDs
SetLed(idx LedIndex, color led.Color) error
// GetPowerStatus returns the current power status of the blade
GetPowerStatus() (PowerStatus, error)
// GetTemperature returns the current temperature of the SoC in °C
GetTemperature() (float64, error)
// GetEdgeButtonPressChan returns a channel emitting edge button press events
// WaitForEdgeButtonPress returns a channel emitting edge button press events
WaitForEdgeButtonPress(ctx context.Context) error
}
// FanUnit abstracts the fan unit
type FanUnit interface {
// Kind returns the kind of the fan FanUnit
Kind() FanUnitKind

View File

@@ -14,8 +14,8 @@ import (
"syscall"
"time"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/log"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/warthog618/gpiod"
"github.com/warthog618/gpiod/device/rpi"
"go.uber.org/zap"
@@ -30,9 +30,10 @@ const (
bcm2711ClkManagerPwd = (0x5A << 24) //(31 - 24) on CM_GP0CTL/CM_GP1CTL/CM_GP2CTL regs
bcm2711PageSize = 4096 // theoretical page size
bcm2711FrontButtonPin = 20
bcm2711StealthPin = 21
bcm2711RegPwmTachPin = 13
// FIXME: no dead code
//bcm2711FrontButtonPin = 20
//bcm2711StealthPin = 21
//bcm2711RegPwmTachPin = 13
bcm2711RegGpfsel1 = 0x01
@@ -96,7 +97,7 @@ type bcm2711 struct {
fanUnit FanUnit
}
func NewCm4Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
func newBcm2711Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
// /dev/gpiomem doesn't allow complex operations for PWM fan control or WS281x
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
if err != nil {
@@ -164,7 +165,7 @@ func (bcm *bcm2711) Close() error {
// Init initialises GPIOs and sets sane defaults
func (bcm *bcm2711) setup(ctx context.Context) error {
var err error = nil
var err error
// Register edge event handler for edge button
bcm.edgeButtonLine, err = bcm.gpioChip0.RequestLine(
@@ -192,19 +193,19 @@ func (bcm *bcm2711) setup(ctx context.Context) error {
defer cancel()
if smartFanUnitPresent, err := SmartFanUnitPresent(detectCtx, smartFanUnitDev); err == nil && smartFanUnitPresent {
log.FromContext(ctx).Error("detected smart fan unit")
log.FromContext(ctx).Info("detected smart fan unit")
bcm.fanUnit, err = NewSmartFanUnit(smartFanUnitDev)
if err != nil {
return err
}
} else {
log.FromContext(ctx).Info("no smart fan unit detected, assuming standard fan unit", zap.Error(err))
log.FromContext(ctx).WithError(err).Info("no smart fan unit detected, assuming standard fan unit")
// FAN PWM output for standard fan unit (GPIO 12)
// -> bcm2711RegGpfsel1 8:6, alt0
bcm.gpioMem[bcm2711RegGpfsel1] = (bcm.gpioMem[bcm2711RegGpfsel1] &^ (0b111 << 6)) | (0b100 << 6)
bcm.fanUnit = &standardFanUnitBcm2711{
GpioChip0: bcm.gpioChip0,
DisableRPMreporting: !bcm.opts.RpmReportingStandardFanUnit,
DisableRpmReporting: !bcm.opts.RpmReportingStandardFanUnit,
SetFanSpeedPwmFunc: func(speed uint8) error {
bcm.setFanSpeedPWM(speed)
return nil
@@ -230,7 +231,7 @@ func (bcm *bcm2711) Run(parentCtx context.Context) error {
}
func (bcm *bcm2711) handleEdgeButtonEdge(evt gpiod.LineEvent) {
// Despite the debounce, we still get multiple events for a single button press
// Despite debouncing, we still get multiple events for a single button press
// -> This is an in-software debounce to ensure we only get one event per button press
select {
case bcm.edgeButtonDebounceChan <- struct{}{}:
@@ -257,7 +258,7 @@ func (bcm *bcm2711) WaitForEdgeButtonPress(parentCtx context.Context) error {
go func() {
err := bcm.fanUnit.WaitForButtonPress(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).Error("failed to wait for button press", zap.Error(err))
log.FromContext(ctx).WithError(err).Error("failed to wait for button press")
} else {
close(fanUnitChan)
}
@@ -385,6 +386,14 @@ func (bcm *bcm2711) SetStealthMode(enable bool) error {
}
}
func (bcm *bcm2711) StealthModeActive() bool {
val, err := bcm.stealthModeLine.Value()
if err != nil {
return false
}
return val > 0
}
// serializePwmDataFrame converts a byte to a 24 bit PWM data frame for WS281x LEDs
func serializePwmDataFrame(data uint8) uint32 {
var result uint32 = 0
@@ -401,14 +410,16 @@ func serializePwmDataFrame(data uint8) uint32 {
return result
}
func (bcm *bcm2711) SetLed(idx uint, color led.Color) error {
func (bcm *bcm2711) SetLed(idx LedIndex, color led.Color) error {
if idx >= 2 {
return fmt.Errorf("invalid led index %d, supported: [0, 1]", idx)
}
// Update the fan unit LED if the index is the same as the fan unit LED index
if idx == LedEdge {
bcm.fanUnit.SetLed(context.TODO(), color)
if err := bcm.fanUnit.SetLed(context.TODO(), color); err != nil {
return err
}
}
bcm.leds[idx] = color
@@ -424,8 +435,10 @@ func (bcm *bcm2711) updateLEDs() error {
ledColorChangeEventCount.Inc()
// Set frequency to 3*800khz.
// we'll bit-bang the data, so we'll need to send 3 bits per bit of data.
bcm.setPwm0Freq(3 * 800000)
// we'll bit-bang the data, so we'll need to send 3 bits per one bit of data.
if err := bcm.setPwm0Freq(3 * 800000); err != nil {
return err
}
time.Sleep(10 * time.Microsecond)
// WS281x Output (GPIO 18)

View File

@@ -6,20 +6,22 @@ import (
"context"
"time"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/spechtlabs/go-otel-utils/otelzap"
"go.uber.org/zap"
)
// fails if SimulatedHal does not implement ComputeBladeHal
var _ ComputeBladeHal = &SimulatedHal{}
// ComputeBladeMock implements a mock for the ComputeBladeHal interface
// SimulatedHal implements a mock for the ComputeBladeHal interface
type SimulatedHal struct {
logger *zap.Logger
logger *zap.Logger
isStealthMode bool
}
func NewCm4Hal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
logger := zap.L().Named("hal").Named("simulated-cm4")
func NewHal(_ context.Context, _ ComputeBladeHalOpts) (ComputeBladeHal, error) {
logger := otelzap.L().Named("hal").Named("simulated-cm4")
logger.Warn("Using simulated hal")
computeModule.WithLabelValues("simulated").Set(1)
@@ -57,10 +59,16 @@ func (m *SimulatedHal) SetStealthMode(enabled bool) error {
} else {
stealthModeEnabled.Set(0)
}
m.isStealthMode = enabled
m.logger.Info("SetStealthMode", zap.Bool("enabled", enabled))
return nil
}
func (m *SimulatedHal) StealthModeActive() bool {
return m.isStealthMode
}
func (m *SimulatedHal) GetPowerStatus() (PowerStatus, error) {
m.logger.Info("GetPowerStatus")
powerStatus.WithLabelValues("simulated").Set(1)
@@ -78,9 +86,9 @@ func (m *SimulatedHal) WaitForEdgeButtonPress(ctx context.Context) error {
}
}
func (m *SimulatedHal) SetLed(idx uint, color led.Color) error {
func (m *SimulatedHal) SetLed(idx LedIndex, color led.Color) error {
ledColorChangeEventCount.Inc()
m.logger.Info("SetLed", zap.Uint("idx", idx), zap.Any("color", color))
m.logger.Info("SetLed", zap.Uint("idx", uint(idx)), zap.Any("color", color))
return nil
}

View File

@@ -6,7 +6,8 @@ import (
"context"
"math"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/warthog618/gpiod"
"github.com/warthog618/gpiod/device/rpi"
)
@@ -14,16 +15,16 @@ import (
type standardFanUnitBcm2711 struct {
GpioChip0 *gpiod.Chip
SetFanSpeedPwmFunc func(speed uint8) error
DisableRPMreporting bool
DisableRpmReporting bool
// Fan tach input
// Fan tachometer input
fanEdgeLine *gpiod.Line
lastFanEdgeEvent *gpiod.LineEvent
fanRpm float64
}
func (fu standardFanUnitBcm2711) Kind() FanUnitKind {
if fu.DisableRPMreporting {
if fu.DisableRpmReporting {
return FanUnitKindStandardNoRPM
}
return FanUnitKindStandard
@@ -33,8 +34,8 @@ func (fu standardFanUnitBcm2711) Run(ctx context.Context) error {
var err error
fanUnit.WithLabelValues("standard").Set(1)
// Register edge event handler for fan tach input
if !fu.DisableRPMreporting {
// Register edge event handler for fan tachometer input
if !fu.DisableRpmReporting {
fu.fanEdgeLine, err = fu.GpioChip0.RequestLine(
rpi.GPIO13,
gpiod.WithEventHandler(fu.handleFanEdge),
@@ -44,14 +45,19 @@ func (fu standardFanUnitBcm2711) Run(ctx context.Context) error {
if err != nil {
return err
}
defer fu.fanEdgeLine.Close()
defer func(fanEdgeLine *gpiod.Line) {
err := fanEdgeLine.Close()
if err != nil {
log.FromContext(ctx).WithError(err).Error("failed to close fanEdgeLine")
}
}(fu.fanEdgeLine)
}
<-ctx.Done()
return ctx.Err()
}
// handleFanEdge handles an edge event on the fan tach input for the standard fan unite.
// handleFanEdge handles an edge event on the fan tachometer input for the standard fan unite.
// Exponential moving average is used to smooth out the fan speed.
func (fu *standardFanUnitBcm2711) handleFanEdge(evt gpiod.LineEvent) {
// Ensure we're always storing the last event

653
pkg/hal/hal_bcm2712.go Normal file
View File

@@ -0,0 +1,653 @@
//go:build linux && !tinygo
package hal
import (
"context"
"errors"
"fmt"
"io"
"os"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/warthog618/gpiod"
"github.com/warthog618/gpiod/device/rpi"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
const (
// RP1 southbridge is connected via PCIe on BCM2712.
// The BAR base address is fixed by firmware at 0x1f00000000.
rp1BarBase int64 = 0x1f00000000
rp1GpioBase int64 = rp1BarBase + 0xd0000
rp1Pwm0Base int64 = rp1BarBase + 0x98000
rp1PageSize = 4096
// RP1 PWM input clock is 50 MHz (from device tree assigned-clock-rates)
rp1PwmClockHz = 50_000_000
// RP1 GPIO register layout: each GPIO has 8 bytes (STATUS + CTRL)
rp1GpioCtrlOffset = 0x04 // CTRL register offset within each GPIO's 8-byte block
rp1GpioRegSize = 0x08
rp1GpioFuncselMask = 0x1f // CTRL bits [4:0]
// GPIO function select values (confirmed via `pinctrl set/get` on CM5)
// GPIO 12: funcsel 0 (a0) = PWM0_CHAN0 (fan PWM)
// GPIO 18: funcsel 3 (a3) = PWM0_CHAN2 (WS281x LEDs)
rp1Gpio12FuncselPwm = 0
rp1Gpio18FuncselPwm = 3
rp1GpioFuncselSio = 5 // Software IO (standard GPIO)
rp1GpioFuncselNull = 0x1f // Null function (disabled)
// RP1 PWM register offsets (byte offsets, divide by 4 for []uint32 index)
rp1PwmGlobalCtrl = 0x00
rp1PwmFifoCtrl = 0x04
rp1PwmFifoPush = 0x08
rp1PwmFifoLevel = 0x0c
// Per-channel registers: base = 0x14 + channel * 0x10
// From kernel pwm-rp1.c: CTRL(x)=0x14+x*16, RANGE(x)=0x18+x*16, DUTY(x)=0x20+x*16
rp1PwmChanCtrlOff = 0x00 // relative to channel base
rp1PwmChanRangeOff = 0x04
rp1PwmChanPhaseOff = 0x08 // undocumented in kernel driver, possibly COUNT
rp1PwmChanDutyOff = 0x0c
rp1PwmChanSize = 0x10
rp1PwmChanBase = 0x14 // first channel base offset (NOT 0x10)
// PWM global ctrl bits (from kernel pwm-rp1.c)
rp1PwmGlobalChanEnBit = 0 // bits [3:0] = per-channel enable, BIT(x)
rp1PwmGlobalSetUpdate = 31 // BIT(31) = global set_update trigger
// PWM channel ctrl bits
rp1PwmChanCtrlModeBit = 0 // bits [1:0]
rp1PwmChanCtrlInvertBit = 2
rp1PwmChanCtrlUseFifoBit = 4
rp1PwmChanCtrlFifoPopMaskBit = 7 // bits [8:7]
// PWM modes
rp1PwmModeTrailingEdge = 0
rp1PwmModeMarkSpace = 1
rp1PwmModeSerializer = 3
// FIFO ctrl bits
rp1PwmFifoFlushBit = 5
// Fan PWM: 25 kHz for Noctua fans
// RANGE = 50 MHz / 25 kHz = 2000
rp1FanPwmRange = rp1PwmClockHz / 25000
bcm2712SmartFanUnitDev = "/dev/ttyAMA4" // UART4 on BCM2712 (same GPIO 12/13 pins as UART5 on BCM2711)
bcm2712ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
bcm2712DebounceInterval = 100 * time.Millisecond
// WS281x LED encoding for RP1 at 50MHz with RANGE=32 serializer mode.
// Each WS281x data bit is encoded as 2 × 32-bit FIFO words (1280ns per bit ≈ 781kHz).
// Channel 2 on GPIO 18 is used independently from fan PWM on channel 0.
rp1Ws281xChan = 2 // PWM0 channel 2 on GPIO 18
rp1Ws281xRange = 32 // full 32-bit word in serializer mode
// "0" bit: T0H=400ns (20 high bits), T0L=880ns (44 low bits)
rp1Ws281xBit0Word0 uint32 = 0xFFFFF000
rp1Ws281xBit0Word1 uint32 = 0x00000000
// "1" bit: T1H=800ns (40 high bits), T1L=480ns (24 low bits)
rp1Ws281xBit1Word0 uint32 = 0xFFFFFFFF
rp1Ws281xBit1Word1 uint32 = 0xFF000000
// Reset: >50μs of low. At 640ns/word, 80 words = 51.2μs.
rp1Ws281xResetWords = 80
// Conservative FIFO depth assumption (BCM2835 has 16, RP1 may differ)
rp1Ws281xFifoMax uint32 = 8
// Safety timeout for FIFO operations
rp1Ws281xTimeout = 5 * time.Millisecond
)
// pwmChanRegIdx returns the []uint32 index for a per-channel register.
func pwmChanRegIdx(channel, regOffset int) int {
return (rp1PwmChanBase + channel*rp1PwmChanSize + regOffset) / 4
}
type bcm2712 struct {
opts ComputeBladeHalOpts
wrMutex sync.Mutex
currFanSpeed uint8
devmem *os.File
gpioMem8 []uint8
gpioMem []uint32
pwmMem8 []uint8
pwmMem []uint32
gpioChip0 *gpiod.Chip
// WS281x LED colors (top + edge)
leds [2]led.Color
// Stealth mode output
stealthModeLine *gpiod.Line
// Edge button input
edgeButtonLine *gpiod.Line
edgeButtonDebounceChan chan struct{}
edgeButtonWatchChan chan struct{}
// PoE detection input
poeLine *gpiod.Line
// Fan unit
fanUnit FanUnit
}
func newBcm2712Hal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
devmem, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, os.ModePerm)
if err != nil {
return nil, fmt.Errorf("failed to open /dev/mem: %w", err)
}
gpioChip0, err := gpiod.NewChip("gpiochip0")
if err != nil {
_ = devmem.Close()
return nil, fmt.Errorf("failed to open gpiochip0: %w", err)
}
// Memory-map RP1 GPIO bank 0 (GPIOs 0-27)
gpioMem, gpioMem8, err := mmap(devmem, rp1GpioBase, rp1PageSize)
if err != nil {
_ = gpioChip0.Close()
_ = devmem.Close()
return nil, fmt.Errorf("failed to mmap RP1 GPIO at 0x%x: %w", rp1GpioBase, err)
}
// Memory-map RP1 PWM0
pwmMem, pwmMem8, err := mmap(devmem, rp1Pwm0Base, rp1PageSize)
if err != nil {
_ = syscall.Munmap(gpioMem8)
_ = gpioChip0.Close()
_ = devmem.Close()
return nil, fmt.Errorf("failed to mmap RP1 PWM0 at 0x%x: %w", rp1Pwm0Base, err)
}
bcm := &bcm2712{
devmem: devmem,
gpioMem: gpioMem,
gpioMem8: gpioMem8,
pwmMem: pwmMem,
pwmMem8: pwmMem8,
gpioChip0: gpioChip0,
opts: opts,
edgeButtonDebounceChan: make(chan struct{}, 1),
edgeButtonWatchChan: make(chan struct{}),
}
computeModule.WithLabelValues("cm5").Set(1)
log.FromContext(ctx).Info("starting hal setup", zap.String("hal", "bcm2712"))
if err := bcm.setup(ctx); err != nil {
_ = bcm.Close()
return nil, err
}
return bcm, nil
}
func (bcm *bcm2712) Close() error {
errs := errors.Join(
bcm.closeFanUnit(),
bcm.unmapMem(),
bcm.closeDevmem(),
bcm.closeGpio(),
)
return errs
}
func (bcm *bcm2712) closeFanUnit() error {
if bcm.fanUnit != nil {
return bcm.fanUnit.Close()
}
return nil
}
func (bcm *bcm2712) unmapMem() error {
return errors.Join(
munmapIfNonNil(bcm.gpioMem8),
munmapIfNonNil(bcm.pwmMem8),
)
}
func munmapIfNonNil(mem []uint8) error {
if mem != nil {
return syscall.Munmap(mem)
}
return nil
}
func (bcm *bcm2712) closeDevmem() error {
if bcm.devmem != nil {
return bcm.devmem.Close()
}
return nil
}
func (bcm *bcm2712) closeGpio() error {
var errs []error
if bcm.gpioChip0 != nil {
errs = append(errs, bcm.gpioChip0.Close())
}
if bcm.poeLine != nil {
errs = append(errs, bcm.poeLine.Close())
}
if bcm.stealthModeLine != nil {
errs = append(errs, bcm.stealthModeLine.Close())
}
return errors.Join(errs...)
}
// setGpioFuncsel sets the function select for a GPIO pin via direct register write.
func (bcm *bcm2712) setGpioFuncsel(gpio int, funcsel uint32) {
ctrlIdx := (gpio*rp1GpioRegSize + rp1GpioCtrlOffset) / 4
ctrl := bcm.gpioMem[ctrlIdx]
ctrl = (ctrl &^ uint32(rp1GpioFuncselMask)) | (funcsel & uint32(rp1GpioFuncselMask))
bcm.gpioMem[ctrlIdx] = ctrl
}
func (bcm *bcm2712) setup(ctx context.Context) error {
var err error
// Register edge event handler for edge button (GPIO 20)
bcm.edgeButtonLine, err = bcm.gpioChip0.RequestLine(
rpi.GPIO20, gpiod.WithEventHandler(bcm.handleEdgeButtonEdge),
gpiod.WithFallingEdge, gpiod.WithPullUp, gpiod.WithDebounce(50*time.Millisecond))
if err != nil {
return fmt.Errorf("failed to request GPIO20 (edge button): %w", err)
}
// Register input for PoE detection (GPIO 23)
bcm.poeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO23, gpiod.AsInput, gpiod.WithPullUp)
if err != nil {
return fmt.Errorf("failed to request GPIO23 (PoE detect): %w", err)
}
// Register output for stealth mode (GPIO 21)
bcm.stealthModeLine, err = bcm.gpioChip0.RequestLine(rpi.GPIO21, gpiod.AsOutput(1))
if err != nil {
return fmt.Errorf("failed to request GPIO21 (stealth mode): %w", err)
}
// Detect fan unit type
log.FromContext(ctx).Info("detecting fan unit")
detectCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if smartFanUnitPresent, err := SmartFanUnitPresent(detectCtx, bcm2712SmartFanUnitDev); err == nil && smartFanUnitPresent {
log.FromContext(ctx).Info("detected smart fan unit")
bcm.fanUnit, err = NewSmartFanUnit(bcm2712SmartFanUnitDev)
if err != nil {
return fmt.Errorf("failed to create smart fan unit: %w", err)
}
} else {
log.FromContext(ctx).WithError(err).Info("no smart fan unit detected, assuming standard fan unit")
// Set GPIO 12 to PWM0_CHAN0 function
bcm.setGpioFuncsel(12, rp1Gpio12FuncselPwm)
// Initialize PWM0 channel 0 for fan control (25 kHz mark-space)
bcm.initFanPwm()
bcm.fanUnit = &standardFanUnitBcm2711{
GpioChip0: bcm.gpioChip0,
DisableRpmReporting: !bcm.opts.RpmReportingStandardFanUnit,
SetFanSpeedPwmFunc: func(speed uint8) error {
bcm.setFanSpeedPWM(speed)
return nil
},
}
}
return nil
}
// initFanPwm configures PWM0 channel 0 in mark-space mode at 25 kHz.
func (bcm *bcm2712) initFanPwm() {
ch := 0
// Disable channel 0
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Configure channel 0: mark-space mode, no FIFO, no invert
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] = uint32(rp1PwmModeMarkSpace) << rp1PwmChanCtrlModeBit
time.Sleep(10 * time.Microsecond)
// Set range (period) for 25 kHz
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1FanPwmRange
time.Sleep(10 * time.Microsecond)
// Set initial duty to 0 (fan off)
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = 0
time.Sleep(10 * time.Microsecond)
// Phase = 0
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
time.Sleep(10 * time.Microsecond)
// Trigger update for channel 0
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Enable channel 0
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
}
func (bcm *bcm2712) Run(parentCtx context.Context) error {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
group := errgroup.Group{}
group.Go(func() error {
defer cancel()
return bcm.fanUnit.Run(ctx)
})
return group.Wait()
}
func (bcm *bcm2712) handleEdgeButtonEdge(evt gpiod.LineEvent) {
select {
case bcm.edgeButtonDebounceChan <- struct{}{}:
go func() {
<-bcm.edgeButtonDebounceChan
time.Sleep(bcm2712DebounceInterval)
edgeButtonEventCount.Inc()
close(bcm.edgeButtonWatchChan)
bcm.edgeButtonWatchChan = make(chan struct{})
}()
default:
return
}
}
func (bcm *bcm2712) WaitForEdgeButtonPress(parentCtx context.Context) error {
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
fanUnitChan := make(chan struct{})
go func() {
err := bcm.fanUnit.WaitForButtonPress(ctx)
if err != nil && err != context.Canceled {
log.FromContext(ctx).WithError(err).Error("failed to wait for button press")
} else {
close(fanUnitChan)
}
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-bcm.edgeButtonWatchChan:
return nil
case <-fanUnitChan:
return nil
}
}
func (bcm *bcm2712) GetFanRPM() (float64, error) {
rpm, err := bcm.fanUnit.FanSpeedRPM(context.TODO())
return float64(rpm), err
}
func (bcm *bcm2712) GetPowerStatus() (PowerStatus, error) {
val, err := bcm.poeLine.Value()
if err != nil {
return PowerPoeOrUsbC, err
}
if val > 0 {
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(1)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(0)
return PowerPoe802at, nil
}
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
return PowerPoeOrUsbC, nil
}
// SetFanSpeed sets the fan speed in percent.
func (bcm *bcm2712) SetFanSpeed(speed uint8) error {
fanTargetPercent.Set(float64(speed))
return bcm.fanUnit.SetFanSpeedPercent(context.TODO(), speed)
}
// setFanSpeedPWM sets the fan PWM duty cycle using RP1 PWM0 channel 0 in mark-space mode.
func (bcm *bcm2712) setFanSpeedPWM(speed uint8) {
ch := 0
var duty uint32
if speed == 0 {
duty = 0
} else if speed <= 100 {
duty = uint32(float64(rp1FanPwmRange) * float64(speed) / 100.0)
} else {
duty = rp1FanPwmRange
}
// Update duty cycle
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanDutyOff)] = duty
// Trigger update
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
bcm.currFanSpeed = speed
}
func (bcm *bcm2712) SetStealthMode(enable bool) error {
if enable {
stealthModeEnabled.Set(1)
return bcm.stealthModeLine.SetValue(1)
}
stealthModeEnabled.Set(0)
return bcm.stealthModeLine.SetValue(0)
}
func (bcm *bcm2712) StealthModeActive() bool {
val, err := bcm.stealthModeLine.Value()
if err != nil {
return false
}
return val > 0
}
// SetLed sets the WS281x LED color via RP1 PWM0 channel 2 serializer mode.
// Unlike BCM2711 which shares PWM0 between fan and LEDs, RP1 has independent channels:
// channel 0 (GPIO 12) handles fan PWM, channel 2 (GPIO 18) handles WS281x LEDs.
func (bcm *bcm2712) SetLed(idx LedIndex, color led.Color) error {
if idx >= 2 {
return fmt.Errorf("invalid led index %d, supported: [0, 1]", idx)
}
// Update the fan unit LED if this is the edge LED
if idx == LedEdge {
if err := bcm.fanUnit.SetLed(context.TODO(), color); err != nil {
return err
}
}
bcm.leds[idx] = color
return bcm.updateLEDs()
}
// updateLEDs sends WS281x data via RP1 PWM0 channel 2 serializer mode.
// Uses the shared FIFO (channel 0 fan PWM uses mark-space mode without FIFO, so no conflict).
// The RP1 PWM clock is fixed at 50MHz, so we use 2 FIFO words per WS281x data bit
// at RANGE=32 to achieve ~1280ns per bit (within WS281x tolerance).
func (bcm *bcm2712) updateLEDs() error {
bcm.wrMutex.Lock()
defer bcm.wrMutex.Unlock()
ledColorChangeEventCount.Inc()
ch := rp1Ws281xChan
// Build complete FIFO data stream
data := bcm.buildWs281xStream()
// Set GPIO 18 to PWM0_CH2 function
bcm.setGpioFuncsel(18, rp1Gpio18FuncselPwm)
time.Sleep(10 * time.Microsecond)
// Disable channel 2
globalCtrl := bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Configure channel 2: serializer mode, use FIFO, SBIT=0 (low when idle)
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanCtrlOff)] =
(rp1PwmModeSerializer << rp1PwmChanCtrlModeBit) | (1 << rp1PwmChanCtrlUseFifoBit)
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanRangeOff)] = rp1Ws281xRange
bcm.pwmMem[pwmChanRegIdx(ch, rp1PwmChanPhaseOff)] = 0
time.Sleep(10 * time.Microsecond)
// Flush FIFO
bcm.pwmMem[rp1PwmFifoCtrl/4] = (1 << rp1PwmFifoFlushBit)
time.Sleep(10 * time.Microsecond)
// Trigger update for channel 2
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << rp1PwmGlobalSetUpdate)
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
time.Sleep(10 * time.Microsecond)
// Pre-fill FIFO before enabling channel
idx := 0
for idx < len(data) && uint32(idx) < rp1Ws281xFifoMax {
bcm.pwmMem[rp1PwmFifoPush/4] = data[idx]
idx++
}
// Lock OS thread for tight FIFO feeding
runtime.LockOSThread()
// Enable channel 2
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl |= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
// Push remaining data, polling FIFO level to avoid overflow
fifoLevelReg := rp1PwmFifoLevel / 4
fifoPushReg := rp1PwmFifoPush / 4
deadline := time.Now().Add(rp1Ws281xTimeout)
for idx < len(data) {
if time.Now().After(deadline) {
break
}
if bcm.pwmMem[fifoLevelReg] < rp1Ws281xFifoMax {
bcm.pwmMem[fifoPushReg] = data[idx]
idx++
}
}
// Wait for FIFO to drain
for bcm.pwmMem[fifoLevelReg] > 0 {
if time.Now().After(deadline) {
break
}
}
runtime.UnlockOSThread()
// Wait for last word to finish shifting out
time.Sleep(200 * time.Microsecond)
// Disable channel 2
globalCtrl = bcm.pwmMem[rp1PwmGlobalCtrl/4]
globalCtrl &^= (1 << (rp1PwmGlobalChanEnBit + ch))
bcm.pwmMem[rp1PwmGlobalCtrl/4] = globalCtrl
// Disconnect GPIO 18 from PWM to prevent residual noise on the data line
bcm.setGpioFuncsel(18, rp1GpioFuncselNull)
return nil
}
// buildWs281xStream constructs the complete FIFO word stream for both WS281x LEDs.
// Format: [reset padding] [top LED RGB] [edge LED RGB] [trailing zeros]
func (bcm *bcm2712) buildWs281xStream() []uint32 {
// Pre-allocate: 80 reset + 96 data (2 LEDs × 3 bytes × 16 words) + 2 trailing
words := make([]uint32, 0, rp1Ws281xResetWords+96+2)
// Reset padding (>50μs of low signal)
for i := 0; i < rp1Ws281xResetWords; i++ {
words = append(words, 0)
}
// Top LED (index 0) - RGB order (matches BCM2711 upstream)
words = appendByteWs281x(words, bcm.leds[0].Red)
words = appendByteWs281x(words, bcm.leds[0].Green)
words = appendByteWs281x(words, bcm.leds[0].Blue)
// Edge LED (index 1)
words = appendByteWs281x(words, bcm.leds[1].Red)
words = appendByteWs281x(words, bcm.leds[1].Green)
words = appendByteWs281x(words, bcm.leds[1].Blue)
// Trailing zeros for clean end-of-frame
words = append(words, 0, 0)
return words
}
// appendByteWs281x encodes one byte as 16 FIFO words (2 per data bit, MSB first) for RP1 WS281x.
func appendByteWs281x(words []uint32, b uint8) []uint32 {
for i := 7; i >= 0; i-- {
if (b>>uint(i))&1 == 0 {
words = append(words, rp1Ws281xBit0Word0, rp1Ws281xBit0Word1)
} else {
words = append(words, rp1Ws281xBit1Word0, rp1Ws281xBit1Word1)
}
}
return words
}
// GetTemperature returns the SoC temperature in degrees Celsius.
func (bcm *bcm2712) GetTemperature() (float64, error) {
f, err := os.Open(bcm2712ThermalZonePath)
if err != nil {
return -1, err
}
defer func() { _ = f.Close() }()
raw, err := io.ReadAll(f)
if err != nil {
return -1, err
}
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
if err != nil {
return -1, err
}
temp := float64(cpuTemp) / 1000.0
socTemperature.Set(temp)
return temp, nil
}

View File

@@ -3,14 +3,14 @@ package hal
import (
"context"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/stretchr/testify/mock"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
)
// fails if ComputeBladeHalMock does not implement ComputeBladeHal
var _ ComputeBladeHal = &ComputeBladeHalMock{}
// ComputeBladeMock implements a mock for the ComputeBladeHal interface
// ComputeBladeHalMock implements a mock for the ComputeBladeHal interface
type ComputeBladeHalMock struct {
mock.Mock
}
@@ -40,6 +40,11 @@ func (m *ComputeBladeHalMock) SetStealthMode(enabled bool) error {
return args.Error(0)
}
func (m *ComputeBladeHalMock) StealthModeActive() bool {
args := m.Called()
return args.Bool(0)
}
func (m *ComputeBladeHalMock) GetPowerStatus() (PowerStatus, error) {
args := m.Called()
return args.Get(0).(PowerStatus), args.Error(1)
@@ -50,7 +55,7 @@ func (m *ComputeBladeHalMock) WaitForEdgeButtonPress(ctx context.Context) error
return args.Error(0)
}
func (m *ComputeBladeHalMock) SetLed(idx uint, color led.Color) error {
func (m *ComputeBladeHalMock) SetLed(idx LedIndex, color led.Color) error {
args := m.Called(idx, color)
return args.Error(0)
}

178
pkg/hal/hal_rk3588.go Normal file
View File

@@ -0,0 +1,178 @@
//go:build linux && !tinygo
package hal
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"go.uber.org/zap"
)
const (
rk3588ThermalZonePath = "/sys/class/thermal/thermal_zone0/temp"
rk3588PwmFanHwmonName = "pwmfan"
)
// rk3588 implements the ComputeBladeHal interface for the Rockchip RK3588 (Radxa CM5).
// Fan control uses the kernel's pwmfan driver via sysfs. GPIO-dependent features
// (button, stealth hardware, PoE detection, LEDs, tachometer) are stubbed until the
// Rockchip-to-B2B-connector pin mapping is determined.
type rk3588 struct {
opts ComputeBladeHalOpts
pwmPath string // e.g. /sys/class/hwmon/hwmon8/pwm1
pwmEnablePath string // e.g. /sys/class/hwmon/hwmon8/pwm1_enable
stealthMode bool
}
// Compile-time interface check
var _ ComputeBladeHal = &rk3588{}
func newRk3588Hal(ctx context.Context, opts ComputeBladeHalOpts) (*rk3588, error) {
logger := log.FromContext(ctx)
pwmPath, err := findHwmonPwm(rk3588PwmFanHwmonName)
if err != nil {
return nil, fmt.Errorf("failed to find pwmfan hwmon device: %w", err)
}
enablePath := pwmPath + "_enable"
// Set manual control mode (1 = manual PWM control)
if err := os.WriteFile(enablePath, []byte("1"), 0644); err != nil {
return nil, fmt.Errorf("failed to set pwm1_enable to manual mode: %w", err)
}
computeModule.WithLabelValues("radxa-cm5").Set(1)
logger.Info("starting hal setup", zap.String("hal", "rk3588"))
logger.Warn("GPIO pin mapping unknown for RK3588 B2B connector — button, stealth hardware, PoE detection, LEDs, and tachometer are stubbed")
return &rk3588{
opts: opts,
pwmPath: pwmPath,
pwmEnablePath: enablePath,
}, nil
}
func (rk *rk3588) Run(ctx context.Context) error {
fanUnit.WithLabelValues("sysfs").Set(1)
<-ctx.Done()
return ctx.Err()
}
func (rk *rk3588) Close() error {
return nil
}
// SetFanSpeed sets the fan speed via sysfs pwm1 (0-100% mapped to 0-255).
func (rk *rk3588) SetFanSpeed(speed uint8) error {
fanTargetPercent.Set(float64(speed))
var pwmVal uint8
if speed == 0 {
pwmVal = 0
} else if speed >= 100 {
pwmVal = 255
} else {
pwmVal = uint8(float64(speed) * 255.0 / 100.0)
}
return os.WriteFile(rk.pwmPath, []byte(strconv.Itoa(int(pwmVal))), 0644)
}
// GetFanRPM returns 0 — no tachometer GPIO is mapped on the RK3588.
func (rk *rk3588) GetFanRPM() (float64, error) {
return 0, nil
}
// GetTemperature returns the SoC temperature in degrees Celsius.
func (rk *rk3588) GetTemperature() (float64, error) {
f, err := os.Open(rk3588ThermalZonePath)
if err != nil {
return -1, err
}
defer func() { _ = f.Close() }()
raw, err := io.ReadAll(f)
if err != nil {
return -1, err
}
cpuTemp, err := strconv.Atoi(strings.TrimSpace(string(raw)))
if err != nil {
return -1, err
}
temp := float64(cpuTemp) / 1000.0
socTemperature.Set(temp)
return temp, nil
}
// SetStealthMode tracks stealth mode in software only (no GPIO mapped).
func (rk *rk3588) SetStealthMode(enabled bool) error {
rk.stealthMode = enabled
if enabled {
stealthModeEnabled.Set(1)
} else {
stealthModeEnabled.Set(0)
}
return nil
}
func (rk *rk3588) StealthModeActive() bool {
return rk.stealthMode
}
// SetLed is a no-op — WS281x LED GPIO pin mapping is unknown on RK3588.
func (rk *rk3588) SetLed(idx LedIndex, color led.Color) error {
ledColorChangeEventCount.Inc()
return nil
}
// GetPowerStatus returns PowerPoeOrUsbC as a safe default (no PoE detection GPIO mapped).
func (rk *rk3588) GetPowerStatus() (PowerStatus, error) {
powerStatus.WithLabelValues(fmt.Sprint(PowerPoe802at)).Set(0)
powerStatus.WithLabelValues(fmt.Sprint(PowerPoeOrUsbC)).Set(1)
return PowerPoeOrUsbC, nil
}
// WaitForEdgeButtonPress blocks until context cancellation (no button GPIO mapped).
func (rk *rk3588) WaitForEdgeButtonPress(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
}
// findHwmonPwm scans /sys/class/hwmon/hwmon*/name for a device matching the given name
// and returns the path to its pwm1 file.
func findHwmonPwm(name string) (string, error) {
matches, err := filepath.Glob("/sys/class/hwmon/hwmon*/name")
if err != nil {
return "", fmt.Errorf("failed to glob hwmon devices: %w", err)
}
for _, namePath := range matches {
raw, err := os.ReadFile(namePath)
if err != nil {
continue
}
if strings.TrimSpace(string(raw)) == name {
dir := filepath.Dir(namePath)
pwmPath := filepath.Join(dir, "pwm1")
if _, err := os.Stat(pwmPath); err != nil {
return "", fmt.Errorf("found %s hwmon at %s but pwm1 does not exist: %w", name, dir, err)
}
return pwmPath, nil
}
}
return "", fmt.Errorf("no hwmon device found with name %q", name)
}

View File

@@ -1,28 +1,28 @@
//go:build linux
package hal
import (
"os"
"reflect"
"syscall"
"unsafe"
)
func mmap(file *os.File, base int64, lenght int) ([]uint32, []uint8, error) {
func mmap(file *os.File, base int64, length int) ([]uint32, []uint8, error) {
mem8, err := syscall.Mmap(
int(file.Fd()),
base,
lenght,
length,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED,
)
if err != nil {
return nil, nil, err
}
// We'll have to work with 32 bit registers, so let's convert it.
header := *(*reflect.SliceHeader)(unsafe.Pointer(&mem8))
header.Len /= (32 / 8)
header.Cap /= (32 / 8)
mem32 := *(*[]uint32)(unsafe.Pointer(&header))
// Convert []uint8 to []uint32 using unsafe.Slice
ptr := unsafe.Pointer(&mem8[0])
mem32 := unsafe.Slice((*uint32)(ptr), len(mem8)/4)
return mem32, mem8, nil
}

View File

@@ -1,4 +1,5 @@
//go:build !tinygo
package hal
import (
@@ -10,7 +11,7 @@ var (
fanTargetPercent = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "computeblade",
Name: "fan_target_percent",
Help: "Target fanspeed in percent",
Help: "Target fan speed in percent",
})
fanSpeed = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "computeblade",
@@ -29,7 +30,7 @@ var (
})
computeModule = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "computeblade",
Name: "compute_modul_present",
Name: "compute_module_present",
Help: "Compute module type",
}, []string{"type"})
ledColorChangeEventCount = promauto.NewCounter(prometheus.CounterOpts{

39
pkg/hal/platform_linux.go Normal file
View File

@@ -0,0 +1,39 @@
//go:build linux && !tinygo
package hal
import (
"context"
"fmt"
"os"
"strings"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"go.uber.org/zap"
)
const deviceTreeCompatiblePath = "/sys/firmware/devicetree/base/compatible"
// NewHal creates the appropriate HAL implementation based on the detected platform.
// It reads the device tree compatible string to determine whether the SoC is a
// BCM2711 (CM4/Pi 4) or BCM2712 (CM5/Pi 5).
func NewHal(ctx context.Context, opts ComputeBladeHalOpts) (ComputeBladeHal, error) {
compatible, err := os.ReadFile(deviceTreeCompatiblePath)
if err != nil {
return nil, fmt.Errorf("failed to read device tree compatible string: %w", err)
}
compatStr := string(compatible)
log.FromContext(ctx).Info("detected platform", zap.String("compatible", strings.ReplaceAll(compatStr, "\x00", ", ")))
switch {
case strings.Contains(compatStr, "bcm2712"):
return newBcm2712Hal(ctx, opts)
case strings.Contains(compatStr, "bcm2711"):
return newBcm2711Hal(ctx, opts)
case strings.Contains(compatStr, "rockchip,rk3588"):
return newRk3588Hal(ctx, opts)
default:
return nil, fmt.Errorf("unsupported platform: %s", strings.ReplaceAll(compatStr, "\x00", ", "))
}
}

View File

@@ -8,34 +8,41 @@ import (
"io"
"sync"
"github.com/uptime-induestries/compute-blade-agent/pkg/eventbus"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/log"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
"github.com/compute-blade-community/compute-blade-agent/pkg/events"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/log"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
"go.bug.st/serial"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
)
func SmartFanUnitPresent(ctx context.Context, portName string) (bool, error) {
// Open the serial port.
log.FromContext(ctx).Warn("Opening serial port")
log.FromContext(ctx).Info("Opening serial port")
rwc, err := serial.Open(portName, &serial.Mode{
BaudRate: smartfanunit.Baudrate,
BaudRate: smartfanunit.BaudRate,
})
if err != nil {
return false, err
}
log.FromContext(ctx).Warn("Opened serial port")
defer rwc.Close()
log.FromContext(ctx).Info("Opened serial port")
defer func(rwc serial.Port) {
err := rwc.Close()
if err != nil {
log.FromContext(ctx).WithError(err).Warn("Error while closing serial port")
}
}(rwc)
// Close reader after context is done
go func() {
<-ctx.Done()
log.FromContext(ctx).Warn("Closing serial port")
rwc.Close()
err := rwc.Close()
if err != nil {
log.FromContext(ctx).WithError(err).Warn("Error while closing serial port")
}
}()
// read byte after byte, matching it to the SOF header used by the smart fan unit protocol.
@@ -56,7 +63,7 @@ func SmartFanUnitPresent(ctx context.Context, portName string) (bool, error) {
func NewSmartFanUnit(portName string) (FanUnit, error) {
// Open the serial port.
rwc, err := serial.Open(portName, &serial.Mode{
BaudRate: smartfanunit.Baudrate,
BaudRate: smartfanunit.BaudRate,
})
if err != nil {
return nil, err
@@ -64,15 +71,15 @@ func NewSmartFanUnit(portName string) (FanUnit, error) {
return &smartFanUnit{
rwc: rwc,
eb: eventbus.New(),
eb: events.New(),
}, nil
}
var ErrCommunicationFailed = errors.New("communication failed")
//var ErrCommunicationFailed = errors.New("communication failed") // FIXME: still required or dead code?
const (
inboundTopic = "smartfanunit:inbound"
outboundTopic = "smartfanunit:outbound"
inboundTopic = "smartfanunit:inbound"
//outboundTopic = "smartfanunit:outbound" // FIXME: still required or dead code?
)
type smartFanUnit struct {
@@ -82,7 +89,7 @@ type smartFanUnit struct {
speed smartfanunit.FanSpeedRPMPacket
airflow smartfanunit.AirFlowTemperaturePacket
eb eventbus.EventBus
eb events.EventBus
}
func (fuc *smartFanUnit) Kind() FanUnitKind {
@@ -109,7 +116,7 @@ func (fuc *smartFanUnit) Run(parentCtx context.Context) error {
pkt, err := proto.ReadPacket(ctx, fuc.rwc)
if err != nil {
log.FromContext(ctx).Error("Failed to read packet from serial port", zap.Error(err))
log.FromContext(ctx).WithError(err).Error("Failed to read packet from serial port")
continue
}
fuc.eb.Publish(inboundTopic, pkt)
@@ -126,7 +133,7 @@ func (fuc *smartFanUnit) Run(parentCtx context.Context) error {
return nil
case pktAny := <-sub.C():
rawPkt := pktAny.(proto.Packet)
if err := fuc.speed.FromPacket(rawPkt); err != nil && err != proto.ErrChecksumMismatch {
if err := fuc.speed.FromPacket(rawPkt); err != nil && !errors.Is(err, proto.ErrChecksumMismatch) {
return err
}
fanSpeed.Set(float64(fuc.speed.RPM))
@@ -144,7 +151,7 @@ func (fuc *smartFanUnit) Run(parentCtx context.Context) error {
return nil
case pktAny := <-sub.C():
rawPkt := pktAny.(proto.Packet)
if err := fuc.airflow.FromPacket(rawPkt); err != nil && err != proto.ErrChecksumMismatch {
if err := fuc.airflow.FromPacket(rawPkt); err != nil && !errors.Is(err, proto.ErrChecksumMismatch) {
return err
}
airFlowTemperature.Set(float64(fuc.airflow.Temperature))

View File

@@ -5,9 +5,9 @@ import (
"errors"
"time"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/util"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
)
// LedEngine is the interface for controlling effects on the computeblade RGB LEDs
@@ -20,7 +20,7 @@ type LedEngine interface {
// ledEngineImpl is the implementation of the LedEngine interface
type ledEngineImpl struct {
ledIdx uint
ledIdx hal.LedIndex
restart chan struct{}
pattern BlinkPattern
hal hal.ComputeBladeHal
@@ -36,21 +36,21 @@ type BlinkPattern struct {
Delays []time.Duration
}
func mapBrighnessUint8(brightness float64) uint8 {
func mapBrightnessUint8(brightness float64) uint8 {
return uint8(255.0 * brightness)
}
func LedColorPurple(brightness float64) led.Color {
return led.Color{
Red: mapBrighnessUint8(brightness),
Red: mapBrightnessUint8(brightness),
Green: 0,
Blue: mapBrighnessUint8(brightness),
Blue: mapBrightnessUint8(brightness),
}
}
func LedColorRed(brightness float64) led.Color {
return led.Color{
Red: mapBrighnessUint8(brightness),
Red: mapBrightnessUint8(brightness),
Green: 0,
Blue: 0,
}
@@ -59,7 +59,7 @@ func LedColorRed(brightness float64) led.Color {
func LedColorGreen(brightness float64) led.Color {
return led.Color{
Red: 0,
Green: mapBrighnessUint8(brightness),
Green: mapBrightnessUint8(brightness),
Blue: 0,
}
}
@@ -101,17 +101,14 @@ func NewSlowBlinkPattern(baseColor led.Color, activeColor led.Color) BlinkPatter
}
}
// LedEngineOpts are the options for the LedEngine
type LedEngineOpts struct {
// LedIdx is the index of the LED to control
LedIdx uint
// Hal is the computeblade hardware abstraction layer
Hal hal.ComputeBladeHal
// Clock is the clock used for timing
Clock util.Clock
func New(hal hal.ComputeBladeHal, ledIdx hal.LedIndex) LedEngine {
return NewLedEngine(Options{
Hal: hal,
LedIdx: ledIdx,
})
}
func NewLedEngine(opts LedEngineOpts) *ledEngineImpl {
func NewLedEngine(opts Options) LedEngine {
clock := opts.Clock
if clock == nil {
clock = util.RealClock{}
@@ -119,7 +116,7 @@ func NewLedEngine(opts LedEngineOpts) *ledEngineImpl {
return &ledEngineImpl{
ledIdx: opts.LedIdx,
hal: opts.Hal,
restart: make(chan struct{}), // restart channel controls cancelation of any pattern
restart: make(chan struct{}), // restart channel controls cancellation of any pattern
pattern: NewStaticPattern(led.Color{}), // Turn off LEDs by default
clock: clock,
}

View File

@@ -8,11 +8,11 @@ import (
"testing"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/ledengine"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/ledengine"
"github.com/uptime-induestries/compute-blade-agent/pkg/util"
)
func TestNewStaticPattern(t *testing.T) {
@@ -113,33 +113,9 @@ func TestNewBurstPattern(t *testing.T) {
}
}
func TestNewSlowBlinkPattern(t *testing.T) {
type args struct {
baseColor led.Color
activeColor led.Color
}
tests := []struct {
name string
args args
want ledengine.BlinkPattern
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ledengine.NewSlowBlinkPattern(tt.args.baseColor, tt.args.activeColor); !reflect.DeepEqual(
got,
tt.want,
) {
t.Errorf("NewSlowledengine.BlinkPattern() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewLedEngine(t *testing.T) {
t.Parallel()
engine := ledengine.LedEngineOpts{
engine := ledengine.Options{
Clock: util.RealClock{},
LedIdx: 0,
Hal: &hal.ComputeBladeHalMock{},
@@ -147,6 +123,17 @@ func TestNewLedEngine(t *testing.T) {
assert.NotNil(t, engine)
}
func TestLedEngine_NewLedEngineWithoutClock(t *testing.T) {
opts := ledengine.Options{
Clock: nil,
LedIdx: 0,
Hal: &hal.ComputeBladeHalMock{},
}
engine := ledengine.NewLedEngine(opts)
assert.NotNil(t, engine)
}
func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
t.Parallel()
@@ -155,10 +142,10 @@ func Test_LedEngine_SetPattern_WhileRunning(t *testing.T) {
clk.On("After", time.Hour).Times(2).Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
opts := ledengine.LedEngineOpts{
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
@@ -202,9 +189,9 @@ func Test_LedEngine_SetPattern_BeforeRun(t *testing.T) {
clk.On("After", time.Hour).Once().Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
opts := ledengine.LedEngineOpts{
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
@@ -244,10 +231,10 @@ func Test_LedEngine_SetPattern_SetLedFailureInPattern(t *testing.T) {
clk.On("After", time.Hour).Once().Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
call0 := cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
cbMock.On("SetLed", uint(0), led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(errors.New("failure")).NotBefore(call0)
call0 := cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(nil)
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 0}).Once().Return(errors.New("failure")).NotBefore(call0)
opts := ledengine.LedEngineOpts{
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
@@ -278,3 +265,29 @@ func Test_LedEngine_SetPattern_SetLedFailureInPattern(t *testing.T) {
clk.AssertExpectations(t)
cbMock.AssertExpectations(t)
}
func Test_LedEngine_SetPattern_NoDelay(t *testing.T) {
t.Parallel()
clk := util.MockClock{}
clkAfterChan := make(chan time.Time)
clk.On("After", time.Hour).Once().Return(clkAfterChan)
cbMock := hal.ComputeBladeHalMock{}
cbMock.On("SetLed", hal.LedTop, led.Color{Green: 0, Blue: 0, Red: 255}).Once().Return(nil)
opts := ledengine.Options{
Hal: &cbMock,
Clock: &clk,
LedIdx: 0,
}
engine := ledengine.NewLedEngine(opts)
invalidPattern := ledengine.NewStaticPattern(led.Color{Red: 255})
invalidPattern.Delays = []time.Duration{}
// We want to change the pattern BEFORE the engine is started
t.Log("Setting pattern")
err := engine.SetPattern(invalidPattern)
assert.Error(t, err)
assert.ErrorContains(t, err, "pattern must have at least one delay")
}

16
pkg/ledengine/options.go Normal file
View File

@@ -0,0 +1,16 @@
package ledengine
import (
"github.com/compute-blade-community/compute-blade-agent/pkg/hal"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
)
// Options are the options for the LedEngine
type Options struct {
// LedIdx is the index of the LED to control
LedIdx hal.LedIndex
// Hal is the computeblade hardware abstraction layer
Hal hal.ComputeBladeHal
// Clock is the clock used for timing
Clock util.Clock
}

View File

@@ -0,0 +1,50 @@
package log
import (
"context"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
"github.com/spechtlabs/go-otel-utils/otelzap"
"go.uber.org/zap"
)
// InterceptorLogger adapts zap logger to interceptor logger.
// This code is simple enough to be copied and not imported.
func InterceptorLogger(l *otelzap.Logger) logging.Logger {
return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
f := make([]zap.Field, 0, len(fields)/2)
for i := 0; i < len(fields); i += 2 {
key := fields[i]
value := fields[i+1]
switch v := value.(type) {
case string:
f = append(f, zap.String(key.(string), v))
case int:
f = append(f, zap.Int(key.(string), v))
case bool:
f = append(f, zap.Bool(key.(string), v))
case zap.Field:
f = append(f, v)
default:
f = append(f, zap.Any(key.(string), v))
}
}
logger := l.WithOptions(zap.AddCallerSkip(4)).With(f...)
switch lvl {
case logging.LevelDebug:
logger.Debug(msg)
case logging.LevelInfo:
logger.Info(msg)
case logging.LevelWarn:
logger.Warn(msg)
case logging.LevelError:
logger.Error(msg)
default:
logger.Warn(msg, zap.String("error", "unknown level"), zap.Int("level", int(lvl)))
}
})
}

View File

@@ -3,20 +3,22 @@ package log
import (
"context"
"github.com/spechtlabs/go-otel-utils/otelzap"
"go.uber.org/zap"
)
type logCtxKey int
func IntoContext(ctx context.Context, logger *zap.Logger) context.Context {
func IntoContext(ctx context.Context, logger *otelzap.Logger) context.Context {
return context.WithValue(ctx, logCtxKey(0), logger)
}
func FromContext(ctx context.Context) *zap.Logger {
func FromContext(ctx context.Context) *otelzap.Logger {
val := ctx.Value(logCtxKey(0))
if val != nil {
return val.(*zap.Logger)
return val.(*otelzap.Logger)
}
zap.L().Warn("No logger in context, passing default")
return zap.L()
otelzap.L().WithOptions(zap.AddCallerSkip(1)).Warn("No logger in context, passing default")
return otelzap.L()
}

View File

@@ -3,19 +3,29 @@ package smartfanunit
import (
"errors"
"github.com/uptime-induestries/compute-blade-agent/pkg/hal/led"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
"github.com/compute-blade-community/compute-blade-agent/pkg/hal/led"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
)
// Blade -> FanUnit communication
const (
// Blade -> FanUnit
// CmdSetFanSpeedPercent sets the fan speed as a percentage, sent from the blade to the fan unit.
CmdSetFanSpeedPercent proto.Command = 0x01
CmdSetLED proto.Command = 0x02
// FanUnit -> Blade, sent in regular intervals
NotifyButtonPress proto.Command = 0xa1
// CmdSetLED represents the command to set the LED color, sent from the blade to the fan unit.
CmdSetLED proto.Command = 0x02
)
// FanUnit -> Blade, sent in regular intervals
const (
// NotifyButtonPress represents a command sent from the fan unit to indicate a button press event.
NotifyButtonPress proto.Command = 0xa1
// NotifyAirFlowTemperature represents a command sent from the fan unit to report the current air flow temperature.
NotifyAirFlowTemperature proto.Command = 0xa2
NotifyFanSpeedRPM proto.Command = 0xa3
// NotifyFanSpeedRPM is a command used to report the current fan speed in RPM from the fan unit to the blade.
NotifyFanSpeedRPM proto.Command = 0xa3
)
var ErrInvalidCommand = errors.New("invalid command")
@@ -93,7 +103,7 @@ type AirFlowTemperaturePacket struct {
func (p *AirFlowTemperaturePacket) Packet() proto.Packet {
return proto.Packet{
Command: NotifyAirFlowTemperature,
Data: proto.Data(float32To24Bit(p.Temperature)),
Data: float32To24Bit(p.Temperature),
}
}

View File

@@ -1,4 +1,4 @@
// This is a driver for the EMC2101 fan controller
// Package emc2101 is a driver for the EMC2101 fan controller
// Based on https://ww1.microchip.com/downloads/en/DeviceDoc/2101.pdf
package emc2101
@@ -57,7 +57,6 @@ func (e *emc2101) updateReg(regAddr, setMask, clearMask uint8) error {
return nil
}
return e.bus.Tx(e.Address, []byte{regAddr, toWrite}, nil)
}
@@ -74,10 +73,10 @@ func (e *emc2101) Init() error {
}
/*
0x3 0b100
0x4b 0b11111
0x4a 0b100000
0x4a 0b100000
0x3 0b100
0x4b 0b11111
0x4a 0b100000
0x4a 0b100000
*/
// Configure fan spin up to ignore tach input
@@ -126,7 +125,7 @@ func (e *emc2101) FanRPM() (float32, error) {
return 0, err
}
var tachCount int = int(high[0])<<8 | int(low[0])
var tachCount = int(high[0])<<8 | int(low[0])
return float32(5400000) / float32(tachCount), nil
}

View File

@@ -1,6 +1,6 @@
package smartfanunit
import "github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
import "github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
func float32To24Bit(val float32) proto.Data {
// Convert float32 to number with 3 bytes (0.1 precision)

View File

@@ -1,4 +1,5 @@
//go:build !tinygo
package smartfanunit
import (

View File

@@ -78,7 +78,7 @@ func WritePacket(_ context.Context, w io.Writer, packet Packet) error {
// ReadPacket reads a packet from an io.Reader with escaping.
// This is blocking and drops invalid bytes until a valid packet is received.
func ReadPacket(ctx context.Context, r io.Reader) (Packet, error) {
buffer := []uint8{}
var buffer []uint8
started := false
escaped := false

View File

@@ -5,8 +5,8 @@ import (
"context"
"testing"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
"github.com/stretchr/testify/assert"
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
)
func TestWritePacket(t *testing.T) {

View File

@@ -1,11 +1,11 @@
package smartfanunit
import (
"github.com/uptime-induestries/compute-blade-agent/pkg/smartfanunit/proto"
"github.com/compute-blade-community/compute-blade-agent/pkg/smartfanunit/proto"
)
const (
Baudrate = 115200
BaudRate = 115200
)
func MatchCmd(cmd proto.Command) func(any) bool {

72
pkg/util/clock_test.go Normal file
View File

@@ -0,0 +1,72 @@
package util_test
import (
"testing"
"time"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
"github.com/stretchr/testify/assert"
)
// TestRealClock_Now ensures that RealClock.Now() returns a time close to the actual time.
func TestRealClock_Now(t *testing.T) {
rc := util.RealClock{}
before := time.Now()
got := rc.Now()
after := time.Now()
if got.Before(before) || got.After(after) {
t.Errorf("RealClock.Now() = %v, want between %v and %v", got, before, after)
}
}
// TestRealClock_After ensures that RealClock.After() returns a channel that sends after the given duration.
func TestRealClock_After(t *testing.T) {
rc := util.RealClock{}
delay := 50 * time.Millisecond
start := time.Now()
ch := rc.After(delay)
<-ch
elapsed := time.Since(start)
if elapsed < delay {
t.Errorf("RealClock.After(%v) triggered too early after %v", delay, elapsed)
}
}
// TestMockClock_Now tests that MockClock.Now() returns the expected time and records the call.
func TestMockClock_Now(t *testing.T) {
mockClock := new(util.MockClock)
expectedTime := time.Date(2025, time.June, 6, 12, 0, 0, 0, time.UTC)
mockClock.On("Now").Return(expectedTime)
actualTime := mockClock.Now()
assert.Equal(t, expectedTime, actualTime)
mockClock.AssertCalled(t, "Now")
mockClock.AssertExpectations(t)
}
// TestMockClock_After tests that MockClock.After() returns the expected channel and records the call.
func TestMockClock_After(t *testing.T) {
mockClock := new(util.MockClock)
duration := 100 * time.Millisecond
expectedChan := make(chan time.Time, 1)
expectedTime := time.Now().Add(duration)
expectedChan <- expectedTime
mockClock.On("After", duration).Return(expectedChan)
resultChan := mockClock.After(duration)
select {
case result := <-resultChan:
assert.WithinDuration(t, expectedTime, result, time.Second)
case <-time.After(time.Second):
t.Fatal("timeout waiting for result from MockClock.After")
}
mockClock.AssertCalled(t, "After", duration)
mockClock.AssertExpectations(t)
}

View File

@@ -0,0 +1,25 @@
package util_test
import (
"os"
"testing"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestFileExists(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "fileexists-test")
assert.NoError(t, err)
// It should exist
assert.True(t, util.FileExists(tmpFile.Name()), "Expected file to exist")
// Close and remove the file
assert.NoError(t, tmpFile.Close())
assert.NoError(t, os.Remove(tmpFile.Name()))
// It should not exist anymore
assert.False(t, util.FileExists(tmpFile.Name()), "Expected file not to exist")
}

9
pkg/util/file_exists.go Normal file
View File

@@ -0,0 +1,9 @@
package util
import "os"
// FileExists checks if a file exists at the given path and returns true if it does, false otherwise.
func FileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

43
pkg/util/host_ips.go Normal file
View File

@@ -0,0 +1,43 @@
package util
import "net"
func GetHostIPs() ([]net.IP, error) {
var ips []net.IP
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, iface := range ifaces {
// Skip down or loopback interfaces
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue // skip interfaces we can't read
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
// Skip loopback or unspecified
if ip == nil || ip.IsLoopback() || ip.IsUnspecified() {
continue
}
ips = append(ips, ip)
}
}
return ips, nil
}

18
pkg/util/host_ips_test.go Normal file
View File

@@ -0,0 +1,18 @@
package util_test
import (
"testing"
"github.com/compute-blade-community/compute-blade-agent/pkg/util"
"github.com/stretchr/testify/assert"
)
func TestGetHostIPs_ReturnsNonLoopbackIPs(t *testing.T) {
ips, err := util.GetHostIPs()
assert.NoError(t, err)
for _, ip := range ips {
assert.False(t, ip.IsLoopback(), "Should not return loopback IPs")
assert.False(t, ip.IsUnspecified(), "Should not return unspecified IPs")
}
}

22
renovate.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"commitMessagePrefix": "chore(deps):",
"packageRules": [
{
"description": "Automatically merge minor and patch-level updates",
"matchUpdateTypes": [
"patch",
"bump",
"minor"
],
"automerge": true,
"automergeType": "branch"
}
],
"ignoreDeps": [
"github.com/warthog618/gpiod"
]
}

115
scripts/find-tach-gpio.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/bin/bash
# find-tach-gpio.sh - Safely probe GPIOs to find fan tachometer signal
# Run on Radxa node with fan at 100%: sudo ./find-tach-gpio.sh
#
# The tachometer generates 2 pulses per revolution. At 5000 RPM:
# 5000 RPM / 60 = 83.33 RPS * 2 pulses = ~167 Hz
# At 3000 RPM: ~100 Hz
# We look for any GPIO showing periodic edge events.
set -e
PROBE_DURATION=1 # seconds to monitor each GPIO
MIN_EVENTS=10 # minimum events to consider "active" (10 events in 1s = 600 RPM minimum)
DELAY_BETWEEN=0.5 # seconds between probes to let system settle
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "=== Fan Tachometer GPIO Finder ==="
echo ""
# Check if running as root (needed for gpiomon)
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root (sudo)${NC}"
exit 1
fi
# Check fan speed
FAN_PWM=$(cat /sys/class/hwmon/hwmon8/pwm1 2>/dev/null || echo "unknown")
echo "Current fan PWM: $FAN_PWM (should be 255 for best detection)"
if [ "$FAN_PWM" != "255" ]; then
echo -e "${YELLOW}Setting fan to 100% for detection...${NC}"
echo 255 > /sys/class/hwmon/hwmon8/pwm1
sleep 2 # Let fan spin up
fi
echo ""
# Get list of gpiochips
CHIPS=$(ls /dev/gpiochip* | sed 's|/dev/||')
echo "Scanning GPIO chips: $CHIPS"
echo "Probe duration: ${PROBE_DURATION}s per line, minimum ${MIN_EVENTS} events to flag"
echo ""
FOUND_CANDIDATES=""
for chip in $CHIPS; do
# Get number of lines for this chip
NUM_LINES=$(gpioinfo $chip 2>/dev/null | wc -l)
NUM_LINES=$((NUM_LINES - 1)) # Subtract header line
if [ "$NUM_LINES" -le 0 ]; then
continue
fi
echo -e "${YELLOW}=== Scanning $chip ($NUM_LINES lines) ===${NC}"
for line in $(seq 0 $((NUM_LINES - 1))); do
# Check if line is already in use
LINE_INFO=$(gpioinfo $chip 2>/dev/null | grep "line *$line:" || true)
if echo "$LINE_INFO" | grep -q "\[used\]"; then
# Skip lines that are in use
continue
fi
# Probe the line
printf " Line %2d: " "$line"
# Run gpiomon with timeout, count events
EVENTS=$(timeout ${PROBE_DURATION}s gpiomon --num-events=100 $chip $line 2>&1 | wc -l || echo "0")
if [ "$EVENTS" -ge "$MIN_EVENTS" ]; then
# Calculate approximate frequency
FREQ=$((EVENTS / PROBE_DURATION))
RPM_ESTIMATE=$((FREQ * 60 / 2)) # 2 pulses per revolution
echo -e "${GREEN}ACTIVE! $EVENTS events (~${FREQ} Hz, ~${RPM_ESTIMATE} RPM)${NC}"
FOUND_CANDIDATES="$FOUND_CANDIDATES\n $chip line $line: $EVENTS events (~${RPM_ESTIMATE} RPM)"
elif [ "$EVENTS" -gt 0 ]; then
echo "$EVENTS events (noise?)"
else
echo "no events"
fi
# Small delay to let system settle
sleep $DELAY_BETWEEN
done
echo ""
done
echo "=== Scan Complete ==="
if [ -n "$FOUND_CANDIDATES" ]; then
echo -e "${GREEN}Candidate tachometer GPIOs found:${NC}"
echo -e "$FOUND_CANDIDATES"
echo ""
echo "To verify, try monitoring the candidate with varying fan speeds:"
echo " gpiomon --num-events=50 <chip> <line>"
echo ""
echo "Then add to device tree or HAL configuration."
else
echo -e "${YELLOW}No active GPIOs found.${NC}"
echo "Possible reasons:"
echo " - Fan tachometer not connected on this carrier board"
echo " - Tachometer uses a different interface (I2C, ADC, etc.)"
echo " - Fan doesn't have tachometer wire connected"
fi
# Restore fan to auto if we changed it
if [ "$FAN_PWM" != "255" ] && [ "$FAN_PWM" != "unknown" ]; then
echo ""
echo "Restoring fan PWM to $FAN_PWM"
echo "$FAN_PWM" > /sys/class/hwmon/hwmon8/pwm1
fi