Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local cache error/unused with Buildkit go client #2599

Closed
TomChv opened this issue Feb 4, 2022 · 14 comments · Fixed by #2659
Closed

Local cache error/unused with Buildkit go client #2599

TomChv opened this issue Feb 4, 2022 · 14 comments · Fixed by #2659

Comments

@TomChv
Copy link
Contributor

TomChv commented Feb 4, 2022

Problem

Importing or exporting local cache directory using the Go buildkit client doesn't work.
The error that I'm always getting on the first run is :

ERRO[0009] (*service).Write failed                       error="rpc error: code = Canceled desc = context canceled" expected="sha256:c4f68cac3b6805a14eef6e8812915bc64f81990df1667d1d97b720bfc05aaff7" ref="sha256:c4f68cac3b6805a14eef6e8812915bc64f81990df1667d1d97b720bfc05aaff7" total=894

This error sometimes happens on subsequent runs too (but not always), but not with buildctl.
In addition, even if the error is not thrown, the local cache directory is never used with the go client, it works as expected with buildctl but I do not understand why.

Buildkit version: 0.9.3

How to reproduce

This gist contains everything that you need to reproduce this issue.

Additional information

This started as part of dagger/dagger#1551 (the repository is private).

/cc @gerhard

@gerhard
Copy link

gerhard commented Feb 4, 2022

We have noticed an interesting moby.filesync.v1.Auth/GetTokenAuthority error in Jaeger traces when running the go buildkit client:

image

We are not seeing this error when running with buildctl:

image

We don't know if this is related, but it does look suspicious. We are still investigating.

@gerhard
Copy link

gerhard commented Feb 4, 2022

The following change fixed the moby.filesync.v1.Auth/GetTokenAuthority error:

@@ -10,6 +10,8 @@ import (
        _ "github.com/moby/buildkit/client/connhelper/dockercontainer" // import the container connection driver
        "github.com/moby/buildkit/client/llb"
        bkgw "github.com/moby/buildkit/frontend/gateway/client"
+       "github.com/moby/buildkit/session"
+       "github.com/moby/buildkit/session/auth/authprovider"
        "golang.org/x/sync/errgroup"
 )

