github.com/mattmoor/mink@v1.3.1/pkg/bundles/git/bundle.go (about)

     1  /*
     2  Copyright 2020 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package git
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path/filepath"
    28  
    29  	"github.com/google/go-containerregistry/pkg/name"
    30  	v1 "github.com/google/go-containerregistry/pkg/v1"
    31  	"github.com/google/go-containerregistry/pkg/v1/mutate"
    32  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    33  	"gopkg.in/src-d/go-billy.v4/memfs"
    34  	"gopkg.in/src-d/go-git.v4"
    35  	"gopkg.in/src-d/go-git.v4/plumbing"
    36  	"gopkg.in/src-d/go-git.v4/plumbing/object"
    37  	"gopkg.in/src-d/go-git.v4/storage/memory"
    38  
    39  	"github.com/mattmoor/mink/pkg/bundles"
    40  	"github.com/mattmoor/mink/pkg/bundles/kontext"
    41  )
    42  
    43  func bundle(ctx context.Context, opts Options) (v1.Layer, error) {
    44  	// Filesystem abstraction based on memory
    45  	fs := memfs.New()
    46  	// Git objects storer based on memory
    47  	storer := memory.NewStorage()
    48  
    49  	// Clones the repository into the worktree (fs) and storer all the .git
    50  	// content into the storer
    51  	repo, err := git.CloneContext(ctx, storer, fs, &git.CloneOptions{
    52  		URL:           opts.URL,
    53  		ReferenceName: opts.Ref,
    54  		SingleBranch:  true,
    55  	})
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	ref, err := repo.Head()
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	commit, err := repo.CommitObject(ref.Hash())
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	tree, err := commit.Tree()
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	buf := bytes.NewBuffer(nil)
    72  	tw := tar.NewWriter(buf)
    73  	defer tw.Close()
    74  
    75  	// Add an entry for the root kontext directory
    76  	// This is to facilitate testing for compatibility with kontext.
    77  	if err := tw.WriteHeader(&tar.Header{
    78  		Name:     kontext.StoragePath,
    79  		Typeflag: tar.TypeDir,
    80  		Mode:     0555,
    81  	}); err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	w := object.NewTreeWalker(tree, true /* recursive */, make(map[plumbing.Hash]bool))
    86  	defer w.Close()
    87  	for {
    88  		name, _, err := w.Next()
    89  		if errors.Is(err, io.EOF) {
    90  			break
    91  		} else if err != nil {
    92  			return nil, err
    93  		}
    94  
    95  		if err := func() error {
    96  			// Do not chase symlinks.
    97  			info, err := fs.Lstat(name)
    98  			if err != nil {
    99  				return err
   100  			}
   101  
   102  			newPath := filepath.Join(kontext.StoragePath, name)
   103  
   104  			// Handle directories
   105  			if info.Mode().IsDir() {
   106  				return tw.WriteHeader(&tar.Header{
   107  					Name:     newPath,
   108  					Typeflag: tar.TypeDir,
   109  					Mode:     0555,
   110  				})
   111  			}
   112  
   113  			// Handle symlinks
   114  			if info.Mode()&os.ModeSymlink != 0 {
   115  				linkname, err := fs.Readlink(name)
   116  				if err != nil {
   117  					return err
   118  				}
   119  				return tw.WriteHeader(&tar.Header{
   120  					Name:     newPath,
   121  					Typeflag: tar.TypeSymlink,
   122  					Mode:     0555,
   123  					Linkname: linkname,
   124  				})
   125  			}
   126  
   127  			// Open the file to copy it into the tarball.
   128  			file, err := fs.Open(name)
   129  			if err != nil {
   130  				return err
   131  			}
   132  			defer file.Close()
   133  
   134  			// Copy the file into the image tarball.
   135  			if err := tw.WriteHeader(&tar.Header{
   136  				Name:     newPath,
   137  				Size:     info.Size(),
   138  				Typeflag: tar.TypeReg,
   139  				// Use a fixed Mode, so that this isn't sensitive to the directory and umask
   140  				// under which it was created. Additionally, windows can only set 0222,
   141  				// 0444, or 0666, none of which are executable.
   142  				Mode: 0555,
   143  			}); err != nil {
   144  				return err
   145  			}
   146  			_, err = io.Copy(tw, file)
   147  			return err
   148  		}(); err != nil {
   149  			return nil, fmt.Errorf("error processing %q: %w", name, err)
   150  		}
   151  	}
   152  
   153  	return tarball.LayerFromReader(bytes.NewBuffer(buf.Bytes()))
   154  }
   155  
   156  // Options contains a collection of options for configuring how things are bundled from git.
   157  type Options struct {
   158  	// URL contains the url from which to clone the git repository.
   159  	URL string
   160  	// Ref contains the ref to check out for bundling.
   161  	Ref plumbing.ReferenceName
   162  }
   163  
   164  // Bundle packages up the given git repo as a self-extracting container image based
   165  // on BaseImage and publishes it to tag.
   166  func Bundle(ctx context.Context, opts Options, tag name.Tag) (name.Digest, error) {
   167  	layer, err := bundle(ctx, opts)
   168  	if err != nil {
   169  		return name.Digest{}, err
   170  	}
   171  
   172  	return bundles.Map(ctx, kontext.BaseImage, tag, func(ctx context.Context, img v1.Image) (v1.Image, error) {
   173  		return mutate.AppendLayers(img, layer)
   174  	})
   175  }