diff --git a/cmd/schedctl/run.go b/cmd/schedctl/run.go index e1ed10c..ef0ad6c 100644 --- a/cmd/schedctl/run.go +++ b/cmd/schedctl/run.go @@ -31,11 +31,18 @@ func run(cmd *cobra.Command, _ []string, attach bool) error { driver := cmd.Flags().Lookup("driver").Value.String() schedulerID := cmd.Flags().Args()[0] - image, err := schedulers.GetScheduler(schedulerID) + result, err := schedulers.GetScheduler(schedulerID) if err != nil { return err } + switch result.Source { + case schedulers.SourceManifest: + _, _ = output.Out("Running scheduler '%s' from manifest: %s\n", schedulerID, result.ImageURI) + case schedulers.SourceDirect: + _, _ = output.Out("Running container image: %s\n", result.ImageURI) + } + if driver == constants.CONTAINERD { // connect to rootful containerd client, err := containerd.NewClient() @@ -44,19 +51,19 @@ func run(cmd *cobra.Command, _ []string, attach bool) error { } defer client.Close() - err = containerd.Run(client, image, schedulerID, attach, true) + err = containerd.Run(client, result.ImageURI, schedulerID, attach, true) if err != nil { return err } } if driver == constants.PODMAN { - err := podman.Run(image, schedulerID) + err := podman.Run(result.ImageURI, schedulerID) if err != nil { panic(err) } - _, _ = output.Out("Container %s started successfully\n", image) + _, _ = output.Out("Container %s started successfully\n", result.ImageURI) } return nil diff --git a/internal/schedulers/schedulers.go b/internal/schedulers/schedulers.go index 637f839..bb7f12f 100644 --- a/internal/schedulers/schedulers.go +++ b/internal/schedulers/schedulers.go @@ -5,6 +5,7 @@ import ( "errors" "io" "net/http" + "strings" "schedctl/internal/output" ) @@ -17,6 +18,18 @@ type ManifestEntry struct { type Manifest map[string]ManifestEntry +type ImageSource string + +const ( + SourceManifest ImageSource = "manifest" + SourceDirect ImageSource = "direct" +) + +type SchedulerImage struct { + ImageURI string + Source ImageSource +} + func List() Manifest { var manifest Manifest @@ -41,19 +54,65 @@ func List() Manifest { return manifest } -func GetScheduler(id string) (string, error) { - var image string +// isContainerImage checks if a string looks like a container image URI. +// Examples: +// - ghcr.io/user/repo +// - docker.io/nginx:latest +// - quay.io/org/image:v1.0 +// - localhost:5000/myimage +// - nginx/alpine +func isContainerImage(input string) bool { + if strings.Contains(input, "/") { + parts := strings.Split(input, "/") + firstPart := parts[0] + + if strings.Contains(firstPart, ".") || strings.Contains(firstPart, ":") { + return true + } + + if len(parts) >= 2 { + return true + } + } + + return false +} + +func ensureImageTag(image string) string { + if strings.Contains(image, "@") { + return image + } + + parts := strings.Split(image, "/") + lastPart := parts[len(parts)-1] + + // Check if the last part (repo name) has a tag + if !strings.Contains(lastPart, ":") { + return image + ":latest" + } + + return image +} + +func GetScheduler(id string) (SchedulerImage, error) { + var result SchedulerImage - for key, entry := range List() { + manifest := List() + for key, entry := range manifest { if key == id { - // For the moment we just append the :latest tag to the image - image = entry.ImageURI + ":latest" + result.ImageURI = ensureImageTag(entry.ImageURI) + result.Source = SourceManifest + return result, nil } } - if len(image) == 0 { - return "", errors.New("scheduler not found") + if isContainerImage(id) { + result.ImageURI = ensureImageTag(id) + result.Source = SourceDirect + return result, nil } - return image, nil + return result, errors.New( + "scheduler not found in manifest and input does not appear to be a valid container image URI", + ) } diff --git a/internal/schedulers/schedulers_test.go b/internal/schedulers/schedulers_test.go index 2fc43eb..90a94ba 100644 --- a/internal/schedulers/schedulers_test.go +++ b/internal/schedulers/schedulers_test.go @@ -9,12 +9,55 @@ import ( ) func TestSchedulerFound(t *testing.T) { - schedulerImage, err := schedulers.GetScheduler("scx_rusty") + result, err := schedulers.GetScheduler("scx_rusty") assert.Nil(t, err) - assert.Equal(t, "ghcr.io/schedkit/scx_rusty:latest", schedulerImage) + assert.Equal(t, "ghcr.io/schedkit/scx_rusty:latest", result.ImageURI) + assert.Equal(t, schedulers.SourceManifest, result.Source) } func TestSchedulerNotFound(t *testing.T) { _, err := schedulers.GetScheduler("unknown") assert.NotNil(t, err) + assert.Contains(t, err.Error(), "scheduler not found") +} + +func TestDirectImageWithRegistry(t *testing.T) { + result, err := schedulers.GetScheduler("ghcr.io/myorg/custom-scheduler") + assert.Nil(t, err) + assert.Equal(t, "ghcr.io/myorg/custom-scheduler:latest", result.ImageURI) + assert.Equal(t, schedulers.SourceDirect, result.Source) +} + +func TestDirectImageWithTag(t *testing.T) { + result, err := schedulers.GetScheduler("docker.io/nginx:alpine") + assert.Nil(t, err) + assert.Equal(t, "docker.io/nginx:alpine", result.ImageURI) + assert.Equal(t, schedulers.SourceDirect, result.Source) +} + +func TestDirectImageWithDigest(t *testing.T) { + result, err := schedulers.GetScheduler("quay.io/org/image@sha256:abc123") + assert.Nil(t, err) + assert.Equal(t, "quay.io/org/image@sha256:abc123", result.ImageURI) + assert.Equal(t, schedulers.SourceDirect, result.Source) +} + +func TestDirectImageLocalhost(t *testing.T) { + result, err := schedulers.GetScheduler("localhost:5000/myimage") + assert.Nil(t, err) + assert.Equal(t, "localhost:5000/myimage:latest", result.ImageURI) + assert.Equal(t, schedulers.SourceDirect, result.Source) +} + +func TestDirectImageDockerHub(t *testing.T) { + result, err := schedulers.GetScheduler("nginx/alpine") + assert.Nil(t, err) + assert.Equal(t, "nginx/alpine:latest", result.ImageURI) + assert.Equal(t, schedulers.SourceDirect, result.Source) +} + +func TestInvalidInput(t *testing.T) { + _, err := schedulers.GetScheduler("just-a-name") + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "scheduler not found") }