Skip to content

Commit 6fc3257

Browse files
author
Levi080513
committed
feat: support empty repo for image registry
1 parent 0c5efd0 commit 6fc3257

17 files changed

+215
-62
lines changed

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,13 @@ docker-test-core: ## Redeploy local neutree-core for testing
266266
docker cp bin/neutree-core neutree-core:/neutree-core
267267
docker restart neutree-core
268268

269+
.PHONY: docker-test-db-scripts
270+
docker-test-db-scripts: ## Overwrite db scripts for testing, and restart related services
271+
docker cp db copy-db-scripts:/
272+
docker restart copy-db-scripts
273+
docker restart migration
274+
docker restart post-migration-hook
275+
269276
VENDIR := $(TOOLS_BIN_DIR)/vendir
270277

271278
vendir: $(VENDIR) # Download vendir if not yet.

controllers/image_registry_controller.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package controllers
22

33
import (
44
"fmt"
5-
"net/url"
65
"strconv"
76

87
"github.com/google/go-containerregistry/pkg/authn"
@@ -11,6 +10,7 @@ import (
1110

1211
v1 "github.com/neutree-ai/neutree/api/v1"
1312
"github.com/neutree-ai/neutree/internal/registry"
13+
"github.com/neutree-ai/neutree/internal/util"
1414
"github.com/neutree-ai/neutree/pkg/storage"
1515
)
1616

@@ -113,20 +113,37 @@ func (c *ImageRegistryController) connectImageRegistry(imageRegistry *v1.ImageRe
113113
Password: imageRegistry.Spec.AuthConfig.Password,
114114
Auth: imageRegistry.Spec.AuthConfig.Auth,
115115
IdentityToken: imageRegistry.Spec.AuthConfig.IdentityToken,
116-
RegistryToken: imageRegistry.Spec.AuthConfig.IdentityToken,
116+
RegistryToken: imageRegistry.Spec.AuthConfig.RegistryToken,
117117
}
118118

119-
registryURL, err := url.Parse(imageRegistry.Spec.URL)
119+
imagePrefix, err := util.GetImagePrefix(imageRegistry)
120120
if err != nil {
121-
return errors.Wrap(err, "failed to parse image registry url "+imageRegistry.Spec.URL)
121+
return errors.Wrapf(err, "failed to get image prefix for image registry %s",
122+
imageRegistry.Metadata.WorkspaceName())
122123
}
123124

124-
imageRepo := fmt.Sprintf("%s/%s/neutree-serve", registryURL.Host, imageRegistry.Spec.Repository)
125+
// For docker.io, we cannot check pull permissions by fetching a non-existent image because Docker Hub supports image-level permission control.
126+
// Instead, we use a known public image under the neutree namespace.
127+
testImage := fmt.Sprintf("%s/neutree/neutree-serve", imagePrefix)
128+
129+
// If no credentials or tokens are provided, use anonymous auth which avoids providing empty
130+
// Authorization headers that could lead some registries to reject a request as "unauthorized".
131+
var authenticator authn.Authenticator
132+
if authConfig.Username == "" && authConfig.Password == "" && authConfig.IdentityToken == "" && authConfig.RegistryToken == "" {
133+
authenticator = authn.Anonymous
134+
} else {
135+
authenticator = authn.FromConfig(authConfig)
136+
}
125137

126-
_, err = c.imageService.ListImageTags(imageRepo, authn.FromConfig(authConfig))
138+
hasPermission, err := c.imageService.CheckPullPermission(testImage, authenticator)
127139
if err != nil {
128-
return errors.Wrapf(err, "failed to authenticate with image registry %s/%s at URL %s, repository %s",
129-
imageRegistry.Metadata.Workspace, imageRegistry.Metadata.Name, imageRegistry.Spec.URL, imageRepo)
140+
return errors.Wrapf(err, "failed to connect %s at URL %s",
141+
imageRegistry.Metadata.WorkspaceName(), imageRegistry.Spec.URL)
142+
}
143+
144+
if !hasPermission {
145+
return errors.Errorf("no pull permission for image registry %s at URL %s",
146+
imageRegistry.Metadata.WorkspaceName(), imageRegistry.Spec.URL)
130147
}
131148

132149
return nil

controllers/image_registry_controller_test.go

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func TestImageRegistryController_Sync_PendingOrNoStatus(t *testing.T) {
9090
Username: "test",
9191
Password: "test",
9292
},
93-
Repository: "neutree",
93+
Repository: "",
9494
URL: "http://test",
9595
},
9696
}
@@ -109,7 +109,7 @@ func TestImageRegistryController_Sync_PendingOrNoStatus(t *testing.T) {
109109
Username: "test",
110110
Password: "test",
111111
},
112-
Repository: "neutree",
112+
Repository: "",
113113
URL: "http://test",
114114
},
115115
}
@@ -122,17 +122,17 @@ func TestImageRegistryController_Sync_PendingOrNoStatus(t *testing.T) {
122122
wantErr bool
123123
}{
124124
{
125-
name: "Pending/NoStatus -> Connected (image service list tags success)",
125+
name: "Pending/NoStatus -> Connected (check pull permission success)",
126126
input: testImageRegistry(),
127127
mockSetup: func(input *v1.ImageRegistry, s *storagemocks.MockStorage, imageSvc *registrymocks.MockImageService) {
128-
imageSvc.On("ListImageTags", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
128+
imageSvc.On("CheckPullPermission", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
129129
image := args.Get(0).(string)
130130
assert.Equal(t, "test/neutree/neutree-serve", image)
131131
arg := args.Get(1).(authn.Authenticator)
132132
authConfig, _ := arg.Authorization()
133133
assert.Equal(t, input.Spec.AuthConfig.Username, authConfig.Username)
134134
assert.Equal(t, input.Spec.AuthConfig.Password, authConfig.Password)
135-
}).Return(nil, nil)
135+
}).Return(true, nil)
136136
s.On("UpdateImageRegistry", "1", mock.Anything).Run(func(args mock.Arguments) {
137137
arg := args.Get(1).(*v1.ImageRegistry)
138138
assert.Equal(t, v1.ImageRegistryPhaseCONNECTED, arg.Status.Phase)
@@ -141,17 +141,17 @@ func TestImageRegistryController_Sync_PendingOrNoStatus(t *testing.T) {
141141
wantErr: false,
142142
},
143143
{
144-
name: "Pending/NoStatus -> Failed (image service list tags failed)",
144+
name: "Pending/NoStatus -> Failed (check pull permission failed)",
145145
input: testImageRegistry(),
146146
mockSetup: func(input *v1.ImageRegistry, s *storagemocks.MockStorage, imageSvc *registrymocks.MockImageService) {
147-
imageSvc.On("ListImageTags", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
147+
imageSvc.On("CheckPullPermission", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
148148
image := args.Get(0).(string)
149149
assert.Equal(t, "test/neutree/neutree-serve", image)
150150
arg := args.Get(1).(authn.Authenticator)
151151
authConfig, _ := arg.Authorization()
152152
assert.Equal(t, input.Spec.AuthConfig.Username, authConfig.Username)
153153
assert.Equal(t, input.Spec.AuthConfig.Password, authConfig.Password)
154-
}).Return(nil, assert.AnError)
154+
}).Return(false, assert.AnError)
155155
s.On("UpdateImageRegistry", "1", mock.Anything).Run(func(args mock.Arguments) {
156156
arg := args.Get(1).(*v1.ImageRegistry)
157157
assert.Equal(t, v1.ImageRegistryPhaseFAILED, arg.Status.Phase)
@@ -204,7 +204,7 @@ func TestImageRegistryController_Sync_Conneted(t *testing.T) {
204204
Password: "test",
205205
},
206206
URL: "http://test",
207-
Repository: "neutree",
207+
Repository: "",
208208
},
209209
Status: &v1.ImageRegistryStatus{Phase: v1.ImageRegistryPhaseCONNECTED},
210210
}
@@ -223,7 +223,7 @@ func TestImageRegistryController_Sync_Conneted(t *testing.T) {
223223
Password: "test",
224224
},
225225
URL: "http://test",
226-
Repository: "neutree",
226+
Repository: "",
227227
},
228228
Status: &v1.ImageRegistryStatus{Phase: v1.ImageRegistryPhaseCONNECTED},
229229
}
@@ -236,32 +236,32 @@ func TestImageRegistryController_Sync_Conneted(t *testing.T) {
236236
wantErr bool
237237
}{
238238
{
239-
name: "Connected -> Connected (image service list tags success)",
239+
name: "Connected -> Connected (check pull permission success)",
240240
input: testImageRegistry(),
241241
mockSetup: func(input *v1.ImageRegistry, s *storagemocks.MockStorage, imageSvc *registrymocks.MockImageService) {
242-
imageSvc.On("ListImageTags", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
242+
imageSvc.On("CheckPullPermission", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
243243
image := args.Get(0).(string)
244244
assert.Equal(t, "test/neutree/neutree-serve", image)
245245
arg := args.Get(1).(authn.Authenticator)
246246
authConfig, _ := arg.Authorization()
247247
assert.Equal(t, input.Spec.AuthConfig.Username, authConfig.Username)
248248
assert.Equal(t, input.Spec.AuthConfig.Password, authConfig.Password)
249-
}).Return(nil, nil)
249+
}).Return(true, nil)
250250
},
251251
wantErr: false,
252252
},
253253
{
254-
name: "Connected -> Failed (image service list tags failed)",
254+
name: "Connected -> Failed (check pull permission failed)",
255255
input: testImageRegistry(),
256256
mockSetup: func(input *v1.ImageRegistry, s *storagemocks.MockStorage, imageSvc *registrymocks.MockImageService) {
257-
imageSvc.On("ListImageTags", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
257+
imageSvc.On("CheckPullPermission", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
258258
image := args.Get(0).(string)
259259
assert.Equal(t, "test/neutree/neutree-serve", image)
260260
arg := args.Get(1).(authn.Authenticator)
261261
authConfig, _ := arg.Authorization()
262262
assert.Equal(t, input.Spec.AuthConfig.Username, authConfig.Username)
263263
assert.Equal(t, input.Spec.AuthConfig.Password, authConfig.Password)
264-
}).Return(nil, assert.AnError)
264+
}).Return(false, assert.AnError)
265265
s.On("UpdateImageRegistry", "1", mock.Anything).Run(func(args mock.Arguments) {
266266
arg := args.Get(1).(*v1.ImageRegistry)
267267
assert.Equal(t, v1.ImageRegistryPhaseFAILED, arg.Status.Phase)
@@ -315,7 +315,7 @@ func TestImageRegistryController_Sync_Failed(t *testing.T) {
315315
Username: "test",
316316
Password: "test",
317317
},
318-
Repository: "neutree",
318+
Repository: "",
319319
URL: "http://test",
320320
},
321321
Status: &v1.ImageRegistryStatus{Phase: v1.ImageRegistryPhaseFAILED},
@@ -334,7 +334,7 @@ func TestImageRegistryController_Sync_Failed(t *testing.T) {
334334
Username: "test",
335335
Password: "test",
336336
},
337-
Repository: "neutree",
337+
Repository: "",
338338
URL: "http://test",
339339
},
340340
Status: &v1.ImageRegistryStatus{Phase: v1.ImageRegistryPhaseFAILED},
@@ -348,17 +348,17 @@ func TestImageRegistryController_Sync_Failed(t *testing.T) {
348348
wantErr bool
349349
}{
350350
{
351-
name: "Failed -> Connected (image service list tags success)",
351+
name: "Failed -> Connected (check pull permission success)",
352352
input: testImageRegistry(),
353353
mockSetup: func(input *v1.ImageRegistry, s *storagemocks.MockStorage, imageSvc *registrymocks.MockImageService) {
354-
imageSvc.On("ListImageTags", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
354+
imageSvc.On("CheckPullPermission", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
355355
image := args.Get(0).(string)
356356
assert.Equal(t, "test/neutree/neutree-serve", image)
357357
arg := args.Get(1).(authn.Authenticator)
358358
authConfig, _ := arg.Authorization()
359359
assert.Equal(t, input.Spec.AuthConfig.Username, authConfig.Username)
360360
assert.Equal(t, input.Spec.AuthConfig.Password, authConfig.Password)
361-
}).Return(nil, nil)
361+
}).Return(true, nil)
362362
s.On("UpdateImageRegistry", "1", mock.Anything).Run(func(args mock.Arguments) {
363363
arg := args.Get(1).(*v1.ImageRegistry)
364364
assert.Equal(t, v1.ImageRegistryPhaseCONNECTED, arg.Status.Phase)
@@ -367,17 +367,17 @@ func TestImageRegistryController_Sync_Failed(t *testing.T) {
367367
wantErr: false,
368368
},
369369
{
370-
name: "Failed -> Failed (image service list tags failed)",
370+
name: "Failed -> Failed (check pull permission failed)",
371371
input: testImageRegistry(),
372372
mockSetup: func(input *v1.ImageRegistry, s *storagemocks.MockStorage, imageSvc *registrymocks.MockImageService) {
373-
imageSvc.On("ListImageTags", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
373+
imageSvc.On("CheckPullPermission", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
374374
image := args.Get(0).(string)
375375
assert.Equal(t, "test/neutree/neutree-serve", image)
376376
arg := args.Get(1).(authn.Authenticator)
377377
authConfig, _ := arg.Authorization()
378378
assert.Equal(t, input.Spec.AuthConfig.Username, authConfig.Username)
379379
assert.Equal(t, input.Spec.AuthConfig.Password, authConfig.Password)
380-
}).Return(nil, assert.AnError)
380+
}).Return(false, assert.AnError)
381381
// Defer block updates status to FAILED when connection fails
382382
s.On("UpdateImageRegistry", "1", mock.Anything).Run(func(args mock.Arguments) {
383383
arg := args.Get(1).(*v1.ImageRegistry)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
------------------------------------
2+
-- Image Registry Repo Validation
3+
------------------------------------
4+
CREATE OR REPLACE FUNCTION api.validate_image_registry_repo()
5+
RETURNS TRIGGER AS $$
6+
BEGIN
7+
IF (NEW.spec).repository IS NULL OR trim((NEW.spec).repository) = '' THEN
8+
RAISE sqlstate 'PGRST'
9+
USING message = '{"code": "10013","message": "spec.repository is required","hint": "Provide Image Registry Repo"}',
10+
detail = '{"status": 400, "headers": {"X-Powered-By": "Neutree"}}';
11+
END IF;
12+
13+
RETURN NEW;
14+
END;
15+
$$ LANGUAGE plpgsql;
16+
17+
DROP TRIGGER IF EXISTS validate_image_registry_repo_on_image_registry ON api.image_registries;
18+
CREATE TRIGGER validate_image_registry_repo_on_image_registry
19+
BEFORE INSERT OR UPDATE ON api.image_registries
20+
FOR EACH ROW
21+
EXECUTE FUNCTION api.validate_image_registry_repo();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TRIGGER IF EXISTS validate_image_registry_repo_on_image_registry on api.image_registries;
2+
DROP FUNCTION IF EXISTS api.validate_image_registry_repo();

internal/accelerator/plugin/amd_gpu.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func (p *AMDGPUAcceleratorPlugin) GetSupportEngines(ctx context.Context) (*v1.Ge
165165
ValuesSchema: llamaCppDefaultEngineSchema,
166166
Images: map[string]*v1.EngineImage{
167167
"cpu": {
168-
ImageName: "llama-cpp",
168+
ImageName: "neutree/llama-cpp-python", // no official llama-cpp-python image, so use neutree image
169169
Tag: "v0.3.6",
170170
},
171171
},
@@ -198,8 +198,8 @@ func (p *AMDGPUAcceleratorPlugin) GetSupportEngines(ctx context.Context) (*v1.Ge
198198
ValuesSchema: vllmDefaultEngineSchema,
199199
Images: map[string]*v1.EngineImage{
200200
"amd_gpu": {
201-
ImageName: "vllm",
202-
Tag: "v0.8.5-rocm",
201+
ImageName: "rocm/vllm", // use official vllm image with amd gpu support
202+
Tag: "rocm6.3.1_vllm_0.8.5_20250521",
203203
},
204204
},
205205
DeployTemplate: map[string]map[string]string{

internal/accelerator/plugin/gpu.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func (p *GPUAcceleratorPlugin) GetSupportEngines(ctx context.Context) (*v1.GetSu
145145
ValuesSchema: llamaCppDefaultEngineSchema,
146146
Images: map[string]*v1.EngineImage{
147147
"cpu": {
148-
ImageName: "llama-cpp",
148+
ImageName: "neutree/llama-cpp-python", // no official llama-cpp-python image, so use neutree image
149149
Tag: "v0.3.6",
150150
},
151151
},
@@ -178,7 +178,7 @@ func (p *GPUAcceleratorPlugin) GetSupportEngines(ctx context.Context) (*v1.GetSu
178178
ValuesSchema: vllmDefaultEngineSchema,
179179
Images: map[string]*v1.EngineImage{
180180
"nvidia_gpu": {
181-
ImageName: "vllm",
181+
ImageName: "vllm/vllm-openai", // use official vllm image with nvidia gpu support
182182
Tag: "v0.8.5",
183183
},
184184
},

internal/cluster/component/metrics/manifests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ spec:
192192
serviceAccountName: vmagent-service-account
193193
containers:
194194
- name: vmagent
195-
image: {{ .ImagePrefix }}/vmagent:{{ .Version }}
195+
image: {{ .ImagePrefix }}/victoriametrics/vmagent:{{ .Version }}
196196
args:
197197
- --promscrape.config=/etc/prometheus/prometheus.yml
198198
- --promscrape.configCheckInterval=10s

internal/cluster/component/metrics/metrics_resource_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func TestBuildVMAgentDeployment(t *testing.T) {
4040
assert.Equal(t, "vmagent", deployment.GetName(), "Deployment name mismatch")
4141
assert.Equal(t, "vmagent", deployment.GetLabels()["app"], "Deployment app label mismatch")
4242
assert.Equal(t, int32(1), *deployment.Spec.Replicas, "Deployment replicas mismatch")
43-
assert.Equal(t, "test-image-prefix/vmagent:v1.115.0", deployment.Spec.Template.Spec.Containers[0].Image, "Deployment image mismatch")
43+
assert.Equal(t, "test-image-prefix/victoriametrics/vmagent:v1.115.0", deployment.Spec.Template.Spec.Containers[0].Image, "Deployment image mismatch")
4444
assert.Equal(t, "test-image-pull-secret", deployment.Spec.Template.Spec.ImagePullSecrets[0].Name, "Deployment image pull secret mismatch")
4545
return
4646
}

internal/cluster/component/router/manifests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ spec:
8181
serviceAccountName: router-service-account
8282
containers:
8383
- name: router
84-
image: {{ .ImagePrefix }}/router:{{ .Version }}
84+
image: {{ .ImagePrefix }}/neutree/router:{{ .Version }}
8585
env:
8686
- name: LMCACHE_LOG_LEVEL
8787
value: DEBUG

0 commit comments

Comments
 (0)