Skip to content

Commit 5baa2cb

Browse files
authored
feat: support interceptor for build (#151)
Signed-off-by: chlins <[email protected]>
1 parent 4551302 commit 5baa2cb

File tree

9 files changed

+164
-7
lines changed

9 files changed

+164
-7
lines changed

pkg/backend/attach.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/CloudNativeAI/modctl/pkg/backend/build"
3333
buildconfig "github.com/CloudNativeAI/modctl/pkg/backend/build/config"
3434
"github.com/CloudNativeAI/modctl/pkg/backend/build/hooks"
35+
"github.com/CloudNativeAI/modctl/pkg/backend/build/interceptor"
3536
"github.com/CloudNativeAI/modctl/pkg/backend/processor"
3637
"github.com/CloudNativeAI/modctl/pkg/backend/remote"
3738
"github.com/CloudNativeAI/modctl/pkg/config"
@@ -299,6 +300,10 @@ func (b *backend) getBuilder(reference string, cfg *config.Attach) (build.Builde
299300
build.WithPlainHTTP(cfg.PlainHTTP),
300301
build.WithInsecure(cfg.Insecure),
301302
}
303+
if cfg.Nydusify {
304+
opts = append(opts, build.WithInterceptor(interceptor.NewNydus()))
305+
}
306+
302307
builder, err := build.NewBuilder(outputType, b.store, repo, tag, opts...)
303308
if err != nil {
304309
return nil, fmt.Errorf("failed to create builder: %w", err)

pkg/backend/build.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/CloudNativeAI/modctl/pkg/backend/build"
2727
buildconfig "github.com/CloudNativeAI/modctl/pkg/backend/build/config"
2828
"github.com/CloudNativeAI/modctl/pkg/backend/build/hooks"
29+
"github.com/CloudNativeAI/modctl/pkg/backend/build/interceptor"
2930
"github.com/CloudNativeAI/modctl/pkg/backend/processor"
3031
"github.com/CloudNativeAI/modctl/pkg/config"
3132
"github.com/CloudNativeAI/modctl/pkg/modelfile"
@@ -67,6 +68,10 @@ func (b *backend) Build(ctx context.Context, modelfilePath, workDir, target stri
6768
build.WithPlainHTTP(cfg.PlainHTTP),
6869
build.WithInsecure(cfg.Insecure),
6970
}
71+
if cfg.Nydusify {
72+
opts = append(opts, build.WithInterceptor(interceptor.NewNydus()))
73+
}
74+
7075
builder, err := build.NewBuilder(outputType, b.store, repo, tag, opts...)
7176
if err != nil {
7277
return fmt.Errorf("failed to create builder: %w", err)

pkg/backend/build/builder.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"io"
2525
"os"
2626
"path/filepath"
27+
"sync"
2728
"time"
2829

2930
modelspec "github.com/CloudNativeAI/model-spec/specs-go/v1"
@@ -34,6 +35,7 @@ import (
3435

3536
buildconfig "github.com/CloudNativeAI/modctl/pkg/backend/build/config"
3637
"github.com/CloudNativeAI/modctl/pkg/backend/build/hooks"
38+
"github.com/CloudNativeAI/modctl/pkg/backend/build/interceptor"
3739
"github.com/CloudNativeAI/modctl/pkg/codec"
3840
"github.com/CloudNativeAI/modctl/pkg/storage"
3941
)
@@ -96,10 +98,11 @@ func NewBuilder(outputType OutputType, store storage.Storage, repo, tag string,
9698
}
9799

98100
return &abstractBuilder{
99-
store: store,
100-
repo: repo,
101-
tag: tag,
102-
strategy: strategy,
101+
store: store,
102+
repo: repo,
103+
tag: tag,
104+
strategy: strategy,
105+
interceptor: cfg.interceptor,
103106
}, nil
104107
}
105108

@@ -110,6 +113,8 @@ type abstractBuilder struct {
110113
tag string
111114
// strategy is the output strategy used to output the blob.
112115
strategy OutputStrategy
116+
// interceptor is the interceptor used to intercept the build process.
117+
interceptor interceptor.Interceptor
113118
}
114119

115120
func (ab *abstractBuilder) BuildLayer(ctx context.Context, mediaType, workDir, path string, hooks hooks.Hooks) (ocispec.Descriptor, error) {
@@ -167,7 +172,39 @@ func (ab *abstractBuilder) BuildLayer(ctx context.Context, mediaType, workDir, p
167172
}
168173
}
169174

170-
return ab.strategy.OutputLayer(ctx, mediaType, relPath, digest, size, reader, hooks)
175+
var (
176+
wg sync.WaitGroup
177+
itErr error
178+
applyDesc interceptor.ApplyDescriptorFn
179+
)
180+
// Intercept the reader if needed.
181+
if ab.interceptor != nil {
182+
var itReader io.Reader
183+
reader, itReader = splitReader(reader)
184+
185+
wg.Add(1)
186+
go func() {
187+
defer wg.Done()
188+
applyDesc, itErr = ab.interceptor.Intercept(ctx, mediaType, relPath, codec.Type(), itReader)
189+
}()
190+
}
191+
192+
desc, err := ab.strategy.OutputLayer(ctx, mediaType, relPath, digest, size, reader, hooks)
193+
if err != nil {
194+
return desc, err
195+
}
196+
197+
// Wait for the interceptor to finish.
198+
wg.Wait()
199+
if itErr != nil {
200+
return desc, itErr
201+
}
202+
203+
if applyDesc != nil {
204+
applyDesc(&desc)
205+
}
206+
207+
return desc, nil
171208
}
172209

173210
func (ab *abstractBuilder) BuildConfig(ctx context.Context, layers []ocispec.Descriptor, modelConfig *buildconfig.Model, hooks hooks.Hooks) (ocispec.Descriptor, error) {
@@ -247,3 +284,23 @@ func buildModelConfig(modelConfig *buildconfig.Model, layers []ocispec.Descripto
247284
ModelFS: fs,
248285
}, nil
249286
}
287+
288+
// splitReader splits the original reader into two readers.
289+
func splitReader(original io.Reader) (io.Reader, io.Reader) {
290+
r1, w1 := io.Pipe()
291+
r2, w2 := io.Pipe()
292+
multiWriter := io.MultiWriter(w1, w2)
293+
294+
go func() {
295+
defer w1.Close()
296+
defer w2.Close()
297+
298+
_, err := io.Copy(multiWriter, original)
299+
if err != nil {
300+
w1.CloseWithError(err)
301+
w2.CloseWithError(err)
302+
}
303+
}()
304+
305+
return r1, r2
306+
}

pkg/backend/build/config.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616

1717
package build
1818

19+
import (
20+
"github.com/CloudNativeAI/modctl/pkg/backend/build/interceptor"
21+
)
22+
1923
type Option func(*config)
2024

2125
// config is the configuration for the building.
2226
type config struct {
23-
plainHTTP bool
24-
insecure bool
27+
plainHTTP bool
28+
insecure bool
29+
interceptor interceptor.Interceptor
2530
}
2631

2732
func WithPlainHTTP(plainHTTP bool) Option {
@@ -35,3 +40,9 @@ func WithInsecure(insecure bool) Option {
3540
c.insecure = insecure
3641
}
3742
}
43+
44+
func WithInterceptor(interceptor interceptor.Interceptor) Option {
45+
return func(c *config) {
46+
c.interceptor = interceptor
47+
}
48+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 The CNAI Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package interceptor
18+
19+
import (
20+
"context"
21+
"io"
22+
23+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24+
)
25+
26+
// ApplyDescriptorFn is a function that applies changes to the descriptor.
27+
type ApplyDescriptorFn func(desc *ocispec.Descriptor)
28+
29+
// Interceptor is an interface that defines the interceptor for the building stream.
30+
type Interceptor interface {
31+
// Intercept intercepts the building stream for some customized logic, readerType is the original stream type, such as raw or tar.
32+
Intercept(ctx context.Context, mediaType string, filepath string, readerType string, reader io.Reader) (ApplyDescriptorFn, error)
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 The CNAI Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package interceptor
18+
19+
import (
20+
"context"
21+
"io"
22+
)
23+
24+
type nydus struct{}
25+
26+
func NewNydus() *nydus {
27+
return &nydus{}
28+
}
29+
30+
func (n *nydus) Intercept(ctx context.Context, mediaType string, filepath string, readerType string, reader io.Reader) (ApplyDescriptorFn, error) {
31+
// TODO: Implement nydus interceptor
32+
return nil, nil
33+
}

pkg/codec/codec.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const (
3434

3535
// Codec is an interface for encoding and decoding the data.
3636
type Codec interface {
37+
// Type returns the type of the codec.
38+
Type() Type
39+
3740
// Encode encodes the target file into a reader.
3841
Encode(targetFilePath, workDirPath string) (io.Reader, error)
3942

pkg/codec/raw.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ func newRaw() *raw {
3030
return &raw{}
3131
}
3232

33+
// Type returns the type of the codec.
34+
func (r *raw) Type() string {
35+
return Raw
36+
}
37+
3338
// Encode reads the target file into a reader.
3439
func (r *raw) Encode(targetFilePath, workDirPath string) (io.Reader, error) {
3540
return os.Open(targetFilePath)

pkg/codec/tar.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ func newTar() *tar {
3030
return &tar{}
3131
}
3232

33+
// Type returns the type of the codec.
34+
func (t *tar) Type() string {
35+
return Tar
36+
}
37+
3338
// Encode tars the target file into a reader.
3439
func (t *tar) Encode(targetFilePath, workDirPath string) (io.Reader, error) {
3540
return archiver.Tar(targetFilePath, workDirPath)

0 commit comments

Comments
 (0)