From 4ed67873fd4f60bf15873d3f1a006a0da1bbe9fd Mon Sep 17 00:00:00 2001 From: Husni Faiz Date: Tue, 2 May 2023 21:38:55 +0530 Subject: [PATCH] index: local: annotate and add functionality --- local/index.go | 249 +++++++++++++++++++++++++++++++++++++++++ local/index_options.go | 18 +++ local/new_index.go | 50 +++++++++ 3 files changed, 317 insertions(+) create mode 100644 local/index.go create mode 100644 local/index_options.go create mode 100644 local/new_index.go diff --git a/local/index.go b/local/index.go new file mode 100644 index 00000000..4d0d7f65 --- /dev/null +++ b/local/index.go @@ -0,0 +1,249 @@ +package local + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +type ImageIndex struct { + docker DockerClient + repoName string + index v1.ImageIndex +} + +var manifestDir string = "out/manifests" + +func (i *ImageIndex) Add(repoName string) error { + ref, err := name.ParseReference(repoName) + if err != nil { + panic(err) + } + + desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + panic(err) + } + + img, err := desc.Image() + if err != nil { + panic(err) + } + + cfg, err := img.ConfigFile() + + if err != nil { + return errors.Wrapf(err, "getting config file for image %q", repoName) + } + if cfg == nil { + return fmt.Errorf("missing config for image %q", repoName) + } + if cfg.OS == "" { + return fmt.Errorf("missing OS for image %q", repoName) + } + + // Not checking the architecture so we can allow to do `manifest annotate` + // if cfg.Architecture == "" { + // return fmt.Errorf("missing Architecture for image %q", repoName) + // } + + platform := v1.Platform{} + platform.Architecture = cfg.Architecture + platform.OS = cfg.OS + + desc.Descriptor.Platform = &platform + + i.index = mutate.AppendManifests(i.index, mutate.IndexAddendum{Add: img, Descriptor: desc.Descriptor}) + + return nil +} + +func (i *ImageIndex) Remove(repoName string) error { + ref, err := name.ParseReference(repoName) + if err != nil { + panic(err) + } + + desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + panic(err) + } + + i.index = mutate.RemoveManifests(i.index, match.Digests(desc.Digest)) + + return nil +} + +func (i *ImageIndex) Save(additionalNames ...string) error { + l := layout.Path(manifestDir) + + rawIndex, err := i.index.RawManifest() + if err != nil { + return err + } + + err = l.WriteFile(makeFileSafeName(i.repoName), rawIndex, os.ModePerm) + if err != nil { + return err + } + + return nil +} + +func makeFileSafeName(ref string) string { + fileName := strings.Replace(ref, ":", "-", -1) + return strings.Replace(fileName, "/", "_", -1) +} + +func createManifestListDirectory(transaction string) error { + path := filepath.Join("/home/drac98/.docker/manifests", makeFileSafeName(transaction)) + return os.MkdirAll(path, 0o755) +} + +func (i *ImageIndex) ManifestSize() (int64, error) { + return 0, nil +} + +func (i *ImageIndex) Name() string { + return i.repoName +} + +func ManifestDir() string { + return manifestDir +} + +func AppendManifest(repoName string, manifestName string) error { + var manifest v1.IndexManifest + + path := filepath.Join(manifestDir, makeFileSafeName(repoName)) + jsonFile, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + err = json.Unmarshal([]byte(jsonFile), &manifest) + if err != nil { + return err + } + + ref, err := name.ParseReference(manifestName) + if err != nil { + return err + } + + desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return err + } + + img, err := desc.Image() + if err != nil { + panic(err) + } + + cfg, err := img.ConfigFile() + + if err != nil { + return errors.Wrapf(err, "getting config file for image %q", repoName) + } + if cfg == nil { + return fmt.Errorf("missing config for image %q", repoName) + } + if cfg.OS == "" { + return fmt.Errorf("missing OS for image %q", repoName) + } + + platform := v1.Platform{} + platform.Architecture = cfg.Architecture + platform.OS = cfg.OS + + desc.Descriptor.Platform = &platform + + manifest.Manifests = append(manifest.Manifests, desc.Descriptor) + + data, err := json.Marshal(manifest) + if err != nil { + return err + } + + err = os.WriteFile(path, data, os.ModePerm) + if err != nil { + return err + } + + return nil +} + +type AnnotateFields struct { + Architecture string + OS string + Variant string +} + +func AnnotateManifest(repoName string, manifestName string, opts AnnotateFields) error { + var manifest v1.IndexManifest + + path := filepath.Join(manifestDir, makeFileSafeName(repoName)) + jsonFile, err := os.ReadFile(path) + if err != nil { + return err + } + + err = json.Unmarshal([]byte(jsonFile), &manifest) + if err != nil { + return err + } + + ref, err := name.ParseReference(manifestName) + if err != nil { + return err + } + + desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return err + } + + for i, desc_i := range manifest.Manifests { + if desc_i.Digest.String() == desc.Digest.String() { + if opts.Architecture != "" { + manifest.Manifests[i].Platform.Architecture = opts.Architecture + } + + if opts.OS != "" { + manifest.Manifests[i].Platform.OS = opts.OS + } + + if opts.Variant != "" { + manifest.Manifests[i].Platform.Variant = opts.Variant + } + + data, err := json.Marshal(manifest) + if err != nil { + return err + } + + err = os.WriteFile(path, data, os.ModePerm) + if err != nil { + return err + } + + return nil + } + } + + return errors.Errorf("Manifest %s not found", manifestName) +} diff --git a/local/index_options.go b/local/index_options.go new file mode 100644 index 00000000..f96f0286 --- /dev/null +++ b/local/index_options.go @@ -0,0 +1,18 @@ +package local + +import "github.com/buildpacks/imgutil" + +type ImageIndexOption func(*indexOptions) error + +type indexOptions struct { + mediaTypes imgutil.MediaTypes +} + +// WithIndexMediaTypes loads an existing index as a source. +// If mediatype is not found ignore. +func WithIndexMediaTypes(requested imgutil.MediaTypes) ImageIndexOption { + return func(opts *indexOptions) error { + opts.mediaTypes = requested + return nil + } +} diff --git a/local/new_index.go b/local/new_index.go new file mode 100644 index 00000000..ac1bc510 --- /dev/null +++ b/local/new_index.go @@ -0,0 +1,50 @@ +package local + +import ( + "github.com/buildpacks/imgutil" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func NewIndex(repoName string, dockerClient DockerClient, ops ...ImageIndexOption) (*ImageIndex, error) { + if _, err := name.ParseReference(repoName, name.WeakValidation); err != nil { + return nil, err + } + + indexOpts := &indexOptions{} + for _, op := range ops { + if err := op(indexOpts); err != nil { + return nil, err + } + } + + mediaType := defaultMediaType() + if indexOpts.mediaTypes.IndexManifestType() != "" { + mediaType = indexOpts.mediaTypes + } + + index, err := emptyIndex(mediaType.IndexManifestType()) + if err != nil { + return nil, err + } + + ridx := &ImageIndex{ + docker: dockerClient, + repoName: repoName, + index: index, + } + + return ridx, nil + +} + +func emptyIndex(mediaType types.MediaType) (v1.ImageIndex, error) { + return mutate.IndexMediaType(empty.Index, mediaType), nil +} + +func defaultMediaType() imgutil.MediaTypes { + return imgutil.DockerTypes +}