github.com/cvmfs/docker-graphdriver@v0.0.0-20181206110523-155ec6df0521/repository-manager/lib/conversion.go (about)

     1  package lib
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"context"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"sync"
    15  
    16  	da "github.com/cvmfs/docker-graphdriver/repository-manager/docker-api"
    17  
    18  	"github.com/docker/docker/api/types"
    19  	"github.com/docker/docker/client"
    20  	log "github.com/sirupsen/logrus"
    21  )
    22  
    23  type ConversionResult int
    24  
    25  const (
    26  	ConversionNotFound = iota
    27  	ConversionMatch    = iota
    28  	ConversionNotMatch = iota
    29  )
    30  
    31  var subDirInsideRepo = ".layers"
    32  
    33  func ConvertWish(wish WishFriendly, convertAgain, forceDownload, convertSingularity bool) (err error) {
    34  
    35  	err = CreateCatalogIntoDir(wish.CvmfsRepo, subDirInsideRepo)
    36  	if err != nil {
    37  		LogE(err).WithFields(log.Fields{
    38  			"directory": subDirInsideRepo}).Error(
    39  			"Impossible to create subcatalog in super-directory.")
    40  	}
    41  	err = CreateCatalogIntoDir(wish.CvmfsRepo, ".flat")
    42  	if err != nil {
    43  		LogE(err).WithFields(log.Fields{
    44  			"directory": ".flat"}).Error(
    45  			"Impossible to create subcatalog in super-directory.")
    46  	}
    47  
    48  	outputImage, err := ParseImage(wish.OutputName)
    49  	outputImage.User = wish.UserOutput
    50  	if err != nil {
    51  		return
    52  	}
    53  	password, err := getPassword()
    54  	if err != nil {
    55  		return
    56  	}
    57  	inputImage, err := ParseImage(wish.InputName)
    58  	inputImage.User = wish.UserInput
    59  	if err != nil {
    60  		return
    61  	}
    62  	manifest, err := inputImage.GetManifest()
    63  	if err != nil {
    64  		return
    65  	}
    66  
    67  	alreadyConverted := AlreadyConverted(wish.CvmfsRepo, inputImage, manifest.Config.Digest)
    68  	Log().WithFields(log.Fields{"alreadyConverted": alreadyConverted}).Info(
    69  		"Already converted the image, skipping.")
    70  
    71  	switch alreadyConverted {
    72  
    73  	case ConversionMatch:
    74  		{
    75  			Log().Info("Already converted the image.")
    76  			if convertAgain == false {
    77  				return nil
    78  			}
    79  
    80  		}
    81  	}
    82  
    83  	layersChanell := make(chan downloadedLayer, 3)
    84  	manifestChanell := make(chan string, 1)
    85  	stopGettingLayers := make(chan bool, 1)
    86  	noErrorInConversion := make(chan bool, 1)
    87  
    88  	type LayerRepoLocation struct {
    89  		Digest   string
    90  		Location string //location does NOT need the prefix `/cvmfs`
    91  	}
    92  	layerRepoLocationChan := make(chan LayerRepoLocation, 3)
    93  	layerDigestChan := make(chan string, 3)
    94  	go func() {
    95  		noErrors := true
    96  		var wg sync.WaitGroup
    97  		defer func() {
    98  			wg.Wait()
    99  			close(layerRepoLocationChan)
   100  			close(layerDigestChan)
   101  		}()
   102  		defer func() {
   103  			noErrorInConversion <- noErrors
   104  			stopGettingLayers <- true
   105  			close(stopGettingLayers)
   106  		}()
   107  		cleanup := func(location string) {
   108  			Log().Info("Running clean up function deleting the last layer.")
   109  
   110  			err := ExecCommand("cvmfs_server", "abort", "-f", wish.CvmfsRepo).Start()
   111  			if err != nil {
   112  				LogE(err).Warning("Error in the abort command inside the cleanup function, this warning is usually normal")
   113  			}
   114  
   115  			err = ExecCommand("cvmfs_server", "ingest", "--delete", location, wish.CvmfsRepo).Start()
   116  			if err != nil {
   117  				LogE(err).Error("Error in the cleanup command")
   118  			}
   119  		}
   120  		for layer := range layersChanell {
   121  
   122  			Log().WithFields(log.Fields{"layer": layer.Name}).Info("Start Ingesting the file into CVMFS")
   123  			layerDigest := strings.Split(layer.Name, ":")[1]
   124  			layerPath := LayerRootfsPath(wish.CvmfsRepo, layerDigest)
   125  
   126  			var pathExists bool
   127  			if _, err := os.Stat(layerPath); os.IsNotExist(err) {
   128  				pathExists = false
   129  			} else {
   130  				pathExists = true
   131  			}
   132  
   133  			// need to run this into a goroutine to avoid a deadlock
   134  			wg.Add(1)
   135  			go func(layerName, layerLocation, layerDigest string) {
   136  				layerRepoLocationChan <- LayerRepoLocation{
   137  					Digest:   layerName,
   138  					Location: layerLocation}
   139  				layerDigestChan <- layerDigest
   140  				wg.Done()
   141  			}(layer.Name, layerPath, layerDigest)
   142  
   143  			if pathExists == false || forceDownload {
   144  
   145  				// need to create the "super-directory", those
   146  				// directory starting with 2 char prefix of the
   147  				// digest itself, and put a .cvmfscatalog files
   148  				// in it, if the directory still doesn't
   149  				// exists. Similarly we need a .cvmfscatalog in
   150  				// the layerfs directory, the one that host the
   151  				// whole layer
   152  
   153  				for _, dir := range []string{
   154  					filepath.Dir(filepath.Dir(TrimCVMFSRepoPrefix(layerPath))),
   155  					//TrimCVMFSRepoPrefix(layerPath)} {
   156  				} {
   157  
   158  					Log().WithFields(log.Fields{"catalogdirectory": dir}).Info("Working on CATALOGDIRECTORY")
   159  					err = CreateCatalogIntoDir(wish.CvmfsRepo, dir)
   160  					if err != nil {
   161  						LogE(err).WithFields(log.Fields{
   162  							"directory": dir}).Error(
   163  							"Impossible to create subcatalog in super-directory.")
   164  					} else {
   165  						Log().WithFields(log.Fields{
   166  							"directory": dir}).Info(
   167  							"Created subcatalog in directory")
   168  					}
   169  				}
   170  				err = ExecCommand("cvmfs_server", "ingest", "--catalog", "-t", layer.Path, "-b", TrimCVMFSRepoPrefix(layerPath), wish.CvmfsRepo).Start()
   171  
   172  				if err != nil {
   173  					LogE(err).WithFields(log.Fields{"layer": layer.Name}).Error("Some error in ingest the layer")
   174  					noErrors = false
   175  					cleanup(TrimCVMFSRepoPrefix(layerPath))
   176  					return
   177  				}
   178  				Log().WithFields(log.Fields{"layer": layer.Name}).Info("Finish Ingesting the file")
   179  			} else {
   180  				Log().WithFields(log.Fields{"layer": layer.Name}).Info("Skipping ingestion of layer, already exists")
   181  			}
   182  			//os.Remove(layer.Path)
   183  		}
   184  		Log().Info("Finished pushing the layers into CVMFS")
   185  	}()
   186  	// we create a temp directory for all the files needed, when this function finish we can remove the temp directory cleaning up
   187  	tmpDir, err := ioutil.TempDir("", "conversion")
   188  	if err != nil {
   189  		LogE(err).Error("Error in creating a temporary direcotry for all the files")
   190  		return
   191  	}
   192  	defer os.RemoveAll(tmpDir)
   193  
   194  	// this wil start to feed the above goroutine by writing into layersChanell
   195  	err = inputImage.GetLayers(layersChanell, manifestChanell, stopGettingLayers, tmpDir)
   196  
   197  	var singularity Singularity
   198  	if convertSingularity {
   199  		singularity, err = inputImage.DownloadSingularityDirectory(tmpDir)
   200  		if err != nil {
   201  			LogE(err).Error("Error in dowloading the singularity image")
   202  			return
   203  		}
   204  		defer os.RemoveAll(singularity.TempDirectory)
   205  	}
   206  	changes, _ := inputImage.GetChanges()
   207  
   208  	var wg sync.WaitGroup
   209  
   210  	layerLocations := make(map[string]string)
   211  	wg.Add(1)
   212  	go func() {
   213  		for layerLocation := range layerRepoLocationChan {
   214  			layerLocations[layerLocation.Digest] = layerLocation.Location
   215  		}
   216  		wg.Done()
   217  	}()
   218  
   219  	var layerDigests []string
   220  	wg.Add(1)
   221  	go func() {
   222  		for layerDigest := range layerDigestChan {
   223  			layerDigests = append(layerDigests, layerDigest)
   224  		}
   225  		wg.Done()
   226  	}()
   227  	wg.Wait()
   228  
   229  	thin, err := da.MakeThinImage(manifest, layerLocations, inputImage.WholeName())
   230  	if err != nil {
   231  		return
   232  	}
   233  
   234  	thinJson, err := json.MarshalIndent(thin, "", "  ")
   235  	if err != nil {
   236  		return
   237  	}
   238  	fmt.Println(string(thinJson))
   239  	var imageTar bytes.Buffer
   240  	tarFile := tar.NewWriter(&imageTar)
   241  	header := &tar.Header{Name: "thin.json", Mode: 0644, Size: int64(len(thinJson))}
   242  	err = tarFile.WriteHeader(header)
   243  	if err != nil {
   244  		return
   245  	}
   246  	_, err = tarFile.Write(thinJson)
   247  	if err != nil {
   248  		return
   249  	}
   250  	err = tarFile.Close()
   251  	if err != nil {
   252  		return
   253  	}
   254  
   255  	dockerClient, err := client.NewClientWithOpts(client.WithVersion("1.19"))
   256  	if err != nil {
   257  		return
   258  	}
   259  
   260  	image := types.ImageImportSource{
   261  		Source:     bytes.NewBuffer(imageTar.Bytes()),
   262  		SourceName: "-",
   263  	}
   264  	importOptions := types.ImageImportOptions{
   265  		Tag:     outputImage.Tag,
   266  		Message: "",
   267  		Changes: changes,
   268  	}
   269  	importResult, err := dockerClient.ImageImport(
   270  		context.Background(),
   271  		image,
   272  		outputImage.GetSimpleName(),
   273  		importOptions)
   274  	if err != nil {
   275  		LogE(err).Error("Error in image import")
   276  		return
   277  	}
   278  	defer importResult.Close()
   279  	Log().Info("Created the image in the local docker daemon")
   280  
   281  	// is necessary this mechanism to pass the authentication to the
   282  	// dockers even if the documentation says otherwise
   283  	authStruct := struct {
   284  		Username string
   285  		Password string
   286  	}{
   287  		Username: outputImage.User,
   288  		Password: password,
   289  	}
   290  	authBytes, _ := json.Marshal(authStruct)
   291  	authCredential := base64.StdEncoding.EncodeToString(authBytes)
   292  	pushOptions := types.ImagePushOptions{
   293  		RegistryAuth: authCredential,
   294  	}
   295  
   296  	res, err := dockerClient.ImagePush(
   297  		context.Background(),
   298  		outputImage.GetSimpleName(),
   299  		pushOptions)
   300  	if err != nil {
   301  		return
   302  	}
   303  	b, _ := ioutil.ReadAll(res)
   304  	fmt.Println(string(b))
   305  	defer res.Close()
   306  	// here is possible to use the result of the above ReadAll to have
   307  	// informantion about the status of the upload.
   308  	_, err = ioutil.ReadAll(res)
   309  	if err != nil {
   310  		return
   311  	}
   312  	Log().Info("Finish pushing the image to the registry")
   313  	// we wait for the goroutines to finish
   314  	// and if there was no error we add everything to the converted table
   315  	noErrorInConversionValue := <-noErrorInConversion
   316  
   317  	// here we can launch the ingestion for the singularity image
   318  	if convertSingularity {
   319  		err = singularity.IngestIntoCVMFS(wish.CvmfsRepo)
   320  		if err != nil {
   321  			LogE(err).Error("Error in ingesting the singularity image into the CVMFS repository")
   322  			noErrorInConversionValue = false
   323  		}
   324  	}
   325  
   326  	err = SaveLayersBacklink(wish.CvmfsRepo, inputImage, layerDigests)
   327  	if err != nil {
   328  		LogE(err).Error("Error in saving the backlinks")
   329  		noErrorInConversionValue = false
   330  	}
   331  
   332  	if noErrorInConversionValue {
   333  		manifestPath := filepath.Join(".metadata", inputImage.GetSimpleName(), "manifest.json")
   334  		errIng := IngestIntoCVMFS(wish.CvmfsRepo, manifestPath, <-manifestChanell)
   335  		if err != nil {
   336  			LogE(errIng).Error("Error in storing the manifest in the repository")
   337  		}
   338  		var errRemoveSchedule error
   339  		if alreadyConverted == ConversionNotMatch {
   340  			Log().Info("Image already converted, but it does not match the manifest, adding it to the remove scheduler")
   341  			errRemoveSchedule = AddManifestToRemoveScheduler(wish.CvmfsRepo, manifest)
   342  			if errRemoveSchedule != nil {
   343  				Log().Warning("Error in adding the image to the remove schedule")
   344  			}
   345  		}
   346  		if errIng == nil && errRemoveSchedule == nil {
   347  			Log().Info("Conversion completed")
   348  		}
   349  		return
   350  	} else {
   351  		Log().Warn("Some error during the conversion, we are not storing it into the database")
   352  		return
   353  	}
   354  }
   355  
   356  func AlreadyConverted(CVMFSRepo string, img Image, reference string) ConversionResult {
   357  	path := filepath.Join("/", "cvmfs", CVMFSRepo, ".metadata", img.GetSimpleName(), "manifest.json")
   358  
   359  	fmt.Println(path)
   360  	manifestStat, err := os.Stat(path)
   361  	if os.IsNotExist(err) {
   362  		Log().Info("Manifest not existing")
   363  		return ConversionNotFound
   364  	}
   365  	if !manifestStat.Mode().IsRegular() {
   366  		Log().Info("Manifest not a regular file")
   367  		return ConversionNotFound
   368  	}
   369  
   370  	manifestFile, err := os.Open(path)
   371  	if err != nil {
   372  		Log().Info("Error in opening the manifest")
   373  		return ConversionNotFound
   374  	}
   375  	defer manifestFile.Close()
   376  
   377  	bytes, _ := ioutil.ReadAll(manifestFile)
   378  
   379  	var manifest da.Manifest
   380  	err = json.Unmarshal(bytes, &manifest)
   381  	if err != nil {
   382  		LogE(err).Warning("Error in unmarshaling the manifest")
   383  		return ConversionNotFound
   384  	}
   385  	fmt.Printf("%s == %s\n", manifest.Config.Digest, reference)
   386  	if manifest.Config.Digest == reference {
   387  		return ConversionMatch
   388  	}
   389  	return ConversionNotMatch
   390  }
   391  
   392  func getPassword() (string, error) {
   393  	envVar := "DOCKER2CVMFS_DOCKER_REGISTRY_PASS"
   394  	pass := os.Getenv(envVar)
   395  	if pass == "" {
   396  		err := fmt.Errorf(
   397  			"Env variable (%s) storing the password to access the docker registry is not set", envVar)
   398  		return "", err
   399  	}
   400  	return pass, nil
   401  }