From a757bd76154717d4bb16f92ad21a9ea626a8e0ba Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 11 Feb 2026 10:30:17 -0600 Subject: [PATCH] Optimize CI caching for Blacksmith runners, fix SNMP bulk walk on Mikrotik - Dockerfile: add BuildKit cache mounts for Go module and build caches, pin both stages to Alpine 3.23 - CI: replace BUILDKIT_INLINE_CACHE with registry-based layer caching (mode=max) for branch and release builds - .dockerignore: exclude test files, Rust legacy, nix, and other non-build artifacts from Docker context - SNMP: lower MaxRepetitions from default 50 to 10 to prevent Mikrotik routers from timing out during bulk walks --- .dockerignore | 13 +++++++++++++ .github/workflows/ci.yml | 7 ++++--- Dockerfile | 11 +++++++---- snmp.go | 32 ++++++++++++++++++++++++-------- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.dockerignore b/.dockerignore index d77c7e1..a762d2c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,24 @@ .git/ +.github/ .gitignore +.direnv/ +.envrc +.cargo/ +.claude/ +.conductor/ Dockerfile .dockerignore README.md +docker-compose.example.yml +flake.nix +flake.lock +cover.out +coverage/ +proto/ *.db *.db-shm *.db-wal +*_test.go src/ native/ target/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 794c63a..df8aa73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,8 +86,8 @@ jobs: context: . push: true provenance: false - build-args: | - BUILDKIT_INLINE_CACHE=1 + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.branch.outputs.name }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} @@ -170,7 +170,8 @@ jobs: provenance: false build-args: | VERSION=${{ needs.version.outputs.version }} - BUILDKIT_INLINE_CACHE=1 + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.arch }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.arch }},mode=max tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.version.outputs.version }}-${{ matrix.arch }} docker.io/${{ env.DOCKERHUB_IMAGE }}:${{ needs.version.outputs.version }}-${{ matrix.arch }} diff --git a/Dockerfile b/Dockerfile index 7f8ebeb..080341e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,15 @@ -FROM golang:1.25-alpine AS builder +FROM golang:1.25-alpine3.23 AS builder WORKDIR /app COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download COPY . . ARG VERSION=dev -RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION}" -o towerops-agent . +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION}" -o towerops-agent . -FROM alpine:3.21 +FROM alpine:3.23 RUN apk add --no-cache ca-certificates iputils COPY --from=builder /app/towerops-agent /usr/local/bin/towerops-agent RUN adduser -D -u 1000 towerops && chown towerops /usr/local/bin/towerops-agent diff --git a/snmp.go b/snmp.go index f059ea0..c6af8e5 100644 --- a/snmp.go +++ b/snmp.go @@ -10,6 +10,21 @@ import ( "github.com/towerops-app/towerops-agent/pb" ) +// snmpQuerier abstracts SNMP operations for testability. +type snmpQuerier interface { + Get(oids []string) (*gosnmp.SnmpPacket, error) + BulkWalkAll(rootOid string) ([]gosnmp.SnmpPDU, error) +} + +// snmpDial connects to an SNMP device and returns a querier + close function. +var snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) { + conn, err := newSnmpConn(dev) + if err != nil { + return nil, nil, err + } + return conn, func() { _ = conn.Conn.Close() }, nil +} + // executeSnmpJob runs SNMP GET/WALK queries for a job and sends results. func executeSnmpJob(job *pb.AgentJob, resultCh chan<- *pb.SnmpResult) { dev := job.SnmpDevice @@ -18,12 +33,12 @@ func executeSnmpJob(job *pb.AgentJob, resultCh chan<- *pb.SnmpResult) { return } - conn, err := newSnmpConn(dev) + conn, closeFn, err := snmpDial(dev) if err != nil { slog.Error("snmp connect", "job_id", job.JobId, "device", dev.Ip, "error", err) return } - defer func() { _ = conn.Conn.Close() }() + defer closeFn() oidValues := make(map[string]string) @@ -85,7 +100,7 @@ func executeCredentialTest(job *pb.AgentJob, resultCh chan<- *pb.CredentialTestR return } - conn, err := newSnmpConn(dev) + conn, closeFn, err := snmpDial(dev) timestamp := time.Now().Unix() if err != nil { @@ -97,7 +112,7 @@ func executeCredentialTest(job *pb.AgentJob, resultCh chan<- *pb.CredentialTestR } return } - defer func() { _ = conn.Conn.Close() }() + defer closeFn() result, err := conn.Get([]string{"1.3.6.1.2.1.1.1.0"}) if err != nil { @@ -126,10 +141,11 @@ func executeCredentialTest(job *pb.AgentJob, resultCh chan<- *pb.CredentialTestR // newSnmpConn creates a gosnmp.GoSNMP connection from protobuf device config. func newSnmpConn(dev *pb.SnmpDevice) (*gosnmp.GoSNMP, error) { conn := &gosnmp.GoSNMP{ - Target: dev.Ip, - Port: uint16(dev.Port), - Timeout: 10 * time.Second, - Retries: 2, + Target: dev.Ip, + Port: uint16(dev.Port), + Timeout: 10 * time.Second, + Retries: 2, + MaxRepetitions: 10, } // Transport