Skip to content

Commit fe6a659

Browse files
committed
test: add integration tests for pushing models to custom registries
1 parent d8c8f68 commit fe6a659

File tree

2 files changed

+153
-5
lines changed

2 files changed

+153
-5
lines changed

cmd/cli/commands/integration_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type testEnv struct {
2929
ctx context.Context
3030
registryURL string
3131
client *desktop.Client
32+
net *testcontainers.DockerNetwork
3233
}
3334

3435
// modelInfo contains all the information needed to generate model references
@@ -128,6 +129,7 @@ func setupTestEnv(t *testing.T) *testEnv {
128129
ctx: ctx,
129130
registryURL: registryURL,
130131
client: client,
132+
net: net,
131133
}
132134
}
133135

@@ -627,3 +629,150 @@ func TestIntegration_TagModel(t *testing.T) {
627629
require.NoError(t, err)
628630
require.Empty(t, strings.TrimSpace(models), "Model should be removed")
629631
}
632+
633+
// TestIntegration_PushModel tests pushing a model to registries with various reference formats
634+
// to ensure proper reference normalization and successful push operations.
635+
func TestIntegration_PushModel(t *testing.T) {
636+
env := setupTestEnv(t)
637+
638+
// Set up custom registry with different alias
639+
t.Log("Starting custom OCI registry container...")
640+
customRegistryCtr, err := tc.Run(context.Background(), "registry:3",
641+
network.WithNetwork([]string{"custom-registry.local"}, env.net),
642+
)
643+
require.NoError(t, err)
644+
testcontainers.CleanupContainer(t, customRegistryCtr)
645+
646+
customRegistryEndpoint, err := customRegistryCtr.Endpoint(t.Context(), "")
647+
require.NoError(t, err)
648+
customRegistryURL := fmt.Sprintf("http://%s", customRegistryEndpoint)
649+
t.Logf("Custom registry available at: %s", customRegistryURL)
650+
651+
// Ensure no models exist initially
652+
models, err := listModels(false, env.client, true, false, "")
653+
require.NoError(t, err)
654+
if len(models) != 0 {
655+
t.Fatal("Expected no initial models, but found some")
656+
}
657+
658+
// Create and push a test model with default org (ai/tag-test:latest)
659+
modelRef := "ai/tag-test:latest"
660+
modelID, hostFQDN, networkFQDN, digest := createAndPushTestModel(t, env.registryURL, modelRef, 2048)
661+
t.Logf("Test model pushed: %s (ID: %s) FQDN: %s Digest: %s", hostFQDN, modelID, networkFQDN, digest)
662+
663+
// Pull the model using a simple reference
664+
pullRef := "tag-test"
665+
t.Logf("Pulling model with reference: %s", pullRef)
666+
err = pullModel(newPullCmd(), env.client, pullRef, true)
667+
require.NoError(t, err, "Failed to pull model")
668+
669+
// Verify the model was pulled
670+
models, err = listModels(false, env.client, true, false, "")
671+
require.NoError(t, err)
672+
truncatedID := modelID[7:19]
673+
require.Equal(t, truncatedID, strings.TrimSpace(models), "Model not found after pull")
674+
675+
// Test 1: Push to default registry with various reference formats
676+
t.Run("push to default registry", func(t *testing.T) {
677+
// Generate test cases for pushing to default registry
678+
pushInfo := modelInfo{
679+
name: "push-test",
680+
org: "ai",
681+
tag: "v1",
682+
registry: "registry.local:5000",
683+
modelID: modelID,
684+
digest: digest,
685+
expectedName: "ai/push-test:v1",
686+
}
687+
testCases := generateReferenceTestCases(pushInfo)
688+
689+
for _, tc := range testCases {
690+
// Skip ID-based or digest-based references for push tests
691+
if strings.Contains(tc.name, "model ID") || strings.Contains(tc.name, "digest") {
692+
continue
693+
}
694+
695+
t.Run(tc.name, func(t *testing.T) {
696+
// First tag the model with the custom registry reference
697+
t.Logf("Tagging %s as %s", "tag-test", tc.ref)
698+
err := tagModel(newTagCmd(), env.client, "tag-test", tc.ref)
699+
require.NoError(t, err, "Failed to tag model for custom registry")
700+
701+
// Push the tagged model
702+
t.Logf("Pushing model to custom registry with reference: %s", tc.ref)
703+
_, _, err = env.client.Push(tc.ref, func(msg string) {
704+
t.Logf("Progress: %s", msg)
705+
})
706+
require.NoError(t, err, "Failed to push model to custom registry")
707+
t.Logf("✓ Successfully pushed model to custom registry: %s", tc.ref)
708+
})
709+
}
710+
})
711+
712+
// Test 2: Push to custom registry with explicit registry in reference
713+
t.Run("push to custom registry", func(t *testing.T) {
714+
customTestCases := []struct {
715+
name string
716+
sourceRef string
717+
targetRef string
718+
}{
719+
{
720+
name: "push with custom registry and org",
721+
sourceRef: "tag-test",
722+
targetRef: "custom-registry.local:5000/ai/push-test:custom",
723+
},
724+
{
725+
name: "push with custom registry and different org",
726+
sourceRef: "tag-test",
727+
targetRef: "custom-registry.local:5000/test/push-test:v2",
728+
},
729+
{
730+
name: "push with custom registry FQDN",
731+
sourceRef: "tag-test",
732+
targetRef: "custom-registry.local:5000/custom/push-test:latest",
733+
},
734+
}
735+
736+
for _, tc := range customTestCases {
737+
t.Run(tc.name, func(t *testing.T) {
738+
// First tag the model with the custom registry reference
739+
t.Logf("Tagging %s as %s", tc.sourceRef, tc.targetRef)
740+
err := tagModel(newTagCmd(), env.client, tc.sourceRef, tc.targetRef)
741+
require.NoError(t, err, "Failed to tag model for custom registry")
742+
743+
// Push the tagged model
744+
t.Logf("Pushing model to custom registry with reference: %s", tc.targetRef)
745+
_, _, err = env.client.Push(tc.targetRef, func(msg string) {
746+
t.Logf("Progress: %s", msg)
747+
})
748+
require.NoError(t, err, "Failed to push model to custom registry")
749+
t.Logf("✓ Successfully pushed model to custom registry: %s", tc.targetRef)
750+
})
751+
}
752+
})
753+
754+
// Test 3: Error cases
755+
t.Run("error cases", func(t *testing.T) {
756+
t.Run("push non-existent model", func(t *testing.T) {
757+
_, _, err := env.client.Push("non-existent-model:v1", func(msg string) {})
758+
require.Error(t, err, "Should fail when pushing non-existent model")
759+
t.Logf("✓ Correctly failed to push non-existent model: %v", err)
760+
})
761+
762+
t.Run("push with invalid reference", func(t *testing.T) {
763+
_, _, err := env.client.Push("", func(msg string) {})
764+
require.Error(t, err, "Should fail with empty reference")
765+
t.Logf("✓ Correctly failed to push with invalid reference: %v", err)
766+
})
767+
})
768+
769+
// Final cleanup: remove the model
770+
t.Logf("Removing model %s", truncatedID)
771+
err = removeModel(env.client, modelID)
772+
require.NoError(t, err, "Failed to remove model")
773+
774+
// Verify model was removed
775+
models, err = listModels(false, env.client, true, false, "")
776+
require.NoError(t, err)
777+
require.Empty(t, strings.TrimSpace(models), "Model should be removed")
778+
}

pkg/inference/models/manager.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -596,14 +596,13 @@ func (m *Manager) handleModelAction(w http.ResponseWriter, r *http.Request) {
596596
model, action := path.Split(r.PathValue("nameAndAction"))
597597
model = strings.TrimRight(model, "/")
598598

599-
// For tag and push actions, we likely expect model references rather than IDs,
600-
// so normalize the model name, but we'll handle both cases in the handlers
601-
normalizedModel := NormalizeModelName(model)
602599
switch action {
603600
case "tag":
604-
m.handleTagModel(w, r, normalizedModel)
601+
// For tag actions, we likely expect model references rather than IDs,
602+
// so normalize the model name, but we'll handle both cases in the handlers
603+
m.handleTagModel(w, r, NormalizeModelName(model))
605604
case "push":
606-
m.handlePushModel(w, r, normalizedModel)
605+
m.handlePushModel(w, r, model)
607606
default:
608607
http.Error(w, fmt.Sprintf("unknown action %q", action), http.StatusNotFound)
609608
}

0 commit comments

Comments
 (0)