Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions pkg/cmd/pack/pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,27 @@ package pack

import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/kitops-ml/kitops/pkg/artifact"
cmdoptions "github.com/kitops-ml/kitops/pkg/cmd/options"
"github.com/kitops-ml/kitops/pkg/lib/constants"
"github.com/kitops-ml/kitops/pkg/lib/constants/mediatype"
"github.com/kitops-ml/kitops/pkg/lib/filesystem"
"github.com/kitops-ml/kitops/pkg/lib/filesystem/ignore"
kfutils "github.com/kitops-ml/kitops/pkg/lib/kitfile"
"github.com/kitops-ml/kitops/pkg/lib/repo/local"
"github.com/kitops-ml/kitops/pkg/lib/repo/remote"
"github.com/kitops-ml/kitops/pkg/lib/repo/util"
"github.com/kitops-ml/kitops/pkg/output"

"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
)

// runPack compresses and stores a modelkit based on a Kitfile. Returns an error if packing
Expand Down Expand Up @@ -79,13 +85,21 @@ func runPack(ctx context.Context, options *packOptions) error {

func pack(ctx context.Context, opts *packOptions, kitfile *artifact.KitFile, localRepo local.LocalRepo) (*ocispec.Descriptor, error) {
var extraLayerPaths []string
var subject *ocispec.Descriptor
if kitfile.Model != nil && artifact.IsModelKitReference(kitfile.Model.Path) {
baseRef := artifact.FormatRepositoryForDisplay(opts.modelRef.String())
parentKitfile, err := kfutils.ResolveKitfile(ctx, opts.configHome, kitfile.Model.Path, baseRef)
if err != nil {
return nil, fmt.Errorf("Failed to resolve referenced modelkit %s: %w", kitfile.Model.Path, err)
}
extraLayerPaths = util.LayerPathsFromKitfile(parentKitfile)

baseDesc, err := resolveBaseManifestDescriptor(ctx, opts.configHome, kitfile.Model.Path)
if err != nil {
output.Logf(output.LogLevelWarn, "failed to resolve base manifest descriptor for OCI subject: %v", err)
} else if baseDesc != nil {
subject = baseDesc
}
}

ignore, err := ignore.NewFromContext(opts.contextDir, kitfile, extraLayerPaths...)
Expand All @@ -105,13 +119,61 @@ func pack(ctx context.Context, opts *packOptions, kitfile *artifact.KitFile, loc
ModelFormat: modelFormat,
Compression: compression,
LayerFormat: mediatype.TarFormat,
Subject: subject,
})
if err != nil {
return nil, err
}
return manifestDesc, nil
}

func resolveBaseManifestDescriptor(ctx context.Context, configHome, baseRef string) (*ocispec.Descriptor, error) {
if strings.HasPrefix(baseRef, "sha256:") {
parsedDigest, err := digest.Parse(baseRef)
if err != nil {
return nil, fmt.Errorf("failed to parse digest-only base reference: %w", err)
}
return &ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest, Digest: parsedDigest}, nil
Comment on lines +132 to +136
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the digest-only baseRef case ("sha256:..."), this returns an OCI descriptor without a valid Size (defaults to 0). OCI descriptors require an accurate size, and emitting a subject with size 0 can break interoperability/validation. Consider skipping subject linkage for digest-only references (return nil) or resolving the full descriptor (digest+size) from an actual repository/tag/@digest reference instead of constructing it manually.

Suggested change
parsedDigest, err := digest.Parse(baseRef)
if err != nil {
return nil, fmt.Errorf("failed to parse digest-only base reference: %w", err)
}
return &ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest, Digest: parsedDigest}, nil
_, err := digest.Parse(baseRef)
if err != nil {
return nil, fmt.Errorf("failed to parse digest-only base reference: %w", err)
}
// For digest-only references, we cannot reliably construct a complete OCI descriptor
// (including accurate Size), so we skip subject linkage to avoid emitting an invalid descriptor.
output.Logf(output.LogLevelWarn,
"base reference %q is a digest-only reference; skipping subject linkage", baseRef)
return nil, nil

Copilot uses AI. Check for mistakes.
}

ref, _, err := artifact.ParseReference(baseRef)
if err != nil {
return nil, fmt.Errorf("failed to parse base reference: %w", err)
}

localRepo, err := local.NewLocalRepo(constants.StoragePath(configHome), ref)
if err != nil {
return nil, fmt.Errorf("failed to create local repository: %w", err)
}

desc, _, _, err := util.ResolveManifestAndConfig(ctx, localRepo, ref.Reference)
if err == nil {
return &desc, nil
}
Comment on lines +149 to +152
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveBaseManifestDescriptor uses util.ResolveManifestAndConfig, which fetches both the manifest and Kitfile config blob. Since pack() already called ResolveKitfile (which fetches the base Kitfile) just above, this adds redundant local/remote reads and network round-trips. Consider resolving only the manifest descriptor (e.g., util.ResolveManifest / store.Resolve) to get digest+size for the OCI subject, and rely on the prior ResolveKitfile result for modelkit validation.

Copilot uses AI. Check for mistakes.
if errors.Is(err, util.ErrNoKitfile) || errors.Is(err, util.ErrNotAModelKit) {
return nil, nil
}
if !errors.Is(err, errdef.ErrNotFound) && !errors.Is(err, errdef.ErrMissingReference) {
return nil, fmt.Errorf("failed to resolve base descriptor locally: %w", err)
}

repository, err := remote.NewRepository(ctx, ref.Registry, ref.Repository, cmdoptions.DefaultNetworkOptions(configHome))
if err != nil {
return nil, fmt.Errorf("failed to create remote repository: %w", err)
}

desc, _, _, err = util.ResolveManifestAndConfig(ctx, repository, ref.Reference)
if err != nil {
if errors.Is(err, util.ErrNoKitfile) || errors.Is(err, util.ErrNotAModelKit) {
return nil, nil
}
output.Logf(output.LogLevelWarn,
"failed to resolve base descriptor, skipping subject linkage: %v", err)
return nil, nil
}
return &desc, nil
}

func readKitfile(modelFile string) (*artifact.KitFile, error) {
// 1. Read the model file
kitfile := &artifact.KitFile{}
Expand Down
4 changes: 4 additions & 0 deletions pkg/lib/filesystem/local-storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type SaveModelOptions struct {
ModelFormat mediatype.ModelFormat
Compression mediatype.CompressionType
LayerFormat mediatype.Format
Subject *ocispec.Descriptor
}

// SaveModel saves an *artifact.Model to the provided oras.Target, compressing layers. It attempts to block
Expand All @@ -63,6 +64,9 @@ func SaveModel(ctx context.Context, localRepo local.LocalRepo, kitfile *artifact
if err != nil {
return nil, fmt.Errorf("error creating manifest: %w", err)
}
if opts.Subject != nil {
manifest.Subject = opts.Subject
}

// If not storing a Kitfile, save the Kitfile to an annotation since it will be lost otherwise
if opts.ModelFormat == mediatype.ModelPackFormat {
Expand Down
Loading