diff --git a/cmd/run.go b/cmd/run.go index 04dd0bb..00b559d 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -57,7 +57,6 @@ downloads; use --no-cache to force re-download. Any flags provided via # Execute target from remote Makefile artifact, bypassing cache remake run -f ghcr.io/myorg/myrepo:latest --no-cache deploy`, - Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { app.Cfg.NoCache = noCache return app.Run(context.Background(), file, makeFlags, args) diff --git a/internal/cache/oci_cache.go b/internal/cache/oci_cache.go index 7f025ec..f1d532a 100644 --- a/internal/cache/oci_cache.go +++ b/internal/cache/oci_cache.go @@ -54,7 +54,7 @@ func (c *OCIRepository) Push(ctx context.Context, reference string, data []byte) if strings.Contains(reference, "://") && !strings.HasPrefix(reference, "oci://") { return fmt.Errorf("invalid OCI reference: %s", reference) } - raw := strings.TrimPrefix(reference, "oci://") + raw := strings.ToLower(strings.TrimPrefix(reference, "oci://")) ref, err := parseRef(raw, name.WithDefaultRegistry(c.cfg.DefaultRegistry)) if err != nil { return err @@ -115,7 +115,7 @@ func (c *OCIRepository) Pull(ctx context.Context, reference string) (string, err if strings.Contains(reference, "://") && !strings.HasPrefix(reference, "oci://") { return "", fmt.Errorf("invalid OCI reference: %s", reference) } - raw := strings.TrimPrefix(reference, "oci://") + raw := strings.ToLower(strings.TrimPrefix(reference, "oci://")) ref, err := name.ParseReference(raw, name.WithDefaultRegistry(c.cfg.DefaultRegistry)) if err != nil { diff --git a/internal/client/client_test.go b/internal/client/client_test.go index 0deecd7..88a70af 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -537,6 +537,24 @@ func TestOCIClientPushFileStoreCloseError(t *testing.T) { } } +func TestOCIClientPushAbsPathError(t *testing.T) { + // Stub absPathFunc to simulate failure + origAbs := absPathFunc + defer func() { absPathFunc = origAbs }() + absPathFunc = func(path string) (string, error) { + return "", fmt.Errorf("abs error") + } + + cfg := &config.Config{DefaultRegistry: "example.com"} + client := NewOCIClient(cfg) + + // Call Push: path value doesn't matter, stub will error first + err := client.Push(context.Background(), "oci://example.com/repo:tag", "somepath") + if err == nil || !strings.Contains(err.Error(), "failed to resolve absolute path somepath: abs error") { + t.Errorf("expected abs path error, got %v", err) + } +} + func TestOCIClientPushPackManifestError(t *testing.T) { tmpFile, err := os.CreateTemp("", "test*.txt") if err != nil { diff --git a/internal/client/oci_client.go b/internal/client/oci_client.go index 0aa4cce..4e88dd4 100644 --- a/internal/client/oci_client.go +++ b/internal/client/oci_client.go @@ -49,6 +49,7 @@ var ( packManifest = oras.PackManifest copyFunc = oras.Copy contentFetcher = content.FetchAll + absPathFunc = filepath.Abs ) // OCIClient provides an implementation of Client for OCI registries. @@ -87,19 +88,23 @@ func (c *OCIClient) Login(ctx context.Context, registry, user, pass string) erro // Push uploads the local file at path as an OCI artifact to the given reference. // It tags the artifact with the reference identifier and pushes it to the remote repository. func (c *OCIClient) Push(ctx context.Context, reference, path string) error { + // Validate and parse reference if strings.Contains(reference, "://") && !strings.HasPrefix(reference, "oci://") { return fmt.Errorf("invalid OCI reference: %s", reference) } - raw := strings.TrimPrefix(reference, "oci://") + raw := strings.ToLower(strings.TrimPrefix(reference, "oci://")) ref, err := name.ParseReference(raw, name.WithDefaultRegistry(c.cfg.DefaultRegistry)) if err != nil { return err } repoRef := ref.Context() + repo, err := newRepository(repoRef.RegistryStr() + "/" + repoRef.RepositoryStr()) if err != nil { return err } + + // Authenticate if credentials present key := config.NormalizeKey(repoRef.RegistryStr()) user := viper.GetString("registries." + key + ".username") pass := viper.GetString("registries." + key + ".password") @@ -111,19 +116,28 @@ func (c *OCIClient) Push(ctx context.Context, reference, path string) error { } } - dir := filepath.Dir(path) + // Resolve absolute path and split directory + absPath, err := absPathFunc(path) + if err != nil { + return fmt.Errorf("failed to resolve absolute path %s: %w", path, err) + } + dir := filepath.Dir(absPath) + + // Prepare a file store rooted at the file's directory fs, err := newFileStore(dir) if err != nil { - return err + return fmt.Errorf("creating file store: %w", err) } defer func() { _ = fs.Close() }() + // Add the file using its absolute path to ensure tests find it mediaType := "application/vnd.remake.file" - fileDesc, err := fs.Add(ctx, path, mediaType, "") + fileDesc, err := fs.Add(ctx, absPath, mediaType, "") if err != nil { return fmt.Errorf("adding file to store: %w", err) } + // Pack manifest using injected function artifactType := "application/vnd.remake.artifact" opts := oras.PackManifestOptions{Layers: []v1.Descriptor{fileDesc}} manifestDesc, err := packManifest(ctx, fs, oras.PackManifestVersion1_1, artifactType, opts) @@ -137,6 +151,7 @@ func (c *OCIClient) Push(ctx context.Context, reference, path string) error { tag := ref.Identifier() _ = fs.Tag(ctx, manifestDesc, tag) + // Push to remote using injected function if _, err := copyFunc(ctx, fs, tag, repo, tag, oras.DefaultCopyOptions); err != nil { return fmt.Errorf("pushing to remote: %w", err) } @@ -149,7 +164,7 @@ func (c *OCIClient) Pull(ctx context.Context, reference string) ([]byte, error) if strings.Contains(reference, "://") && !strings.HasPrefix(reference, "oci://") { return nil, fmt.Errorf("invalid OCI reference: %s", reference) } - raw := strings.TrimPrefix(reference, "oci://") + raw := strings.ToLower(strings.TrimPrefix(reference, "oci://")) ref, err := name.ParseReference(raw, name.WithDefaultRegistry(c.cfg.DefaultRegistry)) if err != nil { return nil, err