Compare commits

...

11 Commits

Author SHA1 Message Date
Fabian Martinez 8b780b4d81
Concurrency ctesting (#133)
* Adds concurrency/ctesting

Adds concurrency/ctesting package, used for concurrently running a set
of runners, and collect the results via a testing assertion.

Signed-off-by: joshvanl <me@joshvanl.dev>

* lint

Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com>

---------

Signed-off-by: joshvanl <me@joshvanl.dev>
Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com>
Co-authored-by: joshvanl <me@joshvanl.dev>
2025-07-17 11:07:48 -03:00
Javier Aliaga 9d4f384c57
feat: Add subsecond precision to jobs (#129)
* feat: Add subsecond precision to jobs

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: linting fixes

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: add tests

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: remove all precision to cron constant schedule

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Time parse supports RFC3339nano

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Fix TestFile_CurrentTrustAnchors flaky test

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: No need to fallback to RFC3339

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Wait until file trustanchors is running

Signed-off-by: Javier Aliaga <javier@diagrid.io>

---------

Signed-off-by: Javier Aliaga <javier@diagrid.io>
2025-07-10 11:03:56 -03:00
Josh van Leeuwen 7c4cedad37
concurrency/dir: fix mkdir permissions (#131)
Fix Mkdir permissions by using 0o700 over `ModePerm`.

Signed-off-by: joshvanl <me@joshvanl.dev>
2025-07-08 21:41:07 -05:00
Javier Aliaga 7409957e9e
Update deps (#130)
* chore: update go and golanci-lint versions

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Fix linting issues

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Remove nolint exclusion

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* chore: Revert range to for loop

Signed-off-by: Javier Aliaga <javier@diagrid.io>

---------

Signed-off-by: Javier Aliaga <javier@diagrid.io>
2025-07-08 17:55:56 -03:00
Josh van Leeuwen 34f8820d2a
concurrency/runner: make cancel with cause (#128)
Useful for making concurrency runner closer errors more useful for
shutdowns.

Signed-off-by: joshvanl <me@joshvanl.dev>
2025-07-03 09:51:31 -05:00
Joni Collinge 598b032bce
Fix deprecation comment to reference correct function (#125)
Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>
2025-06-16 17:06:11 +01:00
Josh van Leeuwen d7d50a1e1b
events/loop: drain queue on close (#124)
* events/loop: drain queue on close

Update looper to drain the Enqueue loop in the event that an error has
occurred when Enqueuing. Handle an error'd `Run` on `Close` by
respecting the `closedCh` channel.

Signed-off-by: joshvanl <me@joshvanl.dev>

* Update loop.go

Signed-off-by: joshvanl <me@joshvanl.dev>

---------

Signed-off-by: joshvanl <me@joshvanl.dev>
2025-06-16 08:26:36 -05:00
Josh van Leeuwen baea626399
Update .golangci.yml to remove deprecations (#122)
* Update .golangci.yml  to remove deprecations

Signed-off-by: joshvanl <me@joshvanl.dev>

* Update .golangci.yml

Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
Signed-off-by: Josh van Leeuwen <me@joshvanl.dev>

---------

Signed-off-by: joshvanl <me@joshvanl.dev>
Signed-off-by: Josh van Leeuwen <me@joshvanl.dev>
Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com>
2025-05-22 08:58:18 -05:00
Joni Collinge bc7dc566c4
Add JWT handling to spiffe package (#118)
* Adds JWT handling to spiffe

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Update log

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Add jwtbundle

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Update file watcher

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Clean up jwt spiffe

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* lint

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Update renewal behavior

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Updates based on joshvanl feedback

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* go mod tidy

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* lint

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* lint

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Move ready chan check to avoid race

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Add small delay after fs write to allow watcher to pick up change

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Resolve feedback

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* Resolve feedback

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

* lint

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>

---------

Signed-off-by: Jonathan Collinge <jonathancollinge@live.com>
2025-05-16 13:15:56 +01:00
Josh van Leeuwen 98fe567235
events/loop: add reset (#120)
* events/loop: add reset

Update loop implementation is include functionality for Reset which is
useful when caching the loop struct for future use to reduce
allocations.

Signed-off-by: joshvanl <me@joshvanl.dev>

* lint

Signed-off-by: joshvanl <me@joshvanl.dev>

---------

Signed-off-by: joshvanl <me@joshvanl.dev>
2025-05-15 23:23:38 +01:00
Josh van Leeuwen e3d4a8f1b4
Add Copyright headers to env, and remove utils. (#103)
Chore to add Copyright headers to `env` files.

Move containing funcs and deletes `utils` package. `utils` packages are
generally a code smell, and better placed in a more descriptive package
name that gives context.

Signed-off-by: joshvanl <me@joshvanl.dev>
2025-04-23 11:29:15 -03:00
79 changed files with 1700 additions and 710 deletions

View File

@ -24,7 +24,7 @@ jobs:
GOOS: ${{ matrix.target_os }}
GOARCH: ${{ matrix.target_arch }}
GOPROXY: https://proxy.golang.org
GOLANGCI_LINT_VER: v1.61.0
GOLANGCI_LINT_VER: v1.64.8
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
@ -54,9 +54,6 @@ jobs:
with:
version: ${{ env.GOLANGCI_LINT_VER }}
skip-cache: true
# TODO: @joshvanl remove once all new linter errors have been
# addressed.
only-new-issues: true
- name: Run make go.mod check-diff
if: matrix.target_arch != 'arm'
run: make go.mod check-diff

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
.idea
.vscode
.vs
vendor

View File

@ -4,7 +4,7 @@ run:
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 10m
timeout: 15m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
@ -15,29 +15,34 @@ run:
# list of build tags, all linters use it. Default is empty list.
build-tags:
- unit
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
- ^pkg.*client.*clientset.*versioned.*
- ^pkg.*client.*informers.*externalversions.*
- ^pkg.*proto.*
- allcomponents
- subtlecrypto
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
# skip-files:
# - ".*\\.my\\.go$"
# - lib/bad.go
issues:
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# third_party$, testdata$, examples$, Godeps$, builtin$
exclude-dirs:
- ^pkg.*client.*clientset.*versioned.*
- ^pkg.*client.*informers.*externalversions.*
- ^pkg.*proto.*
- pkg/proto
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: tab
formats:
- format: tab
# print lines of code with issue, default is true
print-issued-lines: true
@ -57,23 +62,19 @@ linters-settings:
# default is false: such cases aren't reported by default.
check-blank: false
# [deprecated] comma-separated list of pairs of the form pkg:regex
# the regex is used to ignore names within pkg. (default "fmt:.*").
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
ignore: fmt:.*,io/ioutil:^Read.*
exclude-functions:
- fmt:.*
- io/ioutil:^Read.*
# path to a file containing a list of functions to exclude from checking
# see https://github.com/kisielk/errcheck#excluding-functions for details
exclude:
# exclude:
funlen:
lines: 60
statements: 40
govet:
# report about shadowed variables
check-shadowing: true
# settings per analyzer
settings:
printf: # analyzer name, run `go tool vet help` to see all analyzers
@ -86,13 +87,12 @@ linters-settings:
# enable or disable analyzers by name
enable:
- atomicalign
enable-all: false
disable:
- shadow
enable-all: false
disable-all: false
golint:
revive:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
confidence: 0.8
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
@ -106,9 +106,6 @@ linters-settings:
gocognit:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 10
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
@ -121,55 +118,60 @@ linters-settings:
rules:
main:
deny:
- pkg: "github.com/Sirupsen/logrus"
desc: "must use github.com/dapr/kit/logger"
- pkg: "github.com/agrea/ptr"
desc: "must use github.com/dapr/kit/ptr"
- pkg: "go.uber.org/atomic"
desc: "must use sync/atomic"
- pkg: "golang.org/x/net/context"
desc: "must use context"
- pkg: "github.com/pkg/errors"
desc: "must use standard library (errors package and/or fmt.Errorf)"
- pkg: "github.com/go-chi/chi$"
desc: "must use github.com/go-chi/chi/v5"
- pkg: "github.com/cenkalti/backoff$"
desc: "must use github.com/cenkalti/backoff/v4"
- pkg: "github.com/cenkalti/backoff/v2"
desc: "must use github.com/cenkalti/backoff/v4"
- pkg: "github.com/cenkalti/backoff/v3"
desc: "must use github.com/cenkalti/backoff/v4"
- pkg: "github.com/benbjohnson/clock"
desc: "must use k8s.io/utils/clock"
- pkg: "github.com/ghodss/yaml"
desc: "must use sigs.k8s.io/yaml"
- pkg: "gopkg.in/yaml.v2"
desc: "must use gopkg.in/yaml.v3"
- pkg: "github.com/golang-jwt/jwt"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/golang-jwt/jwt/v2"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/golang-jwt/jwt/v3"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/golang-jwt/jwt/v4"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/gogo/status"
desc: "must use google.golang.org/grpc/status"
- pkg: "github.com/gogo/protobuf"
desc: "must use google.golang.org/protobuf"
- pkg: "github.com/lestrrat-go/jwx/jwa"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/lestrrat-go/jwx/jwt"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/labstack/gommon/log"
desc: "must use github.com/dapr/kit/logger"
- pkg: "github.com/gobuffalo/logger"
desc: "must use github.com/dapr/kit/logger"
- pkg: "github.com/Sirupsen/logrus"
desc: "must use github.com/dapr/kit/logger"
- pkg: "github.com/agrea/ptr"
desc: "must use github.com/dapr/kit/ptr"
- pkg: "go.uber.org/atomic"
desc: "must use sync/atomic"
- pkg: "golang.org/x/net/context"
desc: "must use context"
- pkg: "github.com/pkg/errors"
desc: "must use standard library (errors package and/or fmt.Errorf)"
- pkg: "github.com/go-chi/chi$"
desc: "must use github.com/go-chi/chi/v5"
- pkg: "github.com/cenkalti/backoff$"
desc: "must use github.com/cenkalti/backoff/v4"
- pkg: "github.com/cenkalti/backoff/v2"
desc: "must use github.com/cenkalti/backoff/v4"
- pkg: "github.com/cenkalti/backoff/v3"
desc: "must use github.com/cenkalti/backoff/v4"
- pkg: "github.com/benbjohnson/clock"
desc: "must use k8s.io/utils/clock"
- pkg: "github.com/ghodss/yaml"
desc: "must use sigs.k8s.io/yaml"
- pkg: "gopkg.in/yaml.v2"
desc: "must use gopkg.in/yaml.v3"
- pkg: "github.com/golang-jwt/jwt"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/golang-jwt/jwt/v2"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/golang-jwt/jwt/v3"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/golang-jwt/jwt/v4"
desc: "must use github.com/lestrrat-go/jwx/v2"
# pkg: Commonly auto-completed by gopls
- pkg: "github.com/gogo/status"
desc: "must use google.golang.org/grpc/status"
- pkg: "github.com/gogo/protobuf"
desc: "must use google.golang.org/protobuf"
- pkg: "github.com/lestrrat-go/jwx/jwa"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/lestrrat-go/jwx/jwt"
desc: "must use github.com/lestrrat-go/jwx/v2"
- pkg: "github.com/labstack/gommon/log"
desc: "must use github.com/dapr/kit/logger"
- pkg: "github.com/gobuffalo/logger"
desc: "must use github.com/dapr/kit/logger"
- pkg: "k8s.io/utils/pointer"
desc: "must use github.com/dapr/kit/ptr"
- pkg: "k8s.io/utils/ptr"
desc: "must use github.com/dapr/kit/ptr"
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: default
# locale: default
ignore-words:
- someword
lll:
@ -178,17 +180,9 @@ linters-settings:
line-length: 120
# tab width in spaces. Default to 1.
tab-width: 1
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 30
nolintlint:
allow-unused: true
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
@ -203,7 +197,6 @@ linters-settings:
# See https://go-critic.github.io/overview#checks-overview
# To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
# By default list of stable checks is used.
enabled-checks:
# Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty
disabled-checks:
@ -251,62 +244,51 @@ linters-settings:
allow-assign-and-call: true
# Allow multiline assignments to be cuddled. Default is true.
allow-multiline-assign: true
# Allow case blocks to end with a whitespace.
allow-case-traling-whitespace: true
# Allow declarations (var) to be cuddled.
allow-cuddle-declarations: false
# If the number of lines in a case block is equal to or lager than this number,
# the case *must* end white a newline.
# https://github.com/bombsimon/wsl/blob/master/doc/configuration.md#force-case-trailing-whitespace
# Default: 0
force-case-trailing-whitespace: 1
linters:
fast: false
enable-all: true
disable:
# TODO Enforce the below linters later
- musttag
- dupl
- nonamedreturns
- errcheck
- funlen
- goconst
- gochecknoglobals
- gochecknoinits
- gocyclo
- gocognit
- nosnakecase
- varcheck
- structcheck
- deadcode
- godox
- interfacer
- lll
- maligned
- scopelint
- unparam
- wsl
- gomnd
- testpackage
- nestif
- nlreturn
- exhaustive
- exhaustruct
- noctx
- gci
- golint
- tparallel
- paralleltest
- wrapcheck
- tagliatelle
- ireturn
- exhaustive
- exhaustivestruct
- exhaustruct
- errchkjson
- contextcheck
- gomoddirectives
- godot
- cyclop
- varnamelen
- gosec
- tagalign
- errorlint
- forcetypeassert
- ifshort
- maintidx
- nilnil
- predeclared
@ -315,4 +297,13 @@ linters:
- wastedassign
- containedctx
- gosimple
- forbidigo
- nonamedreturns
- asasalint
- rowserrcheck
- sqlclosecheck
- inamedparam
- tagalign
- mnd
- canonicalheader
- err113
- fatcontext

View File

@ -78,6 +78,13 @@ test-race:
lint:
$(GOLANGCI_LINT) run --timeout=20m
################################################################################
# Target: lint-fix #
################################################################################
.PHONY: lint-fix
lint-fix:
$(GOLANGCI_LINT) run --timeout=20m --fix
################################################################################
# Target: go.mod #
################################################################################

View File

@ -32,7 +32,7 @@ func TestByteSlicePool(t *testing.T) {
assert.Equal(t, &bs, &bs2)
assert.Equal(t, minCap, cap(bs2))
for i := 0; i < minCap; i++ {
for range minCap {
bs2 = append(bs2, 0)
}

View File

@ -26,9 +26,7 @@ import (
"github.com/dapr/kit/logger"
)
var (
ErrManagerAlreadyClosed = errors.New("runner manager already closed")
)
var ErrManagerAlreadyClosed = errors.New("runner manager already closed")
// RunnerCloserManager is a RunnerManager that also implements Closing of the
// added closers once the main runners are done.

View File

@ -38,7 +38,7 @@ func (m mockCloser) Close() error {
func Test_RunnerClosterManager(t *testing.T) {
t.Run("runner with no tasks or closers should return nil", func(t *testing.T) {
require.NoError(t, NewRunnerCloserManager(log, nil).Run(context.Background()))
require.NoError(t, NewRunnerCloserManager(log, nil).Run(t.Context()))
})
t.Run("runner with a task that completes should return nil", func(t *testing.T) {
@ -46,7 +46,7 @@ func Test_RunnerClosterManager(t *testing.T) {
require.NoError(t, NewRunnerCloserManager(log, nil, func(context.Context) error {
i.Add(1)
return nil
}).Run(context.Background()))
}).Run(t.Context()))
assert.Equal(t, int32(1), i.Load())
})
@ -60,7 +60,7 @@ func Test_RunnerClosterManager(t *testing.T) {
i.Add(1)
return nil
}))
require.NoError(t, mngr.Run(context.Background()))
require.NoError(t, mngr.Run(t.Context()))
assert.Equal(t, int32(2), i.Load())
})
@ -98,7 +98,7 @@ func Test_RunnerClosterManager(t *testing.T) {
}),
))
require.NoError(t, mngr.Run(context.Background()))
require.NoError(t, mngr.Run(t.Context()))
assert.Equal(t, int32(7), i.Load())
})
@ -125,7 +125,7 @@ func Test_RunnerClosterManager(t *testing.T) {
},
))
require.EqualError(t, mngr.Run(context.Background()), "error")
require.EqualError(t, mngr.Run(t.Context()), "error")
assert.Equal(t, int32(4), i.Load())
})
@ -152,7 +152,7 @@ func Test_RunnerClosterManager(t *testing.T) {
},
))
require.EqualError(t, mngr.Run(context.Background()), "error")
require.EqualError(t, mngr.Run(t.Context()), "error")
assert.Equal(t, int32(4), i.Load())
})
@ -187,7 +187,7 @@ func Test_RunnerClosterManager(t *testing.T) {
}),
))
err := mngr.Run(context.Background())
err := mngr.Run(t.Context())
require.Error(t, err)
require.ErrorContains(t, err, "error\nerror\nerror\nclosererror\nclosererror\nclosererror") //nolint:dupword
assert.Equal(t, int32(6), i.Load())
@ -224,7 +224,7 @@ func Test_RunnerClosterManager(t *testing.T) {
}),
))
err := mngr.Run(context.Background())
err := mngr.Run(t.Context())
require.Error(t, err)
assert.ElementsMatch(t,
[]string{"error1", "error2", "error3", "closererror1", "closererror2", "closererror3"},
@ -265,7 +265,7 @@ func Test_RunnerClosterManager(t *testing.T) {
},
))
require.NoError(t, mngr.Run(context.Background()))
require.NoError(t, mngr.Run(t.Context()))
assert.Equal(t, int32(5), i.Load())
})
@ -325,7 +325,7 @@ func Test_RunnerClosterManager(t *testing.T) {
},
))
require.NoError(t, mngr.Run(context.Background()))
require.NoError(t, mngr.Run(t.Context()))
assert.Equal(t, int32(6), i.Load())
})
@ -377,7 +377,7 @@ func Test_RunnerClosterManager(t *testing.T) {
},
))
err := mngr.Run(context.Background())
err := mngr.Run(t.Context())
require.Error(t, err)
assert.ElementsMatch(t,
[]string{"error1", "error2", "error3", "closererror1", "closererror2"},
@ -392,9 +392,9 @@ func Test_RunnerClosterManager(t *testing.T) {
i.Add(1)
return nil
})
require.NoError(t, m.Run(context.Background()))
require.NoError(t, m.Run(t.Context()))
assert.Equal(t, int32(1), i.Load())
require.EqualError(t, m.Run(context.Background()), "runner manager already started")
require.EqualError(t, m.Run(t.Context()), "runner manager already started")
assert.Equal(t, int32(1), i.Load())
})
@ -410,11 +410,11 @@ func Test_RunnerClosterManager(t *testing.T) {
return nil
}))
require.NoError(t, m.Run(context.Background()))
require.NoError(t, m.Run(t.Context()))
assert.Equal(t, int32(2), i.Load())
require.NoError(t, m.Close())
require.NoError(t, m.Close())
require.EqualError(t, m.Run(context.Background()), "runner manager already started")
require.EqualError(t, m.Run(t.Context()), "runner manager already started")
assert.Equal(t, int32(2), i.Load())
})
@ -424,7 +424,7 @@ func Test_RunnerClosterManager(t *testing.T) {
i.Add(1)
return nil
})
require.NoError(t, m.Run(context.Background()))
require.NoError(t, m.Run(t.Context()))
assert.Equal(t, int32(1), i.Load())
err := m.Add(func(context.Context) error {
i.Add(1)
@ -441,7 +441,7 @@ func Test_RunnerClosterManager(t *testing.T) {
i.Add(1)
return nil
})
require.NoError(t, m.Run(context.Background()))
require.NoError(t, m.Run(t.Context()))
assert.Equal(t, int32(1), i.Load())
require.NoError(t, m.Close())
err := m.AddCloser(func(context.Context) error {
@ -486,7 +486,7 @@ func Test_RunnerClosterManager(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
select {
@ -515,7 +515,7 @@ func Test_RunnerClosterManager(t *testing.T) {
clock := clocktesting.NewFakeClock(time.Now())
mngr.clock = clock
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
fatalCalled := make(chan struct{})
@ -537,7 +537,7 @@ func Test_RunnerClosterManager(t *testing.T) {
}
})
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
assert.Eventually(t, func() bool {
@ -571,7 +571,7 @@ func TestClose(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
select {
@ -629,7 +629,7 @@ func TestClose(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
select {
@ -714,7 +714,7 @@ func TestClose(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
select {
@ -776,7 +776,7 @@ func TestClose(t *testing.T) {
assert.Len(t, mngr.closers, 1)
returnClose := make(chan struct{})
for n := 0; n < 4; n++ {
for range 4 {
require.NoError(t, mngr.AddCloser(func() {
i.Add(1)
<-returnClose
@ -787,7 +787,7 @@ func TestClose(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
select {
@ -850,7 +850,7 @@ func TestClose(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
var err error
@ -879,7 +879,7 @@ func TestClose(t *testing.T) {
require.NoError(t, mngr.Close())
require.NoError(t, mngr.Close())
assert.Equal(t, mngr.Run(context.Background()), errors.New("runner manager already started"))
assert.Equal(t, mngr.Run(t.Context()), errors.New("runner manager already started"))
})
}
@ -918,7 +918,7 @@ func TestAddCloser(t *testing.T) {
return nil
})
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
errCh <- mngr.Run(ctx)
@ -931,7 +931,7 @@ func TestAddCloser(t *testing.T) {
t.Run("should error if closing", func(t *testing.T) {
mngr := NewRunnerCloserManager(log, nil)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
closerCh := make(chan struct{})
require.NoError(t, mngr.AddCloser(func() {
cancel()
@ -940,7 +940,7 @@ func TestAddCloser(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- mngr.Run(context.Background())
errCh <- mngr.Run(t.Context())
}()
select {
@ -973,7 +973,7 @@ func TestAddCloser(t *testing.T) {
t.Run("should error if manager already returned", func(t *testing.T) {
mngr := NewRunnerCloserManager(log, nil)
require.NoError(t, mngr.Run(context.Background()))
require.NoError(t, mngr.Run(t.Context()))
assert.Equal(t, mngr.AddCloser(nil), errors.New("runner manager already closed"))
})
}
@ -999,7 +999,7 @@ func TestWaitUntilShutdown(t *testing.T) {
<-returnClose
}))
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {

View File

@ -57,13 +57,13 @@ func TestAtomicInt32_New_Get_Delete(t *testing.T) {
for _, key := range keys {
go func(k string) {
defer wg.Done()
for i := 0; i < iterations; i++ {
for range iterations {
m.GetOrCreate(k, 0).Add(1)
}
}(key)
go func(k string) {
defer wg.Done()
for i := 0; i < iterations; i++ {
for range iterations {
m.GetOrCreate(k, 0).Add(-1)
}
}(key)

View File

@ -47,7 +47,7 @@ func TestNewMutex_Add_Delete(t *testing.T) {
wg.Add(numGoroutines)
// Concurrently lock and unlock for each key
for i := 0; i < numGoroutines; i++ {
for range numGoroutines {
go func() {
defer wg.Done()
mm.Lock("key1")
@ -75,7 +75,7 @@ func TestNewMutex_Add_Delete(t *testing.T) {
wg.Add(numGoroutines * 2)
// Concurrently RLock and RUnlock for each key
for i := 0; i < numGoroutines; i++ {
for range numGoroutines {
go func() {
defer wg.Done()
mm.RLock("key1")
@ -87,7 +87,7 @@ func TestNewMutex_Add_Delete(t *testing.T) {
assert.Equal(ct, int64(10), counter.Load())
}, 5*time.Second, 10*time.Millisecond)
for i := 0; i < numGoroutines; i++ {
for range numGoroutines {
go func() {
defer wg.Done()
mm.RUnlock("key1")

View File

@ -0,0 +1,94 @@
/*
Copyright 2025 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ctesting
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dapr/kit/concurrency"
"github.com/dapr/kit/concurrency/ctesting/internal"
)
type RunnerFn func(context.Context, assert.TestingT)
// Assert runs the provided test functions in parallel and asserts that they
// all pass.
func Assert(t *testing.T, runners ...RunnerFn) {
t.Helper()
if len(runners) == 0 {
require.Fail(t, "at least one runner function is required")
}
tt := internal.Assert(t)
ctx, cancel := context.WithCancelCause(t.Context())
t.Cleanup(func() { cancel(nil) })
doneCh := make(chan struct{}, len(runners))
for _, runner := range runners {
go func(rfn RunnerFn) {
rfn(ctx, tt)
if errs := tt.Errors(); len(errs) > 0 {
cancel(errors.Join(errs...))
}
doneCh <- struct{}{}
}(runner)
}
for range runners {
select {
case <-doneCh:
case <-t.Context().Done():
require.FailNow(t, "test context was cancelled before all runners completed")
}
}
for _, err := range tt.Errors() {
assert.NoError(t, err)
}
}
// AssertCleanup runs the provided test functions in parallel and asserts that they
// all pass, only after Cleanup,.
func AssertCleanup(t *testing.T, runners ...concurrency.Runner) {
t.Helper()
ctx, cancel := context.WithCancelCause(t.Context())
errCh := make(chan error, len(runners))
for _, runner := range runners {
go func(rfn concurrency.Runner) {
errCh <- rfn(ctx)
}(runner)
}
t.Cleanup(func() {
cancel(nil)
for range runners {
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(10 * time.Second):
assert.Fail(t, "timeout waiting for runner to stop")
}
}
})
}

View File

@ -0,0 +1,49 @@
/*
Copyright 2025 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package internal
import (
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
type Interface interface {
assert.TestingT
Errors() []error
}
type assertT struct {
t *testing.T
lock sync.Mutex
errs []error
}
func Assert(t *testing.T) Interface {
return &assertT{t: t}
}
func (a *assertT) Errorf(format string, args ...any) {
a.lock.Lock()
defer a.lock.Unlock()
a.errs = append(a.errs, fmt.Errorf(format, args...))
}
func (a *assertT) Errors() []error {
a.lock.Lock()
defer a.lock.Unlock()
return a.errs
}

View File

@ -50,17 +50,17 @@ func New(opts Options) *Dir {
func (d *Dir) Write(files map[string][]byte) error {
newDir := filepath.Join(d.base, fmt.Sprintf("%d-%s", time.Now().UTC().UnixNano(), d.targetDir))
if err := os.MkdirAll(d.base, os.ModePerm); err != nil {
if err := os.MkdirAll(d.base, 0o700); err != nil {
return err
}
if err := os.MkdirAll(newDir, os.ModePerm); err != nil {
if err := os.MkdirAll(newDir, 0o700); err != nil {
return err
}
for file, b := range files {
path := filepath.Join(newDir, file)
if err := os.WriteFile(path, b, os.ModePerm); err != nil {
if err := os.WriteFile(path, b, 0o600); err != nil {
return err
}
d.log.Infof("Written file %s", file)

View File

@ -29,14 +29,14 @@ func Test_Context(t *testing.T) {
}{
"Successful Lock": {
action: func(l *Context) error {
return l.Lock(context.Background())
return l.Lock(t.Context())
},
expectError: false,
},
"Lock with Context Timeout": {
action: func(l *Context) error {
l.Lock(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
l.Lock(t.Context())
ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond*50)
defer cancel()
return l.Lock(ctx)
},
@ -44,14 +44,14 @@ func Test_Context(t *testing.T) {
},
"Successful RLock": {
action: func(l *Context) error {
return l.RLock(context.Background())
return l.RLock(t.Context())
},
expectError: false,
},
"RLock with Context Timeout": {
action: func(l *Context) error {
l.Lock(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
l.Lock(t.Context())
ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond*50)
defer cancel()
return l.RLock(ctx)
},

View File

@ -34,7 +34,7 @@ func Test_OuterCancel(t *testing.T) {
l := NewOuterCancel(terr, time.Second)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
go l.Run(ctx)
@ -71,7 +71,7 @@ func Test_OuterCancel(t *testing.T) {
l := NewOuterCancel(terr, time.Second)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
go l.Run(ctx)
@ -97,7 +97,7 @@ func Test_OuterCancel(t *testing.T) {
l := NewOuterCancel(terr, time.Second)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
go l.Run(ctx)
@ -127,7 +127,7 @@ func Test_OuterCancel(t *testing.T) {
l := NewOuterCancel(terr, time.Second)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
go l.Run(ctx)
@ -138,7 +138,7 @@ func Test_OuterCancel(t *testing.T) {
assert.Fail(t, "expected close")
}
_, _, err := l.RLock(context.Background())
_, _, err := l.RLock(t.Context())
require.Error(t, err)
})
@ -147,7 +147,7 @@ func Test_OuterCancel(t *testing.T) {
l := NewOuterCancel(terr, time.Second)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
l.Run(ctx)
@ -163,7 +163,7 @@ func Test_OuterCancel(t *testing.T) {
l := NewOuterCancel(terr, time.Second)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
go l.Run(ctx)
@ -179,6 +179,7 @@ func Test_OuterCancel(t *testing.T) {
t.Cleanup(c1)
close(gotRLock)
}()
t.Cleanup(func() {
require.NoError(t, <-errCh)
})
@ -190,6 +191,7 @@ func Test_OuterCancel(t *testing.T) {
}
lcancel()
<-gotRLock
})
t.Run("lock blocks until outter unlocks", func(t *testing.T) {
@ -197,7 +199,7 @@ func Test_OuterCancel(t *testing.T) {
l := NewOuterCancel(terr, time.Second)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
go l.Run(ctx)

View File

@ -60,17 +60,12 @@ func (r *RunnerManager) Run(ctx context.Context) error {
return ErrManagerAlreadyStarted
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
errCh := make(chan error)
for _, runner := range r.runners {
go func(runner Runner) {
// Since the task returned, we need to cancel all other tasks.
// This is a noop if the parent context is already cancelled, or another
// task returned before this one.
defer cancel()
// Ignore context cancelled errors since errors from a runner manager
// will likely determine the exit code of the program.
// Context cancelled errors are also not really useful to the user in
@ -78,15 +73,20 @@ func (r *RunnerManager) Run(ctx context.Context) error {
rErr := runner(ctx)
if rErr != nil && !errors.Is(rErr, context.Canceled) {
errCh <- rErr
// Since the task returned, we need to cancel all other tasks.
// This is a noop if the parent context is already cancelled, or another
// task returned before this one.
cancel(rErr)
return
}
errCh <- nil
cancel(nil)
}(runner)
}
// Collect all errors
errObjs := make([]error, 0)
for i := 0; i < len(r.runners); i++ {
for range len(r.runners) {
err := <-errCh
if err != nil {
errObjs = append(errObjs, err)

View File

@ -27,7 +27,7 @@ import (
func Test_RunnerManager(t *testing.T) {
t.Run("runner with no tasks should return nil", func(t *testing.T) {
require.NoError(t, NewRunnerManager().Run(context.Background()))
require.NoError(t, NewRunnerManager().Run(t.Context()))
})
t.Run("runner with a task that completes should return nil", func(t *testing.T) {
@ -35,7 +35,7 @@ func Test_RunnerManager(t *testing.T) {
require.NoError(t, NewRunnerManager(func(ctx context.Context) error {
atomic.AddInt32(&i, 1)
return nil
}).Run(context.Background()))
}).Run(t.Context()))
assert.Equal(t, int32(1), i)
})
@ -54,7 +54,7 @@ func Test_RunnerManager(t *testing.T) {
atomic.AddInt32(&i, 1)
return nil
},
).Run(context.Background()))
).Run(t.Context()))
assert.Equal(t, int32(3), i)
})
@ -73,7 +73,7 @@ func Test_RunnerManager(t *testing.T) {
atomic.AddInt32(&i, 1)
return nil
},
).Run(context.Background()), "error")
).Run(t.Context()), "error")
assert.Equal(t, int32(3), i)
})
@ -92,7 +92,7 @@ func Test_RunnerManager(t *testing.T) {
atomic.AddInt32(&i, 1)
return errors.New("error")
},
).Run(context.Background())
).Run(t.Context())
require.Error(t, err)
require.ErrorContains(t, err, "error\nerror\nerror") //nolint:dupword
assert.Equal(t, int32(3), i)
@ -113,7 +113,7 @@ func Test_RunnerManager(t *testing.T) {
atomic.AddInt32(&i, 1)
return errors.New("error3")
},
).Run(context.Background())
).Run(t.Context())
require.Error(t, err)
assert.ElementsMatch(t, []string{"error1", "error2", "error3"}, strings.Split(err.Error(), "\n"))
assert.Equal(t, int32(3), i)
@ -139,7 +139,7 @@ func Test_RunnerManager(t *testing.T) {
return nil
},
))
require.NoError(t, mngr.Run(context.Background()))
require.NoError(t, mngr.Run(t.Context()))
assert.Equal(t, int32(3), i)
})
@ -168,7 +168,7 @@ func Test_RunnerManager(t *testing.T) {
}
return nil
},
).Run(context.Background()))
).Run(t.Context()))
assert.Equal(t, int32(3), i)
})
@ -197,7 +197,7 @@ func Test_RunnerManager(t *testing.T) {
atomic.AddInt32(&i, 1)
return errors.New("error3")
},
).Run(context.Background())
).Run(t.Context())
require.Error(t, err)
assert.ElementsMatch(t, []string{"error1", "error2", "error3"}, strings.Split(err.Error(), "\n"))
assert.Equal(t, int32(3), i)
@ -209,9 +209,9 @@ func Test_RunnerManager(t *testing.T) {
atomic.AddInt32(&i, 1)
return nil
})
require.NoError(t, m.Run(context.Background()))
require.NoError(t, m.Run(t.Context()))
assert.Equal(t, int32(1), i)
require.EqualError(t, m.Run(context.Background()), "runner manager already started")
require.EqualError(t, m.Run(t.Context()), "runner manager already started")
assert.Equal(t, int32(1), i)
})
@ -221,7 +221,7 @@ func Test_RunnerManager(t *testing.T) {
atomic.AddInt32(&i, 1)
return nil
})
require.NoError(t, m.Run(context.Background()))
require.NoError(t, m.Run(t.Context()))
assert.Equal(t, int32(1), i)
err := m.Add(func(ctx context.Context) error {
atomic.AddInt32(&i, 1)

View File

@ -81,7 +81,7 @@ func decodeString(f reflect.Type, t reflect.Type, data any) (any, error) {
if t.Implements(typeStringDecoder) {
result = reflect.New(t.Elem()).Interface()
decoder = result.(StringDecoder)
} else if reflect.PtrTo(t).Implements(typeStringDecoder) {
} else if reflect.PointerTo(t).Implements(typeStringDecoder) {
result = reflect.New(t).Interface()
decoder = result.(StringDecoder)
}

View File

@ -52,6 +52,9 @@ func NewPool(ctx ...context.Context) *Pool {
go func() {
defer cancel()
defer p.lock.RUnlock()
//nolint:intrange
// for loops are evaluated on every loop while range are evaluated over a snapshot of the slice as it
// existed when the loop started
for i := 0; i < len(p.pool); i++ {
ch := p.pool[i]
p.lock.RUnlock()

View File

@ -37,7 +37,7 @@ func Test_Pool(t *testing.T) {
t.Run("a cancelled context given to pool, should have pool cancelled", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
pool := NewPool(ctx)
select {
@ -49,10 +49,10 @@ func Test_Pool(t *testing.T) {
t.Run("a cancelled context given to pool, given a new context, should still have pool cancelled", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
pool := NewPool(ctx)
pool.Add(context.Background())
pool.Add(t.Context())
select {
case <-pool.Done():
case <-time.After(time.Second):
@ -65,13 +65,13 @@ func Test_Pool(t *testing.T) {
var ctx [50]context.Context
var cancel [50]context.CancelFunc
ctx[0], cancel[0] = context.WithCancel(context.Background())
pool := NewPool(ctx[0])
ctxPool := make([]context.Context, 0, 50)
for i := 1; i < 50; i++ {
ctx[i], cancel[i] = context.WithCancel(context.Background())
pool.Add(ctx[i])
for i := range 50 {
ctx[i], cancel[i] = context.WithCancel(t.Context())
ctxPool = append(ctxPool, ctx[i])
}
pool := NewPool(ctxPool...)
//nolint:gosec
r := rand.New(rand.NewSource(time.Now().UnixNano()))
@ -80,7 +80,7 @@ func Test_Pool(t *testing.T) {
cancel[i], cancel[j] = cancel[j], cancel[i]
})
for i := 0; i < 50; i++ {
for i := range 50 {
select {
case <-pool.Done():
t.Error("expected context to not be cancelled")
@ -99,8 +99,8 @@ func Test_Pool(t *testing.T) {
t.Run("pool size will not increase if the given contexts have been cancelled", func(t *testing.T) {
t.Parallel()
ctx1, cancel1 := context.WithCancel(context.Background())
ctx2, cancel2 := context.WithCancel(context.Background())
ctx1, cancel1 := context.WithCancel(t.Context())
ctx2, cancel2 := context.WithCancel(t.Context())
pool := NewPool(ctx1, ctx2)
assert.Equal(t, 2, pool.Size())
@ -111,19 +111,19 @@ func Test_Pool(t *testing.T) {
case <-time.After(time.Second):
t.Error("expected context pool to be cancelled")
}
pool.Add(context.Background())
pool.Add(t.Context())
assert.Equal(t, 2, pool.Size())
})
t.Run("pool size will not increase if the pool has been closed", func(t *testing.T) {
t.Parallel()
ctx1 := context.Background()
ctx2 := context.Background()
ctx1 := t.Context()
ctx2 := t.Context()
pool := NewPool(ctx1, ctx2)
assert.Equal(t, 2, pool.Size())
pool.Cancel()
pool.Add(context.Background())
pool.Add(t.Context())
assert.Equal(t, 0, pool.Size())
select {
case <-pool.Done():
@ -131,4 +131,24 @@ func Test_Pool(t *testing.T) {
t.Error("expected context pool to be cancelled")
}
})
t.Run("wait for added context to be closed", func(t *testing.T) {
t.Parallel()
ctx1, cancel1 := context.WithCancel(t.Context())
pool := NewPool(ctx1)
ctx2, cancel2 := context.WithCancel(t.Context())
pool.Add(ctx2)
assert.Equal(t, 2, pool.Size())
cancel1()
select {
case <-pool.Done():
t.Error("expected context pool to not be cancelled")
case <-time.After(10 * time.Millisecond):
}
cancel2()
})
}

View File

@ -170,7 +170,7 @@ func TestChainDelayIfStillRunning(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
started, done = j.Started(), j.Done()
if started != 2 || done != 2 {
c.Errorf("expected both jobs done, got %v %v", started, done) //nolint:testifylint
c.Errorf("expected both jobs done, got %v %v", started, done)
}
}, 100*time.Millisecond, 10*time.Millisecond)
})
@ -230,7 +230,7 @@ func TestChainSkipIfStillRunning(t *testing.T) {
var j countJob
j.delay = 10 * time.Millisecond
wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j)
for i := 0; i < 11; i++ {
for range 11 {
go wrappedJob.Run()
}
assert.Eventually(t, j.clock.HasWaiters, 50*time.Millisecond, 10*time.Millisecond)
@ -248,7 +248,7 @@ func TestChainSkipIfStillRunning(t *testing.T) {
chain := NewChain(SkipIfStillRunning(DiscardLogger))
wrappedJob1 := chain.Then(&j1)
wrappedJob2 := chain.Then(&j2)
for i := 0; i < 11; i++ {
for range 11 {
go wrappedJob1.Run()
go wrappedJob2.Run()
}

View File

@ -14,31 +14,23 @@ You can check the original license at:
https://github.com/robfig/cron/blob/master/LICENSE
*/
//nolint
package cron
import "time"
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
// It does not support jobs more frequent than once a second.
type ConstantDelaySchedule struct {
Delay time.Duration
}
// Every returns a crontab Schedule that activates once every duration.
// Delays of less than a second are not supported (will round up to 1 second).
// Any fields less than a Second are truncated.
func Every(duration time.Duration) ConstantDelaySchedule {
if duration < time.Second {
duration = time.Second
}
return ConstantDelaySchedule{
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
Delay: duration,
}
}
// Next returns the next time this should be run.
// This rounds so that the next activation time will be on the second.
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
return t.Add(schedule.Delay)
}

View File

@ -14,7 +14,6 @@ You can check the original license at:
https://github.com/robfig/cron/blob/master/LICENSE
*/
//nolint
package cron
import (
@ -29,9 +28,12 @@ func TestConstantDelayNext(t *testing.T) {
expected string
}{
// Simple cases
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00:00.00000005 2012"},
{"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"},
{"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"},
{"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:00.015 2012"},
{"Mon Jul 9 14:45:00.015 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:00.030 2012"},
{"Mon Jul 9 14:45:00.000000050 2012", 15 * time.Nanosecond, "Mon Jul 9 14:45:00.000000065 2012"},
// Wrap around hours
{"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"},
@ -47,18 +49,6 @@ func TestConstantDelayNext(t *testing.T) {
// Wrap around minute, hour, day, month, and year
{"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"},
// Round to nearest second on the delay
{"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
// Round up to 1 second if the duration is less.
{"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"},
// Round to nearest second when calculating the next time.
{"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"},
// Round to nearest second for both.
{"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"},
}
for _, c := range tests {

View File

@ -14,7 +14,6 @@ You can check the original license at:
https://github.com/robfig/cron/blob/master/LICENSE
*/
//nolint:dupword
package cron
import (
@ -35,7 +34,7 @@ import (
// for it to run. This amount is just slightly larger than 1 second to
// compensate for a few milliseconds of runtime.
//
//nolint:revive
const OneSecond = 1*time.Second + 50*time.Millisecond
type syncWriter struct {
@ -783,13 +782,68 @@ func TestMockClock(t *testing.T) {
})
cron.Start()
defer cron.Stop()
for i := 0; i <= 10; i++ {
for range 11 {
assert.Eventually(t, clk.HasWaiters, OneSecond, 10*time.Millisecond)
clk.Step(1 * time.Second)
}
assert.Equal(t, int64(10), counter.Load())
}
func TestMillisecond(t *testing.T) {
clk := clocktesting.NewFakeClock(time.Now())
cron := New(WithClock(clk))
counter1ms := atomic.Int64{}
counter15ms := atomic.Int64{}
counter100ms := atomic.Int64{}
cron.AddFunc("@every 1ms", func() {
counter1ms.Add(1)
})
cron.AddFunc("@every 15ms", func() {
counter15ms.Add(1)
})
cron.AddFunc("@every 100ms", func() {
counter100ms.Add(1)
})
cron.Start()
defer cron.Stop()
for range 1000 {
assert.Eventually(t, clk.HasWaiters, OneSecond, 1*time.Millisecond)
clk.Step(1 * time.Millisecond)
}
ctx := cron.Stop()
<-ctx.Done()
assert.Equal(t, int64(1000), counter1ms.Load())
assert.Equal(t, int64(66), counter15ms.Load())
assert.Equal(t, int64(10), counter100ms.Load())
}
func TestNanoseconds(t *testing.T) {
clk := clocktesting.NewFakeClock(time.Now())
cron := New(WithClock(clk))
counter100ns := atomic.Int64{}
cron.AddFunc("@every 100ns", func() {
counter100ns.Add(1)
})
cron.Start()
defer cron.Stop()
for range 500 {
assert.Eventually(t, clk.HasWaiters, OneSecond, 1*time.Millisecond)
clk.Step(5 * time.Nanosecond)
}
ctx := cron.Stop()
<-ctx.Done()
// 500 * 5 ns = 2500 ns
// 2500 every 100ns = 25
assert.Equal(t, int64(25), counter100ns.Load())
}
func TestMultiThreadedStartAndStop(*testing.T) {
cron := New()
go cron.Run()

View File

@ -1,4 +1,3 @@
//nolint
/*
This package is a fork of "github.com/robfig/cron/v3" that implements cron spec parser and job runner with support for mocking the time.
@ -36,7 +35,9 @@ them in their own goroutines.
# Time mocking
import (
clocktesting "k8s.io/utils/clock/testing"
)
clk := clocktesting.NewFakeClock(time.Now())

View File

@ -59,7 +59,7 @@ func TestWithVerboseLogger(t *testing.T) {
out := buf.String()
if !strings.Contains(out, "schedule,") ||
!strings.Contains(out, "run,") {
c.Errorf("expected to see some actions, got: %v", out) //nolint:testifylint
c.Errorf("expected to see some actions, got: %v", out)
}
}, time.Second, time.Millisecond*10)
}

View File

@ -14,7 +14,6 @@ You can check the original license at:
https://github.com/robfig/cron/blob/master/LICENSE
*/
//nolint
package cron
import (
@ -167,6 +166,8 @@ func TestParseSchedule(t *testing.T) {
{standardParser, "CRON_TZ=UTC 5 * * * *", every5min(time.UTC)},
{secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)},
{secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}},
{secondParser, "@every 5ms", ConstantDelaySchedule{5 * time.Millisecond}},
{secondParser, "@every 5ns", ConstantDelaySchedule{5 * time.Nanosecond}},
{secondParser, "@midnight", midnight(time.Local)},
{secondParser, "TZ=UTC @midnight", midnight(time.UTC)},
{secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)},

View File

@ -214,9 +214,9 @@ func (aead *aesCBCAEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]b
}
// Computes the HMAC tag as per specs.
func (aead aesCBCAEAD) hmacTag(h hash.Hash, additionalData, nonce, ciphertext []byte, l int) []byte {
func (aead *aesCBCAEAD) hmacTag(h hash.Hash, additionalData, nonce, ciphertext []byte, l int) []byte {
al := make([]byte, 8)
binary.BigEndian.PutUint64(al, uint64(len(additionalData)<<3)) // In bits
binary.BigEndian.PutUint64(al, uint64(len(additionalData)<<3)) // #nosec G115 // In bits
h.Write(additionalData)
h.Write(nonce)

View File

@ -48,14 +48,14 @@ func Wrap(block cipher.Block, cek []byte) ([]byte, error) {
copy(r[i], cek[i*8:])
}
for j := 0; j <= 5; j++ {
for j := range 6 {
for i := 1; i <= n; i++ {
b := arrConcat(a, r[i-1])
block.Encrypt(b, b)
t := (n * j) + i
tBytes := make([]byte, 8)
binary.BigEndian.PutUint64(tBytes, uint64(t))
binary.BigEndian.PutUint64(tBytes, uint64(t)) // #nosec G115
copy(a, arrXor(b[:len(b)/2], tBytes))
copy(r[i-1], b[len(b)/2:])
@ -92,7 +92,7 @@ func Unwrap(block cipher.Block, cipherText []byte) ([]byte, error) {
for i := n; i >= 1; i-- {
t := (n * j) + i
tBytes := make([]byte, 8)
binary.BigEndian.PutUint64(tBytes, uint64(t))
binary.BigEndian.PutUint64(tBytes, uint64(t)) // #nosec G115
b := arrConcat(arrXor(a, tBytes), r[i-1])
block.Decrypt(b, b)

View File

@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
//nolint:nosnakecase,stylecheck,revive
//nolint:nosnakecase,stylecheck
package crypto
import (

View File

@ -23,6 +23,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"os"
)
// DecodePEMCertificatesChain takes a PEM-encoded x509 certificates byte array
@ -35,7 +36,7 @@ func DecodePEMCertificatesChain(crtb []byte) ([]*x509.Certificate, error) {
return nil, err
}
for i := 0; i < len(certs)-1; i++ {
for i := range len(certs) - 1 {
if certs[i].CheckSignatureFrom(certs[i+1]) != nil {
return nil, errors.New("certificate chain is not valid")
}
@ -187,3 +188,24 @@ func PublicKeysEqual(a, b crypto.PublicKey) (bool, error) {
return false, fmt.Errorf("unrecognised public key type: %T", a)
}
}
// GetPEM loads a PEM-encoded file (certificate or key).
func GetPEM(val string) ([]byte, error) {
// If val is already a PEM-encoded string, return it as-is
if IsValidPEM(val) {
return []byte(val), nil
}
// Assume it's a file
pemBytes, err := os.ReadFile(val)
if err != nil {
return nil, fmt.Errorf("value is neither a valid file path or nor a valid PEM-encoded string: %w", err)
}
return pemBytes, nil
}
// IsValidPEM validates the provided input has PEM formatted block.
func IsValidPEM(val string) bool {
block, _ := pem.Decode([]byte(val))
return block != nil
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2024 The Dapr Authors
Copyright 2025 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@ -16,6 +16,7 @@ package context
import (
"context"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
"github.com/dapr/kit/crypto/spiffe"
@ -23,13 +24,48 @@ import (
type ctxkey int
const svidKey ctxkey = iota
const (
x509SvidKey ctxkey = iota
jwtSvidKey
)
// Deprecated: use WithX509 instead.
// With adds the x509 SVID source from the SPIFFE object to the context.
func With(ctx context.Context, spiffe *spiffe.SPIFFE) context.Context {
return context.WithValue(ctx, svidKey, spiffe.SVIDSource())
return context.WithValue(ctx, x509SvidKey, spiffe.X509SVIDSource())
}
// Deprecated: use X509From instead.
// From retrieves the x509 SVID source from the context.
func From(ctx context.Context) (x509svid.Source, bool) {
svid, ok := ctx.Value(svidKey).(x509svid.Source)
svid, ok := ctx.Value(x509SvidKey).(x509svid.Source)
return svid, ok
}
// WithX509 adds an x509 SVID source to the context.
func WithX509(ctx context.Context, source x509svid.Source) context.Context {
return context.WithValue(ctx, x509SvidKey, source)
}
// WithJWT adds a JWT SVID source to the context.
func WithJWT(ctx context.Context, source jwtsvid.Source) context.Context {
return context.WithValue(ctx, jwtSvidKey, source)
}
// X509From retrieves the x509 SVID source from the context.
func X509From(ctx context.Context) (x509svid.Source, bool) {
svid, ok := ctx.Value(x509SvidKey).(x509svid.Source)
return svid, ok
}
// JWTFrom retrieves the JWT SVID source from the context.
func JWTFrom(ctx context.Context) (jwtsvid.Source, bool) {
svid, ok := ctx.Value(jwtSvidKey).(jwtsvid.Source)
return svid, ok
}
// WithSpiffe adds both X509 and JWT SVID sources to the context.
func WithSpiffe(ctx context.Context, spiffe *spiffe.SPIFFE) context.Context {
ctx = WithX509(ctx, spiffe.X509SVIDSource())
return WithJWT(ctx, spiffe.JWTSVIDSource())
}

View File

@ -0,0 +1,64 @@
/*
Copyright 2025 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package context
import (
"context"
"testing"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
"github.com/stretchr/testify/assert"
)
type mockX509Source struct{}
func (m *mockX509Source) GetX509SVID() (*x509svid.SVID, error) {
return nil, nil
}
type mockJWTSource struct{}
func (m *mockJWTSource) FetchJWTSVID(context.Context, jwtsvid.Params) (*jwtsvid.SVID, error) {
return nil, nil
}
func TestWithX509FromX509(t *testing.T) {
source := &mockX509Source{}
ctx := WithX509(t.Context(), source)
retrieved, ok := X509From(ctx)
assert.True(t, ok, "Failed to retrieve X509 source from context")
assert.Equal(t, x509svid.Source(source), retrieved, "Retrieved source does not match the original source")
}
func TestWithJWTFromJWT(t *testing.T) {
source := &mockJWTSource{}
ctx := WithJWT(t.Context(), source)
retrieved, ok := JWTFrom(ctx)
assert.True(t, ok, "Failed to retrieve JWT source from context")
assert.Equal(t, jwtsvid.Source(source), retrieved, "Retrieved source does not match the original source")
}
func TestWithFrom(t *testing.T) {
x509Source := &mockX509Source{}
ctx := WithX509(t.Context(), x509Source)
// Should be able to retrieve using the legacy From function
retrieved, ok := From(ctx)
assert.True(t, ok, "Failed to retrieve X509 source from context using legacy From")
assert.Equal(t, x509svid.Source(x509Source), retrieved, "Retrieved source does not match the original source using legacy From")
}

View File

@ -25,6 +25,7 @@ import (
"sync/atomic"
"time"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
"k8s.io/utils/clock"
@ -34,8 +35,29 @@ import (
"github.com/dapr/kit/logger"
)
const (
// renewalDivisor represents the divisor for calculating renewal time.
// A value of 2 means renewal at 50% of the validity period.
renewalDivisor = 2
)
// SVIDResponse represents the response from the SVID request function,
// containing both X.509 certificates and a JWT token.
type SVIDResponse struct {
X509Certificates []*x509.Certificate
JWT *string
}
// Identity contains both X.509 and JWT SVIDs for a workload.
type Identity struct {
X509SVID *x509svid.SVID
JWTSVID *jwtsvid.SVID
}
type (
RequestSVIDFn func(context.Context, []byte) ([]*x509.Certificate, error)
// RequestSVIDFn is the function type that requests SVIDs from a SPIFFE server,
// returning both X.509 certificates and a JWT token.
RequestSVIDFn func(context.Context, []byte) (*SVIDResponse, error)
)
type Options struct {
@ -51,11 +73,12 @@ type Options struct {
TrustAnchors trustanchors.Interface
}
// SPIFFE is a readable/writeable store of a SPIFFE X.509 SVID.
// Used to manage a workload SVID, and share read-only interfaces to consumers.
// SPIFFE is a readable/writeable store of SPIFFE SVID credentials.
// Used to manage workload SVIDs, and share read-only interfaces to consumers.
type SPIFFE struct {
currentSVID *x509svid.SVID
requestSVIDFn RequestSVIDFn
currentX509SVID *x509svid.SVID
currentJWTSVID *jwtsvid.SVID
requestSVIDFn RequestSVIDFn
dir *dir.Dir
trustAnchors trustanchors.Interface
@ -92,15 +115,16 @@ func (s *SPIFFE) Run(ctx context.Context) error {
}
s.lock.Lock()
s.log.Info("Fetching initial identity certificate")
initialCert, err := s.fetchIdentityCertificate(ctx)
s.log.Info("Fetching initial identity")
initialIdentity, err := s.fetchIdentity(ctx)
if err != nil {
close(s.readyCh)
s.lock.Unlock()
return fmt.Errorf("failed to retrieve the initial identity certificate: %w", err)
return fmt.Errorf("failed to retrieve the initial identity: %w", err)
}
s.currentSVID = initialCert
s.currentX509SVID = initialIdentity.X509SVID
s.currentJWTSVID = initialIdentity.JWTSVID
close(s.readyCh)
s.lock.Unlock()
@ -121,28 +145,48 @@ func (s *SPIFFE) Ready(ctx context.Context) error {
}
}
// runRotation starts up the manager responsible for renewing the workload
// certificate. Receives the initial certificate to calculate the next rotation
// time.
// logIdentityInfo creates a log message with expiry details for both X.509 and JWT SVIDs
func (s *SPIFFE) logIdentityInfo(prefix string, cert *x509.Certificate, jwtSVID *jwtsvid.SVID, renewTime *time.Time) {
msg := prefix + "; cert expires on: %s"
args := []any{cert.NotAfter.String()}
if jwtSVID != nil {
msg += ", jwt expires on: %s"
args = append(args, jwtSVID.Expiry.String())
}
if renewTime != nil {
msg += ", renewal at: %s"
args = append(args, renewTime.String())
}
s.log.Infof(msg, args...)
}
// runRotation starts up the manager responsible for renewing the workload identity
func (s *SPIFFE) runRotation(ctx context.Context) {
defer s.log.Debug("stopping workload cert expiry watcher")
defer s.log.Debug("stopping workload identity expiry watcher")
s.lock.RLock()
cert := s.currentSVID.Certificates[0]
cert := s.currentX509SVID.Certificates[0]
jwtSVID := s.currentJWTSVID
s.lock.RUnlock()
renewTime := renewalTime(cert.NotBefore, cert.NotAfter)
s.log.Infof("Starting workload cert expiry watcher; current cert expires on: %s, renewing at %s",
cert.NotAfter.String(), renewTime.String())
renewTime := calculateRenewalTime(time.Now(), cert, jwtSVID)
s.logIdentityInfo("Starting workload identity expiry watcher", cert, jwtSVID, renewTime)
for {
select {
case <-s.clock.After(min(time.Minute, renewTime.Sub(s.clock.Now()))):
if s.clock.Now().Before(renewTime) {
if s.clock.Now().Before(*renewTime) {
continue
}
s.log.Infof("Renewing workload cert; current cert expires on: %s", cert.NotAfter.String())
svid, err := s.fetchIdentityCertificate(ctx)
s.logIdentityInfo("Renewing workload identity", cert, jwtSVID, nil)
identity, err := s.fetchIdentity(ctx)
if err != nil {
s.log.Errorf("Error renewing identity certificate, trying again in 10 seconds: %s", err)
s.log.Errorf("Error renewing identity, trying again in 10 seconds: %s", err)
select {
case <-s.clock.After(10 * time.Second):
continue
@ -150,12 +194,16 @@ func (s *SPIFFE) runRotation(ctx context.Context) {
return
}
}
s.lock.Lock()
s.currentSVID = svid
cert = svid.Certificates[0]
s.currentX509SVID = identity.X509SVID
s.currentJWTSVID = identity.JWTSVID
cert = identity.X509SVID.Certificates[0]
jwtSVID = identity.JWTSVID
s.lock.Unlock()
renewTime = renewalTime(cert.NotBefore, cert.NotAfter)
s.log.Infof("Successfully renewed workload cert; new cert expires on: %s", cert.NotAfter.String())
renewTime = calculateRenewalTime(time.Now(), cert, jwtSVID)
s.logIdentityInfo("Successfully renewed workload identity", cert, jwtSVID, renewTime)
case <-ctx.Done():
return
@ -163,8 +211,8 @@ func (s *SPIFFE) runRotation(ctx context.Context) {
}
}
// fetchIdentityCertificate fetches a new SVID using the configured requester.
func (s *SPIFFE) fetchIdentityCertificate(ctx context.Context) (*x509svid.SVID, error) {
// Returns both X.509 SVID and JWT SVID (if available).
func (s *SPIFFE) fetchIdentity(ctx context.Context) (*Identity, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate private key: %w", err)
@ -175,27 +223,55 @@ func (s *SPIFFE) fetchIdentityCertificate(ctx context.Context) (*x509svid.SVID,
return nil, fmt.Errorf("failed to create sidecar csr: %w", err)
}
workloadcert, err := s.requestSVIDFn(ctx, csrDER)
svidResponse, err := s.requestSVIDFn(ctx, csrDER)
if err != nil {
return nil, err
}
if len(workloadcert) == 0 {
if len(svidResponse.X509Certificates) == 0 {
return nil, errors.New("no certificates received from sentry")
}
spiffeID, err := x509svid.IDFromCert(workloadcert[0])
spiffeID, err := x509svid.IDFromCert(svidResponse.X509Certificates[0])
if err != nil {
return nil, fmt.Errorf("error parsing spiffe id from newly signed certificate: %w", err)
}
identity := &Identity{
X509SVID: &x509svid.SVID{
ID: spiffeID,
Certificates: svidResponse.X509Certificates,
PrivateKey: key,
},
}
// If we have a JWT token, parse it and include it in the identity
if svidResponse.JWT != nil {
// we are using ParseInsecure here as the expectation is that the
// requestSVIDFn will have already parsed and validate the JWT SVID
// before returning it.
//
// we are parsing the token using our SPIFFE ID's trust domain
// as the audience as we expect the issuer to always include
// that as an audience since that ensures that the token is
// valid for us and our trust domain.
audiences := []string{spiffeID.TrustDomain().Name()}
jwtSvid, err := jwtsvid.ParseInsecure(*svidResponse.JWT, audiences)
if err != nil {
return nil, fmt.Errorf("failed to parse JWT SVID: %w", err)
}
identity.JWTSVID = jwtSvid
s.log.Infof("Successfully received JWT SVID with expiry: %s", jwtSvid.Expiry.String())
}
if s.dir != nil {
pkPEM, err := pem.EncodePrivateKey(key)
if err != nil {
return nil, err
}
certPEM, err := pem.EncodeX509Chain(workloadcert)
certPEM, err := pem.EncodeX509Chain(svidResponse.X509Certificates)
if err != nil {
return nil, err
}
@ -205,27 +281,72 @@ func (s *SPIFFE) fetchIdentityCertificate(ctx context.Context) (*x509svid.SVID,
return nil, err
}
if err := s.dir.Write(map[string][]byte{
files := map[string][]byte{
"key.pem": pkPEM,
"cert.pem": certPEM,
"ca.pem": td,
}); err != nil {
}
if svidResponse.JWT != nil {
files["jwt_svid.token"] = []byte(*svidResponse.JWT)
}
if err := s.dir.Write(files); err != nil {
return nil, err
}
}
return &x509svid.SVID{
ID: spiffeID,
Certificates: workloadcert,
PrivateKey: key,
}, nil
return identity, nil
}
func (s *SPIFFE) SVIDSource() x509svid.Source {
func (s *SPIFFE) X509SVIDSource() x509svid.Source {
return &svidSource{spiffe: s}
}
func (s *SPIFFE) JWTSVIDSource() jwtsvid.Source {
return &svidSource{spiffe: s}
}
// renewalTime is 50% through the certificate validity period.
func renewalTime(notBefore, notAfter time.Time) time.Time {
return notBefore.Add(notAfter.Sub(notBefore) / 2)
return notBefore.Add(notAfter.Sub(notBefore) / renewalDivisor)
}
// calculateRenewalTime returns the earlier renewal time between the X.509 certificate
// and JWT SVID (if available) to ensure timely renewal.
func calculateRenewalTime(now time.Time, cert *x509.Certificate, jwtSVID *jwtsvid.SVID) *time.Time {
certRenewal := renewalTime(cert.NotBefore, cert.NotAfter)
if jwtSVID == nil {
return &certRenewal
}
jwtRenewal := now.Add(jwtSVID.Expiry.Sub(now) / renewalDivisor)
if jwtRenewal.Before(certRenewal) {
return &jwtRenewal
}
return &certRenewal
}
// audiencesMatch checks if the SVID audiences contain all the requested audiences
func audiencesMatch(svidAudiences []string, requestedAudiences []string) bool {
if len(requestedAudiences) == 0 {
return true
}
// Create a map for faster lookup
audienceMap := make(map[string]struct{}, len(svidAudiences))
for _, audience := range svidAudiences {
audienceMap[audience] = struct{}{}
}
// Check if all requested audiences are in the SVID
for _, requested := range requestedAudiences {
if _, ok := audienceMap[requested]; !ok {
return false
}
}
return true
}

View File

@ -22,6 +22,7 @@ import (
"time"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
clocktesting "k8s.io/utils/clock/testing"
@ -39,16 +40,82 @@ func Test_renewalTime(t *testing.T) {
assert.Equal(t, in30, renewalTime(now, in1Min))
}
func Test_calculateRenewalTime(t *testing.T) {
now := time.Now()
certShort := &x509.Certificate{
NotBefore: now,
NotAfter: now.Add(10 * time.Hour),
}
certLong := &x509.Certificate{
NotBefore: now,
NotAfter: now.Add(24 * time.Hour),
}
// Expected renewal times for certificates (50% of validity period)
certShortRenewal := now.Add(5 * time.Hour)
// Create JWT SVIDs with different expiry times
jwtEarlier := &jwtsvid.SVID{
Expiry: now.Add(8 * time.Hour),
}
jwtLater := &jwtsvid.SVID{
Expiry: now.Add(30 * time.Hour),
}
// Expected JWT renewal time (50% of remaining time)
jwtEarlierRenewal := now.Add(4 * time.Hour)
tests := []struct {
name string
cert *x509.Certificate
jwt *jwtsvid.SVID
expected time.Time
}{
{
name: "Certificate only",
cert: certShort,
jwt: nil,
expected: certShortRenewal,
},
{
name: "Certificate and JWT, JWT earlier",
cert: certLong,
jwt: jwtEarlier,
expected: jwtEarlierRenewal,
},
{
name: "Certificate and JWT, Certificate earlier",
cert: certShort,
jwt: jwtLater,
expected: certShortRenewal,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := calculateRenewalTime(now, tt.cert, tt.jwt)
assert.WithinDuration(t, tt.expected, *actual, time.Millisecond,
"Renewal time does not match expected value")
})
}
}
func Test_Run(t *testing.T) {
t.Run("should return error multiple Runs are called", func(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{
LeafID: spiffeid.RequireFromString("spiffe://example.com/foo/bar"),
})
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
s := New(Options{
Log: logger.NewLogger("test"),
RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) {
return []*x509.Certificate{pki.LeafCert}, nil
RequestSVIDFn: func(context.Context, []byte) (*SVIDResponse, error) {
return &SVIDResponse{
X509Certificates: []*x509.Certificate{pki.LeafCert},
}, nil
},
})
@ -79,12 +146,12 @@ func Test_Run(t *testing.T) {
t.Run("should return error if initial fetch errors", func(t *testing.T) {
s := New(Options{
Log: logger.NewLogger("test"),
RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) {
RequestSVIDFn: func(context.Context, []byte) (*SVIDResponse, error) {
return nil, errors.New("this is an error")
},
})
require.Error(t, s.Run(context.Background()))
require.Error(t, s.Run(t.Context()))
})
t.Run("should renew certificate when it has expired", func(t *testing.T) {
@ -95,27 +162,29 @@ func Test_Run(t *testing.T) {
var fetches atomic.Int32
s := New(Options{
Log: logger.NewLogger("test"),
RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) {
RequestSVIDFn: func(context.Context, []byte) (*SVIDResponse, error) {
fetches.Add(1)
return []*x509.Certificate{pki.LeafCert}, nil
return &SVIDResponse{
X509Certificates: []*x509.Certificate{pki.LeafCert},
}, nil
},
})
now := time.Now()
clock := clocktesting.NewFakeClock(now)
s.clock = clock
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
select {
case <-s.readyCh:
assert.Fail(t, "readyCh should not be closed")
default:
}
errCh <- s.Run(ctx)
}()
select {
case <-s.readyCh:
assert.Fail(t, "readyCh should not be closed")
default:
}
assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond)
assert.Equal(t, int32(1), fetches.Load())
@ -144,27 +213,29 @@ func Test_Run(t *testing.T) {
var fetches atomic.Int32
s := New(Options{
Log: logger.NewLogger("test"),
RequestSVIDFn: func(context.Context, []byte) ([]*x509.Certificate, error) {
RequestSVIDFn: func(context.Context, []byte) (*SVIDResponse, error) {
fetches.Add(1)
return respCert, respErr
return &SVIDResponse{
X509Certificates: respCert,
}, respErr
},
})
now := time.Now()
clock := clocktesting.NewFakeClock(now)
s.clock = clock
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
select {
case <-s.readyCh:
assert.Fail(t, "readyCh should not be closed")
default:
}
errCh <- s.Run(ctx)
}()
select {
case <-s.readyCh:
assert.Fail(t, "readyCh should not be closed")
default:
}
assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond)
assert.Equal(t, int32(1), fetches.Load())

View File

@ -14,27 +14,81 @@ limitations under the License.
package spiffe
import (
"context"
"errors"
"fmt"
"strings"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
)
// svidSource is an implementation of the Go spiffe x509svid Source interface.
var (
errNoX509SVIDAvailable = errors.New("no X509 SVID available")
errNoJWTSVIDAvailable = errors.New("no JWT SVID available")
errAudienceRequired = errors.New("JWT audience is required")
)
// svidSource is an implementation of both go-spiffe x509svid.Source and jwtsvid.Source interfaces.
type svidSource struct {
spiffe *SPIFFE
}
// GetX509SVID returns the current X.509 certificate identity as a SPIFFE SVID.
// Implements the go-spiffe x509 source interface.
// Implements the go-spiffe x509svid.Source interface.
func (s *svidSource) GetX509SVID() (*x509svid.SVID, error) {
s.spiffe.lock.RLock()
defer s.spiffe.lock.RUnlock()
<-s.spiffe.readyCh
svid := s.spiffe.currentSVID
svid := s.spiffe.currentX509SVID
if svid == nil {
return nil, errors.New("no SVID available")
return nil, errNoX509SVIDAvailable
}
return svid, nil
}
// audienceMismatchError is an error that contains information about mismatched audiences
type audienceMismatchError struct {
expected []string
actual []string
}
func (e *audienceMismatchError) Error() string {
return fmt.Sprintf("JWT SVID has different audiences than requested: expected %s, got %s",
strings.Join(e.expected, ", "), strings.Join(e.actual, ", "))
}
// FetchJWTSVID returns the current JWT SVID.
// Implements the go-spiffe jwtsvid.Source interface.
func (s *svidSource) FetchJWTSVID(ctx context.Context, params jwtsvid.Params) (*jwtsvid.SVID, error) {
s.spiffe.lock.RLock()
defer s.spiffe.lock.RUnlock()
if params.Audience == "" {
return nil, errAudienceRequired
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-s.spiffe.readyCh:
}
svid := s.spiffe.currentJWTSVID
if svid == nil {
return nil, errNoJWTSVIDAvailable
}
// verify that the audience being requested is the same as the audience in the SVID
// WARN: we do not check extra audiences here.
if !audiencesMatch(svid.Audience, []string{params.Audience}) {
return nil, &audienceMismatchError{
expected: []string{params.Audience},
actual: svid.Audience,
}
}
return svid, nil

View File

@ -14,11 +14,177 @@ limitations under the License.
package spiffe
import (
"context"
"sync"
"testing"
"time"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
"github.com/stretchr/testify/require"
)
func Test_svidSource(*testing.T) {
var _ x509svid.Source = new(svidSource)
var _ jwtsvid.Source = new(svidSource)
}
// createMockJWTSVID creates a mock JWT SVID for testing
func createMockJWTSVID(audiences []string) (*jwtsvid.SVID, error) {
td, err := spiffeid.TrustDomainFromString("example.org")
if err != nil {
return nil, err
}
id, err := spiffeid.FromSegments(td, "workload")
if err != nil {
return nil, err
}
svid := &jwtsvid.SVID{
ID: id,
Audience: audiences,
Expiry: time.Now().Add(time.Hour),
}
return svid, nil
}
func TestFetchJWTSVID(t *testing.T) {
t.Run("should return error when audience is empty", func(t *testing.T) {
s := &svidSource{
spiffe: &SPIFFE{
readyCh: make(chan struct{}),
lock: sync.RWMutex{},
},
}
close(s.spiffe.readyCh) // Mark as ready
svid, err := s.FetchJWTSVID(t.Context(), jwtsvid.Params{
Audience: "",
})
require.Nil(t, svid)
require.ErrorIs(t, err, errAudienceRequired)
})
t.Run("should return error when no JWT SVID available", func(t *testing.T) {
s := &svidSource{
spiffe: &SPIFFE{
readyCh: make(chan struct{}),
lock: sync.RWMutex{},
currentJWTSVID: nil,
},
}
close(s.spiffe.readyCh) // Mark as ready
svid, err := s.FetchJWTSVID(t.Context(), jwtsvid.Params{
Audience: "test-audience",
})
require.Nil(t, svid)
require.ErrorIs(t, err, errNoJWTSVIDAvailable)
})
t.Run("should return error when audience doesn't match", func(t *testing.T) {
// Create a mock SVID with a specific audience
mockJWTSVID, err := createMockJWTSVID([]string{"actual-audience"})
require.NoError(t, err)
s := &svidSource{
spiffe: &SPIFFE{
readyCh: make(chan struct{}),
lock: sync.RWMutex{},
currentJWTSVID: mockJWTSVID,
},
}
close(s.spiffe.readyCh) // Mark as ready
svid, err := s.FetchJWTSVID(t.Context(), jwtsvid.Params{
Audience: "requested-audience",
})
require.Nil(t, svid)
require.Error(t, err)
// Verify the specific error type and contents
audienceErr, ok := err.(*audienceMismatchError)
require.True(t, ok, "Expected audienceMismatchError")
require.Equal(t, "JWT SVID has different audiences than requested: expected requested-audience, got actual-audience", audienceErr.Error())
})
t.Run("should return JWT SVID when audience matches", func(t *testing.T) {
mockJWTSVID, err := createMockJWTSVID([]string{"test-audience", "extra-audience"})
require.NoError(t, err)
s := &svidSource{
spiffe: &SPIFFE{
readyCh: make(chan struct{}),
lock: sync.RWMutex{},
currentJWTSVID: mockJWTSVID,
},
}
close(s.spiffe.readyCh) // Mark as ready
svid, err := s.FetchJWTSVID(t.Context(), jwtsvid.Params{
Audience: "test-audience",
})
require.NoError(t, err)
require.Equal(t, mockJWTSVID, svid)
})
t.Run("should wait for readyCh before checking SVID", func(t *testing.T) {
mockJWTSVID, err := createMockJWTSVID([]string{"test-audience"})
require.NoError(t, err)
readyCh := make(chan struct{})
s := &svidSource{
spiffe: &SPIFFE{
readyCh: readyCh,
lock: sync.RWMutex{},
currentJWTSVID: mockJWTSVID,
},
}
// Start goroutine to fetch SVID
ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
defer cancel()
resultCh := make(chan struct {
svid *jwtsvid.SVID
err error
})
go func() {
svid, err := s.FetchJWTSVID(ctx, jwtsvid.Params{
Audience: "test-audience",
})
resultCh <- struct {
svid *jwtsvid.SVID
err error
}{svid, err}
}()
// require that fetch is blocked
select {
case <-resultCh:
t.Fatal("FetchJWTSVID should be blocked until readyCh is closed")
case <-time.After(100 * time.Millisecond):
// Expected behavior - fetch is blocked
}
// Close readyCh to unblock fetch
close(readyCh)
// Now fetch should complete
select {
case result := <-resultCh:
require.NoError(t, result.err)
require.NotNil(t, result.svid)
case <-time.After(100 * time.Millisecond):
t.Fatal("FetchJWTSVID should have completed after readyCh was closed")
}
})
}

View File

@ -11,40 +11,52 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package trustanchors
package file
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/spiffe/go-spiffe/v2/bundle/jwtbundle"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"k8s.io/utils/clock"
"github.com/dapr/kit/concurrency"
"github.com/dapr/kit/crypto/pem"
"github.com/dapr/kit/crypto/spiffe/trustanchors"
"github.com/dapr/kit/fswatcher"
"github.com/dapr/kit/logger"
)
type OptionsFile struct {
Log logger.Logger
Path string
var (
// ErrTrustAnchorsClosed is returned when an operation is performed on closed trust anchors.
ErrTrustAnchorsClosed = errors.New("trust anchors is closed")
// ErrFailedToReadTrustAnchorsFile is returned when the trust anchors file cannot be read.
ErrFailedToReadTrustAnchorsFile = errors.New("failed to read trust anchors file")
)
type Options struct {
Log logger.Logger
CAPath string
JwksPath *string
}
// file is a TrustAnchors implementation that uses a file as the source of trust
// anchors. The trust anchors will be updated when the file changes.
type file struct {
log logger.Logger
path string
bundle *x509bundle.Bundle
rootPEM []byte
log logger.Logger
caPath string
jwksPath *string
x509Bundle *x509bundle.Bundle
jwtBundle *jwtbundle.Bundle
rootPEM []byte
// fswatcherInterval is the interval at which the trust anchors file changes
// are batched. Used for testing only, and 500ms otherwise.
@ -65,17 +77,18 @@ type file struct {
caEvent chan struct{}
}
func FromFile(opts OptionsFile) Interface {
func From(opts Options) trustanchors.Interface {
return &file{
fsWatcherInterval: time.Millisecond * 500,
initFileWatchInterval: time.Second,
log: opts.Log,
path: opts.Path,
clock: clock.RealClock{},
readyCh: make(chan struct{}),
closeCh: make(chan struct{}),
caEvent: make(chan struct{}),
log: opts.Log,
caPath: opts.CAPath,
jwksPath: opts.JwksPath,
clock: clock.RealClock{},
readyCh: make(chan struct{}),
closeCh: make(chan struct{}),
caEvent: make(chan struct{}),
}
}
@ -87,31 +100,39 @@ func (f *file) Run(ctx context.Context) error {
defer close(f.closeCh)
for {
_, err := os.Stat(f.path)
if err == nil {
break
fs := []string{f.caPath}
if f.jwksPath != nil {
fs = append(fs, *f.jwksPath)
}
if !errors.Is(err, os.ErrNotExist) {
if found, err := filesExist(fs...); err != nil {
return err
} else if found {
break
}
// Trust anchors file not be provided yet, wait.
select {
case <-ctx.Done():
return fmt.Errorf("failed to find trust anchors file '%s': %w", f.path, ctx.Err())
return fmt.Errorf("failed to find trust anchors file '%s': %w", f.caPath, ctx.Err())
case <-f.clock.After(f.initFileWatchInterval):
f.log.Warnf("Trust anchors file '%s' not found, waiting...", f.path)
f.log.Warnf("Trust anchors file '%s' not found, waiting...", f.caPath)
}
}
f.log.Infof("Trust anchors file '%s' found", f.path)
f.log.Infof("Trust anchors file '%s' found", f.caPath)
if err := f.updateAnchors(ctx); err != nil {
return err
}
targets := []string{f.caPath}
if f.jwksPath != nil {
targets = append(targets, *f.jwksPath)
}
fs, err := fswatcher.New(fswatcher.Options{
Targets: []string{filepath.Dir(f.path)},
Targets: targets,
Interval: &f.fsWatcherInterval,
})
if err != nil {
@ -120,7 +141,11 @@ func (f *file) Run(ctx context.Context) error {
close(f.readyCh)
f.log.Infof("Watching trust anchors file '%s' for changes", f.path)
f.log.Infof("Watching trust anchors file '%s' for changes", f.caPath)
if f.jwksPath != nil {
f.log.Infof("Watching JWT bundle file '%s' for changes", f.jwksPath)
}
return concurrency.NewRunnerManager(
func(ctx context.Context) error {
return fs.Run(ctx, f.caEvent)
@ -134,7 +159,7 @@ func (f *file) Run(ctx context.Context) error {
f.log.Info("Trust anchors file changed, reloading trust anchors")
if err = f.updateAnchors(ctx); err != nil {
return fmt.Errorf("failed to read trust anchors file '%s': %v", f.path, err)
return fmt.Errorf("%w: '%s': %v", ErrFailedToReadTrustAnchorsFile, f.caPath, err)
}
}
}
@ -147,7 +172,7 @@ func (f *file) CurrentTrustAnchors(ctx context.Context) ([]byte, error) {
case <-ctx.Done():
return nil, ctx.Err()
case <-f.closeCh:
return nil, errors.New("trust anchors is closed")
return nil, ErrTrustAnchorsClosed
case <-f.readyCh:
}
@ -162,9 +187,9 @@ func (f *file) updateAnchors(ctx context.Context) error {
f.lock.Lock()
defer f.lock.Unlock()
rootPEMs, err := os.ReadFile(f.path)
rootPEMs, err := os.ReadFile(f.caPath)
if err != nil {
return fmt.Errorf("failed to read trust anchors file '%s': %w", f.path, err)
return fmt.Errorf("failed to read trust anchors file '%s': %w", f.caPath, err)
}
trustAnchorCerts, err := pem.DecodePEMCertificates(rootPEMs)
@ -173,7 +198,20 @@ func (f *file) updateAnchors(ctx context.Context) error {
}
f.rootPEM = rootPEMs
f.bundle = x509bundle.FromX509Authorities(spiffeid.TrustDomain{}, trustAnchorCerts)
f.x509Bundle = x509bundle.FromX509Authorities(spiffeid.TrustDomain{}, trustAnchorCerts)
if f.jwksPath != nil {
jwks, err := os.ReadFile(*f.jwksPath)
if err != nil {
return fmt.Errorf("failed to read JWT bundle file '%s': %w", *f.jwksPath, err)
}
jwtBundle, err := jwtbundle.Parse(spiffeid.TrustDomain{}, jwks)
if err != nil {
return fmt.Errorf("failed to parse JWT bundle: %w", err)
}
f.jwtBundle = jwtBundle
}
var wg sync.WaitGroup
defer wg.Wait()
@ -195,13 +233,26 @@ func (f *file) updateAnchors(ctx context.Context) error {
func (f *file) GetX509BundleForTrustDomain(_ spiffeid.TrustDomain) (*x509bundle.Bundle, error) {
select {
case <-f.closeCh:
return nil, errors.New("trust anchors is closed")
return nil, ErrTrustAnchorsClosed
case <-f.readyCh:
}
f.lock.RLock()
defer f.lock.RUnlock()
bundle := f.bundle
bundle := f.x509Bundle
return bundle, nil
}
func (f *file) GetJWTBundleForTrustDomain(_ spiffeid.TrustDomain) (*jwtbundle.Bundle, error) {
select {
case <-f.closeCh:
return nil, ErrTrustAnchorsClosed
case <-f.readyCh:
}
f.lock.RLock()
defer f.lock.RUnlock()
bundle := f.jwtBundle
return bundle, nil
}
@ -231,3 +282,19 @@ func (f *file) Watch(ctx context.Context, ch chan<- []byte) {
}
}
}
func filesExist(paths ...string) (bool, error) {
for _, path := range paths {
if path == "" {
continue
}
if _, err := os.Stat(path); err != nil {
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, fmt.Errorf("failed to stat file '%s': %w", path, err)
}
}
return true, nil
}

View File

@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package trustanchors
package file
import (
"context"
@ -31,15 +31,15 @@ import (
func TestFile_Run(t *testing.T) {
t.Run("if Run multiple times, expect error", func(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
f.initFileWatchInterval = time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
errCh <- f.Run(ctx)
@ -74,15 +74,15 @@ func TestFile_Run(t *testing.T) {
t.Run("if file is not found and context cancelled, should return ctx.Err", func(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
f.initFileWatchInterval = time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
errCh <- f.Run(ctx)
@ -102,9 +102,9 @@ func TestFile_Run(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, nil, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -112,7 +112,7 @@ func TestFile_Run(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- f.Run(context.Background())
errCh <- f.Run(t.Context())
}()
select {
@ -127,9 +127,9 @@ func TestFile_Run(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, []byte("garbage data"), 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -137,7 +137,7 @@ func TestFile_Run(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- f.Run(context.Background())
errCh <- f.Run(t.Context())
}()
select {
@ -154,9 +154,9 @@ func TestFile_Run(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, root, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -164,7 +164,7 @@ func TestFile_Run(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- f.Run(context.Background())
errCh <- f.Run(t.Context())
}()
select {
@ -180,9 +180,9 @@ func TestFile_Run(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, pki.RootCertPEM, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -190,7 +190,7 @@ func TestFile_Run(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- f.Run(context.Background())
errCh <- f.Run(t.Context())
}()
select {
@ -199,7 +199,7 @@ func TestFile_Run(t *testing.T) {
assert.Fail(t, "expected to be ready in time")
}
b, err := f.CurrentTrustAnchors(context.Background())
b, err := f.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(t, pki.RootCertPEM, b)
})
@ -211,9 +211,9 @@ func TestFile_Run(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, root, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -221,7 +221,7 @@ func TestFile_Run(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- f.Run(context.Background())
errCh <- f.Run(t.Context())
}()
select {
@ -230,7 +230,7 @@ func TestFile_Run(t *testing.T) {
assert.Fail(t, "expected to be ready in time")
}
b, err := f.CurrentTrustAnchors(context.Background())
b, err := f.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(t, root, b)
})
@ -242,9 +242,9 @@ func TestFile_Run(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, roots, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -252,7 +252,7 @@ func TestFile_Run(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- f.Run(context.Background())
errCh <- f.Run(t.Context())
}()
select {
@ -261,7 +261,7 @@ func TestFile_Run(t *testing.T) {
assert.Fail(t, "expected to be ready in time")
}
b, err := f.CurrentTrustAnchors(context.Background())
b, err := f.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(t, roots, b)
})
@ -273,9 +273,9 @@ func TestFile_Run(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, roots, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -284,7 +284,7 @@ func TestFile_Run(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- f.Run(context.Background())
errCh <- f.Run(t.Context())
}()
select {
@ -311,15 +311,15 @@ func TestFile_GetX509BundleForTrustDomain(t *testing.T) {
root := append(pki.RootCertPEM, []byte("garbage data")...)
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, root, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
errCh := make(chan error)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
go func() {
errCh <- ta.Run(ctx)
}()
@ -337,7 +337,7 @@ func TestFile_GetX509BundleForTrustDomain(t *testing.T) {
require.NoError(t, err)
bundle, err := f.GetX509BundleForTrustDomain(trustDomain1)
require.NoError(t, err)
assert.Equal(t, f.bundle, bundle)
assert.Equal(t, f.x509Bundle, bundle)
b1, err := bundle.Marshal()
require.NoError(t, err)
assert.Equal(t, pki.RootCertPEM, b1)
@ -346,7 +346,7 @@ func TestFile_GetX509BundleForTrustDomain(t *testing.T) {
require.NoError(t, err)
bundle, err = f.GetX509BundleForTrustDomain(trustDomain2)
require.NoError(t, err)
assert.Equal(t, f.bundle, bundle)
assert.Equal(t, f.x509Bundle, bundle)
b2, err := bundle.Marshal()
require.NoError(t, err)
assert.Equal(t, pki.RootCertPEM, b2)
@ -359,23 +359,24 @@ func TestFile_Watch(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, pki.RootCertPEM, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
f.initFileWatchInterval = time.Millisecond
errCh := make(chan error)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
go func() {
errCh <- f.Run(ctx)
}()
time.Sleep(time.Millisecond * 10) // adding a small delay to ensure f.Run has finished and running
watchDone := make(chan struct{})
go func() {
ta.Watch(context.Background(), make(chan []byte))
ta.Watch(t.Context(), make(chan []byte))
close(watchDone)
}()
@ -400,22 +401,23 @@ func TestFile_Watch(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, pki.RootCertPEM, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
f.initFileWatchInterval = time.Millisecond
errCh := make(chan error)
ctx1, cancel1 := context.WithCancel(context.Background())
ctx1, cancel1 := context.WithCancel(t.Context())
go func() {
errCh <- f.Run(ctx1)
}()
time.Sleep(time.Millisecond * 10) // adding a small delay to ensure f.Run has finished and running
watchDone := make(chan struct{})
ctx2, cancel2 := context.WithCancel(context.Background())
ctx2, cancel2 := context.WithCancel(t.Context())
go func() {
ta.Watch(ctx2, make(chan []byte))
close(watchDone)
@ -446,9 +448,9 @@ func TestFile_Watch(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, pki1.RootCertPEM, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
@ -456,10 +458,11 @@ func TestFile_Watch(t *testing.T) {
f.fsWatcherInterval = time.Millisecond
errCh := make(chan error)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
go func() {
errCh <- f.Run(ctx)
}()
time.Sleep(time.Millisecond * 10) // adding a small delay to ensure f.Run has finished and running
select {
case <-f.readyCh:
@ -470,11 +473,11 @@ func TestFile_Watch(t *testing.T) {
watchDone1, watchDone2 := make(chan struct{}), make(chan struct{})
tCh1, tCh2 := make(chan []byte), make(chan []byte)
go func() {
ta.Watch(context.Background(), tCh1)
ta.Watch(t.Context(), tCh1)
close(watchDone1)
}()
go func() {
ta.Watch(context.Background(), tCh2)
ta.Watch(t.Context(), tCh2)
close(watchDone2)
}()
@ -529,26 +532,28 @@ func TestFile_CurrentTrustAnchors(t *testing.T) {
tmp := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(tmp, pki1.RootCertPEM, 0o600))
ta := FromFile(OptionsFile{
Log: logger.NewLogger("test"),
Path: tmp,
ta := From(Options{
Log: logger.NewLogger("test"),
CAPath: tmp,
})
f, ok := ta.(*file)
require.True(t, ok)
f.initFileWatchInterval = time.Millisecond
f.fsWatcherInterval = time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
errCh <- f.Run(ctx)
}()
time.Sleep(time.Millisecond * 10) // adding a small delay to ensure f.Run has finished and running
//nolint:gocritic
roots := append(pki1.RootCertPEM, pki2.RootCertPEM...)
require.NoError(t, os.WriteFile(tmp, roots, 0o600))
time.Sleep(time.Millisecond * 10) // adding a small delay to ensure the file watcher has time to pick up the change
assert.EventuallyWithT(t, func(c *assert.CollectT) {
pem, err := ta.CurrentTrustAnchors(context.Background())
pem, err := ta.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(c, roots, pem)
}, time.Second, time.Millisecond)
@ -556,8 +561,9 @@ func TestFile_CurrentTrustAnchors(t *testing.T) {
//nolint:gocritic
roots = append(pki1.RootCertPEM, append(pki2.RootCertPEM, pki3.RootCertPEM...)...)
require.NoError(t, os.WriteFile(tmp, roots, 0o600))
time.Sleep(time.Millisecond * 10) // adding a small delay to ensure the file watcher has time to pick up the change
assert.EventuallyWithT(t, func(c *assert.CollectT) {
pem, err := ta.CurrentTrustAnchors(context.Background())
pem, err := ta.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(c, roots, pem)
}, time.Second, time.Millisecond)

View File

@ -11,16 +11,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package trustanchors
package multi
import (
"context"
"errors"
"github.com/spiffe/go-spiffe/v2/bundle/jwtbundle"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/dapr/kit/concurrency"
"github.com/dapr/kit/crypto/spiffe/trustanchors"
)
var (
@ -28,17 +30,17 @@ var (
ErrTrustDomainNotFound = errors.New("trust domain not found")
)
type OptionsMulti struct {
TrustAnchors map[spiffeid.TrustDomain]Interface
type Options struct {
TrustAnchors map[spiffeid.TrustDomain]trustanchors.Interface
}
// multi is a TrustAnchors implementation which uses multiple trust anchors
// which are indexed by trust domain.
type multi struct {
trustAnchors map[spiffeid.TrustDomain]Interface
trustAnchors map[spiffeid.TrustDomain]trustanchors.Interface
}
func FromMulti(opts OptionsMulti) Interface {
func From(opts Options) trustanchors.Interface {
return &multi{
trustAnchors: opts.TrustAnchors,
}
@ -69,6 +71,16 @@ func (m *multi) GetX509BundleForTrustDomain(td spiffeid.TrustDomain) (*x509bundl
return nil, ErrTrustDomainNotFound
}
func (m *multi) GetJWTBundleForTrustDomain(td spiffeid.TrustDomain) (*jwtbundle.Bundle, error) {
for tad, ta := range m.trustAnchors {
if td.Compare(tad) == 0 {
return ta.GetJWTBundleForTrustDomain(td)
}
}
return nil, ErrTrustDomainNotFound
}
func (m *multi) Watch(context.Context, chan<- []byte) {
return
}

View File

@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package trustanchors
package static
import (
"context"
@ -19,31 +19,52 @@ import (
"fmt"
"sync/atomic"
"github.com/spiffe/go-spiffe/v2/bundle/jwtbundle"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/dapr/kit/crypto/pem"
"github.com/dapr/kit/crypto/spiffe/trustanchors"
)
// static is a TrustAcnhors implementation that uses a static list of trust
// anchors.
type static struct {
bundle *x509bundle.Bundle
anchors []byte
running atomic.Bool
closeCh chan struct{}
x509Bundle *x509bundle.Bundle
jwtBundle *jwtbundle.Bundle
anchors []byte
running atomic.Bool
closeCh chan struct{}
}
func FromStatic(anchors []byte) (Interface, error) {
trustAnchorCerts, err := pem.DecodePEMCertificates(anchors)
type Options struct {
Anchors []byte
Jwks []byte
}
func From(opts Options) (trustanchors.Interface, error) {
// Create empty trust domain for now
emptyTD := spiffeid.TrustDomain{}
var jwtBundle *jwtbundle.Bundle
if opts.Jwks != nil {
var err error
jwtBundle, err = jwtbundle.Parse(emptyTD, opts.Jwks)
if err != nil {
return nil, fmt.Errorf("failed to create JWT bundle: %w", err)
}
}
trustAnchorCerts, err := pem.DecodePEMCertificates(opts.Anchors)
if err != nil {
return nil, fmt.Errorf("failed to decode trust anchors: %w", err)
}
return &static{
anchors: anchors,
bundle: x509bundle.FromX509Authorities(spiffeid.TrustDomain{}, trustAnchorCerts),
closeCh: make(chan struct{}),
anchors: opts.Anchors,
x509Bundle: x509bundle.FromX509Authorities(emptyTD, trustAnchorCerts),
jwtBundle: jwtBundle,
closeCh: make(chan struct{}),
}, nil
}
@ -63,7 +84,11 @@ func (s *static) Run(ctx context.Context) error {
}
func (s *static) GetX509BundleForTrustDomain(spiffeid.TrustDomain) (*x509bundle.Bundle, error) {
return s.bundle, nil
return s.x509Bundle, nil
}
func (s *static) GetJWTBundleForTrustDomain(_ spiffeid.TrustDomain) (*jwtbundle.Bundle, error) {
return s.jwtBundle, nil
}
func (s *static) Watch(ctx context.Context, _ chan<- []byte) {

View File

@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package trustanchors
package static
import (
"context"
@ -27,32 +27,32 @@ import (
func TestFromStatic(t *testing.T) {
t.Run("empty root should return error", func(t *testing.T) {
_, err := FromStatic(nil)
_, err := From(Options{})
require.Error(t, err)
})
t.Run("garbage data should return error", func(t *testing.T) {
_, err := FromStatic([]byte("garbage data"))
_, err := From(Options{Anchors: []byte("garbage data")})
require.Error(t, err)
})
t.Run("just garbage data should return error", func(t *testing.T) {
_, err := FromStatic([]byte("garbage data"))
_, err := From(Options{Anchors: []byte("garbage data")})
require.Error(t, err)
})
t.Run("garbage data in root should return error", func(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{})
root := pki.RootCertPEM[10:]
_, err := FromStatic(root)
_, err := From(Options{Anchors: root})
require.Error(t, err)
})
t.Run("single root should be correctly parsed", func(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{})
ta, err := FromStatic(pki.RootCertPEM)
ta, err := From(Options{Anchors: pki.RootCertPEM})
require.NoError(t, err)
taPEM, err := ta.CurrentTrustAnchors(context.Background())
taPEM, err := ta.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(t, pki.RootCertPEM, taPEM)
})
@ -61,9 +61,9 @@ func TestFromStatic(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{})
//nolint:gocritic
root := append(pki.RootCertPEM, []byte("garbage data")...)
ta, err := FromStatic(root)
ta, err := From(Options{Anchors: root})
require.NoError(t, err)
taPEM, err := ta.CurrentTrustAnchors(context.Background())
taPEM, err := ta.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(t, root, taPEM)
})
@ -72,9 +72,9 @@ func TestFromStatic(t *testing.T) {
pki1, pki2 := test.GenPKI(t, test.PKIOptions{}), test.GenPKI(t, test.PKIOptions{})
//nolint:gocritic
roots := append(pki1.RootCertPEM, pki2.RootCertPEM...)
ta, err := FromStatic(roots)
ta, err := From(Options{Anchors: roots})
require.NoError(t, err)
taPEM, err := ta.CurrentTrustAnchors(context.Background())
taPEM, err := ta.CurrentTrustAnchors(t.Context())
require.NoError(t, err)
assert.Equal(t, roots, taPEM)
})
@ -85,7 +85,7 @@ func TestStatic_GetX509BundleForTrustDomain(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{})
//nolint:gocritic
root := append(pki.RootCertPEM, []byte("garbage data")...)
ta, err := FromStatic(root)
ta, err := From(Options{Anchors: root})
require.NoError(t, err)
s, ok := ta.(*static)
require.True(t, ok)
@ -94,7 +94,7 @@ func TestStatic_GetX509BundleForTrustDomain(t *testing.T) {
require.NoError(t, err)
bundle, err := s.GetX509BundleForTrustDomain(trustDomain1)
require.NoError(t, err)
assert.Equal(t, s.bundle, bundle)
assert.Equal(t, s.x509Bundle, bundle)
b1, err := bundle.Marshal()
require.NoError(t, err)
assert.Equal(t, pki.RootCertPEM, b1)
@ -103,7 +103,7 @@ func TestStatic_GetX509BundleForTrustDomain(t *testing.T) {
require.NoError(t, err)
bundle, err = s.GetX509BundleForTrustDomain(trustDomain2)
require.NoError(t, err)
assert.Equal(t, s.bundle, bundle)
assert.Equal(t, s.x509Bundle, bundle)
b2, err := bundle.Marshal()
require.NoError(t, err)
assert.Equal(t, pki.RootCertPEM, b2)
@ -113,12 +113,12 @@ func TestStatic_GetX509BundleForTrustDomain(t *testing.T) {
func TestStatic_Run(t *testing.T) {
t.Run("Run multiple times should return error", func(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{})
ta, err := FromStatic(pki.RootCertPEM)
ta, err := From(Options{Anchors: pki.RootCertPEM})
require.NoError(t, err)
s, ok := ta.(*static)
require.True(t, ok)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
errCh <- s.Run(ctx)
@ -154,10 +154,10 @@ func TestStatic_Run(t *testing.T) {
func TestStatic_Watch(t *testing.T) {
t.Run("should return when context is cancelled", func(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{})
ta, err := FromStatic(pki.RootCertPEM)
ta, err := From(Options{Anchors: pki.RootCertPEM})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
doneCh := make(chan struct{})
go func() {
@ -176,10 +176,10 @@ func TestStatic_Watch(t *testing.T) {
t.Run("should return when cancel is closed via closed Run", func(t *testing.T) {
pki := test.GenPKI(t, test.PKIOptions{})
ta, err := FromStatic(pki.RootCertPEM)
ta, err := From(Options{Anchors: pki.RootCertPEM})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
doneCh := make(chan struct{})
errCh := make(chan error)
@ -188,7 +188,7 @@ func TestStatic_Watch(t *testing.T) {
}()
go func() {
ta.Watch(context.Background(), nil)
ta.Watch(t.Context(), nil)
close(doneCh)
}()

View File

@ -16,6 +16,7 @@ package trustanchors
import (
"context"
"github.com/spiffe/go-spiffe/v2/bundle/jwtbundle"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
)
@ -23,8 +24,10 @@ import (
// Allows consumers to get the current trust anchor bundle, and subscribe to
// bundle updates.
type Interface interface {
// Source implements the SPIFFE trust anchor bundle source.
// Source implements the SPIFFE trust anchor x509 bundle source.
x509bundle.Source
// Source implements the SPIFFE trust anchor jwt bundle source.
jwtbundle.Source
// CurrentTrustAnchors returns the current trust anchor PEM bundle.
CurrentTrustAnchors(ctx context.Context) ([]byte, error)

View File

@ -155,12 +155,12 @@ func (p PKI) ClientGRPCCtx(t *testing.T) context.Context {
server.Serve(lis)
}()
//nolint:staticcheck
conn, err := grpc.DialContext(context.Background(), lis.Addr().String(),
conn, err := grpc.DialContext(t.Context(), lis.Addr().String(),
grpc.WithTransportCredentials(grpccredentials.MTLSClientCredentials(clientSVID, clientSVID, tlsconfig.AuthorizeAny())),
)
require.NoError(t, err)
_, err = helloworld.NewGreeterClient(conn).SayHello(context.Background(), new(helloworld.HelloRequest))
_, err = helloworld.NewGreeterClient(conn).SayHello(t.Context(), new(helloworld.HelloRequest))
require.NoError(t, err)
lis.Close()

View File

@ -41,7 +41,7 @@ pHZ3vWGFAoGAc5Um3YYkhh2QScQBy5+kumH40LhFFy2ETznWEp0tS2NwmTfTm/Nl
Sg+Ct2nOw93cIhwDjWyoilkIapuuX2obY+sUc3kj2ugU+hONfuBStsF020IPP1sk
A9okIZVbz8ycqcjaBiNc4+TeiXED1K7bV9Kg+A9lxDxfGRybJ1/ECWA=
-----END RSA PRIVATE KEY-----
`
` // #nosec G101
privateKeyRSAPKCS8 = `-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcjaZ0griZFG77
LAytiNRnMHG3Q2UBUusyEaVomxvLs9ZMyIullWKnhIEP0bCcJTRMYUPuTb7u1+zT
@ -128,7 +128,7 @@ MHcCAQEEIOcFe4Q6ardS97ml2tV4+194nmlfQPh8o9ir/qsacEozoAoGCCqGSM49
AwEHoUQDQgAEUMn1c2ioMNi2DqvC8hdBVUERFZ97eVFsNVcQIgR0Hsq5PVrQ/dQ4
uI5u97b6k4wXHYFXMvPmsW1T6qZAE9bB3Q==
-----END EC PRIVATE KEY-----
`
` // #nosec G101
privateKeyP256PKCS8 = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5wV7hDpqt1L3uaXa
1Xj7X3ieaV9A+Hyj2Kv+qxpwSjOhRANCAARQyfVzaKgw2LYOq8LyF0FVQREVn3t5

42
env/env.go vendored Normal file
View File

@ -0,0 +1,42 @@
/*
Copyright 2024 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package env
import (
"fmt"
"os"
"time"
)
// GetDurationWithRange returns the time.Duration value of the environment variable specified by `envVar`.
// If the environment variable is not set, it returns `defaultValue`.
// If the value is set but is not valid (not a valid time.Duration or falls outside the specified range
// [minValue, maxValue] inclusively), it returns `defaultValue` and an error.
func GetDurationWithRange(envVar string, defaultValue, min, max time.Duration) (time.Duration, error) {
v := os.Getenv(envVar)
if v == "" {
return defaultValue, nil
}
val, err := time.ParseDuration(v)
if err != nil {
return defaultValue, fmt.Errorf("invalid time.Duration value %s for the %s env variable: %w", val, envVar, err)
}
if val < min || val > max {
return defaultValue, fmt.Errorf("invalid value for the %s env variable: value should be between %s and %s, got %s", envVar, min, max, val)
}
return val, nil
}

View File

@ -1,4 +1,17 @@
package utils
/*
Copyright 2024 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package env
import (
"testing"
@ -7,7 +20,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestGetEnvIntWithRangeWrongValues(t *testing.T) {
func TestGetIntWithRangeWrongValues(t *testing.T) {
testValues := []struct {
name string
envVarVal string
@ -43,7 +56,7 @@ func TestGetEnvIntWithRangeWrongValues(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("MY_ENV", tt.envVarVal)
val, err := GetEnvDurationWithRange("MY_ENV", defaultValue, tt.min, tt.max)
val, err := GetDurationWithRange("MY_ENV", defaultValue, tt.min, tt.max)
require.Error(t, err)
require.Contains(t, err.Error(), tt.error)
require.Equal(t, defaultValue, val)
@ -75,7 +88,7 @@ func TestGetEnvDurationWithRangeValidValues(t *testing.T) {
t.Setenv("MY_ENV", tt.envVarVal)
}
val, err := GetEnvDurationWithRange("MY_ENV", 3*time.Second, time.Second, 5*time.Second)
val, err := GetDurationWithRange("MY_ENV", 3*time.Second, time.Second, 5*time.Second)
require.NoError(t, err)
require.Equal(t, tt.result, val)
})

View File

@ -24,7 +24,7 @@ const (
CodePrefixStateStore = "DAPR_STATE_"
CodePrefixPubSub = "DAPR_PUBSUB_"
CodePrefixBindings = "DAPR_BINDING_"
CodePrefixSecretStore = "DAPR_SECRET_"
CodePrefixSecretStore = "DAPR_SECRET_" // #nosec G101
CodePrefixConfigurationStore = "DAPR_CONFIGURATION_"
CodePrefixLock = "DAPR_LOCK_"
CodePrefixNameResolution = "DAPR_NAME_RESOLUTION_"

View File

@ -63,7 +63,7 @@ type Error struct {
// ErrorBuilder is used to build the error
type ErrorBuilder struct {
err Error
err *Error
}
// errorJSON is used to build the error for the HTTP Methods json output
@ -106,12 +106,12 @@ func (e *Error) Category() string {
}
// Error implements the error interface.
func (e Error) Error() string {
func (e *Error) Error() string {
return e.String()
}
// String returns the string representation.
func (e Error) String() string {
func (e *Error) String() string {
return fmt.Sprintf(errStringFormat, e.grpcCode.String(), e.message)
}
@ -140,9 +140,9 @@ func FromError(err error) (*Error, bool) {
return nil, false
}
var kitErr Error
var kitErr *Error
if errors.As(err, &kitErr) {
return &kitErr, true
return kitErr, true
}
return nil, false
@ -151,7 +151,7 @@ func FromError(err error) (*Error, bool) {
/*** GRPC Methods ***/
// GRPCStatus returns the gRPC status.Status object.
func (e Error) GRPCStatus() *status.Status {
func (e *Error) GRPCStatus() *status.Status {
stat := status.New(e.grpcCode, e.message)
// convert details from proto.Msg -> protoiface.MsgV1
@ -178,7 +178,7 @@ func (e Error) GRPCStatus() *status.Status {
/*** HTTP Methods ***/
// JSONErrorValue implements the errorResponseValue interface.
func (e Error) JSONErrorValue() []byte {
func (e *Error) JSONErrorValue() []byte {
grpcStatus := e.GRPCStatus().Proto()
// Make httpCode human readable
@ -200,7 +200,7 @@ func (e Error) JSONErrorValue() []byte {
if len(details) > 0 {
errJSON.Details = make([]any, len(details))
for i, detail := range details {
detailMap, errorCode := convertErrorDetails(detail, e)
detailMap, errorCode := convertErrorDetails(detail, *e)
errJSON.Details[i] = detailMap
// If there is an errorCode, update the overall ErrorCode
@ -357,7 +357,7 @@ ErrorBuilder
// NewBuilder create a new ErrorBuilder using the supplied required error fields
func NewBuilder(grpcCode grpcCodes.Code, httpCode int, message string, tag string, category string) *ErrorBuilder {
return &ErrorBuilder{
err: Error{
err: &Error{
details: make([]proto.Message, 0),
grpcCode: grpcCode,
httpCode: httpCode,

View File

@ -50,7 +50,7 @@ func TestError_HTTPStatusCode(t *testing.T) {
WithErrorInfo("fake", map[string]string{"fake": "test"}).
Build()
err, ok := kitErr.(Error)
err, ok := kitErr.(*Error)
require.True(t, ok, httpStatusCode, err.HTTPStatusCode())
}
@ -66,7 +66,7 @@ func TestError_GrpcStatusCode(t *testing.T) {
WithErrorInfo("fake", map[string]string{"fake": "test"}).
Build()
err, ok := kitErr.(Error)
err, ok := kitErr.(*Error)
require.True(t, ok, grpcStatusCode, err.GrpcStatusCode())
}
@ -171,7 +171,7 @@ func TestError_Error(t *testing.T) {
t.Errorf("got = %v, want %v", got, tt.want)
}
err, ok := kitErr.(Error)
err, ok := kitErr.(*Error)
require.True(t, ok, err.Is(kitErr))
})
}
@ -186,7 +186,7 @@ func TestErrorBuilder_WithErrorInfo(t *testing.T) {
Metadata: metadata,
}
expected := Error{
expected := &Error{
grpcCode: grpcCodes.ResourceExhausted,
httpCode: http.StatusTeapot,
message: "fake_message",
@ -240,7 +240,7 @@ func TestErrorBuilder_WithDetails(t *testing.T) {
name string
fields fields
args args
want Error
want *Error
}{
{
name: "Has_Multiple_Details",
@ -263,7 +263,7 @@ func TestErrorBuilder_WithDetails(t *testing.T) {
Description: "test_description",
},
}},
want: Error{
want: &Error{
grpcCode: grpcCodes.ResourceExhausted,
httpCode: http.StatusTeapot,
message: "fake_message",
@ -802,7 +802,7 @@ func TestErrorBuilder_Build(t *testing.T) {
"some_category",
).WithErrorInfo("fake", map[string]string{"fake": "test"}).Build()
builtErr, ok := built.(Error)
builtErr, ok := built.(*Error)
require.True(t, ok)
containsErrorInfo := false
@ -828,7 +828,7 @@ func TestErrorBuilder_Build(t *testing.T) {
"some_category",
).WithErrorInfo("SOME_ERROR", map[string]string{"fake": "test"}).Build()
builtErr, ok := built.(Error)
builtErr, ok := built.(*Error)
require.True(t, ok)
containsErrorInfo := false
@ -990,7 +990,7 @@ func TestFromError(t *testing.T) {
t.Errorf("Expected result to be nil and ok to be false, got result: %v, ok: %t", result, ok)
}
kitErr := Error{
kitErr := &Error{
grpcCode: grpcCodes.ResourceExhausted,
httpCode: http.StatusTeapot,
message: "fake_message",
@ -999,8 +999,8 @@ func TestFromError(t *testing.T) {
}
result, ok = FromError(kitErr)
if !ok || !reflect.DeepEqual(result, &kitErr) {
t.Errorf("Expected result to be %#v and ok to be true, got result: %#v, ok: %t", &kitErr, result, ok)
if !ok || !reflect.DeepEqual(result, kitErr) {
t.Errorf("Expected result to be %#v and ok to be true, got result: %#v, ok: %t", kitErr, result, ok)
}
var nonKitError error
@ -1011,7 +1011,7 @@ func TestFromError(t *testing.T) {
wrapped := fmt.Errorf("wrapped: %w", kitErr)
result, ok = FromError(wrapped)
if !ok || !reflect.DeepEqual(result, &kitErr) {
t.Errorf("Expected result to be %#v and ok to be true, got result: %#v, ok: %t", &kitErr, result, ok)
if !ok || !reflect.DeepEqual(result, kitErr) {
t.Errorf("Expected result to be %#v and ok to be true, got result: %#v, ok: %t", kitErr, result, ok)
}
}

View File

@ -14,7 +14,6 @@ limitations under the License.
package batcher
import (
"context"
"testing"
"time"
@ -44,7 +43,7 @@ func TestSubscribe(t *testing.T) {
b := New[string, struct{}](Options{Interval: time.Millisecond * 10})
ch := make(chan struct{})
b.Subscribe(context.Background(), ch)
b.Subscribe(t.Context(), ch)
assert.Len(t, b.eventChs, 1)
}
@ -59,8 +58,8 @@ func TestBatch(t *testing.T) {
ch1 := make(chan struct{})
ch2 := make(chan struct{})
ch3 := make(chan struct{})
b.Subscribe(context.Background(), ch1, ch2)
b.Subscribe(context.Background(), ch3)
b.Subscribe(t.Context(), ch1, ch2)
b.Subscribe(t.Context(), ch3)
b.Batch("key1", struct{}{})
b.Batch("key1", struct{}{})
@ -95,7 +94,7 @@ func TestBatch(t *testing.T) {
fakeClock.Step(time.Millisecond * 5)
for i := 0; i < 3; i++ {
for range 3 {
for _, ch := range []chan struct{}{ch1, ch2, ch3} {
select {
case <-ch:
@ -115,10 +114,10 @@ func TestBatch(t *testing.T) {
ch1 := make(chan int, 10)
ch2 := make(chan int, 10)
ch3 := make(chan int, 10)
b.Subscribe(context.Background(), ch1, ch2)
b.Subscribe(context.Background(), ch3)
b.Subscribe(t.Context(), ch1, ch2)
b.Subscribe(t.Context(), ch3)
for i := 0; i < 10; i++ {
for i := range 10 {
b.Batch(i, i)
b.Batch(i, i+1)
b.Batch(i, i+2)
@ -126,7 +125,7 @@ func TestBatch(t *testing.T) {
}
for _, ch := range []chan int{ch1} {
for i := 0; i < 10; i++ {
for i := range 10 {
select {
case v := <-ch:
assert.Equal(t, i+2, v)
@ -143,7 +142,7 @@ func TestClose(t *testing.T) {
b := New[string, struct{}](Options{Interval: time.Millisecond * 10})
ch := make(chan struct{})
b.Subscribe(context.Background(), ch)
b.Subscribe(t.Context(), ch)
assert.Len(t, b.eventChs, 1)
b.Batch("key1", struct{}{})
b.Close()
@ -156,6 +155,6 @@ func TestSubscribeAfterClose(t *testing.T) {
b := New[string, struct{}](Options{Interval: time.Millisecond * 10})
b.Close()
ch := make(chan struct{})
b.Subscribe(context.Background(), ch)
b.Subscribe(t.Context(), ch)
assert.Empty(t, b.eventChs)
}

65
events/loop/fake/fake.go Normal file
View File

@ -0,0 +1,65 @@
/*
Copyright 2025 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fake
import (
"context"
"github.com/dapr/kit/events/loop"
)
type Fake[T any] struct {
runFn func(context.Context) error
enqueueFn func(T)
closeFn func(T)
}
func New[T any]() *Fake[T] {
return &Fake[T]{
runFn: func(context.Context) error { return nil },
enqueueFn: func(T) {},
closeFn: func(T) {},
}
}
func (f *Fake[T]) WithRun(fn func(context.Context) error) *Fake[T] {
f.runFn = fn
return f
}
func (f *Fake[T]) WithEnqueue(fn func(T)) *Fake[T] {
f.enqueueFn = fn
return f
}
func (f *Fake[T]) WithClose(fn func(T)) *Fake[T] {
f.closeFn = fn
return f
}
func (f *Fake[T]) Run(ctx context.Context) error {
return f.runFn(ctx)
}
func (f *Fake[T]) Enqueue(t T) {
f.enqueueFn(t)
}
func (f *Fake[T]) Close(t T) {
f.closeFn(t)
}
func (f *Fake[T]) Reset(loop.Handler[T], uint64) loop.Interface[T] {
return f
}

View File

@ -0,0 +1,24 @@
/*
Copyright 2025 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fake
import (
"testing"
"github.com/dapr/kit/events/loop"
)
func Test_Fake(*testing.T) {
var _ loop.Interface[int] = New[int]()
}

View File

@ -15,58 +15,97 @@ package loop
import (
"context"
"sync"
)
type HandlerFunc[T any] func(context.Context, T) error
type Options[T any] struct {
Handler HandlerFunc[T]
BufferSize *uint64
type Handler[T any] interface {
Handle(ctx context.Context, t T) error
}
type Loop[T any] struct {
type Interface[T any] interface {
Run(ctx context.Context) error
Enqueue(t T)
Close(t T)
Reset(h Handler[T], size uint64) Interface[T]
}
type loop[T any] struct {
queue chan T
handler HandlerFunc[T]
handler Handler[T]
closed bool
closeCh chan struct{}
lock sync.RWMutex
}
func New[T any](opts Options[T]) *Loop[T] {
size := 1
if opts.BufferSize != nil {
size = int(*opts.BufferSize)
}
return &Loop[T]{
func New[T any](h Handler[T], size uint64) Interface[T] {
return &loop[T]{
queue: make(chan T, size),
handler: h,
closeCh: make(chan struct{}),
handler: opts.Handler,
}
}
func (l *Loop[T]) Run(ctx context.Context) error {
func Empty[T any]() Interface[T] {
return new(loop[T])
}
func (l *loop[T]) Run(ctx context.Context) error {
defer close(l.closeCh)
for {
var req T
select {
case req = <-l.queue:
case <-ctx.Done():
req, ok := <-l.queue
if !ok {
return nil
}
if err := ctx.Err(); err != nil {
return err
}
if err := l.handler(ctx, req); err != nil {
if err := l.handler.Handle(ctx, req); err != nil {
return err
}
}
}
func (l *Loop[T]) Enqueue(req T) {
func (l *loop[T]) Enqueue(req T) {
l.lock.RLock()
defer l.lock.RUnlock()
if l.closed {
return
}
select {
case l.queue <- req:
case <-l.closeCh:
}
}
func (l *loop[T]) Close(req T) {
l.lock.Lock()
l.closed = true
select {
case l.queue <- req:
case <-l.closeCh:
}
close(l.queue)
l.lock.Unlock()
<-l.closeCh
}
func (l *loop[T]) Reset(h Handler[T], size uint64) Interface[T] {
if l == nil {
return New[T](h, size)
}
l.lock.Lock()
defer l.lock.Unlock()
l.closed = false
l.closeCh = make(chan struct{})
l.handler = h
// TODO: @joshvanl: use a ring buffer so that we don't need to reallocate and
// improve performance.
l.queue = make(chan T, size)
return l
}

View File

@ -59,7 +59,7 @@ func ExampleProcessor() {
// Using Dequeue allows removing an item from the queue
processor.Dequeue("item4")
for i := 0; i < 3; i++ {
for range 3 {
fmt.Println(<-executed)
}
// Output:

View File

@ -271,7 +271,7 @@ func TestProcessor(t *testing.T) {
)
now := clock.Now()
wg := sync.WaitGroup{}
for i := 0; i < count; i++ {
for i := range count {
wg.Add(1)
go func(i int) {
defer wg.Done()
@ -312,7 +312,7 @@ func TestProcessor(t *testing.T) {
close(doneCh)
// Ensure all items are true
for i := 0; i < count; i++ {
for i := range count {
assert.Truef(t, collected[i], "item %d not received", i)
}
})
@ -402,7 +402,7 @@ func TestClose(t *testing.T) {
default:
}
for i := 0; i < 3; i++ {
for range 3 {
select {
case err := <-closeCh:
require.NoError(t, err)

View File

@ -39,7 +39,7 @@ func TestCoalescing(t *testing.T) {
ch := make(chan struct{})
errCh := make(chan error)
go func() {
errCh <- c.Run(context.Background(), ch)
errCh <- c.Run(t.Context(), ch)
}()
t.Cleanup(func() {
@ -78,7 +78,7 @@ func TestCoalescing(t *testing.T) {
c, err := NewCoalescing(OptionsCoalescing{})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
errCh := make(chan error)
go func() {
errCh <- c.Run(ctx, make(chan struct{}))
@ -100,7 +100,7 @@ func TestCoalescing(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- c.Run(context.Background(), make(chan struct{}))
errCh <- c.Run(t.Context(), make(chan struct{}))
}()
c.Close()
@ -119,7 +119,7 @@ func TestCoalescing(t *testing.T) {
errCh := make(chan error)
go func() {
errCh <- c.Run(context.Background(), make(chan struct{}))
errCh <- c.Run(t.Context(), make(chan struct{}))
}()
c.Close()
@ -132,7 +132,7 @@ func TestCoalescing(t *testing.T) {
}
go func() {
errCh <- c.Run(context.Background(), make(chan struct{}))
errCh <- c.Run(t.Context(), make(chan struct{}))
}()
select {
@ -277,7 +277,7 @@ func TestCoalescing(t *testing.T) {
c.Add()
assertNoChannel(t, ch)
for i := 0; i < 4; i++ {
for range 4 {
assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond)
clock.Step(time.Second * 4)
c.Add()
@ -345,7 +345,7 @@ func TestCoalescing(t *testing.T) {
assertChannel(t, ch)
assert.Eventually(t, c.hasTimer.Load, time.Second, time.Millisecond)
for i := 0; i < 10; i++ {
for range 10 {
c.Add()
}
assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond)

View File

@ -43,7 +43,7 @@ func TestFSWatcher(t *testing.T) {
}
errCh := make(chan error)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
eventsCh := make(chan struct{})
go func() {
errCh <- f.Run(ctx, eventsCh)
@ -84,7 +84,7 @@ func TestFSWatcher(t *testing.T) {
t.Run("running Run twice should error", func(t *testing.T) {
fs, err := New(Options{})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
require.NoError(t, fs.Run(ctx, make(chan struct{})))
require.Error(t, fs.Run(ctx, make(chan struct{})))
@ -101,7 +101,7 @@ func TestFSWatcher(t *testing.T) {
t.Run("should fire event when event occurs on target file", func(t *testing.T) {
fp := filepath.Join(t.TempDir(), "test.txt")
require.NoError(t, os.WriteFile(fp, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp, []byte{}, 0o600))
eventsCh := runWatcher(t, Options{
Targets: []string{fp},
Interval: ptr.Of(time.Duration(1)),
@ -112,7 +112,7 @@ func TestFSWatcher(t *testing.T) {
// If running in windows, wait for notify to be ready.
time.Sleep(time.Second)
}
require.NoError(t, os.WriteFile(fp, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp, []byte{}, 0o600))
select {
case <-eventsCh:
@ -124,16 +124,16 @@ func TestFSWatcher(t *testing.T) {
t.Run("should fire 2 events when event occurs on 2 file target", func(t *testing.T) {
fp1 := filepath.Join(t.TempDir(), "test.txt")
fp2 := filepath.Join(t.TempDir(), "test.txt")
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o600))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o600))
eventsCh := runWatcher(t, Options{
Targets: []string{fp1, fp2},
Interval: ptr.Of(time.Duration(1)),
}, nil)
assert.Empty(t, eventsCh)
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o644))
for i := 0; i < 2; i++ {
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o600))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o600))
for range 2 {
select {
case <-eventsCh:
case <-time.After(time.Second):
@ -146,8 +146,8 @@ func TestFSWatcher(t *testing.T) {
dir := t.TempDir()
fp1 := filepath.Join(dir, "test1.txt")
fp2 := filepath.Join(dir, "test2.txt")
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o600))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o600))
eventsCh := runWatcher(t, Options{
Targets: []string{fp1, fp2},
Interval: ptr.Of(time.Duration(1)),
@ -157,9 +157,9 @@ func TestFSWatcher(t *testing.T) {
time.Sleep(time.Second)
}
assert.Empty(t, eventsCh)
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o644))
for i := 0; i < 2; i++ {
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o600))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o600))
for range 2 {
select {
case <-eventsCh:
case <-time.After(time.Second):
@ -178,9 +178,9 @@ func TestFSWatcher(t *testing.T) {
Interval: ptr.Of(time.Duration(1)),
}, nil)
assert.Empty(t, eventsCh)
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o644))
for i := 0; i < 2; i++ {
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o600))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o600))
for range 2 {
select {
case <-eventsCh:
case <-time.After(time.Second):
@ -207,9 +207,9 @@ func TestFSWatcher(t *testing.T) {
time.Sleep(time.Second)
}
for i := 0; i < 10; i++ {
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o644))
for range 10 {
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o600))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o600))
}
assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond*10)
@ -222,9 +222,9 @@ func TestFSWatcher(t *testing.T) {
clock.Step(time.Millisecond * 250)
for i := 0; i < 10; i++ {
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o644))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o644))
for range 10 {
require.NoError(t, os.WriteFile(fp1, []byte{}, 0o600))
require.NoError(t, os.WriteFile(fp2, []byte{}, 0o600))
}
select {
@ -236,7 +236,7 @@ func TestFSWatcher(t *testing.T) {
assert.Eventually(t, clock.HasWaiters, time.Second, time.Millisecond*10)
clock.Step(time.Millisecond * 500)
for i := 0; i < 2; i++ {
for range 2 {
select {
case <-eventsCh:
case <-time.After(time.Second):

29
go.mod
View File

@ -1,6 +1,6 @@
module github.com/dapr/kit
go 1.23.1
go 1.24.3
require (
github.com/alphadose/haxmap v1.3.1
@ -11,16 +11,16 @@ require (
github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.5.1
github.com/spiffe/go-spiffe/v2 v2.1.7
github.com/stretchr/testify v1.9.0
github.com/spiffe/go-spiffe/v2 v2.5.0
github.com/stretchr/testify v1.10.0
github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde
golang.org/x/crypto v0.24.0
golang.org/x/crypto v0.39.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237
google.golang.org/grpc v1.64.0
golang.org/x/tools v0.33.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822
google.golang.org/grpc v1.73.0
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20
google.golang.org/protobuf v1.33.0
google.golang.org/protobuf v1.36.6
k8s.io/apimachinery v0.26.9
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
)
@ -28,6 +28,7 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
@ -36,12 +37,12 @@ require (
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/zeebo/errs v1.3.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

84
go.sum
View File

@ -1,5 +1,5 @@
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alphadose/haxmap v1.3.1 h1:KmZh75duO1tC8pt3LmUwoTYiZ9sh4K52FX8p7/yrlqU=
github.com/alphadose/haxmap v1.3.1/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
@ -13,16 +13,24 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -57,70 +65,82 @@ 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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.1.7 h1:VUkM1yIyg/x8X7u1uXqSRVRCdMdfRIEdFBzpqoeASGk=
github.com/spiffe/go-spiffe/v2 v2.1.7/go.mod h1:QJDGdhXllxjxvd5B+2XnhhXB/+rC8gr+lNrtOryiWeE=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0=
github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde/go.mod h1:MvrEmduDUz4ST5pGZ7CABCnOU5f3ZiOAZzT6b1A6nX8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
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/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
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.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
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.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -37,9 +37,9 @@ import (
"github.com/lestrrat-go/httprc"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/dapr/kit/crypto/pem"
"github.com/dapr/kit/fswatcher"
"github.com/dapr/kit/logger"
"github.com/dapr/kit/utils"
)
const (
@ -198,7 +198,7 @@ func (c *JWKSCache) initJWKSFromURL(ctx context.Context, url string) error {
// Load CA certificates if we have one
if c.caCertificate != "" {
caCert, err := utils.GetPEM(c.caCertificate)
caCert, err := pem.GetPEM(c.caCertificate)
if err != nil {
return fmt.Errorf("failed to load CA certificate: %w", err)
}

View File

@ -40,7 +40,7 @@ func TestJWKSCache(t *testing.T) {
t.Run("init with value", func(t *testing.T) {
cache := NewJWKSCache(testJWKS1, log)
err := cache.initCache(context.Background())
err := cache.initCache(t.Context())
require.NoError(t, err)
set := cache.KeySet()
@ -53,7 +53,7 @@ func TestJWKSCache(t *testing.T) {
t.Run("init with base64-encoded value", func(t *testing.T) {
cache := NewJWKSCache(base64.StdEncoding.EncodeToString([]byte(testJWKS1)), log)
err := cache.initCache(context.Background())
err := cache.initCache(t.Context())
require.NoError(t, err)
set := cache.KeySet()
@ -68,12 +68,12 @@ func TestJWKSCache(t *testing.T) {
// Create a temporary directory and put the JWKS in there
dir := t.TempDir()
path := filepath.Join(dir, "jwks.json")
err := os.WriteFile(path, []byte(testJWKS1), 0o666)
err := os.WriteFile(path, []byte(testJWKS1), 0o600)
require.NoError(t, err)
// Should wait for first file to be loaded before initialization is reported as completed
cache := NewJWKSCache(path, log)
err = cache.initCache(context.Background())
err = cache.initCache(t.Context())
require.NoError(t, err)
set := cache.KeySet()
@ -87,7 +87,7 @@ func TestJWKSCache(t *testing.T) {
time.Sleep(time.Second)
// Update the file and verify it's picked up
err = os.WriteFile(path, []byte(testJWKS2), 0o666)
err = os.WriteFile(path, []byte(testJWKS2), 0o600)
require.NoError(t, err)
assert.Eventually(t, func() bool {
@ -127,7 +127,7 @@ func TestJWKSCache(t *testing.T) {
cache := NewJWKSCache("http://localhost/jwks.json", log)
cache.SetHTTPClient(client)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
err := cache.initCache(ctx)
require.NoError(t, err)
@ -142,7 +142,7 @@ func TestJWKSCache(t *testing.T) {
t.Run("start and wait for init", func(t *testing.T) {
cache := NewJWKSCache(testJWKS1, log)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
// Start in background
@ -174,7 +174,7 @@ func TestJWKSCache(t *testing.T) {
cache := NewJWKSCache("https://localhost/jwks.json", log)
cache.SetHTTPClient(client)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
// Start in background
@ -194,7 +194,7 @@ func TestJWKSCache(t *testing.T) {
})
t.Run("start and init times out", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 1500*time.Millisecond)
defer cancel()
// Create a custom HTTP client with a RoundTripper that doesn't require starting a TCP listener
@ -223,7 +223,7 @@ func TestJWKSCache(t *testing.T) {
}()
// Wait for initialization
err := cache.WaitForCacheReady(context.Background())
err := cache.WaitForCacheReady(t.Context())
require.Error(t, err)
require.ErrorContains(t, err, "failed to fetch JWKS")
require.ErrorIs(t, err, context.DeadlineExceeded)

View File

@ -25,7 +25,6 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
)
const fakeLoggerName = "fakeLogger"
@ -286,7 +285,7 @@ func TestWithTypeFields(t *testing.T) {
testLogger.Info("testLogger with log LogType")
b, _ = buf.ReadBytes('\n')
maps.Clear(o)
clear(o)
require.NoError(t, json.Unmarshal(b, &o))
assert.Equalf(t, LogTypeLog, o[logFieldType], "testLogger must be %s type", LogTypeLog)
@ -309,12 +308,12 @@ func TestWithFields(t *testing.T) {
}).Info("🙃")
b, _ := buf.ReadBytes('\n')
maps.Clear(o)
clear(o)
require.NoError(t, json.Unmarshal(b, &o))
assert.Equal(t, "🙃", o["msg"])
assert.Equal(t, "world", o["hello"])
assert.Equal(t, float64(42), o["answer"])
assert.InDelta(t, float64(42), o["answer"], 000.1)
// Test with other fields
testLogger.WithFields(map[string]any{
@ -322,7 +321,7 @@ func TestWithFields(t *testing.T) {
}).Info("🐶")
b, _ = buf.ReadBytes('\n')
maps.Clear(o)
clear(o)
require.NoError(t, json.Unmarshal(b, &o))
assert.Equal(t, "🐶", o["msg"])
@ -336,7 +335,7 @@ func TestWithFields(t *testing.T) {
testLogger.Info("🤔")
b, _ = buf.ReadBytes('\n')
maps.Clear(o)
clear(o)
require.NoError(t, json.Unmarshal(b, &o))
assert.Equal(t, "🤔", o["msg"])

View File

@ -14,7 +14,6 @@ limitations under the License.
package logger
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@ -78,7 +77,7 @@ func TestToLogLevel(t *testing.T) {
func TestNewContext(t *testing.T) {
t.Run("input nil logger", func(t *testing.T) {
ctx := NewContext(context.Background(), nil)
ctx := NewContext(t.Context(), nil)
assert.NotNil(t, ctx, "ctx is not nil")
logger := FromContextOrDefault(ctx)
@ -91,7 +90,7 @@ func TestNewContext(t *testing.T) {
logger := NewLogger(testLoggerName)
assert.NotNil(t, logger)
ctx := NewContext(context.Background(), logger)
ctx := NewContext(t.Context(), logger)
assert.NotNil(t, ctx, "ctx is not nil")
logger2 := FromContextOrDefault(ctx)
assert.NotNil(t, logger2)

View File

@ -29,7 +29,7 @@ type Duration struct {
time.Duration
}
func (d Duration) MarshalJSON() ([]byte, error) {
func (d *Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
@ -114,7 +114,7 @@ func toTimeDurationHookFunc() mapstructure.DecodeHookFunc {
// This methods supports days, hours, minutes, and seconds. It assumes all durations are in UTC time and are not impacted by DST (so all days are 24-hours long).
// This method does not support fractions of seconds, and durations are truncated to seconds.
// See https://en.wikipedia.org/wiki/ISO_8601#Durations for referece.
func (d Duration) ToISOString() string {
func (d *Duration) ToISOString() string {
// Truncate to seconds, removing fractional seconds
trunc := d.Truncate(time.Second)

View File

@ -23,7 +23,7 @@ import (
"github.com/mitchellh/mapstructure"
"github.com/dapr/kit/ptr"
"github.com/dapr/kit/utils"
kitstrings "github.com/dapr/kit/strings"
)
func toTruthyBoolHookFunc() mapstructure.DecodeHookFunc {
@ -37,10 +37,10 @@ func toTruthyBoolHookFunc() mapstructure.DecodeHookFunc {
data any,
) (any, error) {
if f == stringType && t == boolType {
return utils.IsTruthy(data.(string)), nil
return kitstrings.IsTruthy(data.(string)), nil
}
if f == stringType && t == boolPtrType {
return ptr.Of(utils.IsTruthy(data.(string))), nil
return ptr.Of(kitstrings.IsTruthy(data.(string))), nil
}
return data, nil
}

View File

@ -126,7 +126,7 @@ func resolveAliases(md map[string]string, t reflect.Type) error {
func resolveAliasesInType(md map[string]string, keys map[string]string, t reflect.Type) {
// Iterate through all the properties of the type to see if anyone has the "mapstructurealiases" property
for i := 0; i < t.NumField(); i++ {
for i := range t.NumField() {
currentField := t.Field(i)
// Ignored fields that are not exported or that don't have a "mapstructure" tag

View File

@ -14,13 +14,13 @@ limitations under the License.
package metadata
import (
"maps"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
)
func TestMetadataDecode(t *testing.T) {

View File

@ -55,9 +55,9 @@ type Config struct {
}
// String implements fmt.Stringer and is used for debugging.
func (c Config) String() string {
func (c *Config) String() string {
return fmt.Sprintf(
"policy='%s' duration='%v' initialInterval='%v' randomizationFactor='%f' multiplier='%f' maxInterval='%v' maxElapsedTime='%v' maxRetries='%d'",
"policy='%v' duration='%v' initialInterval='%v' randomizationFactor='%f' multiplier='%f' maxInterval='%v' maxElapsedTime='%v' maxRetries='%d'",
c.Policy, c.Duration, c.InitialInterval, c.RandomizationFactor, c.Multiplier, c.MaxInterval, c.MaxElapsedTime, c.MaxRetries,
)
}
@ -204,8 +204,8 @@ func (p *PolicyType) DecodeString(value string) error {
}
// String implements fmt.Stringer and is used for debugging.
func (p PolicyType) String() string {
switch p {
func (p *PolicyType) String() string {
switch *p {
case PolicyConstant:
return "constant"
case PolicyExponential:

View File

@ -241,7 +241,7 @@ func TestRetryNotifyRecoverCancel(t *testing.T) {
var notifyCalls, recoveryCalls int
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
b := config.NewBackOffWithContext(ctx)
errC := make(chan error, 1)
startedC := make(chan struct{}, 100)

View File

@ -20,6 +20,8 @@ import (
)
// Algorithm used to wrap the file key.
//
//nolint:recvcheck
type KeyAlgorithm string
const (

View File

@ -20,6 +20,8 @@ import (
)
// Cipher used to encrypt the file.
//
//nolint:recvcheck
type Cipher string
const (

View File

@ -15,7 +15,6 @@ package v1
import (
"encoding/hex"
"fmt"
"reflect"
"testing"
@ -120,7 +119,7 @@ func TestFileKey(t *testing.T) {
// Validate that headerMessage returns the right message, and that there's a newline at the end
const manifest = `{"foo":"bar"}`
const expect = SchemeName + "\n" + manifest + "\n"
fmt.Println(hex.EncodeToString([]byte(expect)))
t.Log(hex.EncodeToString([]byte(expect)))
got := fileKey{}.headerMessage([]byte(manifest))
require.Equal(t, expect, string(got))

View File

@ -34,11 +34,11 @@ var (
func TestScheme(t *testing.T) {
// Fake wrapKeyFn and unwrapKeyFn, which just return the plaintext key
//nolint:stylecheck,revive
//nolint:stylecheck
var wrapKeyFn WrapKeyFn = func(plaintextKey []byte, algorithm, keyName string, nonce []byte) (wrappedKey []byte, tag []byte, err error) {
return plaintextKey, nil, nil
}
//nolint:stylecheck,revive
//nolint:stylecheck
var unwrapKeyFn UnwrapKeyFn = func(wrappedKey []byte, algorithm, keyName string, nonce, tag []byte) (plaintextKey []byte, err error) {
return wrappedKey, nil
}
@ -91,7 +91,7 @@ func TestScheme(t *testing.T) {
// Second, check that the JSON manifest is present and valid
start := idx + 1
idx = bytes.IndexByte(encData[start:], '\n')
require.Greater(t, idx, 0)
require.Positive(t, idx)
var manifest Manifest
err = json.Unmarshal(encData[start:(start+idx)], &manifest)
require.NoError(t, err)
@ -106,7 +106,7 @@ func TestScheme(t *testing.T) {
// We are not validating the MAC here as the decryption code will do it; we'll just check it's present and 44-byte long (when encoded as base64)
start += idx + 1
idx = bytes.IndexByte(encData[start:], '\n')
require.Greater(t, idx, 0)
require.Positive(t, idx)
require.Len(t, encData[start:(start+idx)], 44)
// Decrypt the encrypted data

View File

@ -11,7 +11,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
package strings
import (
"path/filepath"

View File

@ -192,6 +192,7 @@ func ParseDuration(from string) (int, int, int, time.Duration, int, error) {
// - ISO8601 duration format
// - time.Duration string format
// - RFC3339 datetime format
// - RFC3339nano datetime format
// For duration formats, an offset is added.
func ParseTime(from string, offset *time.Time) (time.Time, error) {
var start time.Time
@ -210,8 +211,9 @@ func ParseTime(from string, offset *time.Time) (time.Time, error) {
if dur, err = time.ParseDuration(from); err == nil {
return start.Add(dur), nil
}
if t, err := time.Parse(time.RFC3339, from); err == nil {
if t, err := time.Parse(time.RFC3339Nano, from); err == nil {
return t, nil
}
return time.Time{}, errors.New("unsupported time/duration format: " + from)
}

View File

@ -212,6 +212,13 @@ func TestParseTime(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, time.Duration(0), expected.Sub(tm))
})
t.Run("parse RFC3339nano datetime", func(t *testing.T) {
dummy := time.Now().Add(1000 * time.Nanosecond)
expected := time.Now().Add(100 * time.Nanosecond)
tm, err := ParseTime(expected.Format(time.RFC3339Nano), &dummy)
require.NoError(t, err)
assert.Equal(t, time.Duration(0), expected.Sub(tm))
})
t.Run("parse empty string", func(t *testing.T) {
_, err := ParseTime("", nil)
require.ErrorContains(t, err, "unsupported time/duration format")

View File

@ -42,7 +42,7 @@ func TestCache(t *testing.T) {
cache.Set("key4", "val4", 5)
// Retrieve values
for i := 0; i < 16; i++ {
for i := range 16 {
v, ok := cache.Get("key1")
if i < 2 {
require.True(t, ok)

View File

@ -1,29 +0,0 @@
package utils
import (
"fmt"
"os"
"time"
)
// GetEnvDurationWithRange returns the time.Duration value of the environment variable specified by `envVar`.
// If the environment variable is not set, it returns `defaultValue`.
// If the value is set but is not valid (not a valid time.Duration or falls outside the specified range
// [minValue, maxValue] inclusively), it returns `defaultValue` and an error.
func GetEnvDurationWithRange(envVar string, defaultValue, min, max time.Duration) (time.Duration, error) {
v := os.Getenv(envVar)
if v == "" {
return defaultValue, nil
}
val, err := time.ParseDuration(v)
if err != nil {
return defaultValue, fmt.Errorf("invalid time.Duration value %s for the %s env variable: %w", val, envVar, err)
}
if val < min || val > max {
return defaultValue, fmt.Errorf("invalid value for the %s env variable: value should be between %s and %s, got %s", envVar, min, max, val)
}
return val, nil
}

View File

@ -1,41 +0,0 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"encoding/pem"
"fmt"
"os"
)
// GetPEM loads a PEM-encoded file (certificate or key).
func GetPEM(val string) ([]byte, error) {
// If val is already a PEM-encoded string, return it as-is
if IsValidPEM(val) {
return []byte(val), nil
}
// Assume it's a file
pemBytes, err := os.ReadFile(val)
if err != nil {
return nil, fmt.Errorf("value is neither a valid file path or nor a valid PEM-encoded string: %w", err)
}
return pemBytes, nil
}
// IsValidPEM validates the provided input has PEM formatted block.
func IsValidPEM(val string) bool {
block, _ := pem.Decode([]byte(val))
return block != nil
}