diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index f9ecc83..d84b91f 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -13,11 +13,11 @@ jobs: - name: checkout main branch uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: - go-version: 1.22 + go-version: '1.25' - - run: go test -coverprofile=cover.out ./... + - run: go test -v -race -coverprofile=cover.out ./... - name: checkout covrage branch uses: actions/checkout@v4 diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 34dc084..9e42913 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -12,19 +12,25 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: extractions/setup-just@v2 - - run: just lint + - uses: actions/setup-go@v5 + with: + go-version: '1.25' + - uses: golangci/golangci-lint-action@v7 + with: + version: v2.6.1 test: name: Unit Tests runs-on: ubuntu-latest strategy: matrix: - go: ["1.21", "1.22"] + go: ["1.24", "1.25"] steps: - uses: actions/checkout@v4 - - uses: extractions/setup-just@v2 - - run: GO_VERSION=${{ matrix.go }} just test + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + - run: go test -race ./... inttest: name: Integration Tests diff --git a/.gitignore b/.gitignore index 3626c4d..3a8c3c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/ confd-basic*.zip out +cover.out +*.test diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..3e88561 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,28 @@ +version: "2" +linters: + enable: + - errorlint + settings: + errcheck: + check-type-assertions: true + check-blank: true + errorlint: + errorf: true + asserts: true + comparison: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile index 3296ae5..760195b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -ARG GO_VER=1.20 -ARG GOLANGCI_VER=1.51 +ARG GO_VER=1.24 FROM golang:${GO_VER} as base WORKDIR /src @@ -7,22 +6,6 @@ COPY go.mod go.sum /src/ RUN --mount=type=cache,target=/go/pkg/mod \ go mod download -x -FROM golangci/golangci-lint:v${GOLANGCI_VER}-alpine AS lint-base - -FROM base AS lint -RUN --mount=target=. \ - --mount=from=lint-base,src=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \ - --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - --mount=type=cache,target=/root/.cache/golangci-lint \ - golangci-lint run --timeout 10m0s ./... - -FROM base as unittest -RUN --mount=target=/src \ - --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - mkdir /out && \ - go test -v -race -coverprofile=/out/cover.out ./... | tee /out/test.stdout FROM base AS inttest RUN apt update && apt install -y \ @@ -37,8 +20,4 @@ RUN --mount=target=/src \ go test --tags=inttest -c -o /out/inttest.test WORKDIR /out COPY inttest/wait-for-hello.sh . -CMD ./inttest.test -test.v -test.race - - -FROM scratch AS unittest-coverage -COPY --from=unittest /out / \ No newline at end of file +CMD ./inttest.test -test.v -test.race \ No newline at end of file diff --git a/README.md b/README.md index a2fd730..cc81cdd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This library is used to create client applications for connecting to network devices via NETCONF. -Like Go itself, only the latest two Go versions are tested and supported (Go 1.24 or Go 1.23). +Like Go itself, only the latest two Go versions are tested and supported (Go 1.24 or Go 1.26). NOTICE: The API is pretty stable but not finalized yet. So changes may happen before a 1.0 release. Check for API changes when you upgrade. diff --git a/example_ssh_test.go b/example_ssh_test.go index 6f6924f..544f717 100644 --- a/example_ssh_test.go +++ b/example_ssh_test.go @@ -5,9 +5,9 @@ import ( "log" "time" + "golang.org/x/crypto/ssh" "nemith.io/netconf" ncssh "nemith.io/netconf/transport/ssh" - "golang.org/x/crypto/ssh" ) const sshAddr = "myrouter.example.com:830" @@ -27,7 +27,7 @@ func Example_ssh() { if err != nil { panic(err) } - defer transport.Close() + defer transport.Close() // nolint:errcheck session, err := netconf.Open(transport) if err != nil { diff --git a/example_tls_test.go b/example_tls_test.go index ee91fc0..c7c9ab7 100644 --- a/example_tls_test.go +++ b/example_tls_test.go @@ -55,13 +55,13 @@ func Example_tls() { if err != nil { panic(err) } - defer transport.Close() + defer transport.Close() // nolint:errcheck session, err := netconf.Open(transport) if err != nil { panic(err) } - defer session.Close(context.Background()) + defer session.Close(context.Background()) // nolint:errcheck // timeout for the call itself. ctx, cancel = context.WithTimeout(ctx, 5*time.Second) diff --git a/go.mod b/go.mod index 0bf8888..a6824de 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,16 @@ module nemith.io/netconf -go 1.23.0 - -toolchain go1.24.1 +go 1.24.10 require ( - github.com/stretchr/testify v1.11.0 - golang.org/x/crypto v0.34.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.45.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/sys v0.30.0 // indirect + golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 + golang.org/x/sys v0.38.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f835c5a..7f59f97 100644 --- a/go.sum +++ b/go.sum @@ -2,16 +2,16 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA= -golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= +golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= +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/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/inttest/README.md b/inttest/README.md index 44d1654..1824a31 100644 --- a/inttest/README.md +++ b/inttest/README.md @@ -88,6 +88,6 @@ CONFD_IMAGE=conf-basic:7.8 just ceos Netopeer2 is an opensource NETCONF server. A docker image is automaticall built when running the tests so no additional work is needed. ```plain -cd inttent +cd inttest just netopeer2 ``` diff --git a/inttest/justfile b/inttest/justfile index 18ae056..6cf6728 100644 --- a/inttest/justfile +++ b/inttest/justfile @@ -17,7 +17,7 @@ args := '\ --exit-code-from inttest ' -all: csrx ceos confd netopeer2 +all: csrx ceos confd csrx: #!/usr/bin/env bash diff --git a/justfile b/justfile index 763e3bc..13c02ce 100644 --- a/justfile +++ b/justfile @@ -5,26 +5,14 @@ pwd := justfile_directory() @list: just --list -golangci_version := "1.51" -go_version := env_var_or_default("GO_VERSION", "1.20") - test *args: - docker buildx build \ - --build-arg GO_VER={{ go_version }} \ - --output out/test \ - --target unittest-coverage . \ - {{ args }} - cat out/test/test.stdout + go test -v -race ./... {{ args }} lint *args: - @docker buildx build \ - --build-arg GO_VER={{ go_version }} \ - --build-arg GOLANGCI_VER={{ golangci_version }} \ - --target lint . \ - {{ args }} + golangci-lint run --timeout 10m0s ./... {{ args }} inttest: just inttest/all clean: - @rm -rf ./out \ No newline at end of file + @rm -rf ./out diff --git a/ops.go b/ops.go index 6fdf571..b7ed6cf 100644 --- a/ops.go +++ b/ops.go @@ -322,7 +322,7 @@ type LockReq struct { func (s *Session) Lock(ctx context.Context, target Datastore) error { req := LockReq{ - XMLName: xml.Name{Local: "lock"}, + XMLName: xml.Name{Space: "urn:ietf:params:xml:ns:netconf:base:1.0", Local: "lock"}, Target: target, } @@ -332,7 +332,7 @@ func (s *Session) Lock(ctx context.Context, target Datastore) error { func (s *Session) Unlock(ctx context.Context, target Datastore) error { req := LockReq{ - XMLName: xml.Name{Local: "unlock"}, + XMLName: xml.Name{Space: "urn:ietf:params:xml:ns:netconf:base:1.0", Local: "unlock"}, Target: target, } diff --git a/ops_test.go b/ops_test.go index b3879ee..79097a4 100644 --- a/ops_test.go +++ b/ops_test.go @@ -339,7 +339,7 @@ func TestLock(t *testing.T) { { target: Candidate, matches: []*regexp.Regexp{ - regexp.MustCompile(`\S*\S*\S*\S*`), + regexp.MustCompile(`\S*\S*\S*\S*`), }, }, } @@ -373,7 +373,7 @@ func TestUnlock(t *testing.T) { { target: Candidate, matches: []*regexp.Regexp{ - regexp.MustCompile(`\S*\S*\S*\S*`), + regexp.MustCompile(`\S*\S*\S*\S*`), }, }, } diff --git a/session.go b/session.go index cedb286..0ea1fb3 100644 --- a/session.go +++ b/session.go @@ -94,7 +94,7 @@ func Open(transport transport.Transport, opts ...SessionOption) (*Session, error // this needs a timeout of some sort. if err := s.handshake(); err != nil { - s.tr.Close() + s.tr.Close() // nolint:errcheck // TODO: catch and log err return nil, err } @@ -115,8 +115,7 @@ func (s *Session) handshake() error { if err != nil { return err } - // TODO: capture this error some how (ah defer and errors) - defer r.Close() + defer r.Close() // nolint:errcheck // TODO: catch and log err var serverMsg helloMsg if err := xml.NewDecoder(r).Decode(&serverMsg); err != nil { @@ -188,7 +187,7 @@ func (s *Session) recvMsg() error { if err != nil { return err } - defer r.Close() + defer r.Close() // nolint:errcheck // TODO: catch error and log dec := xml.NewDecoder(r) root, err := startElement(dec) @@ -379,7 +378,7 @@ func (s *Session) Close(ctx context.Context) error { } } - if callErr != io.EOF { + if !errors.Is(callErr, io.EOF) { return callErr } diff --git a/transport/frame.go b/transport/frame.go index 9fdabce..208089b 100644 --- a/transport/frame.go +++ b/transport/frame.go @@ -219,7 +219,9 @@ func (r *chunkReader) Read(p []byte) (int, error) { return 0, ErrInvalidIO } // make sure we can't try to read more than the max chunk - p = p[:maxChunk] + if len(p) > maxChunk { + p = p[:maxChunk] + } // done with existing chunk so grab the next one if r.chunkLeft <= 0 { @@ -271,10 +273,10 @@ func (r *chunkReader) Close() error { // readHeader return io.EOF when it encounter the end-of-frame // marker ("\n##\n") err := r.readHeader() - switch err { - case nil: + switch { + case err == nil: break - case io.EOF: + case errors.Is(err, io.EOF): return nil default: return err @@ -381,7 +383,7 @@ func (r *eomReader) Close() error { var err error for err == nil { _, err = r.ReadByte() - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil } } diff --git a/transport/frame_test.go b/transport/frame_test.go index 3cac2a2..edb2c40 100644 --- a/transport/frame_test.go +++ b/transport/frame_test.go @@ -3,6 +3,7 @@ package transport import ( "bufio" "bytes" + "errors" "io" "testing" @@ -97,13 +98,12 @@ func TestChunkReaderReadByte(t *testing.T) { } buf = buf[:n] - if err != io.EOF { + if !errors.Is(err, io.EOF) { assert.Equal(t, tc.err, err) } assert.Equal(t, tc.want, buf) - // TODO: validate the return error - r.Close() + r.Close() //nolint:errcheck // TODO: validate the close error }) } } @@ -120,7 +120,7 @@ func TestChunkReaderRead(t *testing.T) { assert.Equal(t, tc.want, got) // TODO: validate the return error - r.Close() + r.Close() // nolint:errcheck // TODO: validate the close error }) } } @@ -159,7 +159,7 @@ func BenchmarkChunkedReadByte(b *testing.B) { b.Run(bc.name, func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - _, _ = bc.r.ReadByte() + _, _ = bc.r.ReadByte() //nolint:errcheck b.SetBytes(1) } }) @@ -266,14 +266,13 @@ func TestEOMReadByte(t *testing.T) { } buf = buf[:n] - if err != io.EOF { + if !errors.Is(err, io.EOF) { assert.Equal(t, err, tc.err) } assert.Equal(t, tc.want, buf) - // TODO: validate the return error - r.Close() + r.Close() // nolint:errcheck // TODO: validate the close error }) } } @@ -287,8 +286,8 @@ func TestEOMRead(t *testing.T) { got, err := io.ReadAll(r) assert.Equal(t, err, tc.err) assert.Equal(t, tc.want, got) - // TODO: validate the return error - r.Close() + + r.Close() // nolint:errcheck // TODO: validate the close error }) } } @@ -334,7 +333,7 @@ func BenchmarkEOMReadByte(b *testing.B) { b.Run(bc.name, func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - _, _ = bc.r.ReadByte() + _, _ = bc.r.ReadByte() // nolint:errcheck b.SetBytes(1) } }) diff --git a/transport/ssh/ssh.go b/transport/ssh/ssh.go index b979132..80359fe 100644 --- a/transport/ssh/ssh.go +++ b/transport/ssh/ssh.go @@ -6,12 +6,12 @@ import ( "io" "net" - "nemith.io/netconf/transport" "golang.org/x/crypto/ssh" + "nemith.io/netconf/transport" ) // alias it to a private type so we can make it private when embedding -type framer = transport.Framer //nolint:golint,unused +type framer = transport.Framer // Transport implements RFC6242 for implementing NETCONF protocol over SSH. type Transport struct { @@ -55,7 +55,7 @@ func Dial(ctx context.Context, network, addr string, config *ssh.ClientConfig) ( case <-ctx.Done(): // context is canceled so close the underlying connection. Will // will catch ctx.Err() later. - conn.Close() + _ = conn.Close() // nolint:errcheck // TODO: catch and log err case <-done: } }() diff --git a/transport/ssh/ssh_test.go b/transport/ssh/ssh_test.go index d118974..307bfba 100644 --- a/transport/ssh/ssh_test.go +++ b/transport/ssh/ssh_test.go @@ -80,7 +80,8 @@ func newTestServer(t *testing.T, handlerFn func(*testing.T, ssh.Channel, <-chan for newChannel := range chans { if newChannel.ChannelType() != "session" { - _ = newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + err := newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + assert.NoError(t, err) continue } @@ -110,11 +111,15 @@ func TestTransport(t *testing.T) { if req.Type != "subsystem" || !bytes.Equal(req.Payload[4:], []byte("netconf")) { panic(fmt.Sprintf("unknown ssh request: %q: %q", req.Type, req.Payload)) } - _ = req.Reply(true, nil) + err := req.Reply(true, nil) + assert.NoError(t, err) } }() - _, _ = io.WriteString(ch, "muffins]]>]]>") - _, _ = io.Copy(&srvIn, ch) + _, err := io.WriteString(ch, "muffins]]>]]>") + require.NoError(t, err) + + _, err = io.Copy(&srvIn, ch) + require.NoError(t, err) close(srvDone) }) require.NoError(t, err) @@ -137,7 +142,8 @@ func TestTransport(t *testing.T) { assert.NoError(t, err) out := "a man a plan a canal panama" - _, _ = io.WriteString(w, out) + _, err = io.WriteString(w, out) + assert.NoError(t, err) err = w.Close() assert.NoError(t, err) diff --git a/transport/tls/tls.go b/transport/tls/tls.go index 71e5cdb..1ac17b5 100644 --- a/transport/tls/tls.go +++ b/transport/tls/tls.go @@ -9,7 +9,7 @@ import ( ) // alias it to a private type so we can make it private when embedding -type framer = transport.Framer //nolint:golint,unused +type framer = transport.Framer // Transport implements RFC7589 for implementing NETCONF over TLS. type Transport struct {