diff --git a/builtin/lookup.go b/builtin/lookup.go index 1e712220..dc733b61 100644 --- a/builtin/lookup.go +++ b/builtin/lookup.go @@ -26,6 +26,13 @@ var ( Params: []*ast.Field{}, Effects: []*ast.Field{}, }, + "cache": { + Params: []*ast.Field{ + ast.NewField(ast.Filesystem, "input", false), + ast.NewField(ast.String, "ref", false), + }, + Effects: []*ast.Field{}, + }, "image": { Params: []*ast.Field{ ast.NewField(ast.String, "ref", false), @@ -692,6 +699,14 @@ var ( # @return a scratch filesystem. fs scratch() +# Caches an input fs by its vertex digest. +# If the image exists then use that image instead of executing input. +# +# @param input a filesystem to cache by vertex digest. +# @param ref a docker registry reference. +# @return a filesystem of input from either building or image. +fs cache(fs input, string ref) + # An OCI image's filesystem. # # @param ref a docker registry reference. if not fully qualified, it will be diff --git a/codegen/builtin.go b/codegen/builtin.go index 46da6a87..f67d4e19 100644 --- a/codegen/builtin.go +++ b/codegen/builtin.go @@ -17,6 +17,7 @@ var ( "git": Git{}, "local": Local{}, "frontend": Frontend{}, + "cache": Cache{}, "run": Run{}, "env": Env{}, "dir": Dir{}, @@ -108,7 +109,7 @@ var ( "readonly": Readonly{}, "tmpfs": Tmpfs{}, "sourcePath": SourcePath{}, - "cache": Cache{}, + "cache": MountCache{}, }, "option::mkdir": { "createParents": CreateParents{}, diff --git a/codegen/builtin_fs.go b/codegen/builtin_fs.go index ce0ea76b..23988779 100644 --- a/codegen/builtin_fs.go +++ b/codegen/builtin_fs.go @@ -328,6 +328,74 @@ func (f Frontend) Call(ctx context.Context, cln *client.Client, val Value, opts return NewValue(ctx, fs) } +type Cache struct{} + +func (c Cache) Call(ctx context.Context, cln *client.Client, val Value, opts Option, input Filesystem, ref string) (Value, error) { + inputDgst, err := input.Digest(ctx) + if err != nil { + return nil, Arg(ctx, 1).WithError(err) + } + named, err := reference.ParseNormalizedNamed(ref) + if err != nil { + return nil, errdefs.WithInvalidImageRef(err, Arg(ctx, 1), ref) + } + namedTagged, err := reference.WithTag(named, inputDgst.Encoded()) + if err != nil { + return nil, Arg(ctx, 1).WithError(err) + } + ref = namedTagged.String() + + resolver := ImageResolver(ctx) + if resolver != nil { + resolveOpt := llb.ResolveImageConfigOpt{ + Platform: &input.Platform, + } + + dgst, config, err := resolver.ResolveImageConfig(ctx, ref, resolveOpt) + if err == nil { + var imageOpts []llb.ImageOption + imageOpts = append(imageOpts, llb.Platform(input.Platform)) + for _, opt := range SourceMap(ctx) { + imageOpts = append(imageOpts, opt) + } + + canonical, err := reference.WithDigest(named, dgst) + if err != nil { + return nil, errdefs.WithInvalidImageRef(err, Arg(ctx, 1), ref) + } + + cacheVal, err := NewValue(ctx, llb.Image(canonical.String())) + if err != nil { + return nil, Arg(ctx, 1).WithError(err) + } + + input, err = cacheVal.Filesystem() + if err != nil { + return nil, Arg(ctx, 1).WithError(err) + } + + input.State, err = input.State.WithImageConfig(config) + if err != nil { + return nil, Arg(ctx, 1).WithError(err) + } + + input.Image = &solver.ImageSpec{} + err = json.Unmarshal(config, input.Image) + if err != nil { + return nil, Arg(ctx, 1).WithError(err) + } + } else { // not found + inputVal, err := NewValue(ctx, input) + if err != nil { + return nil, Arg(ctx, 0).WithError(err) + } + return (DockerPush{}).Call(ctx, cln, inputVal, opts, ref) + } + } + + return NewValue(ctx, input) +} + type Env struct{} func (e Env) Call(ctx context.Context, cln *client.Client, val Value, opts Option, key, value string) (Value, error) { diff --git a/codegen/builtin_option.go b/codegen/builtin_option.go index cbfb60b7..12b5485a 100644 --- a/codegen/builtin_option.go +++ b/codegen/builtin_option.go @@ -693,10 +693,10 @@ func (m Mount) Call(ctx context.Context, cln *client.Client, val Value, opts Opt return nil, err } - var cache *Cache + var cache *MountCache for _, opt := range opts { var ok bool - cache, ok = opt.(*Cache) + cache, ok = opt.(*MountCache) if ok { break } @@ -858,11 +858,11 @@ func (sp SourcePath) Call(ctx context.Context, cln *client.Client, val Value, op return NewValue(ctx, append(retOpts, llbutil.WithSourcePath(path))) } -type Cache struct { +type MountCache struct { ast.Node } -func (c Cache) Call(ctx context.Context, cln *client.Client, val Value, opts Option, id, mode string) (Value, error) { +func (mc MountCache) Call(ctx context.Context, cln *client.Client, val Value, opts Option, id, mode string) (Value, error) { retOpts, err := val.Option() if err != nil { return nil, err @@ -880,7 +880,7 @@ func (c Cache) Call(ctx context.Context, cln *client.Client, val Value, opts Opt return nil, errdefs.WithInvalidSharingMode(Arg(ctx, 1), mode, []string{"shared", "private", "locked"}) } - retOpts = append(retOpts, &Cache{ProgramCounter(ctx)}, llbutil.WithPersistentCacheDir(id, sharing)) + retOpts = append(retOpts, &MountCache{ProgramCounter(ctx)}, llbutil.WithPersistentCacheDir(id, sharing)) return NewValue(ctx, retOpts) } diff --git a/docs/reference.md b/docs/reference.md index 87f04eb7..5305918e 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1,4 +1,20 @@ ## fs functions +### fs cache(fs input, string ref) + +!!! info "fs input" + +!!! info "string ref" + + + + + #!hlb + fs default() { + cache scratch "ref" + } + + + ### fs cmd(string args) !!! info "string args" diff --git a/language/builtin.hlb b/language/builtin.hlb index fcb6b7b0..77cb08f7 100644 --- a/language/builtin.hlb +++ b/language/builtin.hlb @@ -3,6 +3,14 @@ # @return a scratch filesystem. fs scratch() +# Caches an input fs by its vertex digest. +# If the image exists then use that image instead of executing input. +# +# @param input a filesystem to cache by vertex digest. +# @param ref a docker registry reference. +# @return a filesystem of input from either building or image. +fs cache(fs input, string ref) + # An OCI image's filesystem. # # @param ref a docker registry reference. if not fully qualified, it will be