diff --git a/go.mod b/go.mod index 5040e70..0104916 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,20 @@ module github.com/octohelm/crkit go 1.22.2 require ( + github.com/containerd/containerd v1.7.16 github.com/distribution/distribution/v3 v3.0.0-alpha.1 github.com/distribution/reference v0.6.0 github.com/go-courier/logr v0.3.0 github.com/google/go-containerregistry v0.19.1 github.com/innoai-tech/infra v0.0.0-20240508041032-12069adfe35c - github.com/octohelm/courier v0.0.0-20240506093745-6b6f6acaf660 - github.com/octohelm/gengo v0.0.0-20240409082121-aeffa5400f19 + github.com/octohelm/courier v0.0.0-20240510063732-a8aa1af87601 + github.com/octohelm/gengo v0.0.0-20240510051519-974fb897453b github.com/octohelm/kubekit v0.0.0-20240508035712-15cb61729772 + github.com/octohelm/kubepkgspec v0.0.0-20240514102555-08917801bb86 github.com/octohelm/storage v0.0.0-20240430010427-f412a0c84f3b github.com/octohelm/x v0.0.0-20240513022938-1bd86d96adef github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0 github.com/pelletier/go-toml/v2 v2.2.2 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 @@ -25,11 +28,13 @@ require ( require ( cuelang.org/go v0.8.2 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -76,7 +81,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.33.1 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.53.0 // indirect diff --git a/go.sum b/go.sum index b8a51c4..4345439 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e/go.mod h1:ApHc cuelang.org/go v0.8.2 h1:vWfHI1kQlBvwkna7ktAqXjV5LUEAgU6vyMlJjvZZaDw= cuelang.org/go v0.8.2/go.mod h1:CoDbYolfMms4BhWUlhD+t5ORnihR7wvjcfgyO9lL5FI= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= @@ -24,6 +26,10 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/containerd/containerd v1.7.16 h1:7Zsfe8Fkj4Wi2My6DXGQ87hiqIrmOXolm72ZEkFU5Mg= +github.com/containerd/containerd v1.7.16/go.mod h1:NL49g7A/Fui7ccmxV6zkBWwqMgmMxFWzujYCc+JLt7k= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -178,12 +184,14 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/octohelm/courier v0.0.0-20240506093745-6b6f6acaf660 h1:D3jgAqV+5ObUlj/9JEQWirGZ/oC8liFH6+0Bz9h8tMc= -github.com/octohelm/courier v0.0.0-20240506093745-6b6f6acaf660/go.mod h1:lYPuTnuOSyTHOweXCoDQbPphAZ00ZMRtctUTGSI0YNE= -github.com/octohelm/gengo v0.0.0-20240409082121-aeffa5400f19 h1:l8ahFVtDhXAcKFmR1/6Sevs1R71Kai7dfp95ZlG7raY= -github.com/octohelm/gengo v0.0.0-20240409082121-aeffa5400f19/go.mod h1:Q0f7oofuZJ6T2vk5vsXgjKwKNlOJO0TTrjGfA9yRJwA= +github.com/octohelm/courier v0.0.0-20240510063732-a8aa1af87601 h1:xfvMM+bQ4ONY3DCU7nZiQPqTXnCynvEFB4n2JBHN3v4= +github.com/octohelm/courier v0.0.0-20240510063732-a8aa1af87601/go.mod h1:lYPuTnuOSyTHOweXCoDQbPphAZ00ZMRtctUTGSI0YNE= +github.com/octohelm/gengo v0.0.0-20240510051519-974fb897453b h1:z/XJuBmwnxoPPMl2mY11CsU2w8yiLq5q3bwdmkrPCWo= +github.com/octohelm/gengo v0.0.0-20240510051519-974fb897453b/go.mod h1:mlnC4bXnp0RdKxBf1u4bFQ0pmyDHUfXI9czal6K8lS0= github.com/octohelm/kubekit v0.0.0-20240508035712-15cb61729772 h1:X+mCc0CTxFo0BMMVeGBEDudulp2fGiftUD9rJS9OmOQ= github.com/octohelm/kubekit v0.0.0-20240508035712-15cb61729772/go.mod h1:wbxe94x0pWeoE77qvcBDjvn7tnndvZ0pv2VeSz0UscA= +github.com/octohelm/kubepkgspec v0.0.0-20240514102555-08917801bb86 h1:Q46GYugI8+DQGEwUncRVUK/Zes0rEBcLE6TOAN5O7nM= +github.com/octohelm/kubepkgspec v0.0.0-20240514102555-08917801bb86/go.mod h1:ZWxreIiVDp2PA6CrHnZ0HJBu6ibhyL4LqTVigEMjt34= github.com/octohelm/storage v0.0.0-20240430010427-f412a0c84f3b h1:lLYKRyVNrS+72y4k9ZZohVrJUIyfYIg1un0CtFBCixw= github.com/octohelm/storage v0.0.0-20240430010427-f412a0c84f3b/go.mod h1:6eArB7GTmErZjXofekKO9Y7zJbUl1dJiQ/JT/jBmuwM= github.com/octohelm/x v0.0.0-20240513022938-1bd86d96adef h1:BoG/tg33jwNZ8BZA6nFc1wIl88P+2SgTzJmr4c+E8sY= diff --git a/internal/cmd/crkit/zz_generated.runtimedoc.go b/internal/cmd/crkit/zz_generated.runtimedoc.go index cb8e519..c6e9107 100644 --- a/internal/cmd/crkit/zz_generated.runtimedoc.go +++ b/internal/cmd/crkit/zz_generated.runtimedoc.go @@ -1,5 +1,5 @@ /* -Package main GENERATED BY gengo:runtimedoc +Package main GENERATED BY gengo:runtimedoc DON'T EDIT THIS FILE */ package main @@ -21,7 +21,6 @@ func (v KubeClient) RuntimeDoc(names ...string) ([]string, bool) { return []string{ "Paths to a kubeconfig. Only required if out-of-cluster.", }, true - } return nil, false diff --git a/pkg/artifact/config.go b/pkg/artifact/config.go new file mode 100644 index 0000000..20d0443 --- /dev/null +++ b/pkg/artifact/config.go @@ -0,0 +1,29 @@ +package artifact + +type Config interface { + ArtifactType() (string, error) + ConfigMediaType() (string, error) + RawConfigFile() ([]byte, error) +} + +func EmptyConfig(artifactType string) Config { + return &emptyConfigArtifact{ + artifactType: artifactType, + } +} + +type emptyConfigArtifact struct { + artifactType string +} + +func (i *emptyConfigArtifact) ArtifactType() (string, error) { + return i.artifactType, nil +} + +func (i *emptyConfigArtifact) ConfigMediaType() (string, error) { + return "application/vnd.oci.empty.v1+json", nil +} + +func (i *emptyConfigArtifact) RawConfigFile() ([]byte, error) { + return []byte("{}"), nil +} diff --git a/pkg/artifact/image.go b/pkg/artifact/image.go new file mode 100644 index 0000000..725ede8 --- /dev/null +++ b/pkg/artifact/image.go @@ -0,0 +1,148 @@ +package artifact + +import ( + "bytes" + "encoding/json" + "sync/atomic" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/opencontainers/go-digest" + specv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func Artifact(img v1.Image, c Config) (v1.Image, error) { + return &artifactImage{ + Image: img, + config: c, + }, nil +} + +type artifactImage struct { + v1.Image + config Config + + m atomic.Pointer[specv1.Manifest] +} + +func (img *artifactImage) MediaType() (types.MediaType, error) { + return types.OCIManifestSchema1, nil +} + +func (i *artifactImage) ConfigName() (v1.Hash, error) { + return partial.ConfigName(i) +} + +func (img *artifactImage) RawConfigFile() ([]byte, error) { + return img.config.RawConfigFile() +} + +func (img *artifactImage) Manifest() (*v1.Manifest, error) { + raw, err := img.RawManifest() + if err != nil { + return nil, err + } + + m := &v1.Manifest{} + + if err := json.Unmarshal(raw, m); err != nil { + return nil, err + } + + return m, nil +} + +func (img *artifactImage) RawManifest() ([]byte, error) { + m, err := img.OCIManifest() + if err != nil { + return nil, err + } + return json.MarshalIndent(m, "", " ") +} + +func (img *artifactImage) OCIManifest() (*specv1.Manifest, error) { + if m := img.m.Load(); m != nil { + return m, nil + } + + configRaw, err := img.RawConfigFile() + if err != nil { + return nil, err + } + + cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(configRaw)) + if err != nil { + return nil, err + } + + mediaType, err := img.MediaType() + if err != nil { + return nil, err + } + + artifactType, err := img.config.ArtifactType() + if err != nil { + return nil, err + } + + configMediaType, err := img.config.ConfigMediaType() + if err != nil { + return nil, err + } + + m := &specv1.Manifest{ + MediaType: string(mediaType), + ArtifactType: artifactType, + Config: specv1.Descriptor{ + MediaType: configMediaType, + Size: cfgSize, + Digest: digest.Digest(cfgHash.String()), + }, + } + m.SchemaVersion = 2 + + layers, err := img.Image.Layers() + if err != nil { + return nil, err + } + + for _, l := range layers { + desc, err := partial.Descriptor(l) + if err != nil { + return nil, err + } + + d := specv1.Descriptor{ + MediaType: string(desc.MediaType), + Digest: digest.Digest(desc.Digest.String()), + Size: desc.Size, + Annotations: desc.Annotations, + ArtifactType: desc.ArtifactType, + } + + if p := desc.Platform; p != nil { + d.Platform = &specv1.Platform{ + Architecture: p.Architecture, + OS: p.OS, + OSVersion: p.OSVersion, + OSFeatures: p.OSFeatures, + Variant: p.Variant, + } + } + + m.Layers = append(m.Layers, d) + } + + img.m.Store(m) + + return m, nil +} + +func (img *artifactImage) Size() (int64, error) { + return partial.Size(img) +} + +func (img *artifactImage) Digest() (v1.Hash, error) { + return partial.Digest(img) +} diff --git a/pkg/artifact/layer.go b/pkg/artifact/layer.go new file mode 100644 index 0000000..2944484 --- /dev/null +++ b/pkg/artifact/layer.go @@ -0,0 +1,101 @@ +package artifact + +import ( + "bytes" + "fmt" + "io" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type Layer = v1.Layer + +func FromBytes(mediaType string, data []byte) (Layer, error) { + return FromOpener(mediaType, func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(data)), nil + }) +} + +func FromReader(mediaType string, r io.Reader) (Layer, error) { + return FromOpener(mediaType, func() (io.ReadCloser, error) { + return io.NopCloser(r), nil + }) +} + +func FromOpener(mediaType string, uncompressed func() (io.ReadCloser, error)) (Layer, error) { + return NonCompressedLayer(&artifact{ + mediaType: mediaType, + uncompressed: uncompressed, + }), nil +} + +type artifact struct { + mediaType string + uncompressed func() (io.ReadCloser, error) +} + +func (a *artifact) MediaType() (types.MediaType, error) { + return types.MediaType(a.mediaType), nil +} + +func (a *artifact) Uncompressed() (io.ReadCloser, error) { + return a.uncompressed() +} + +func Gzipped(l Layer) Layer { + return &compressed{ + Layer: l, + compressedLayer: sync.OnceValues(func() (v1.Layer, error) { + return partial.UncompressedToLayer(l) + }), + } +} + +type compressed struct { + Layer + compressedLayer func() (v1.Layer, error) +} + +func (a *compressed) MediaType() (types.MediaType, error) { + m, err := a.Layer.MediaType() + if err != nil { + return "", err + } + return types.MediaType(fmt.Sprintf("%s+gzip", m)), nil +} + +func (a *compressed) Compressed() (io.ReadCloser, error) { + l, err := a.compressedLayer() + if err != nil { + return nil, err + } + return l.Compressed() +} + +func (a *compressed) Digest() (v1.Hash, error) { + l, err := a.compressedLayer() + if err != nil { + return v1.Hash{}, err + } + return l.Digest() +} + +func WithDescriptor(l Layer, descriptor v1.Descriptor) Layer { + return &artifactWithDescriptor{ + desc: descriptor, + Layer: l, + } +} + +type artifactWithDescriptor struct { + Layer + + desc v1.Descriptor +} + +func (w *artifactWithDescriptor) Descriptor() (*v1.Descriptor, error) { + return &w.desc, nil +} diff --git a/pkg/artifact/layer__uncompressed.go b/pkg/artifact/layer__uncompressed.go new file mode 100644 index 0000000..5395a41 --- /dev/null +++ b/pkg/artifact/layer__uncompressed.go @@ -0,0 +1,62 @@ +package artifact + +import ( + "io" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type UncompressedLayer interface { + MediaType() (types.MediaType, error) + + Uncompressed() (io.ReadCloser, error) +} + +func NonCompressedLayer(u UncompressedLayer) v1.Layer { + return &nonCompressedLayer{ + UncompressedLayer: u, + } +} + +type nonCompressedLayer struct { + UncompressedLayer + + hashSizeError error + hash v1.Hash + size int64 + once sync.Once +} + +func (a *nonCompressedLayer) DiffID() (v1.Hash, error) { + a.calcSizeHash() + + return a.hash, a.hashSizeError +} + +func (a *nonCompressedLayer) Size() (int64, error) { + a.calcSizeHash() + + return a.size, a.hashSizeError +} + +func (a *nonCompressedLayer) Digest() (v1.Hash, error) { + return a.DiffID() +} + +func (a *nonCompressedLayer) Compressed() (io.ReadCloser, error) { + return a.Uncompressed() +} + +func (a *nonCompressedLayer) calcSizeHash() { + a.once.Do(func() { + r, err := a.Uncompressed() + if err != nil { + a.hashSizeError = err + return + } + defer r.Close() + a.hash, a.size, a.hashSizeError = v1.SHA256(r) + }) +} diff --git a/pkg/containerdhost/controller/zz_generated.runtimedoc.go b/pkg/containerdhost/controller/zz_generated.runtimedoc.go index 59fde17..8764a73 100644 --- a/pkg/containerdhost/controller/zz_generated.runtimedoc.go +++ b/pkg/containerdhost/controller/zz_generated.runtimedoc.go @@ -1,5 +1,5 @@ /* -Package controller GENERATED BY gengo:runtimedoc +Package controller GENERATED BY gengo:runtimedoc DON'T EDIT THIS FILE */ package controller @@ -19,7 +19,6 @@ func (v Reconciler) RuntimeDoc(names ...string) ([]string, bool) { switch names[0] { case "ConfigPath": return []string{}, true - } return nil, false diff --git a/pkg/kubepkg/config.go b/pkg/kubepkg/config.go new file mode 100644 index 0000000..58514d5 --- /dev/null +++ b/pkg/kubepkg/config.go @@ -0,0 +1,39 @@ +package kubepkg + +import ( + "encoding/json" + "sync" + + "github.com/octohelm/crkit/pkg/artifact" + kubepkgv1alpha1 "github.com/octohelm/kubepkgspec/pkg/apis/kubepkg/v1alpha1" +) + +const ( + ConfigMediaType = "application/vnd.kubepkg.config.v1+json" + ArtifactType = "application/vnd.kubepkg+type" +) + +var _ artifact.Config = &Config{} + +type Config struct { + KubePkg *kubepkgv1alpha1.KubePkg + + once sync.Once + raw []byte + err error +} + +func (*Config) ArtifactType() (string, error) { + return ArtifactType, nil +} + +func (*Config) ConfigMediaType() (string, error) { + return ConfigMediaType, nil +} + +func (c *Config) RawConfigFile() ([]byte, error) { + c.once.Do(func() { + c.raw, c.err = json.Marshal(c.KubePkg) + }) + return c.raw, c.err +} diff --git a/pkg/kubepkg/packer.go b/pkg/kubepkg/packer.go new file mode 100644 index 0000000..1b34de3 --- /dev/null +++ b/pkg/kubepkg/packer.go @@ -0,0 +1,308 @@ +package kubepkg + +import ( + "context" + "iter" + "sort" + "strings" + "sync" + + "github.com/pkg/errors" + + "github.com/containerd/containerd/images" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/cache" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/octohelm/crkit/pkg/artifact" + kubepkgv1alpha1 "github.com/octohelm/kubepkgspec/pkg/apis/kubepkg/v1alpha1" + "github.com/octohelm/kubepkgspec/pkg/object" + "github.com/octohelm/kubepkgspec/pkg/workload" + specv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +const ( + AnnotationSourceBaseImageName = "kubepkg.source.image.base.name" +) + +type Packer struct { + CreatePuller func(repo name.Repository, options ...remote.Option) (*remote.Puller, error) + Cache cache.Cache + Registry *name.Registry + Platforms []string + Renamer Renamer + + sourceImages sync.Map +} + +func (p *Packer) SupportedPlatforms(supportedPlatform []string) iter.Seq[v1.Platform] { + if len(p.Platforms) == 0 { + return func(yield func(v1.Platform) bool) { + for _, platform := range supportedPlatform { + p, _ := v1.ParsePlatform(platform) + if p != nil { + if !yield(*p) { + return + } + } + } + } + } + + supportedPlatforms := map[string]bool{} + for _, platform := range supportedPlatform { + supportedPlatforms[platform] = true + } + + return func(yield func(v1.Platform) bool) { + for _, platform := range p.Platforms { + if len(supportedPlatforms) > 0 { + _, supported := supportedPlatforms[platform] + if !supported { + continue + } + } + + p, _ := v1.ParsePlatform(platform) + if p != nil { + if !yield(*p) { + return + } + } + } + } +} + +func (p *Packer) Repository(repoName string) (name.Repository, error) { + if registry := p.Registry; registry != nil { + registryName := registry.Name() + if strings.HasPrefix(repoName, registryName) { + return registry.Repo(repoName[len(registryName)+1:]), nil + } + return registry.Repo(repoName), nil + } + return name.NewRepository(repoName) +} + +func (p *Packer) Puller(repo name.Repository, options ...remote.Option) (*remote.Puller, error) { + puller, err := p.CreatePuller(repo, options...) + if err != nil { + return nil, err + } + return puller, nil +} + +func (p *Packer) Image(i v1.Image) v1.Image { + if c := p.Cache; c != nil { + return cache.Image(i, c) + } + return i +} + +func (p *Packer) PackAsIndex(ctx context.Context, kpkg *kubepkgv1alpha1.KubePkg) (v1.ImageIndex, error) { + kubePkgImage, err := p.PackAsKubePkgImage(ctx, kpkg) + if err != nil { + return nil, err + } + + var finalIndex v1.ImageIndex = empty.Index + + finalIndex, err = p.appendManifests(finalIndex, kubePkgImage, nil, nil) + if err != nil { + return nil, err + } + + layers, err := kubePkgImage.Layers() + if err != nil { + return nil, err + } + + imageNames := make([]string, 0) + imageIndexes := make(map[string]v1.ImageIndex) + + for _, l := range layers { + desc, err := partial.Descriptor(l) + if err != nil { + return nil, err + } + + if desc.MediaType.IsImage() && len(desc.Annotations) > 0 { + imageName := desc.Annotations[images.AnnotationImageName] + + sourceRepo := desc.Annotations[AnnotationSourceBaseImageName] + repo, err := p.Repository(sourceRepo) + if err != nil { + return nil, err + } + + if _, ok := imageIndexes[imageName]; !ok { + imageNames = append(imageNames, imageName) + imageIndexes[imageName] = empty.Index + } + + puller, err := p.Puller(repo) + if err != nil { + return nil, err + } + + resolvedDesc, err := puller.Get(ctx, repo.Digest(desc.Digest.String())) + if err != nil { + return nil, err + } + + img, err := resolvedDesc.Image() + if err != nil { + return nil, err + } + + imageIndexes[imageName], err = p.appendManifests(imageIndexes[imageName], p.Image(img), desc, nil) + if err != nil { + return nil, err + } + } + } + + sort.Strings(imageNames) + + for _, imageName := range imageNames { + index := imageIndexes[imageName] + + nameAndTag := strings.Split(imageName, ":") + if len(nameAndTag) != 2 { + return nil, errors.Errorf("invalid image name %s", nameAndTag) + } + + finalIndex, err = p.appendManifests(finalIndex, index, nil, &kubepkgv1alpha1.Image{ + Name: nameAndTag[0], + Tag: nameAndTag[1], + }) + if err != nil { + return nil, err + } + } + + return finalIndex, nil +} + +func (p *Packer) PackAsKubePkgImage(ctx context.Context, kpkg *kubepkgv1alpha1.KubePkg) (v1.Image, error) { + workloadImages := workload.Images(func(yield func(object.Object) bool) { + if !yield(kpkg) { + return + } + }) + + var kubepkgImage v1.Image = empty.Image + + for image := range workloadImages { + repo, err := p.Repository(image.Name) + if err != nil { + return nil, err + } + + image.Name = p.ImageName(repo) + image.Digest = "" + + for platform := range p.SupportedPlatforms(image.Platforms) { + puller, err := p.CreatePuller(repo, remote.WithPlatform(platform)) + if err != nil { + return nil, err + } + + desc, err := puller.Get(ctx, repo.Tag(image.Tag)) + if err != nil { + return nil, err + } + + img, err := desc.Image() + if err != nil { + return nil, err + } + + kubepkgImage, err = p.appendArtifactLayer(kubepkgImage, p.Image(img), image) + if err != nil { + return nil, err + } + } + } + + return artifact.Artifact(kubepkgImage, &Config{KubePkg: kpkg}) +} + +func (p *Packer) appendArtifactLayer(kubepkgImage v1.Image, src v1.Image, img *kubepkgv1alpha1.Image) (v1.Image, error) { + d, err := partial.Descriptor(src) + if err != nil { + return nil, err + } + + if d.Annotations == nil { + d.Annotations = map[string]string{} + } + + d.Annotations[specv1.AnnotationBaseImageName] = img.Name + d.Annotations[AnnotationSourceBaseImageName] = p.SourceImageName(img.Name) + + d.Annotations[specv1.AnnotationRefName] = img.Tag + d.Annotations[images.AnnotationImageName] = img.FullName() + + raw, err := src.RawManifest() + if err != nil { + return nil, err + } + + layer, err := artifact.FromBytes(string(d.MediaType), raw) + if err != nil { + return nil, err + } + + return mutate.AppendLayers(kubepkgImage, artifact.WithDescriptor(layer, *d)) +} + +func (p *Packer) appendManifests(idx v1.ImageIndex, source partial.Describable, desc *v1.Descriptor, image *kubepkgv1alpha1.Image) (v1.ImageIndex, error) { + if desc == nil { + d, err := partial.Descriptor(source) + if err != nil { + return nil, err + } + desc = d + } + + add := mutate.IndexAddendum{ + Add: source, + Descriptor: *desc, + } + + if image != nil { + if add.Annotations == nil { + add.Annotations = map[string]string{} + } + add.Annotations[specv1.AnnotationBaseImageName] = image.Name + add.Annotations[specv1.AnnotationRefName] = image.Tag + add.Annotations[images.AnnotationImageName] = image.FullName() + } + + return mutate.AppendManifests(idx, add), nil +} + +func (p *Packer) SourceImageName(name string) string { + if v, ok := p.sourceImages.Load(name); ok { + return v.(string) + } + return name +} + +func (p *Packer) ImageName(repoName name.Repository) (name string) { + defer func() { + p.sourceImages.Store(name, repoName.String()) + }() + + if p.Renamer != nil { + return p.Renamer.Rename(repoName) + } + if strings.HasPrefix(repoName.String(), "index.docker.io/") { + return "docker.io/" + repoName.RepositoryStr() + } + return repoName.String() +} diff --git a/pkg/kubepkg/packer_test.go b/pkg/kubepkg/packer_test.go new file mode 100644 index 0000000..db193f6 --- /dev/null +++ b/pkg/kubepkg/packer_test.go @@ -0,0 +1,82 @@ +package kubepkg + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/cache" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/octohelm/crkit/pkg/ocitar" + kubepkgv1alpha1 "github.com/octohelm/kubepkgspec/pkg/apis/kubepkg/v1alpha1" + testingx "github.com/octohelm/x/testing" +) + +//go:embed testdata/example.kubepkg.json +var kubepkgExample []byte + +func TestPacker(t *testing.T) { + t.Skip() + + registry, _ := name.NewRegistry(os.Getenv("CONTAINER_REGISTRY")) + + a := authn.FromConfig(authn.AuthConfig{ + Username: os.Getenv("CONTAINER_REGISTRY_USERNAME"), + Password: os.Getenv("CONTAINER_REGISTRY_PASSWORD"), + }) + + // {{registry}}/{{namespace}}/{{name}} + renamer, _ := NewTemplateRenamer("docker.io/x/{{ .name }}") + + t.Run("test rename", func(t *testing.T) { + r, _ := name.NewRepository("docker.io/library/nginx") + testingx.Expect(t, renamer.Rename(r), testingx.Be("docker.io/x/nginx")) + }) + + p := &Packer{ + Cache: cache.NewFilesystemCache("testdata/.tmp/cache"), + Registry: ®istry, + CreatePuller: func(name name.Repository, options ...remote.Option) (*remote.Puller, error) { + return remote.NewPuller(append(options, remote.WithAuth(a))...) + }, + Platforms: []string{ + "linux/amd64", + }, + Renamer: renamer, + } + + t.Run("should pack as kubepkg image", func(t *testing.T) { + kpkg := &kubepkgv1alpha1.KubePkg{} + _ = json.Unmarshal(kubepkgExample, kpkg) + + ctx := context.Background() + + i, err := p.PackAsKubePkgImage(ctx, kpkg) + testingx.Expect(t, err, testingx.BeNil[error]()) + + raw, _ := i.RawManifest() + fmt.Println(string(raw)) + }) + + t.Run("should pack as index", func(t *testing.T) { + kpkg := &kubepkgv1alpha1.KubePkg{} + _ = json.Unmarshal(kubepkgExample, kpkg) + + ctx := context.Background() + + idx, err := p.PackAsIndex(ctx, kpkg) + testingx.Expect(t, err, testingx.BeNil[error]()) + + f, err := os.OpenFile("testdata/.tmp/example.kubepkg.tar", os.O_TRUNC|os.O_WRONLY|os.O_CREATE, os.ModePerm) + testingx.Expect(t, err, testingx.BeNil[error]()) + defer f.Close() + + err = ocitar.Write(f, idx) + testingx.Expect(t, err, testingx.BeNil[error]()) + }) +} diff --git a/pkg/kubepkg/puller.go b/pkg/kubepkg/puller.go new file mode 100644 index 0000000..bb6a037 --- /dev/null +++ b/pkg/kubepkg/puller.go @@ -0,0 +1,3 @@ +package kubepkg + +type Puller interface{} diff --git a/pkg/kubepkg/renamer.go b/pkg/kubepkg/renamer.go new file mode 100644 index 0000000..aef3a2b --- /dev/null +++ b/pkg/kubepkg/renamer.go @@ -0,0 +1,44 @@ +package kubepkg + +import ( + "path/filepath" + "strings" + "text/template" + + "github.com/google/go-containerregistry/pkg/name" +) + +type Renamer interface { + Rename(repo name.Repository) string +} + +func NewTemplateRenamer(text string) (Renamer, error) { + t, err := template.New(text).Parse(text) + if err != nil { + return nil, err + } + + return &templateRenamer{ + Template: t, + }, nil +} + +type templateRenamer struct { + *template.Template +} + +func (t *templateRenamer) Rename(repo name.Repository) string { + b := &strings.Builder{} + + ctx := map[string]any{ + "registry": repo.RegistryStr(), + "namespace": filepath.Dir(repo.RepositoryStr()), + "name": filepath.Base(repo.Name()), + } + + if err := t.Execute(b, ctx); err == nil { + return b.String() + } + + return repo.String() +} diff --git a/pkg/kubepkg/testdata/example.kubepkg.json b/pkg/kubepkg/testdata/example.kubepkg.json new file mode 100644 index 0000000..624ab27 --- /dev/null +++ b/pkg/kubepkg/testdata/example.kubepkg.json @@ -0,0 +1,60 @@ +{ + "apiVersion": "octohelm.tech/v1alpha1", + "kind": "KubePkg", + "metadata": { + "name": "demo", + "namespace": "default", + "annotations": { + "ingress.octohelm.tech/gateway": "public+https://{{ .Name }}---{{ .Namespace }}.public,internal+https://{{ .Name }}---{{ .Namespace }}.internal?always=true" + } + }, + "spec": { + "version": "0.0.2", + "config": { + "X": "x" + }, + "deploy": { + "kind": "Deployment", + "spec": { + "replicas": 1 + } + }, + "containers": { + "web": { + "image": { + "name": "docker.io/library/nginx", + "tag": "1.25.0-alpine", + "pullPolicy": "IfNotPresent", + "platforms": [ + "linux/amd64", + "linux/arm64" + ] + }, + "ports": { + "http": 80 + } + } + }, + "services": { + "#": { + "ports": { + "http": 80 + }, + "paths": { + "http": "/" + } + } + }, + "volumes": { + "html": { + "mountPath": "/usr/share/nginx/html", + "type": "ConfigMap", + "spec": { + "data": { + "index.html": "