Skip to content

Commit 5bc227a

Browse files
Migrate package command from gcr to oras
1 parent f4f3442 commit 5bc227a

File tree

4 files changed

+296
-60
lines changed

4 files changed

+296
-60
lines changed

cmd/kro/commands/package/package.go

Lines changed: 118 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,18 @@
1515
package commands
1616

1717
import (
18+
"archive/tar"
19+
"context"
1820
"fmt"
21+
"io"
1922
"os"
2023
"path/filepath"
21-
"time"
22-
23-
"github.com/google/go-containerregistry/pkg/name"
24-
v1 "github.com/google/go-containerregistry/pkg/v1"
25-
"github.com/google/go-containerregistry/pkg/v1/empty"
26-
"github.com/google/go-containerregistry/pkg/v1/mutate"
27-
"github.com/google/go-containerregistry/pkg/v1/static"
28-
"github.com/google/go-containerregistry/pkg/v1/tarball"
29-
"github.com/google/go-containerregistry/pkg/v1/types"
24+
"strings"
25+
26+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3027
"github.com/spf13/cobra"
28+
oras "oras.land/oras-go/v2"
29+
"oras.land/oras-go/v2/content/oci"
3130
"sigs.k8s.io/yaml"
3231

3332
"github.com/kro-run/kro/api/v1alpha1"
@@ -42,7 +41,7 @@ var packageConfig = &PackageConfig{}
4241

4342
func init() {
4443
packageRGDCmd.PersistentFlags().StringVarP(&packageConfig.resourceGraphDefinitionFile,
45-
"file", "f", "",
44+
"file", "f", "rgd.yaml",
4645
"Path to the ResourceGraphDefinition file",
4746
)
4847

@@ -55,7 +54,7 @@ var packageRGDCmd = &cobra.Command{
5554
Use: "package",
5655
Short: "Create an OCI Image packaging the ResourceGraphDefinition",
5756
Long: "Package command packages the ResourceGraphDefinition" +
58-
"file into an OCI image, which can be used for distribution and deployment.",
57+
"file into an OCI image bundle, which can be used for distribution and deployment.",
5958
RunE: func(cmd *cobra.Command, args []string) error {
6059
if packageConfig.resourceGraphDefinitionFile == "" {
6160
return fmt.Errorf("ResourceGraphDefinition file is required")
@@ -72,61 +71,142 @@ var packageRGDCmd = &cobra.Command{
7271
}
7372

7473
basename := filepath.Base(packageConfig.resourceGraphDefinitionFile)
75-
extension := filepath.Ext(basename)
76-
nameWithoutExt := basename[:len(basename)-len(extension)]
77-
outputFile := nameWithoutExt + ".tar"
74+
ext := filepath.Ext(basename)
75+
nameWithoutExt := basename[:len(basename)-len(ext)]
76+
outputTar := nameWithoutExt + ".tar"
7877

79-
if err = packageRGD(outputFile, data, &rgd); err != nil {
78+
if err := packageRGD(outputTar, data, &rgd); err != nil {
8079
return fmt.Errorf("failed to package ResourceGraphDefinition: %w", err)
8180
}
8281

83-
fmt.Println("Successfully packaged ResourceGraphDefinition to", outputFile)
84-
82+
fmt.Println("Successfully packaged ResourceGraphDefinition to", outputTar)
8583
return nil
8684
},
8785
}
8886

89-
func packageRGD(outputFile string, data []byte, rgd *v1alpha1.ResourceGraphDefinition) error {
90-
layer := static.NewLayer(data, types.MediaType("application/vnd.kro.resourcegraphdefinition.v1alpha1+yaml"))
91-
92-
img := empty.Image
93-
94-
img, err := mutate.AppendLayers(img, layer)
87+
func packageRGD(outputTar string, data []byte, rgd *v1alpha1.ResourceGraphDefinition) error {
88+
ctx := context.Background()
9589

90+
tempDir, err := os.MkdirTemp("", "kro-oci-*")
9691
if err != nil {
97-
return fmt.Errorf("failed to append layer: %w", err)
92+
return fmt.Errorf("failed to create temp dir: %w", err)
9893
}
94+
defer func() {
95+
if err := os.RemoveAll(tempDir); err != nil {
96+
fmt.Println("failed to remove temp dir", tempDir, ":", err)
97+
}
98+
}()
9999

100-
configFile, err := img.ConfigFile()
100+
store, err := oci.New(tempDir)
101101
if err != nil {
102-
return fmt.Errorf("failed to get config file: %w", err)
102+
return fmt.Errorf("failed to create OCI layout store: %w", err)
103103
}
104104

105-
now := time.Now()
106-
configFile.Created = v1.Time{Time: now}
105+
mediaType := "application/vnd.kro.resourcegraphdefinition"
106+
blobDesc, err := oras.PushBytes(ctx, store, mediaType, data)
107+
if err != nil {
108+
return fmt.Errorf("failed to push RGD blob: %w", err)
109+
}
107110

108-
configFile.Config.Labels = map[string]string{
109-
"kro.run/type": "resourcegraphdefinition",
110-
"kro.run/name": rgd.Name,
111+
layer := blobDesc
112+
if layer.Annotations == nil {
113+
layer.Annotations = map[string]string{}
114+
}
115+
layer.Annotations[ocispec.AnnotationTitle] = filepath.Base(packageConfig.resourceGraphDefinitionFile)
116+
117+
artifactType := "application/vnd.kro.resourcegraphdefinition"
118+
packOpts := oras.PackManifestOptions{
119+
Layers: []ocispec.Descriptor{layer},
120+
ManifestAnnotations: map[string]string{
121+
"kro.run/type": "resourcegraphdefinition",
122+
"kro.run/name": rgd.Name,
123+
},
111124
}
112125

113-
img, err = mutate.ConfigFile(img, configFile)
126+
manifestDesc, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, artifactType, packOpts)
114127
if err != nil {
115-
return fmt.Errorf("failed to update image config: %w", err)
128+
return fmt.Errorf("failed to pack manifest: %w", err)
116129
}
117130

118-
ref, err := name.ParseReference(fmt.Sprintf("kro.run/rgd/%s:%s", rgd.Name, packageConfig.tag))
119-
if err != nil {
120-
return fmt.Errorf("failed to parse image reference: %w", err)
131+
tag := fmt.Sprintf("%s:%s", rgd.Name, packageConfig.tag)
132+
if err := store.Tag(ctx, manifestDesc, tag); err != nil {
133+
return fmt.Errorf("failed to tag manifest in layout: %w", err)
121134
}
122135

123-
if err := tarball.WriteToFile(outputFile, ref, img); err != nil {
124-
return fmt.Errorf("failed to write image to file: %w", err)
136+
if err := os.RemoveAll(filepath.Join(tempDir, "ingest")); err != nil {
137+
fmt.Println("failed to remove ingest folder: ", err)
138+
}
139+
140+
if err := tarDir(tempDir, outputTar); err != nil {
141+
return fmt.Errorf("failed to write OCI layout tar: %w", err)
125142
}
126143

127144
return nil
128145
}
129146

147+
func tarDir(srcDir, tarPath string) error {
148+
out, err := os.Create(tarPath)
149+
if err != nil {
150+
return err
151+
}
152+
defer func() {
153+
if err := out.Close(); err != nil {
154+
fmt.Println("failed to close tar file: ", err)
155+
}
156+
}()
157+
158+
tw := tar.NewWriter(out)
159+
defer func() {
160+
if err := tw.Close(); err != nil {
161+
fmt.Println("failed to close tar writer: ", err)
162+
}
163+
}()
164+
165+
return filepath.WalkDir(srcDir, func(path string, d os.DirEntry, err error) error {
166+
if err != nil || path == srcDir {
167+
return err
168+
}
169+
170+
info, _ := d.Info()
171+
rel, _ := filepath.Rel(srcDir, path)
172+
rel = filepath.ToSlash(rel)
173+
174+
switch {
175+
case strings.HasPrefix(rel, "ingest/"),
176+
strings.HasPrefix(rel, "blobs/sha256/") && info.Size() == 0,
177+
filepath.Clean(path) == filepath.Clean(tarPath):
178+
return nil
179+
}
180+
181+
hdr, err := tar.FileInfoHeader(info, "")
182+
if err != nil {
183+
return err
184+
}
185+
hdr.Name = rel
186+
187+
if err := tw.WriteHeader(hdr); err != nil {
188+
return err
189+
}
190+
191+
if !info.Mode().IsRegular() {
192+
return nil
193+
}
194+
195+
f, err := os.Open(path)
196+
if err != nil {
197+
return err
198+
}
199+
defer func() {
200+
if err := f.Close(); err != nil {
201+
fmt.Println("failed to close file ", path, ":", err)
202+
}
203+
}()
204+
205+
_, err = io.Copy(tw, f)
206+
return err
207+
})
208+
}
209+
130210
func AddPackageCommand(rootCmd *cobra.Command) {
131211
rootCmd.AddCommand(packageRGDCmd)
132212
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2025 The Kube Resource Orchestrator Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package commands
16+
17+
import (
18+
"archive/tar"
19+
"context"
20+
"encoding/json"
21+
"fmt"
22+
"io"
23+
"os"
24+
"path/filepath"
25+
26+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
27+
"github.com/spf13/cobra"
28+
"oras.land/oras-go/v2"
29+
"oras.land/oras-go/v2/content/oci"
30+
"oras.land/oras-go/v2/registry/remote"
31+
)
32+
33+
type PublishConfig struct {
34+
ociTarballPath string
35+
remoteRef string
36+
}
37+
38+
var publishConfig = &PublishConfig{}
39+
40+
func init() {
41+
publishCmd.PersistentFlags().StringVarP(&publishConfig.ociTarballPath,
42+
"file", "f", "",
43+
"Path to the OCI image tarball created by the 'package' command",
44+
)
45+
publishCmd.PersistentFlags().StringVarP(&publishConfig.remoteRef,
46+
"ref", "r", "",
47+
"Remote reference to publish the OCI image to (e.g., 'ghcr.io/user/repo:tag')",
48+
)
49+
}
50+
51+
var publishCmd = &cobra.Command{
52+
Use: "publish",
53+
Short: "Publish a packaged OCI image to a remote registry",
54+
Long: "The publish command takes an OCI image tarball, created by the 'package' command, " +
55+
"and pushes it to a specified container registry.",
56+
RunE: func(cmd *cobra.Command, args []string) error {
57+
if publishConfig.ociTarballPath == "" {
58+
return fmt.Errorf("path to the OCI tarball is required, please use the --file flag")
59+
}
60+
if publishConfig.remoteRef == "" {
61+
return fmt.Errorf("remote reference is required, please use the --ref flag")
62+
}
63+
64+
tempDir, err := os.MkdirTemp("", "kro-publish-*")
65+
if err != nil {
66+
return fmt.Errorf("failed to create temp dir: %w", err)
67+
}
68+
defer func() {
69+
if err := os.RemoveAll(tempDir); err != nil {
70+
fmt.Printf("Warning: failed to remove temp dir %s: %v\n", tempDir, err)
71+
}
72+
}()
73+
74+
fmt.Printf("Extracting %s to %s...\n", publishConfig.ociTarballPath, tempDir)
75+
if err := untar(publishConfig.ociTarballPath, tempDir); err != nil {
76+
return fmt.Errorf("failed to extract OCI tarball: %w", err)
77+
}
78+
79+
store, err := oci.New(tempDir)
80+
if err != nil {
81+
return fmt.Errorf("failed to open OCI layout at %s: %w", tempDir, err)
82+
}
83+
84+
rootDesc, err := findRootManifestDescriptor(tempDir)
85+
if err != nil {
86+
return fmt.Errorf("failed to find root manifest in OCI layout: %w", err)
87+
}
88+
fmt.Printf("Found manifest to push: %s\n", rootDesc.Digest)
89+
90+
ctx := context.Background()
91+
repo, err := remote.NewRepository(publishConfig.remoteRef)
92+
if err != nil {
93+
return fmt.Errorf("failed to create remote repository for %s: %w", publishConfig.remoteRef, err)
94+
}
95+
96+
fmt.Printf("Publishing to %s...\n", publishConfig.remoteRef)
97+
_, err = oras.Copy(ctx, store, rootDesc.Digest.String(), repo, publishConfig.remoteRef, oras.DefaultCopyOptions)
98+
if err != nil {
99+
return fmt.Errorf("failed to publish OCI image: %w", err)
100+
}
101+
102+
fmt.Println("Successfully published OCI image to", publishConfig.remoteRef)
103+
return nil
104+
},
105+
}
106+
107+
func untar(tarballPath, destDir string) error {
108+
file, err := os.Open(tarballPath)
109+
if err != nil {
110+
return err
111+
}
112+
defer file.Close()
113+
114+
tr := tar.NewReader(file)
115+
116+
for {
117+
header, err := tr.Next()
118+
if err == io.EOF {
119+
break
120+
}
121+
if err != nil {
122+
return err
123+
}
124+
125+
target := filepath.Join(destDir, header.Name)
126+
127+
switch header.Typeflag {
128+
case tar.TypeDir:
129+
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
130+
return err
131+
}
132+
case tar.TypeReg:
133+
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
134+
return err
135+
}
136+
outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
137+
if err != nil {
138+
return err
139+
}
140+
if _, err := io.Copy(outFile, tr); err != nil {
141+
outFile.Close()
142+
return err
143+
}
144+
outFile.Close()
145+
default:
146+
return fmt.Errorf("unsupported file type in tar: %c for file %s", header.Typeflag, header.Name)
147+
}
148+
}
149+
return nil
150+
}
151+
152+
func findRootManifestDescriptor(layoutPath string) (ocispec.Descriptor, error) {
153+
indexPath := filepath.Join(layoutPath, "index.json")
154+
indexBytes, err := os.ReadFile(indexPath)
155+
if err != nil {
156+
return ocispec.Descriptor{}, fmt.Errorf("could not read index.json: %w", err)
157+
}
158+
159+
var index ocispec.Index
160+
if err := json.Unmarshal(indexBytes, &index); err != nil {
161+
return ocispec.Descriptor{}, fmt.Errorf("could not unmarshal index.json: %w", err)
162+
}
163+
164+
if len(index.Manifests) == 0 {
165+
return ocispec.Descriptor{}, fmt.Errorf("no manifests found in index.json")
166+
}
167+
168+
return index.Manifests[0], nil
169+
}
170+
171+
func AddPublishCommand(rootCmd *cobra.Command) {
172+
rootCmd.AddCommand(publishCmd)
173+
}

0 commit comments

Comments
 (0)