@@ -28,6 +30,7 @@ func New(ctx context.Context) (*Client, error) {

 func (c *Client) Do(ctx context.Context) error {
        // Fill options
+       attachable := []session.Attachable{authprovider.NewDockerAuthProvider(os.Stderr)}
        opts := bk.SolveOpt{
                Exports: []bk.ExportEntry{{Type: "local", OutputDir: "result"}},
                CacheImports: []bk.CacheOptionsEntry{{
@@ -43,6 +46,7 @@ func (c *Client) Do(ctx context.Context) error {
                                "dest": "store",
                        },
                }},
+               Session: attachable,
        }

        return c.exec(ctx, opts)

It did not fix the issue that we reported.

@gerhard
Copy link

gerhard commented Feb 4, 2022

We have noticed a difference in the cacheOpts that we generate:

&client.cacheOptions{options:moby_buildkit_v1.CacheOptions{ExportRefDeprecated:"", ImportRefsDeprecated:[]string(nil), ExportAttrsDeprecated:map[
string]string{}, Exports:[]*moby_buildkit_v1.CacheOptionsEntry{(*moby_buildkit_v1.CacheOptionsEntry)(0xc000148240)}, Imports:[]*moby_buildkit_v1.
CacheOptionsEntry{(*moby_buildkit_v1.CacheOptionsEntry)(0xc000148340)}, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_size
cache:0}, contentStores:map[string]content.Store{"local:store":(*local.store)(0xc00007c100)}, indicesToUpdate:map[string]string{"store/index.json
":"latest"}, frontendAttrs:map[string]string{}}

vs what buildctl generates:

&client.cacheOptions{options:moby_buildkit_v1.CacheOptions{ExportRefDeprecated:"",
ImportRefsDeprecated:[]string(nil), ExportAttrsDeprecated:map[string]string{},
Exports:[]*moby_buildkit_v1.CacheOptionsEntry{(*moby_buildkit_v1.CacheOptionsEntry)(0x14000690140)},
Imports:[]*moby_buildkit_v1.CacheOptionsEntry{(*moby_buildkit_v1.CacheOptionsEntry)(0x14000690240)},
XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil),
XXX_sizecache:0},
contentStores:map[string]content.Store{"local:store":(*local.store)(0x140006a8120)},
indicesToUpdate:map[string]string{"store/index.json":"latest"},
frontendAttrs:map[string]string{"cache-imports":"[{\"Type\":\"local\",\"Attrs\":{\"digest\":\"sha256:fc046d2f4e63eb1ce6d32e7f1f68904d1531e29b09dda0b27d954f3d99ae6ea5\",\"src\":\"store\"}}]"}}

The frontendAttrs map is empty in our case.

@gerhard
Copy link

gerhard commented Feb 4, 2022

Based on this code in the buildkit client, the frontend must not be set:

buildkit/client/build.go

Lines 23 to 32 in a640b47

if opt.Frontend != "" {
return nil, errors.New("invalid SolveOpt, Build interface cannot use Frontend")
}
if product == "" {
product = apicaps.ExportedProduct
}
feOpts := opt.FrontendAttrs
opt.FrontendAttrs = nil

If that is the case, how do we pass cache instructions to buildkit?

We understand that one way of doing this is via frontendAttrs, but since we cannot have a frontend, do we really need to set frontendAttrs explicitly for caching to work? We assumed that setting Exports & Imports is enough, but apparently that is not the case.

buildkit/client/solve.go

Lines 487 to 500 in a640b47

res := cacheOptions{
options: controlapi.CacheOptions{
// old API (for registry caches, planned to be removed in early 2019)
ExportRefDeprecated: legacyExportRef,
ExportAttrsDeprecated: legacyExportAttrs,
ImportRefsDeprecated: legacyImportRefs,
// new API
Exports: cacheExports,
Imports: cacheImports,
},
contentStores: contentStores,
indicesToUpdate: indicesToUpdate,
frontendAttrs: frontendAttrs,
}

Are we on the right track? 🤔

https://gist.github.com/TomChv/ecf651b43553a2bd7da2bd5448c9dd13#file-main-go-L33-L45

@tonistiigi
Copy link
Member

If you are using Build then imports can be in the internal SolveRequest (I guess they work for both sides). But the initial question was about export I believe.

If the local export doesn't work at all then check that the contentStores is set up properly. If it finishes too early then there looks to be some sync error with context canceling too early.

@TomChv
Copy link
Contributor Author

TomChv commented Feb 11, 2022

If you are using Build then imports can be in the internal SolveRequest (I guess they work for both sides). But the initial question was about export I believe.

I tried to update SolveRequest with a CacheImport but it didn't solved the issue.

// See the gist for complete code
res, err := c.Solve(ctx, bkgw.SolveRequest{
	Definition: def.ToPB(),
	CacheImports: []bkgw.CacheOptionsEntry{{
		Type: "local",
		Attrs: map[string]string{
			"src": "store",
		},
	}},
})

Result

# Remove old daemon
docker container stop dagger-buildkitd && docker container rm dagger-buildkitd && docker volume rm dagger-buildkitd                      
dagger-buildkitd
dagger-buildkitd
dagger-buildkitd

# Start buildkit daemon
docker run --net=host -d --restart always -v dagger-buildkitd:/var/lib/buildkit --name dagger-buildkitd --privileged moby/buildkit:v0.9.3
7712e7878dafac5edefbbf1a3452e1142348842635c7e4e364f2fd4504e7fe07

# List files
ls
go.mod  go.sum  main.go

# Run example
time go run ./main.go 
go run ./main.go  0.92s user 0.64s system 13% cpu 11.854 total

# Check files
l store/blobs/sha256 
total 5328
-r--r--r--  1 tomchauveau  staff   894B Feb 11 15:09 11d64926da617f9a5ca9695d30b085de2d8ce7ddf938c4d98ea5478ce7822521
-r--r--r--  1 tomchauveau  staff   560B Feb 11 15:09 6293955c0b870a4d79d354f2fe6394c47f3906cd9c785ab2e08647e486ed883d
-r--r--r--  1 tomchauveau  staff   2.6M Feb 11 15:09 9b3977197b4f2147bdd31e1271f811319dcd5c2fc595f14e81f5351ab6275b99 // Cache is successfully exported
-r--r--r--  1 tomchauveau  staff   105B Feb 11 15:09 c37711adfbf5490df2a3ad0be475411e4e54f1829da8770e2a08367782806b96

# Stop and remove container
docker container stop dagger-buildkitd && docker container rm dagger-buildkitd && docker volume rm dagger-buildkitd                      
dagger-buildkitd
dagger-buildkitd
dagger-buildkitd

# Rerun buildkit daemon
docker run --net=host -d --restart always -v dagger-buildkitd:/var/lib/buildkit --name dagger-buildkitd --privileged moby/buildkit:v0.9.3
f789e7deb3d2ec1f5f5fe4eb7aca2b44bb642463ba78d6eb4adc5a8251447065

# Re execute
time go run ./main.go
go run ./main.go  0.92s user 0.68s system 13% cpu 11.725 total

But the initial question was about export I believe

Actually we thought that they were an issue with export, but it just a matter of sync error that produce that error

ERRO[0009] (*service).Write failed                       error="rpc error: code = Canceled desc = context canceled" expected="sha256:c4f68cac3b6805a14eef6e8812915bc64f81990df1667d1d97b720bfc05aaff7" ref="sha256:c4f68cac3b6805a14eef6e8812915bc64f81990df1667d1d97b720bfc05aaff7" total=894

But this error do not always happens.

The real problem is that the buildkit go client do not use the local cache directory when we are using raw LLB.

@gerhard
Copy link

gerhard commented Feb 12, 2022

Thank you @tonistiigi for the suggestions.

As a recap, the store directory is not used for the import cache - this is our main problem.

Given our starting CacheImports config in main.go this is how contentStores gets configured:

// fmt.Printf("contentStores: %#v\n", contentStores)
contentStores: map[string]content.Store{"local:store":(*local.store)(0xc0000a80e0)}
// fmt.Printf("contentStore: %#v\n", cs)
contentStore: &local.store{root:"store", ls:local.LabelStore(nil)}

If we run the same with buildctl and this Dockerfile:

FROM alpine

RUN sleep 5 && echo test

this is how contentStores get configured:

// fmt.Printf("contentStores: %#v\n", contentStores)
contentStores: map[string]content.Store{"local:store":(*local.store)(0x140002fafc0)}
// fmt.Printf("contentStore: %#v\n", cs)
contentStore: &local.store{root:"store", ls:local.LabelStore(nil)}&{{ [] map[] [0x14000331a00] [0x14000331b00] {} [] 0} map[local:store:0x140002fafc0] map[store/index.json:latest] map[cache-imports:[{"Type":"local","Attrs":{"digest":"sha256:a2c807e50263db527d8160cc1601503a6e4d218ec8be46d2a16d42305b8f96a9","src":"store"}}]]}

This suggests to us that contentStores are not being set correctly when running with the go client.

As a next step, we will try to understand why the local store directory is configured differently when running via buildctl vs the go client. This is the code where we will continue digging:

buildkit/client/solve.go

Lines 402 to 418 in 63eff24

if ex.Type == "local" {
csDir := ex.Attrs["dest"]
if csDir == "" {
return nil, errors.New("local cache exporter requires dest")
}
if err := os.MkdirAll(csDir, 0755); err != nil {
return nil, err
}
cs, err := contentlocal.NewStore(csDir)
if err != nil {
return nil, err
}
contentStores["local:"+csDir] = cs
// TODO(AkihiroSuda): support custom index JSON path and tag
indexJSONPath := filepath.Join(csDir, "index.json")
indicesToUpdate[indexJSONPath] = "latest"
}

By the way, we have confirmed that the index.json file gets populated with similar values (the sha differs) when we run both with the go client & buildctl:

{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:dc45a8a989c08248dc081675f96036c87360121b50a4caa854d4c4e1f104f43d","size":895,"annotations":{"org.opencontainers.image.ref.name":"latest"}}]}

It is possible that our failing export is the reason why our import is not getting configured correctly, but we are not sure.

@gerhard
Copy link

gerhard commented Feb 18, 2022

We (+@TomChv) continued with this today and noticed that we made a mistake last time.

The contentStore value is the same in both the go client, and when running via buildctl.

We have confirmed that client.cacheOptions is different however, as mentioned a few weeks back.
This is the diff between go client (red) and buildctl (green):

image

This confirms our assumption: the cache options are not propagated correctly when using the go client.

The next step is to confirm our next assumption: if frontendAttrs is not populated, which is the case when using the go client, the cache imports that we set are not passed correctly to controlClient().Solve():

buildkit/client/solve.go

Lines 196 to 216 in 63eff24

frontendInputs := make(map[string]*pb.Definition)
for key, st := range opt.FrontendInputs {
def, err := st.Marshal(ctx)
if err != nil {
return err
}
frontendInputs[key] = def.ToPB()
}
resp, err := c.controlClient().Solve(ctx, &controlapi.SolveRequest{
Ref: ref,
Definition: pbd,
Exporter: ex.Type,
ExporterAttrs: ex.Attrs,
Session: s.ID(),
Frontend: opt.Frontend,
FrontendAttrs: opt.FrontendAttrs,
FrontendInputs: frontendInputs,
Cache: cacheOpt.options,
Entitlements: opt.AllowedEntitlements,
})

I will skip next week, but @TomChv should be around to continue with this.
I look forward to what he discovers next!

@TomChv
Copy link
Contributor Author

TomChv commented Feb 24, 2022

Thank you for the PR, unfortunately, I tried with your changes and it didn't solves the cache issue.
I created a new gist with changes.

I also continue digging into the codebase and that's really strange because cache-imports is correctly forward to the daemon.
To be sure, I displayed the request in frontend/gateway/gateway.pb.go and we can see that cache options are correctly set

func (c *lLBBridgeClient) Solve(ctx context.Context, in *SolveRequest, opts ...grpc.CallOption) (*SolveResponse, error) {
	out := new(SolveResponse)
	fmt.Printf("Solve LLB client %#v\n", in)
	err := c.cc.Invoke(ctx, "/moby.buildkit.v1.frontend.LLBBridge/Solve", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}
Solve LLB client &moby_buildkit_v1_frontend.SolveRequest{
    Definition:(*pb.Definition)(0x1400080ede0), 
    Frontend:"", FrontendOpt:map[string]string{
       "cache-imports":"[{
             \"Type\":\"local\",
              \"Attrs\":{\"digest\":\"sha256:f06a4acc9859f03173a79ffa559c4450c60be83052f5981301ac7ed9e8df4cb7\",\"src\":\"store\"}
       }]"
   }, 
   ImportCacheRefsDeprecated:[]string(nil), 
  AllowResultReturn:true,
  AllowResultArrayRef:true, 
  Final:false,
  ExporterAttr:[]uint8(nil),
  CacheImports:[]*moby_buildkit_v1_frontend.CacheOptionsEntry{(*moby_buildkit_v1_frontend.CacheOptionsEntry)(0x14000337700)}, 
  FrontendInputs:map[string]*pb.Definition(nil), 
  Evaluate:false,
  XXX_NoUnkeyedLiteral:struct {}{}, 
  XXX_unrecognized:[]uint8(nil), 
  XXX_sizecache:0
}

With this, we are sure that cache is forwarded to the daemon, do you think the problem come from the daemon?
Is there anything we are doing wrong in the frontend

Click to see a demo with logs
# List current files
ls
buildkit-repro go.mod         go.sum         main.go        vendor


# Run buildkit daemon
docker run --net=host -d --restart always -v dagger-buildkitd:/var/lib/buildkit --name dagger-buildkitd --privileged moby/buildkit:v0.10.0-rc1
7fbf5df0e24b246bd4c1a57dd05a70e2c3bf50c46a65e382bb4c7972ad8cd697

# Export host
export BUILDKIT_HOST=docker-container://dagger-buildkitd

# Execute repro case => no cache exist for now
time ./buildkit-repro 
WARN[0000] local cache import at store not found due to err: could not read store/index.json: open store/index.json: no such file or directory 
Solve LLB client &moby_buildkit_v1_frontend.SolveRequest{Definition:(*pb.Definition)(0x140003b02a0), Frontend:"", FrontendOpt:map[string]string(nil), ImportCacheRefsDeprecated:[]string(nil), AllowResultReturn:true, AllowResultArrayRef:true, Final:false, ExporterAttr:[]uint8(nil), CacheImports:[]*moby_buildkit_v1_frontend.CacheOptionsEntry{(*moby_buildkit_v1_frontend.CacheOptionsEntry)(0x140006f2340)}, FrontendInputs:map[string]*pb.Definition(nil), Evaluate:false, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
./buildkit-repro  0.21s user 0.20s system 2% cpu 14.816 total

# List files => Cache is correctly created
ls      
buildkit-repro go.mod         go.sum         main.go        result         store          vendor

# List store
l store/blobs/sha256/
total 5328
drwxr-xr-x  6 tomchauveau  staff   192B Feb 24 17:42 .
drwxr-xr-x  3 tomchauveau  staff    96B Feb 24 17:42 ..
-r--r--r--  1 tomchauveau  staff   895B Feb 24 17:42 7c1adf2606c30da26a4ed4ad887ee6c6c814183e315b2711cac4f49c0b135a9a
-r--r--r--  1 tomchauveau  staff   2.6M Feb 24 17:42 9b3977197b4f2147bdd31e1271f811319dcd5c2fc595f14e81f5351ab6275b99
-r--r--r--  1 tomchauveau  staff   105B Feb 24 17:42 9ebb20b96f11fb5412be45455f55eb110c787dd2cc0364fe48b233d58857bfa2
-r--r--r--  1 tomchauveau  staff   559B Feb 24 17:42 a5e548dc90fe39a789811dbd6a186eb8383b6f892568ee83aa843b07f64a49ba

# Down and restart daemon
docker container stop dagger-buildkitd && docker container rm dagger-buildkitd && docker volume rm dagger-buildkitd                           
dagger-buildkitd
dagger-buildkitd
dagger-buildkitd

docker run --net=host -d --restart always -v dagger-buildkitd:/var/lib/buildkit --name dagger-buildkitd --privileged moby/buildkit:v0.10.0-rc1
62b5ab5a52b7364a909601589f4e66784a3372866b36ac8c395b305f0a0af7e1

# Execute
time ./buildkit-repro
Solve LLB client &moby_buildkit_v1_frontend.SolveRequest{Definition:(*pb.Definition)(0x140000fea20), Frontend:"", FrontendOpt:map[string]string{"cache-imports":"[{\"Type\":\"local\",\"Attrs\":{\"digest\":\"sha256:7c1adf2606c30da26a4ed4ad887ee6c6c814183e315b2711cac4f49c0b135a9a\",\"src\":\"store\"}}]"}, ImportCacheRefsDeprecated:[]string(nil), AllowResultReturn:true, AllowResultArrayRef:true, Final:false, ExporterAttr:[]uint8(nil), CacheImports:[]*moby_buildkit_v1_frontend.CacheOptionsEntry{(*moby_buildkit_v1_frontend.CacheOptionsEntry)(0x140003c9780)}, FrontendInputs:map[string]*pb.Definition(nil), Evaluate:false, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
./buildkit-repro  0.18s user 0.21s system 2% cpu 13.841 total # Cache do not works even it's sent :/

@TomChv
Copy link
Contributor Author

TomChv commented Feb 24, 2022

Okay, I figure out which piece of code is broken 🥳
I tried a simple repro case that follows what buildctl do and I meet the cache issue.

Basically, the cache is not imported when Solve is encapsulate in Build, but it is when using simple Solve.

Here's a new gist to reproduce the issue and understand the difference.

Since both are calling solve, the error certainly happens in that block of code or when the cbis called.
I'll continue my investigation.

func (c *Client) Build(ctx context.Context, opt SolveOpt, product string, buildFunc gateway.BuildFunc, statusChan chan *SolveStatus) (*SolveResponse, error) {
	defer func() {
		if statusChan != nil {
			close(statusChan)
		}
	}()

	if opt.Frontend != "" {
		return nil, errors.New("invalid SolveOpt, Build interface cannot use Frontend")
	}

	if product == "" {
		product = apicaps.ExportedProduct
	}

	feOpts := opt.FrontendAttrs
	opt.FrontendAttrs = nil

	workers, err := c.ListWorkers(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "listing workers for Build")
	}
	var gworkers []gateway.WorkerInfo
	for _, w := range workers {
		gworkers = append(gworkers, gateway.WorkerInfo{
			ID:        w.ID,
			Labels:    w.Labels,
			Platforms: w.Platforms,
		})
	}

	cb := func(ref string, s *session.Session, opts map[string]string) error {
		for k, v := range opts {
			if feOpts == nil {
				feOpts = map[string]string{}
			}
			feOpts[k] = v
		}
		gwClient := c.gatewayClientForBuild(ref)
		g, err := grpcclient.New(ctx, feOpts, s.ID(), product, gwClient, gworkers)
		if err != nil {
			return err
		}

		gwClient.caps = g.BuildOpts().Caps

		if err := g.Run(ctx, buildFunc); err != nil {
			return errors.Wrap(err, "failed to run Build function")
		}
		return nil
	}

	return c.solve(ctx, nil, cb, opt, statusChan)

@TomChv
Copy link
Contributor Author

TomChv commented Feb 25, 2022

Finally! The current issue come from a missing digest in cache-imports options

Here's an example

# Store is already existing
➜  ls
go.mod  go.sum  main.go result  store   vendor

➜ docker run --net=host -d --restart always -v dagger-buildkitd:/var/lib/buildkit --name dagger-buildkitd --privileged moby/buildkit:v0.10.0-rc1
3006ad43256b01061322f2d51d019f7c4d017cd36d600b1867cafa10d03107ac

➜ time go run ./main.go
Support Import Caches: true
GrpcClient.Solve: cacheImports[0]: &moby_buildkit_v1_frontend.CacheOptionsEntry{Type:"local", Attrs:map[string]string{"src":"store"}, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
go run ./main.go  0.89s user 0.67s system 13% cpu 11.705 total

As you see in Attrs, there is no digest

Attrs:map[string]string{"src":"store"}

Now if I run and hardcode the digest of the cache in the Attrs, it works!

# Clean and run docker
➜ docker container stop dagger-buildkitd && docker container rm dagger-buildkitd && docker volume rm dagger-buildkitd
➜  docker run --net=host -d --restart always -v dagger-buildkitd:/var/lib/buildkit --name dagger-buildkitd --privileged moby/buildkit:v0.10.0-rc1
f26162a4b48c007b66579f0c0fdb16685a25289084b14615cc06286bc075e2af

➜  time go run ./main.go
Support Import Caches: true
GrpcClient.Solve: cacheImports[0]: &moby_buildkit_v1_frontend.CacheOptionsEntry{Type:"local", Attrs:map[string]string{"digest":"sha256:8a2ca54e54fc09ed5a0c2293bbe0795b93fb5d06dae824a6d6cb7df174547403", "src":"store"}, XXX_NoUnkeyedLiteral:struct {}{}, XXX_unrecognized:[]uint8(nil), XXX_sizecache:0}
go run ./main.go  0.88s user 0.66s system 45% cpu 3.390 total # Cache is used

As you see, with a digest in Attrs, it works

Attrs:map[string]string{"digest":"sha256:8a2ca54e54fc09ed5a0c2293bbe0795b93fb5d06dae824a6d6cb7df174547403", "src":"store"}

Now, I have to figure out why digest is not set in CacheImports.

@TomChv
Copy link
Contributor Author

TomChv commented Feb 25, 2022

After more research, I spotted the problem.

After comparing with solve, I found that we call a function parseCacheOptions. This function will add the key digest to imports options if it's not present.

When we use Build + Solve, cacheImports is set from the SolveRequest.CacheImports.
Basically, it fit with what @tonistiigi said

If you are using Build then imports can be in the internal SolveRequest (I guess they work for both sides). But the initial question was about export I believe.

But SolveRequest.CacheImports is not parsed though parseCacheOptions so no digest is added and then no cache is used.
As well, since we have no frontend, even if #2659 correctly forward cache options into FrontendAttrs, it's ignore by the solver.

So here's possible fixes:

  • Parse cacheImports option
  • Find a way to merge SolveOpt.CacheImports into SolveRequest.CacheImports
  • Use FrontendAttrs even if there is no frontend (not accurate IMO)

@TomChv
Copy link
Contributor Author

TomChv commented Feb 25, 2022

I implemented a possible fix there, but I'm not sure this is the best solution since there is a lot of code duplication with previous solve

diff --git a/frontend/gateway/grpcclient/client.go b/frontend/gateway/grpcclient/client.go
index d8e2799f..7fca61b4 100644
--- a/frontend/gateway/grpcclient/client.go
+++ b/frontend/gateway/grpcclient/client.go
@@ -7,6 +7,7 @@ import (
        "io"
        "net"
        "os"
+       "path/filepath"
        "strings"
        "sync"
        "syscall"
@@ -16,6 +17,7 @@ import (
        gogotypes "github.com/gogo/protobuf/types"
        "github.com/golang/protobuf/ptypes/any"
        "github.com/moby/buildkit/client/llb"
+       "github.com/moby/buildkit/client/ociindex"
        "github.com/moby/buildkit/frontend/gateway/client"
        pb "github.com/moby/buildkit/frontend/gateway/pb"
        "github.com/moby/buildkit/identity"
@@ -25,6 +27,7 @@ import (
        "github.com/moby/buildkit/util/grpcerrors"
        "github.com/moby/sys/signal"
        digest "github.com/opencontainers/go-digest"
+       ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
        "github.com/pkg/errors"
        fstypes "github.com/tonistiigi/fsutil/types"
        "golang.org/x/sync/errgroup"
@@ -313,24 +316,45 @@ func (c *grpcClient) Warn(ctx context.Context, dgst digest.Digest, msg string, o
        return err
 }
 
-func (c *grpcClient) Solve(ctx context.Context, creq client.SolveRequest) (res *client.Result, err error) {
-       if creq.Definition != nil {
-               for _, md := range creq.Definition.Metadata {
-                       for cap := range md.Caps {
-                               if err := c.llbCaps.Supports(cap); err != nil {
-                                       return nil, err
-                               }
-                       }
-               }
-       }
+type cacheOptions struct {
+       legacyRegistryCacheImports []string
+       cacheImports               []*pb.CacheOptionsEntry
+}
+
+func parseCacheOptions(ctx context.Context, reqCacheImports []client.CacheOptionsEntry, supportCapImportCaches bool) (*cacheOptions, error) {
        var (
                // old API
                legacyRegistryCacheImports []string
                // new API (CapImportCaches)
                cacheImports []*pb.CacheOptionsEntry
        )
-       supportCapImportCaches := c.caps.Supports(pb.CapImportCaches) == nil
-       for _, im := range creq.CacheImports {
+
+       for _, im := range reqCacheImports {
+               attrs := im.Attrs
+               if im.Type == "local" {
+                       csDir := im.Attrs["src"]
+                       if csDir == "" {
+                               return nil, errors.New("local cache importer requires src")
+                       }
+
+                       // if digest is not specified, load from "latest" tag
+                       if attrs["digest"] == "" {
+                               idx, err := ociindex.ReadIndexJSONFileLocked(filepath.Join(csDir, "index.json"))
+                               if err != nil {
+                                       bklog.G(ctx).Warning("local cache import at " + csDir + " not found due to err: " + err.Error())
+                                       continue
+                               }
+                               for _, m := range idx.Manifests {
+                                       if (m.Annotations[ocispecs.AnnotationRefName] == "latest" && attrs["tag"] == "") || (attrs["tag"] != "" && m.Annotations[ocispecs.AnnotationRefName] == attrs["tag"]) {
+                                               attrs["digest"] = string(m.Digest)
+                                               break
+                                       }
+                               }
+                               if attrs["digest"] == "" {
+                                       return nil, errors.New("local cache importer requires either explicit digest, \"latest\" tag or custom tag on index.json")
+                               }
+                       }
+               }
                if !supportCapImportCaches && im.Type == "registry" {
                        legacyRegistryCacheImports = append(legacyRegistryCacheImports, im.Attrs["ref"])
                } else {
@@ -340,6 +364,27 @@ func (c *grpcClient) Solve(ctx context.Context, creq client.SolveRequest) (res *
                        })
                }
        }
+       return &cacheOptions{
+               cacheImports:               cacheImports,
+               legacyRegistryCacheImports: legacyRegistryCacheImports,
+       }, nil
+}
+
+func (c *grpcClient) Solve(ctx context.Context, creq client.SolveRequest) (res *client.Result, err error) {
+       if creq.Definition != nil {
+               for _, md := range creq.Definition.Metadata {
+                       for cap := range md.Caps {
+                               if err := c.llbCaps.Supports(cap); err != nil {
+                                       return nil, err
+                               }
+                       }
+               }
+       }
+       supportCapImportCaches := c.caps.Supports(pb.CapImportCaches) == nil
+       cacheOpts, err := parseCacheOptions(ctx, creq.CacheImports, supportCapImportCaches)
+       if err != nil {
+               return nil, err
+       }
 
        // these options are added by go client in solve()
        if _, ok := creq.FrontendOpt["cache-imports"]; !ok {
@@ -367,9 +412,9 @@ func (c *grpcClient) Solve(ctx context.Context, creq client.SolveRequest) (res *
                AllowResultReturn:   true,
                AllowResultArrayRef: true,
                // old API
-               ImportCacheRefsDeprecated: legacyRegistryCacheImports,
+               ImportCacheRefsDeprecated: cacheOpts.legacyRegistryCacheImports,
                // new API
-               CacheImports: cacheImports,
+               CacheImports: cacheOpts.cacheImports,
        }
 
        // backwards compatibility with inline return

@aluzzardi
Copy link
Member

Hey @tonistiigi -- TLDR: we have spotted an issue with local cache imports.

  • client.Solve --> import cache works
  • client.Build + gw.Solve --> import cache does NOT work

Repro case here.

@TomChv managed to find the issue in the codebase, details above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants