Merge pull request #3562 from tonistiigi/docker-config-scope-workaround
auth: add option to load docker config for specific repo/scope
This commit is contained in:
@@ -23,12 +23,10 @@ import (
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
"github.com/docker/buildx/util/platformutil"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/config"
|
||||
dockeropts "github.com/docker/cli/opts"
|
||||
hcl "github.com/hashicorp/hcl/v2"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/session/auth/authprovider"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
@@ -1256,18 +1254,12 @@ func (t *Target) GetName(ectx *hcl.EvalContext, block *hcl.Block, loadDeps func(
|
||||
}
|
||||
|
||||
func TargetsToBuildOpt(m map[string]*Target, inp *Input) (map[string]build.Options, error) {
|
||||
// make sure local credentials are loaded multiple times for different targets
|
||||
authProvider := authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
|
||||
ConfigFile: config.LoadDefaultConfigFile(os.Stderr),
|
||||
})
|
||||
|
||||
m2 := make(map[string]build.Options, len(m))
|
||||
for k, v := range m {
|
||||
bo, err := toBuildOpt(v, inp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bo.Session = append(bo.Session, authProvider)
|
||||
m2[k] = *bo
|
||||
}
|
||||
return m2, nil
|
||||
|
||||
@@ -29,11 +29,13 @@ import (
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
"github.com/docker/buildx/util/dockerutil"
|
||||
"github.com/docker/buildx/util/dockerutil/dockerconfig"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/buildx/util/tracing"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/session/auth/authprovider"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -252,6 +254,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||
return err
|
||||
}
|
||||
|
||||
// make sure local credentials aren't loaded multiple times for different targets
|
||||
authProvider := authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
|
||||
AuthConfigProvider: dockerconfig.LoadAuthConfig(dockerCli),
|
||||
})
|
||||
|
||||
for k, opt := range bo {
|
||||
opt.Session = append(opt.Session, authProvider)
|
||||
bo[k] = opt
|
||||
}
|
||||
|
||||
def := struct {
|
||||
Group map[string]*bake.Group `json:"group,omitempty"`
|
||||
Target map[string]*bake.Target `json:"target"`
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
"github.com/docker/buildx/util/dockerutil"
|
||||
"github.com/docker/buildx/util/dockerutil/dockerconfig"
|
||||
"github.com/docker/buildx/util/ioset"
|
||||
"github.com/docker/buildx/util/metricutil"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
@@ -1019,7 +1020,7 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
|
||||
opts.Platforms = platforms
|
||||
|
||||
opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
|
||||
ConfigFile: dockerCli.ConfigFile(),
|
||||
AuthConfigProvider: dockerconfig.LoadAuthConfig(dockerCli),
|
||||
}))
|
||||
|
||||
secrets, err := build.CreateSecrets(in.Secrets)
|
||||
|
||||
@@ -29,8 +29,8 @@ require (
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/in-toto/in-toto-golang v0.9.0
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/moby/buildkit v0.26.2
|
||||
github.com/moby/go-archive v0.1.0
|
||||
github.com/moby/buildkit v0.26.1-0.20260106154623-ed6dc749ce40 // v0.27.0-dev
|
||||
github.com/moby/go-archive v0.2.0
|
||||
github.com/moby/moby/api v1.52.0
|
||||
github.com/moby/moby/client v0.2.1
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
@@ -47,7 +47,7 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f
|
||||
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0
|
||||
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117
|
||||
github.com/zclconf/go-cty v1.17.0
|
||||
|
||||
@@ -210,12 +210,12 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/moby/buildkit v0.26.2 h1:EIh5j0gzRsCZmQzvgNNWzSDbuKqwUIiBH7ssqLv8RU8=
|
||||
github.com/moby/buildkit v0.26.2/go.mod h1:ylDa7IqzVJgLdi/wO7H1qLREFQpmhFbw2fbn4yoTw40=
|
||||
github.com/moby/buildkit v0.26.1-0.20260106154623-ed6dc749ce40 h1:h45o8dTo7Cq1Su49bs9VHP83Fdp3olgMfgo3tZULXUw=
|
||||
github.com/moby/buildkit v0.26.1-0.20260106154623-ed6dc749ce40/go.mod h1:LtUhyzVLAD0ilniDZRbghEyRq9CgZAY7ywMXlVo9MBI=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
@@ -319,8 +319,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
|
||||
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f h1:Z4NEQ86qFl1mHuCu9gwcE+EYCwDKfXAYXZbdIXyxmEA=
|
||||
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
|
||||
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
|
||||
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117 h1:XFwyh2JZwR5aiKLXHX2C1n0v5F11dCJpyGL1W/Cpl3U=
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package dockerconfig
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/moby/buildkit/session/auth/authprovider"
|
||||
)
|
||||
|
||||
func LoadAuthConfig(cli command.Cli) authprovider.AuthConfigProvider {
|
||||
acp := &authConfigProvider{
|
||||
buildxConfig: confutil.NewConfig(cli),
|
||||
defaultConfig: cli.ConfigFile(),
|
||||
authConfigCache: map[string]authConfigCacheEntry{},
|
||||
}
|
||||
return acp.load
|
||||
}
|
||||
|
||||
type authConfigProvider struct {
|
||||
initOnce sync.Once
|
||||
defaultConfig *configfile.ConfigFile
|
||||
buildxConfig *confutil.Config
|
||||
authConfigCache map[string]authConfigCacheEntry
|
||||
mu sync.Mutex // mutex for authConfigCache
|
||||
alternativeConfigs []*alternativeConfig
|
||||
}
|
||||
|
||||
func (ap *authConfigProvider) load(ctx context.Context, host string, scopes []string, cacheExpireCheck authprovider.ExpireCachedAuthCheck) (types.AuthConfig, error) {
|
||||
ap.initOnce.Do(func() {
|
||||
ap.init()
|
||||
})
|
||||
|
||||
ap.mu.Lock()
|
||||
defer ap.mu.Unlock()
|
||||
|
||||
candidates := []*alternativeConfig{}
|
||||
parsedScopes := parseScopes(scopes)
|
||||
|
||||
if len(parsedScopes) == 1 {
|
||||
for _, cfg := range ap.alternativeConfigs {
|
||||
if cfg.host != host {
|
||||
continue
|
||||
}
|
||||
if cfg.matchesScopes(parsedScopes) {
|
||||
candidates = append(candidates, cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
key := host
|
||||
cfg := ap.defaultConfig
|
||||
if len(candidates) > 0 {
|
||||
// matches with repo before those without repo
|
||||
// matches with scope set sorted before those without scope
|
||||
slices.SortFunc(candidates, func(a, b *alternativeConfig) int {
|
||||
return cmp.Or(
|
||||
strings.Compare(b.repo, a.repo),
|
||||
cmp.Compare(len(b.scope), len(a.scope)),
|
||||
)
|
||||
})
|
||||
candidates = candidates[:1]
|
||||
key += "|" + candidates[0].dir
|
||||
if candidates[0].configFile == nil {
|
||||
if cfgDir, err := config.Load(candidates[0].dir); err == nil {
|
||||
cfg = cfgDir
|
||||
candidates[0].configFile = cfg
|
||||
}
|
||||
} else {
|
||||
cfg = candidates[0].configFile
|
||||
}
|
||||
}
|
||||
|
||||
entry, exists := ap.authConfigCache[key]
|
||||
if exists && (cacheExpireCheck == nil || !cacheExpireCheck(entry.Created, key)) {
|
||||
return *entry.Auth, nil
|
||||
}
|
||||
|
||||
hostKey := host
|
||||
if host == authprovider.DockerHubRegistryHost {
|
||||
hostKey = authprovider.DockerHubConfigfileKey
|
||||
}
|
||||
|
||||
ac, err := cfg.GetAuthConfig(hostKey)
|
||||
if err != nil {
|
||||
return types.AuthConfig{}, err
|
||||
}
|
||||
|
||||
entry = authConfigCacheEntry{
|
||||
Created: time.Now(),
|
||||
Auth: &ac,
|
||||
}
|
||||
|
||||
ap.authConfigCache[key] = entry
|
||||
|
||||
return ac, nil
|
||||
}
|
||||
|
||||
func (ap *authConfigProvider) init() error {
|
||||
base := filepath.Join(ap.buildxConfig.Dir(), "config")
|
||||
return filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if d.Name() != config.ConfigFileName {
|
||||
return nil
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
rdir, err := filepath.Rel(base, dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg := parseConfigKey(rdir)
|
||||
cfg.dir = dir
|
||||
ap.alternativeConfigs = append(ap.alternativeConfigs, &cfg)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func parseConfigKey(key string) alternativeConfig {
|
||||
var out alternativeConfig
|
||||
|
||||
var mainPart, scopePart string
|
||||
if i := strings.IndexByte(key, '@'); i >= 0 {
|
||||
mainPart = key[:i]
|
||||
scopePart = key[i+1:]
|
||||
} else {
|
||||
mainPart = key
|
||||
}
|
||||
|
||||
if scopePart != "" {
|
||||
out.scope = make(map[string]struct{})
|
||||
for s := range strings.SplitSeq(scopePart, ",") {
|
||||
if s = strings.TrimSpace(s); s != "" {
|
||||
out.scope[s] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mainPart == "" {
|
||||
return out
|
||||
}
|
||||
|
||||
slash := strings.IndexByte(mainPart, '/')
|
||||
if slash < 0 {
|
||||
out.host = mainPart
|
||||
return out
|
||||
}
|
||||
|
||||
out.host = mainPart[:slash]
|
||||
out.repo = mainPart[slash+1:]
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
type alternativeConfig struct {
|
||||
dir string
|
||||
|
||||
host string
|
||||
repo string
|
||||
scope map[string]struct{}
|
||||
|
||||
configFile *configfile.ConfigFile
|
||||
}
|
||||
|
||||
func (a *alternativeConfig) matchesScopes(q scopes) bool {
|
||||
if a.repo != "" {
|
||||
if _, ok := q["repository:"+a.repo]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(a.scope) > 0 {
|
||||
if a.repo == "" {
|
||||
// no repo means one query must match all scopes
|
||||
for _, scopeActions := range q {
|
||||
ok := true
|
||||
for s := range a.scope {
|
||||
if _, exists := scopeActions[s]; !exists {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
for s := range a.scope {
|
||||
for k, scopeActions := range q {
|
||||
if k == "repository:"+a.repo {
|
||||
if _, ok := scopeActions[s]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type authConfigCacheEntry struct {
|
||||
Created time.Time
|
||||
Auth *types.AuthConfig
|
||||
}
|
||||
|
||||
type scopes map[string]map[string]struct{}
|
||||
|
||||
func parseScopes(s []string) scopes {
|
||||
// https://distribution.github.io/distribution/spec/auth/scope/
|
||||
m := map[string]map[string]struct{}{}
|
||||
for _, scopeStr := range s {
|
||||
if scopeStr == "" {
|
||||
return nil
|
||||
}
|
||||
// The scopeStr may have strings that contain multiple scopes separated by a space.
|
||||
for scope := range strings.SplitSeq(scopeStr, " ") {
|
||||
parts := strings.SplitN(scope, ":", 3)
|
||||
names := []string{parts[0]}
|
||||
if len(parts) > 1 {
|
||||
names = append(names, parts[1])
|
||||
}
|
||||
var actions []string
|
||||
if len(parts) == 3 {
|
||||
actions = append(actions, strings.Split(parts[2], ",")...)
|
||||
}
|
||||
name := strings.Join(names, ":")
|
||||
ma, ok := m[name]
|
||||
if !ok {
|
||||
ma = map[string]struct{}{}
|
||||
m[name] = ma
|
||||
}
|
||||
|
||||
for _, a := range actions {
|
||||
ma[a] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package dockerconfig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseConfigKey(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
in string
|
||||
host string
|
||||
repo string
|
||||
scope map[string]struct{}
|
||||
}{
|
||||
"domain-only": {
|
||||
in: "example.com",
|
||||
host: "example.com",
|
||||
},
|
||||
"domain-with-path": {
|
||||
in: "example.com/a/b",
|
||||
host: "example.com",
|
||||
repo: "a/b",
|
||||
},
|
||||
"with-scopes": {
|
||||
in: "example.com/x@y,z",
|
||||
host: "example.com",
|
||||
repo: "x",
|
||||
scope: map[string]struct{}{
|
||||
"y": {},
|
||||
"z": {},
|
||||
},
|
||||
},
|
||||
"empty-scope-list": {
|
||||
in: "example.com/a/b@",
|
||||
host: "example.com",
|
||||
repo: "a/b",
|
||||
},
|
||||
"path-empty-before-at": {
|
||||
in: "example.com/@foo",
|
||||
host: "example.com",
|
||||
repo: "",
|
||||
scope: map[string]struct{}{
|
||||
"foo": {},
|
||||
},
|
||||
},
|
||||
"scope-no-slash": {
|
||||
in: "registry-1.docker.io@pull,push",
|
||||
host: "registry-1.docker.io",
|
||||
repo: "",
|
||||
scope: map[string]struct{}{
|
||||
"pull": {},
|
||||
"push": {},
|
||||
},
|
||||
},
|
||||
"with-port-and-scopes": {
|
||||
in: "example.com:5000/x@y,z",
|
||||
host: "example.com:5000",
|
||||
repo: "x",
|
||||
scope: map[string]struct{}{
|
||||
"y": {},
|
||||
"z": {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
out := parseConfigKey(tc.in)
|
||||
|
||||
require.Equal(t, tc.host, out.host)
|
||||
require.Equal(t, tc.repo, out.repo)
|
||||
|
||||
if tc.scope == nil {
|
||||
require.Nil(t, out.scope)
|
||||
} else {
|
||||
require.Equal(t, tc.scope, out.scope)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestMatchesScopes(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
cfgKey string
|
||||
query []string
|
||||
matches bool
|
||||
}{
|
||||
"invalid-scope": {
|
||||
cfgKey: "example.com/foo/bar@a,b",
|
||||
query: []string{"repository:foo/bar:pull"},
|
||||
matches: false,
|
||||
},
|
||||
"sub-scope-match": {
|
||||
cfgKey: "example.com/foo/bar@push",
|
||||
query: []string{"repository:foo/bar:pull,push"},
|
||||
matches: true,
|
||||
},
|
||||
"invalid-scope-single": {
|
||||
cfgKey: "example.com/foo/bar@push",
|
||||
query: []string{"repository:foo/bar:pull"},
|
||||
matches: false,
|
||||
},
|
||||
"wrong-repo": {
|
||||
cfgKey: "example.com/foo/bar@push",
|
||||
query: []string{"repository:foo/bar2:pull,push"},
|
||||
matches: false,
|
||||
},
|
||||
"multi-scope-match": {
|
||||
cfgKey: "example.com/foo/bar@push,pull",
|
||||
query: []string{"repository:foo/bar:pull,push"},
|
||||
matches: true,
|
||||
},
|
||||
"empty-action-list-in-query": {
|
||||
cfgKey: "example.com/foo/bar@pull",
|
||||
query: []string{"repository:foo/bar:"}, // no actions
|
||||
matches: false,
|
||||
},
|
||||
|
||||
"repo-no-scopes-required": {
|
||||
cfgKey: "example.com/foo/bar",
|
||||
query: []string{"repository:foo/bar:pull"},
|
||||
matches: true,
|
||||
},
|
||||
|
||||
"repo-empty-scope-in-config": {
|
||||
cfgKey: "example.com/foo/bar@", // explicit empty scope list
|
||||
query: []string{"repository:foo/bar:pull"},
|
||||
matches: true,
|
||||
},
|
||||
|
||||
"multiple-repos-config-targets-only-one": {
|
||||
cfgKey: "example.com/foo/bar@pull",
|
||||
query: []string{
|
||||
"repository:foo/bar:pull",
|
||||
"repository:other:push",
|
||||
},
|
||||
matches: true,
|
||||
},
|
||||
|
||||
"multiple-repos-but-target-lacks-scope": {
|
||||
cfgKey: "example.com/foo/bar@push",
|
||||
query: []string{
|
||||
"repository:foo/bar:pull", // missing push
|
||||
"repository:other:push", // irrelevant
|
||||
},
|
||||
matches: false,
|
||||
},
|
||||
|
||||
"no-repo-in-config-match-any": {
|
||||
cfgKey: "example.com@pull",
|
||||
query: []string{
|
||||
"repository:a:pull",
|
||||
"repository:b:pull,push",
|
||||
},
|
||||
matches: true,
|
||||
},
|
||||
|
||||
"no-repo-in-config-but-no-repo-has-scope": {
|
||||
cfgKey: "example.com@push",
|
||||
query: []string{
|
||||
"repository:a:pull",
|
||||
"repository:b:pull",
|
||||
},
|
||||
matches: false,
|
||||
},
|
||||
|
||||
"single-action-in-query-with-spaces": {
|
||||
cfgKey: "example.com/foo/bar@pull",
|
||||
query: []string{"repository:foo/bar:pull repository:foo/bar2:pull"},
|
||||
matches: true,
|
||||
},
|
||||
|
||||
"single-action-in-query-with-spaces-one-match": {
|
||||
cfgKey: "example.com@push",
|
||||
query: []string{"repository:foo/bar:push repository:foo/bar2:pull"},
|
||||
matches: true,
|
||||
},
|
||||
|
||||
"single-action-in-query-with-spaces-invalid": {
|
||||
cfgKey: "example.com@push",
|
||||
query: []string{"repository:foo/bar:pull repository:foo/bar2:pull"},
|
||||
matches: false,
|
||||
},
|
||||
|
||||
"multi-scope-config-order-does-not-matter": {
|
||||
cfgKey: "example.com/foo/bar@push,pull",
|
||||
query: []string{"repository:foo/bar:pull,push"},
|
||||
matches: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cfg := parseConfigKey(tc.cfgKey)
|
||||
q := parseScopes(tc.query)
|
||||
got := cfg.matchesScopes(q)
|
||||
require.Equal(t, tc.matches, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
+27
-2
@@ -236,6 +236,9 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
|
||||
frontendAttrs := maps.Clone(opt.FrontendAttrs)
|
||||
maps.Copy(frontendAttrs, cacheOpt.frontendAttrs)
|
||||
|
||||
const statusInactivityTimeout = 5 * time.Second
|
||||
statusActivity := make(chan struct{}, 1)
|
||||
|
||||
solveCtx, cancelSolve := context.WithCancelCause(ctx)
|
||||
var res *SolveResponse
|
||||
eg.Go(func() error {
|
||||
@@ -244,8 +247,21 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
|
||||
|
||||
defer func() { // make sure the Status ends cleanly on build errors
|
||||
go func() {
|
||||
<-time.After(3 * time.Second)
|
||||
cancelStatus(errors.WithStack(context.Canceled))
|
||||
// Start inactivity monitoring after solve completes
|
||||
statusInactivityTimer := time.NewTimer(statusInactivityTimeout)
|
||||
defer statusInactivityTimer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-statusContext.Done():
|
||||
return
|
||||
case <-statusActivity:
|
||||
// Reset timer on activity
|
||||
statusInactivityTimer.Reset(statusInactivityTimeout)
|
||||
case <-statusInactivityTimer.C:
|
||||
cancelStatus(errors.WithStack(context.Canceled))
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
if !opt.SessionPreInitialized {
|
||||
bklog.G(ctx).Debugf("stopping session")
|
||||
@@ -345,8 +361,17 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
// Ignore context canceled, triggered after inactivity timeout
|
||||
if errors.Is(err, context.Canceled) || statusContext.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "failed to receive status")
|
||||
}
|
||||
// Signal activity (non-blocking)
|
||||
select {
|
||||
case statusActivity <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
if statusChan != nil {
|
||||
statusChan <- NewSolveStatus(resp)
|
||||
}
|
||||
|
||||
+56
-5
@@ -2,6 +2,7 @@ package linter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -19,7 +20,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Linter struct {
|
||||
CalledRules []string
|
||||
CalledRules *[]string
|
||||
ExperimentalAll bool
|
||||
ExperimentalRules map[string]struct{}
|
||||
ReturnAsError bool
|
||||
@@ -32,7 +33,7 @@ func New(config *Config) *Linter {
|
||||
toret := &Linter{
|
||||
SkippedRules: map[string]struct{}{},
|
||||
ExperimentalRules: map[string]struct{}{},
|
||||
CalledRules: []string{},
|
||||
CalledRules: new([]string),
|
||||
Warn: config.Warn,
|
||||
}
|
||||
toret.SkipAll = config.SkipAll
|
||||
@@ -65,20 +66,70 @@ func (lc *Linter) Run(rule LinterRuleI, location []parser.Range, txt ...string)
|
||||
}
|
||||
}
|
||||
|
||||
lc.CalledRules = append(lc.CalledRules, rulename)
|
||||
*lc.CalledRules = append(*lc.CalledRules, rulename)
|
||||
rule.Run(lc.Warn, location, txt...)
|
||||
}
|
||||
|
||||
func (lc *Linter) WithMergedConfig(other *Config) *Linter {
|
||||
cloned := *lc
|
||||
if other.ExperimentalAll {
|
||||
cloned.ExperimentalAll = true
|
||||
}
|
||||
|
||||
if len(other.ExperimentalRules) > 0 {
|
||||
cloned.ExperimentalRules = maps.Clone(cloned.ExperimentalRules)
|
||||
for _, rulename := range other.ExperimentalRules {
|
||||
cloned.ExperimentalRules[rulename] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if other.SkipAll {
|
||||
cloned.SkipAll = true
|
||||
}
|
||||
|
||||
if len(other.SkipRules) > 0 {
|
||||
cloned.SkippedRules = maps.Clone(cloned.SkippedRules)
|
||||
for _, rulename := range other.SkipRules {
|
||||
cloned.SkippedRules[rulename] = struct{}{}
|
||||
}
|
||||
}
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func (lc *Linter) WithMergedConfigFromComments(comments []string) *Linter {
|
||||
if comments == nil {
|
||||
return lc
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
p := parser.DirectiveParser{}
|
||||
p.SetComment("")
|
||||
|
||||
d, _ := p.ParseLine([]byte(comment))
|
||||
if d == nil || d.Name != "check" {
|
||||
continue
|
||||
}
|
||||
|
||||
v, _, _ := strings.Cut(d.Value, " ")
|
||||
lintConfig, err := ParseLintOptions(v)
|
||||
if err != nil {
|
||||
return lc
|
||||
}
|
||||
return lc.WithMergedConfig(lintConfig)
|
||||
}
|
||||
return lc
|
||||
}
|
||||
|
||||
func (lc *Linter) Error() error {
|
||||
if lc == nil || !lc.ReturnAsError {
|
||||
return nil
|
||||
}
|
||||
if len(lc.CalledRules) == 0 {
|
||||
if len(*lc.CalledRules) == 0 {
|
||||
return nil
|
||||
}
|
||||
var rules []string
|
||||
uniqueRules := map[string]struct{}{}
|
||||
for _, r := range lc.CalledRules {
|
||||
for _, r := range *lc.CalledRules {
|
||||
uniqueRules[r] = struct{}{}
|
||||
}
|
||||
for r := range uniqueRules {
|
||||
|
||||
+23
-11
@@ -4,9 +4,10 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -32,14 +33,18 @@ type Directive struct {
|
||||
// DirectiveParser is a parser for Dockerfile directives that enforces the
|
||||
// quirks of the directive parser.
|
||||
type DirectiveParser struct {
|
||||
line int
|
||||
regexp *regexp.Regexp
|
||||
seen map[string]struct{}
|
||||
done bool
|
||||
line int
|
||||
comment *string
|
||||
seen map[string]struct{}
|
||||
done bool
|
||||
}
|
||||
|
||||
func (d *DirectiveParser) setComment(comment string) {
|
||||
d.regexp = regexp.MustCompile(fmt.Sprintf(`^%s\s*([a-zA-Z][a-zA-Z0-9]*)\s*=\s*(.+?)\s*$`, comment))
|
||||
var directiveRegexp = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9]*)\s*=\s*(.+?)\s*$`)
|
||||
})
|
||||
|
||||
func (d *DirectiveParser) SetComment(comment string) {
|
||||
d.comment = &comment
|
||||
}
|
||||
|
||||
func (d *DirectiveParser) ParseLine(line []byte) (*Directive, error) {
|
||||
@@ -47,11 +52,18 @@ func (d *DirectiveParser) ParseLine(line []byte) (*Directive, error) {
|
||||
if d.done {
|
||||
return nil, nil
|
||||
}
|
||||
if d.regexp == nil {
|
||||
d.setComment("#")
|
||||
if d.comment == nil {
|
||||
d.SetComment("#")
|
||||
}
|
||||
|
||||
match := d.regexp.FindSubmatch(line)
|
||||
line, ok := bytes.CutPrefix(line, []byte(*d.comment))
|
||||
if !ok {
|
||||
d.done = true
|
||||
return nil, nil
|
||||
}
|
||||
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
|
||||
|
||||
match := directiveRegexp().FindSubmatch(line)
|
||||
if len(match) == 0 {
|
||||
d.done = true
|
||||
return nil, nil
|
||||
@@ -142,7 +154,7 @@ func parseDirective(key string, dt []byte, anyFormat bool) (string, string, []Ra
|
||||
|
||||
// use directive with different comment prefix, and search for //key=
|
||||
directiveParser = DirectiveParser{line: line}
|
||||
directiveParser.setComment("//")
|
||||
directiveParser.SetComment("//")
|
||||
if syntax, cmdline, loc, ok := detectDirectiveFromParser(key, dt, directiveParser); ok {
|
||||
return syntax, cmdline, loc, true
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package authprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
)
|
||||
|
||||
func LoadAuthConfig(config *configfile.ConfigFile) AuthConfigProvider {
|
||||
acp := &authConfigProvider{
|
||||
config: config,
|
||||
authConfigCache: map[string]authConfigCacheEntry{},
|
||||
}
|
||||
return acp.load
|
||||
}
|
||||
|
||||
type authConfigProvider struct {
|
||||
config *configfile.ConfigFile
|
||||
authConfigCache map[string]authConfigCacheEntry
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (ap *authConfigProvider) load(ctx context.Context, host string, scopes []string, cacheExpireCheck ExpireCachedAuthCheck) (types.AuthConfig, error) {
|
||||
ap.mu.Lock()
|
||||
defer ap.mu.Unlock()
|
||||
|
||||
entry, exists := ap.authConfigCache[host]
|
||||
if exists && (cacheExpireCheck == nil || !cacheExpireCheck(entry.Created, host)) {
|
||||
return *entry.Auth, nil
|
||||
}
|
||||
|
||||
hostKey := host
|
||||
if host == DockerHubRegistryHost {
|
||||
hostKey = DockerHubConfigfileKey
|
||||
}
|
||||
|
||||
ac, err := ap.config.GetAuthConfig(hostKey)
|
||||
if err != nil {
|
||||
return types.AuthConfig{}, err
|
||||
}
|
||||
|
||||
entry = authConfigCacheEntry{
|
||||
Created: time.Now(),
|
||||
Auth: &ac,
|
||||
}
|
||||
|
||||
ap.authConfigCache[host] = entry
|
||||
|
||||
return ac, nil
|
||||
}
|
||||
|
||||
type authConfigCacheEntry struct {
|
||||
Created time.Time
|
||||
Auth *types.AuthConfig
|
||||
}
|
||||
+44
-61
@@ -19,7 +19,6 @@ import (
|
||||
authutil "github.com/containerd/containerd/v2/core/remotes/docker/auth"
|
||||
remoteserrors "github.com/containerd/containerd/v2/core/remotes/errors"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/moby/buildkit/session"
|
||||
@@ -36,13 +35,17 @@ import (
|
||||
|
||||
const (
|
||||
defaultExpiration = 60
|
||||
dockerHubConfigfileKey = "https://index.docker.io/v1/"
|
||||
dockerHubRegistryHost = "registry-1.docker.io"
|
||||
DockerHubConfigfileKey = "https://index.docker.io/v1/"
|
||||
DockerHubRegistryHost = "registry-1.docker.io"
|
||||
)
|
||||
|
||||
type AuthConfigProvider func(ctx context.Context, host string, scope []string, cacheCheck ExpireCachedAuthCheck) (types.AuthConfig, error)
|
||||
|
||||
type ExpireCachedAuthCheck func(created time.Time, serverURL string) bool
|
||||
|
||||
type DockerAuthProviderConfig struct {
|
||||
// ConfigFile is the docker config file
|
||||
ConfigFile *configfile.ConfigFile
|
||||
// AuthConfigProvider is a function that provides auth config for a given host and scope
|
||||
AuthConfigProvider AuthConfigProvider
|
||||
// TLSConfigs is a map of host to TLS config
|
||||
TLSConfigs map[string]*AuthTLSConfig
|
||||
// ExpireCachedAuth is a function that returns true auth config should be refreshed
|
||||
@@ -50,12 +53,7 @@ type DockerAuthProviderConfig struct {
|
||||
// If nil then the cached result will expire after 4 minutes and 50 seconds.
|
||||
// The function is called with the time the cached auth config was created
|
||||
// and the server URL the auth config is for.
|
||||
ExpireCachedAuth func(created time.Time, serverURL string) bool
|
||||
}
|
||||
|
||||
type authConfigCacheEntry struct {
|
||||
Created time.Time
|
||||
Auth *types.AuthConfig
|
||||
ExpireCachedAuth ExpireCachedAuthCheck
|
||||
}
|
||||
|
||||
func NewDockerAuthProvider(cfg DockerAuthProviderConfig) session.Attachable {
|
||||
@@ -66,23 +64,21 @@ func NewDockerAuthProvider(cfg DockerAuthProviderConfig) session.Attachable {
|
||||
}
|
||||
}
|
||||
return &authProvider{
|
||||
authConfigCache: map[string]authConfigCacheEntry{},
|
||||
expireAc: cfg.ExpireCachedAuth,
|
||||
config: cfg.ConfigFile,
|
||||
seeds: &tokenSeeds{dir: config.Dir()},
|
||||
loggerCache: map[string]struct{}{},
|
||||
tlsConfigs: cfg.TLSConfigs,
|
||||
expireAc: cfg.ExpireCachedAuth,
|
||||
provider: cfg.AuthConfigProvider,
|
||||
seeds: &tokenSeeds{dir: config.Dir()},
|
||||
loggerCache: map[string]struct{}{},
|
||||
tlsConfigs: cfg.TLSConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
type authProvider struct {
|
||||
authConfigCache map[string]authConfigCacheEntry
|
||||
expireAc func(time.Time, string) bool
|
||||
config *configfile.ConfigFile
|
||||
seeds *tokenSeeds
|
||||
logger progresswriter.Logger
|
||||
loggerCache map[string]struct{}
|
||||
tlsConfigs map[string]*AuthTLSConfig
|
||||
expireAc func(time.Time, string) bool
|
||||
provider AuthConfigProvider
|
||||
seeds *tokenSeeds
|
||||
logger progresswriter.Logger
|
||||
loggerCache map[string]struct{}
|
||||
tlsConfigs map[string]*AuthTLSConfig
|
||||
|
||||
// The need for this mutex is not well understood.
|
||||
// Without it, the docker cli on OS X hangs when
|
||||
@@ -102,7 +98,7 @@ func (ap *authProvider) Register(server *grpc.Server) {
|
||||
}
|
||||
|
||||
func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequest) (rr *auth.FetchTokenResponse, err error) {
|
||||
ac, err := ap.getAuthConfig(ctx, req.Host)
|
||||
ac, err := ap.getAuthConfig(ctx, req.Host, req.Scopes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -112,11 +108,7 @@ func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequ
|
||||
return toTokenResponse(ac.RegistryToken, time.Time{}, 0), nil
|
||||
}
|
||||
|
||||
creds, err := ap.credentials(ctx, req.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
creds := toCredentials(*ac)
|
||||
to := authutil.TokenOptions{
|
||||
Realm: req.Realm,
|
||||
Service: req.Service,
|
||||
@@ -215,11 +207,7 @@ func (ap *authProvider) tlsConfig(host string) (*tls.Config, error) {
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (ap *authProvider) credentials(ctx context.Context, host string) (*auth.CredentialsResponse, error) {
|
||||
ac, err := ap.getAuthConfig(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func toCredentials(ac types.AuthConfig) *auth.CredentialsResponse {
|
||||
res := &auth.CredentialsResponse{}
|
||||
if ac.IdentityToken != "" {
|
||||
res.Secret = ac.IdentityToken
|
||||
@@ -227,12 +215,16 @@ func (ap *authProvider) credentials(ctx context.Context, host string) (*auth.Cre
|
||||
res.Username = ac.Username
|
||||
res.Secret = ac.Password
|
||||
}
|
||||
return res, nil
|
||||
return res
|
||||
}
|
||||
|
||||
func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) {
|
||||
resp, err := ap.credentials(ctx, req.Host)
|
||||
if err != nil || resp.Secret != "" {
|
||||
ac, err := ap.getAuthConfig(ctx, req.Host, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := toCredentials(*ac)
|
||||
if resp.Secret != "" {
|
||||
ap.mu.Lock()
|
||||
defer ap.mu.Unlock()
|
||||
_, ok := ap.loggerCache[req.Host]
|
||||
@@ -267,33 +259,22 @@ func (ap *authProvider) VerifyTokenAuthority(ctx context.Context, req *auth.Veri
|
||||
return &auth.VerifyTokenAuthorityResponse{Signed: sign.Sign(nil, req.Payload, priv)}, nil
|
||||
}
|
||||
|
||||
func (ap *authProvider) getAuthConfig(ctx context.Context, host string) (*types.AuthConfig, error) {
|
||||
func (ap *authProvider) getAuthConfig(ctx context.Context, host string, scopes []string) (*types.AuthConfig, error) {
|
||||
ap.mu.Lock()
|
||||
defer ap.mu.Unlock()
|
||||
|
||||
if host == dockerHubRegistryHost {
|
||||
host = dockerHubConfigfileKey
|
||||
var ac types.AuthConfig
|
||||
if ap.provider != nil {
|
||||
span, _ := tracing.StartSpan(ctx, fmt.Sprintf("load credentials for %s", host))
|
||||
res, err := ap.provider(ctx, host, scopes, ap.expireAc)
|
||||
tracing.FinishWithError(span, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ac = res
|
||||
}
|
||||
|
||||
entry, exists := ap.authConfigCache[host]
|
||||
if exists && !ap.expireAc(entry.Created, host) {
|
||||
return entry.Auth, nil
|
||||
}
|
||||
|
||||
span, _ := tracing.StartSpan(ctx, fmt.Sprintf("load credentials for %s", host))
|
||||
ac, err := ap.config.GetAuthConfig(host)
|
||||
tracing.FinishWithError(span, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry = authConfigCacheEntry{
|
||||
Created: time.Now(),
|
||||
Auth: &ac,
|
||||
}
|
||||
|
||||
ap.authConfigCache[host] = entry
|
||||
|
||||
return entry.Auth, nil
|
||||
return &ac, nil
|
||||
}
|
||||
|
||||
func (ap *authProvider) getAuthorityKey(ctx context.Context, host string, salt []byte) (ed25519.PrivateKey, error) {
|
||||
@@ -301,10 +282,12 @@ func (ap *authProvider) getAuthorityKey(ctx context.Context, host string, salt [
|
||||
return nil, status.Errorf(codes.Unavailable, "client side tokens disabled")
|
||||
}
|
||||
|
||||
creds, err := ap.credentials(ctx, host)
|
||||
ac, err := ap.getAuthConfig(ctx, host, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
creds := toCredentials(*ac)
|
||||
seed, err := ap.seeds.getSeed(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+7
@@ -95,7 +95,14 @@ func (b *buffer) Writer(ctx context.Context, opts ...content.WriterOpt) (content
|
||||
}
|
||||
}
|
||||
b.mu.Lock()
|
||||
if wOpts.Desc.Digest != "" {
|
||||
if _, ok := b.buffers[wOpts.Desc.Digest]; ok {
|
||||
b.mu.Unlock()
|
||||
return nil, errors.Wrapf(cerrdefs.ErrAlreadyExists, "content %v already exists", wOpts.Desc.Digest)
|
||||
}
|
||||
}
|
||||
if _, ok := b.refs[wOpts.Ref]; ok {
|
||||
b.mu.Unlock()
|
||||
return nil, errors.Wrapf(cerrdefs.ErrUnavailable, "ref %s locked", wOpts.Ref)
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
package contentutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/content"
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func ReferrersProviderWithBuffer(p ReferrersProvider, buffer Buffer, name string) *ReferrersProviderBuffer {
|
||||
return &ReferrersProviderBuffer{
|
||||
p: p,
|
||||
cache: buffer,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
var _ ReferrersProvider = &ReferrersProviderBuffer{}
|
||||
|
||||
type ReferrersProviderBuffer struct {
|
||||
p ReferrersProvider
|
||||
cache Buffer
|
||||
name string
|
||||
|
||||
mu sync.Mutex
|
||||
blobs map[digest.Digest]ocispecs.Descriptor
|
||||
refs map[digest.Digest][]ocispecs.Descriptor
|
||||
}
|
||||
|
||||
func (p *ReferrersProviderBuffer) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) {
|
||||
cw, err := content.OpenWriter(ctx, p.cache, content.WithDescriptor(desc), content.WithRef(desc.Digest.String()))
|
||||
if err != nil {
|
||||
if cerrdefs.IsAlreadyExists(err) {
|
||||
ra, err := p.cache.ReaderAt(ctx, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.mu.Lock()
|
||||
if p.blobs == nil {
|
||||
p.blobs = make(map[digest.Digest]ocispecs.Descriptor)
|
||||
}
|
||||
p.blobs[desc.Digest] = desc
|
||||
p.mu.Unlock()
|
||||
return ra, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ra, err := p.p.ReaderAt(ctx, desc)
|
||||
if err != nil {
|
||||
cw.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := content.CopyReaderAt(cw, ra, ra.Size()); err != nil {
|
||||
cw.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := cw.Commit(ctx, desc.Size, desc.Digest); err != nil {
|
||||
cw.Close()
|
||||
return nil, err
|
||||
}
|
||||
ra, err = p.cache.ReaderAt(ctx, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.mu.Lock()
|
||||
if p.blobs == nil {
|
||||
p.blobs = make(map[digest.Digest]ocispecs.Descriptor)
|
||||
}
|
||||
p.blobs[desc.Digest] = desc
|
||||
p.mu.Unlock()
|
||||
return ra, nil
|
||||
}
|
||||
|
||||
func (p *ReferrersProviderBuffer) FetchReferrers(ctx context.Context, dgst digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispecs.Descriptor, error) {
|
||||
cfg := remotes.FetchReferrersConfig{}
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
info, err := p.cache.Info(ctx, dgst)
|
||||
if err == nil && len(info.Labels) != 0 {
|
||||
refs := []ocispecs.Descriptor{}
|
||||
for l, v := range info.Labels {
|
||||
if !strings.HasPrefix(l, "containerd.io/gc.ref.content.buildkit.refs.") {
|
||||
continue
|
||||
}
|
||||
dgst, err := digest.Parse(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dt, err := content.ReadBlob(ctx, p.cache, ocispecs.Descriptor{Digest: dgst})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
desc := ocispecs.Descriptor{
|
||||
Digest: dgst,
|
||||
Size: int64(len(dt)),
|
||||
ArtifactType: readArtifactType(dt),
|
||||
}
|
||||
refs = append(refs, desc)
|
||||
}
|
||||
refs = filterRefs(refs, &cfg)
|
||||
if len(refs) > 0 {
|
||||
return refs, nil
|
||||
}
|
||||
v, ok := info.Labels["buildkit/refs.null"]
|
||||
if ok {
|
||||
for name := range strings.SplitSeq(v, ",") {
|
||||
if name == p.name {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refs, err := p.p.FetchReferrers(ctx, dgst, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refs = filterRefs(refs, &cfg)
|
||||
p.mu.Lock()
|
||||
if p.refs == nil {
|
||||
p.refs = make(map[digest.Digest][]ocispecs.Descriptor)
|
||||
}
|
||||
p.refs[dgst] = append(p.refs[dgst], refs...)
|
||||
p.mu.Unlock()
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func (p *ReferrersProviderBuffer) SetGCLabels(ctx context.Context, root ocispecs.Descriptor) error {
|
||||
labels := map[string]string{}
|
||||
fieldpaths := []string{}
|
||||
|
||||
p.mu.Lock()
|
||||
for _, desc := range p.blobs {
|
||||
shaPrefix := desc.Digest.Hex()[:12]
|
||||
key := "containerd.io/gc.ref.content.buildkit." + shaPrefix
|
||||
labels[key] = desc.Digest.String()
|
||||
fieldpaths = append(fieldpaths, "labels."+key)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
_, err := p.cache.Update(ctx, content.Info{
|
||||
Digest: root.Digest,
|
||||
Labels: labels,
|
||||
}, fieldpaths...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for dgst, refs := range p.refs {
|
||||
info, err := p.cache.Info(ctx, dgst)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
labels := map[string]string{}
|
||||
fieldpaths := []string{}
|
||||
for _, ref := range refs {
|
||||
shaPrefix := ref.Digest.Hex()[:12]
|
||||
key := "containerd.io/gc.ref.content.buildkit.refs." + shaPrefix
|
||||
labels[key] = ref.Digest.String()
|
||||
fieldpaths = append(fieldpaths, "labels."+key)
|
||||
}
|
||||
if len(refs) == 0 {
|
||||
key := "buildkit/refs.null"
|
||||
labels[key] = addName(info.Labels[key], p.name)
|
||||
fieldpaths = append(fieldpaths, "labels."+key)
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = p.cache.Update(ctx, content.Info{
|
||||
Digest: dgst,
|
||||
Labels: labels,
|
||||
}, fieldpaths...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterRefs(refs []ocispecs.Descriptor, cfg *remotes.FetchReferrersConfig) []ocispecs.Descriptor {
|
||||
if len(cfg.ArtifactTypes) == 0 {
|
||||
return refs
|
||||
}
|
||||
out := []ocispecs.Descriptor{}
|
||||
for _, ref := range refs {
|
||||
if slices.Contains(cfg.ArtifactTypes, ref.ArtifactType) {
|
||||
out = append(out, ref)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func addName(existing, name string) string {
|
||||
if existing == "" {
|
||||
return name
|
||||
}
|
||||
m := map[string]struct{}{}
|
||||
for n := range strings.SplitSeq(existing, ",") {
|
||||
m[n] = struct{}{}
|
||||
}
|
||||
m[name] = struct{}{}
|
||||
var names []string
|
||||
for n := range m {
|
||||
names = append(names, n)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
|
||||
func readArtifactType(dt []byte) string {
|
||||
var mfst ocispecs.Manifest
|
||||
if err := json.Unmarshal(dt, &mfst); err != nil {
|
||||
return ""
|
||||
}
|
||||
return mfst.ArtifactType
|
||||
}
|
||||
+1
-1
@@ -12,7 +12,7 @@ import (
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/sdk"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+2
-3
@@ -11,10 +11,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
@@ -43,7 +42,7 @@ func FinishWithError(span trace.Span, err error) {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
if hasStacktrace(err) {
|
||||
span.SetAttributes(attribute.String(string(semconv.ExceptionStacktraceKey), fmt.Sprintf("%+v", stack.Formatter(err))))
|
||||
span.SetAttributes(semconv.ExceptionStacktrace(fmt.Sprintf("%+v", stack.Formatter(err))))
|
||||
}
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
}
|
||||
|
||||
-40
@@ -36,10 +36,6 @@ import (
|
||||
const ImpliedDirectoryMode = 0o755
|
||||
|
||||
type (
|
||||
// Compression is the state represents if compressed or not.
|
||||
//
|
||||
// Deprecated: use [compression.Compression].
|
||||
Compression = compression.Compression
|
||||
// WhiteoutFormat is the format of whiteouts unpacked
|
||||
WhiteoutFormat int
|
||||
|
||||
@@ -95,14 +91,6 @@ func NewDefaultArchiver() *Archiver {
|
||||
// in order for the test to pass.
|
||||
type breakoutError error
|
||||
|
||||
const (
|
||||
Uncompressed = compression.None // Deprecated: use [compression.None].
|
||||
Bzip2 = compression.Bzip2 // Deprecated: use [compression.Bzip2].
|
||||
Gzip = compression.Gzip // Deprecated: use [compression.Gzip].
|
||||
Xz = compression.Xz // Deprecated: use [compression.Xz].
|
||||
Zstd = compression.Zstd // Deprecated: use [compression.Zstd].
|
||||
)
|
||||
|
||||
const (
|
||||
AUFSWhiteoutFormat WhiteoutFormat = 0 // AUFSWhiteoutFormat is the default format for whiteouts
|
||||
OverlayWhiteoutFormat WhiteoutFormat = 1 // OverlayWhiteoutFormat formats whiteout according to the overlay standard.
|
||||
@@ -126,27 +114,6 @@ func IsArchivePath(path string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// DetectCompression detects the compression algorithm of the source.
|
||||
//
|
||||
// Deprecated: use [compression.Detect].
|
||||
func DetectCompression(source []byte) compression.Compression {
|
||||
return compression.Detect(source)
|
||||
}
|
||||
|
||||
// DecompressStream decompresses the archive and returns a ReaderCloser with the decompressed archive.
|
||||
//
|
||||
// Deprecated: use [compression.DecompressStream].
|
||||
func DecompressStream(archive io.Reader) (io.ReadCloser, error) {
|
||||
return compression.DecompressStream(archive)
|
||||
}
|
||||
|
||||
// CompressStream compresses the dest with specified compression algorithm.
|
||||
//
|
||||
// Deprecated: use [compression.CompressStream].
|
||||
func CompressStream(dest io.Writer, comp compression.Compression) (io.WriteCloser, error) {
|
||||
return compression.CompressStream(dest, comp)
|
||||
}
|
||||
|
||||
// TarModifierFunc is a function that can be passed to ReplaceFileTarWrapper to
|
||||
// modify the contents or header of an entry in the archive. If the file already
|
||||
// exists in the archive the TarModifierFunc will be called with the Header and
|
||||
@@ -235,13 +202,6 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
|
||||
return pipeReader
|
||||
}
|
||||
|
||||
// FileInfoHeaderNoLookups creates a partially-populated tar.Header from fi.
|
||||
//
|
||||
// Deprecated: use [tarheader.FileInfoHeaderNoLookups].
|
||||
func FileInfoHeaderNoLookups(fi os.FileInfo, link string) (*tar.Header, error) {
|
||||
return tarheader.FileInfoHeaderNoLookups(fi, link)
|
||||
}
|
||||
|
||||
// FileInfoHeader creates a populated Header from fi.
|
||||
//
|
||||
// Compared to the archive/tar package, this function fills in less information
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//go:build !linux && !windows
|
||||
//go:build darwin || freebsd || netbsd
|
||||
|
||||
package archive
|
||||
|
||||
|
||||
+52
-10
@@ -111,6 +111,7 @@ func Copy(ctx context.Context, srcRoot, src, dstRoot, dst string, opts ...Opt) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.testHookLstat = ci.testHookLstat
|
||||
srcs := []string{src}
|
||||
|
||||
if ci.AllowWildcards {
|
||||
@@ -206,6 +207,9 @@ type CopyInfo struct {
|
||||
// replace any existing symlink or file)
|
||||
AlwaysReplaceExistingDestPaths bool
|
||||
ChangeFunc fsutil.ChangeFunc
|
||||
|
||||
// testHookLstat is called before each os.Lstat if non-nil (for testing only)
|
||||
testHookLstat func(path string)
|
||||
}
|
||||
|
||||
type Opt func(*CopyInfo)
|
||||
@@ -272,6 +276,7 @@ type copier struct {
|
||||
changefn fsutil.ChangeFunc
|
||||
root string
|
||||
alwaysReplaceExistingDestPaths bool
|
||||
testHookLstat func(string)
|
||||
}
|
||||
|
||||
type parentDir struct {
|
||||
@@ -317,6 +322,7 @@ func newCopier(root string, chown Chowner, tm *time.Time, mode *int, modeSet *mo
|
||||
excludePatternMatcher: excludePatternMatcher,
|
||||
changefn: changeFunc,
|
||||
alwaysReplaceExistingDestPaths: alwaysReplaceExistingDestPaths,
|
||||
testHookLstat: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -328,16 +334,10 @@ func (c *copier) copy(ctx context.Context, src, srcComponents, target string, ov
|
||||
default:
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(src)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to stat %s", src)
|
||||
}
|
||||
targetFi, err := os.Lstat(target)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "failed to stat %s", src)
|
||||
}
|
||||
|
||||
// Check exclude patterns BEFORE calling os.Lstat to avoid permission errors
|
||||
// on inaccessible files/directories (e.g., protected Windows system folders)
|
||||
include := true
|
||||
excluded := false
|
||||
var (
|
||||
includeMatchInfo patternmatcher.MatchInfo
|
||||
excludeMatchInfo patternmatcher.MatchInfo
|
||||
@@ -345,6 +345,7 @@ func (c *copier) copy(ctx context.Context, src, srcComponents, target string, ov
|
||||
if srcComponents != "" {
|
||||
matchesIncludePattern := false
|
||||
matchesExcludePattern := false
|
||||
var err error
|
||||
matchesIncludePattern, includeMatchInfo, err = c.include(srcComponents, parentIncludeMatchInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -357,7 +358,40 @@ func (c *copier) copy(ctx context.Context, src, srcComponents, target string, ov
|
||||
}
|
||||
if matchesExcludePattern {
|
||||
include = false
|
||||
excluded = true
|
||||
}
|
||||
|
||||
// Optimization: Skip os.Lstat() for excluded paths when safe to do so.
|
||||
// We can skip Lstat if:
|
||||
// 1. The path is explicitly excluded
|
||||
// 2. There are no include patterns (no need to check children)
|
||||
// 3. There are no negation patterns in excludes (no exceptions to exclusions)
|
||||
//
|
||||
// This prevents "Access is denied" errors on Windows protected folders
|
||||
// like "System Volume Information" and "WcSandboxState".
|
||||
canSkip := !include && c.includePatternMatcher == nil &&
|
||||
(c.excludePatternMatcher == nil || !c.excludePatternMatcher.Exclusions())
|
||||
if canSkip {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if c.testHookLstat != nil {
|
||||
c.testHookLstat(src)
|
||||
}
|
||||
fi, err := os.Lstat(src)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to stat %s", src)
|
||||
}
|
||||
|
||||
// After Lstat, if this item is excluded and is NOT a directory, skip it
|
||||
if !include && !fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
targetFi, err := os.Lstat(target)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "failed to stat %s", target)
|
||||
}
|
||||
|
||||
if include {
|
||||
@@ -388,7 +422,7 @@ func (c *copier) copy(ctx context.Context, src, srcComponents, target string, ov
|
||||
case fi.IsDir():
|
||||
if created, err := c.copyDirectory(
|
||||
ctx, src, srcComponents, target, fi, overwriteTargetMetadata,
|
||||
include, includeMatchInfo, excludeMatchInfo,
|
||||
include, excluded, includeMatchInfo, excludeMatchInfo,
|
||||
); err != nil {
|
||||
return err
|
||||
} else if !overwriteTargetMetadata {
|
||||
@@ -539,6 +573,7 @@ func (c *copier) copyDirectory(
|
||||
stat os.FileInfo,
|
||||
overwriteTargetMetadata bool,
|
||||
include bool,
|
||||
excluded bool,
|
||||
includeMatchInfo patternmatcher.MatchInfo,
|
||||
excludeMatchInfo patternmatcher.MatchInfo,
|
||||
) (bool, error) {
|
||||
@@ -577,6 +612,13 @@ func (c *copier) copyDirectory(
|
||||
c.parentDirs = c.parentDirs[:len(c.parentDirs)-1]
|
||||
}()
|
||||
|
||||
// Skip reading directory contents if explicitly excluded AND no negation patterns exist.
|
||||
// If negation patterns exist (e.g., "!bar/baz"), we must traverse excluded directories
|
||||
// to find children that might be un-excluded by the negation.
|
||||
if excluded && (c.excludePatternMatcher == nil || !c.excludePatternMatcher.Exclusions()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fis, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "failed to read %s", src)
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
# Semconv v1.26.0
|
||||
|
||||
[](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.26.0)
|
||||
-8996
File diff suppressed because it is too large
Load Diff
-9
@@ -1,9 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package semconv implements OpenTelemetry semantic conventions.
|
||||
//
|
||||
// OpenTelemetry semantic conventions are agreed standardized naming
|
||||
// patterns for OpenTelemetry things. This package represents the v1.26.0
|
||||
// version of the OpenTelemetry semantic conventions.
|
||||
package semconv // import "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package semconv // import "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
|
||||
const (
|
||||
// ExceptionEventName is the name of the Span event representing an exception.
|
||||
ExceptionEventName = "exception"
|
||||
)
|
||||
-1307
File diff suppressed because it is too large
Load Diff
-9
@@ -1,9 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package semconv // import "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
|
||||
// SchemaURL is the schema URL that matches the version of the semantic conventions
|
||||
// that this package defines. Semconv packages starting from v1.4.0 must declare
|
||||
// non-empty schema URL in the form https://opentelemetry.io/schemas/<version>
|
||||
const SchemaURL = "https://opentelemetry.io/schemas/1.26.0"
|
||||
Vendored
+3
-4
@@ -421,7 +421,7 @@ github.com/mitchellh/go-wordwrap
|
||||
# github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
## explicit; go 1.14
|
||||
github.com/mitchellh/hashstructure/v2
|
||||
# github.com/moby/buildkit v0.26.2
|
||||
# github.com/moby/buildkit v0.26.1-0.20260106154623-ed6dc749ce40
|
||||
## explicit; go 1.24.3
|
||||
github.com/moby/buildkit/api/services/control
|
||||
github.com/moby/buildkit/api/types
|
||||
@@ -514,7 +514,7 @@ github.com/moby/buildkit/version
|
||||
# github.com/moby/docker-image-spec v1.3.1
|
||||
## explicit; go 1.18
|
||||
github.com/moby/docker-image-spec/specs-go/v1
|
||||
# github.com/moby/go-archive v0.1.0
|
||||
# github.com/moby/go-archive v0.2.0
|
||||
## explicit; go 1.23.0
|
||||
github.com/moby/go-archive
|
||||
github.com/moby/go-archive/compression
|
||||
@@ -670,7 +670,7 @@ github.com/stretchr/testify/require
|
||||
# github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323
|
||||
## explicit; go 1.21
|
||||
github.com/tonistiigi/dchapes-mode
|
||||
# github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f
|
||||
# github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f
|
||||
## explicit; go 1.21
|
||||
github.com/tonistiigi/fsutil
|
||||
github.com/tonistiigi/fsutil/copy
|
||||
@@ -732,7 +732,6 @@ go.opentelemetry.io/otel/codes
|
||||
go.opentelemetry.io/otel/internal/baggage
|
||||
go.opentelemetry.io/otel/internal/global
|
||||
go.opentelemetry.io/otel/propagation
|
||||
go.opentelemetry.io/otel/semconv/v1.26.0
|
||||
go.opentelemetry.io/otel/semconv/v1.37.0
|
||||
go.opentelemetry.io/otel/semconv/v1.37.0/httpconv
|
||||
go.opentelemetry.io/otel/semconv/v1.37.0/otelconv
|
||||
|
||||
Reference in New Issue
Block a user