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

     1  package lib
     2  
     3  import (
     4  	"compress/gzip"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/docker/docker/image"
    17  	"github.com/olekukonko/tablewriter"
    18  	log "github.com/sirupsen/logrus"
    19  
    20  	da "github.com/cvmfs/docker-graphdriver/repository-manager/docker-api"
    21  )
    22  
    23  type ManifestRequest struct {
    24  	Image    Image
    25  	Password string
    26  }
    27  
    28  type Image struct {
    29  	Id         int
    30  	User       string
    31  	Scheme     string
    32  	Registry   string
    33  	Repository string
    34  	Tag        string
    35  	Digest     string
    36  	IsThin     bool
    37  	Manifest   *da.Manifest
    38  }
    39  
    40  func (i Image) GetSimpleName() string {
    41  	name := fmt.Sprintf("%s/%s", i.Registry, i.Repository)
    42  	if i.Tag == "" {
    43  		return name
    44  	} else {
    45  		return name + ":" + i.Tag
    46  	}
    47  }
    48  
    49  func (i Image) WholeName() string {
    50  	root := fmt.Sprintf("%s://%s/%s", i.Scheme, i.Registry, i.Repository)
    51  	if i.Tag != "" {
    52  		root = fmt.Sprintf("%s:%s", root, i.Tag)
    53  	}
    54  	if i.Digest != "" {
    55  		root = fmt.Sprintf("%s@%s", root, i.Digest)
    56  	}
    57  	return root
    58  }
    59  
    60  func (i Image) GetManifestUrl() string {
    61  	url := fmt.Sprintf("%s://%s/v2/%s/manifests/", i.Scheme, i.Registry, i.Repository)
    62  	if i.Digest != "" {
    63  		url = fmt.Sprintf("%s%s", url, i.Digest)
    64  	} else {
    65  		url = fmt.Sprintf("%s%s", url, i.Tag)
    66  	}
    67  	return url
    68  }
    69  
    70  func (i Image) GetReference() string {
    71  	if i.Digest == "" && i.Tag != "" {
    72  		return ":" + i.Tag
    73  	}
    74  	if i.Digest != "" && i.Tag == "" {
    75  		return "@" + i.Digest
    76  	}
    77  	if i.Digest != "" && i.Tag != "" {
    78  		return ":" + i.Tag + "@" + i.Digest
    79  	}
    80  	panic("Image wrong format, missing both tag and digest")
    81  }
    82  
    83  func (i Image) GetSimpleReference() string {
    84  	if i.Tag != "" {
    85  		return i.Tag
    86  	}
    87  	if i.Digest != "" {
    88  		return i.Digest
    89  	}
    90  	panic("Image wrong format, missing both tag and digest")
    91  }
    92  
    93  func (img Image) PrintImage(machineFriendly, csv_header bool) {
    94  	if machineFriendly {
    95  		if csv_header {
    96  			fmt.Printf("name,user,scheme,registry,repository,tag,digest,is_thin\n")
    97  		}
    98  		fmt.Printf("%s,%s,%s,%s,%s,%s,%s,%s\n",
    99  			img.WholeName(), img.User, img.Scheme,
   100  			img.Registry, img.Repository,
   101  			img.Tag, img.Digest,
   102  			fmt.Sprint(img.IsThin))
   103  	} else {
   104  		table := tablewriter.NewWriter(os.Stdout)
   105  		table.SetAlignment(tablewriter.ALIGN_LEFT)
   106  		table.SetHeader([]string{"Key", "Value"})
   107  		table.Append([]string{"Name", img.WholeName()})
   108  		table.Append([]string{"User", img.User})
   109  		table.Append([]string{"Scheme", img.Scheme})
   110  		table.Append([]string{"Registry", img.Registry})
   111  		table.Append([]string{"Repository", img.Repository})
   112  		table.Append([]string{"Tag", img.Tag})
   113  		table.Append([]string{"Digest", img.Digest})
   114  		var is_thin string
   115  		if img.IsThin {
   116  			is_thin = "true"
   117  		} else {
   118  			is_thin = "false"
   119  		}
   120  		table.Append([]string{"IsThin", is_thin})
   121  		table.Render()
   122  	}
   123  }
   124  
   125  func (img Image) GetManifest() (da.Manifest, error) {
   126  	if img.Manifest != nil {
   127  		return *img.Manifest, nil
   128  	}
   129  	bytes, err := img.getByteManifest()
   130  	if err != nil {
   131  		return da.Manifest{}, err
   132  	}
   133  	var manifest da.Manifest
   134  	err = json.Unmarshal(bytes, &manifest)
   135  	if err != nil {
   136  		return manifest, err
   137  	}
   138  	if reflect.DeepEqual(da.Manifest{}, manifest) {
   139  		return manifest, fmt.Errorf("Got empty manifest")
   140  	}
   141  	img.Manifest = &manifest
   142  	return manifest, nil
   143  }
   144  
   145  func (img Image) GetChanges() (changes []string, err error) {
   146  	user := img.User
   147  	pass, err := getPassword()
   148  	if err != nil {
   149  		LogE(err).Warning("Unable to get the credential for downloading the configuration blog, trying anonymously")
   150  		user = ""
   151  		pass = ""
   152  	}
   153  
   154  	changes = []string{"ENV CVMFS_IMAGE true"}
   155  	manifest, err := img.GetManifest()
   156  	if err != nil {
   157  		LogE(err).Warning("Impossible to retrieve the manifest of the image, not changes set")
   158  		return
   159  	}
   160  	configUrl := fmt.Sprintf("%s://%s/v2/%s/blobs/%s",
   161  		img.Scheme, img.Registry, img.Repository, manifest.Config.Digest)
   162  	token, err := firstRequestForAuth(configUrl, user, pass)
   163  	if err != nil {
   164  		LogE(err).Warning("Impossible to retrieve the token for getting the changes from the repository, not changes set")
   165  		return
   166  	}
   167  	client := &http.Client{}
   168  	req, err := http.NewRequest("GET", configUrl, nil)
   169  	if err != nil {
   170  		LogE(err).Warning("Impossible to create a request for getting the changes no chnages set.")
   171  		return
   172  	}
   173  	req.Header.Set("Authorization", token)
   174  	req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
   175  
   176  	resp, err := client.Do(req)
   177  	defer resp.Body.Close()
   178  	body, err := ioutil.ReadAll(resp.Body)
   179  	if err != nil {
   180  		LogE(err).Warning("Error in reading the body from the configuration, no change set")
   181  		return
   182  	}
   183  
   184  	var config image.Image
   185  	err = json.Unmarshal(body, &config)
   186  	if err != nil {
   187  		LogE(err).Warning("Error in unmarshaling the configuration of the image")
   188  		return
   189  	}
   190  	env := config.Config.Env
   191  
   192  	if len(env) > 0 {
   193  		for _, e := range env {
   194  			envs := strings.SplitN(e, "=", 2)
   195  			if len(envs) != 2 {
   196  				continue
   197  			}
   198  			change := fmt.Sprintf("ENV %s=\"%s\"", envs[0], envs[1])
   199  			changes = append(changes, change)
   200  		}
   201  	}
   202  
   203  	cmd := config.Config.Cmd
   204  
   205  	if len(cmd) > 0 {
   206  		for _, c := range cmd {
   207  			changes = append(changes, fmt.Sprintf("CMD %s", c))
   208  		}
   209  	}
   210  
   211  	return
   212  }
   213  
   214  func (img Image) GetSingularityLocation() string {
   215  	return fmt.Sprintf("docker://%s/%s%s", img.Registry, img.Repository, img.GetReference())
   216  }
   217  
   218  func GetSingularityPathFromManifest(manifest da.Manifest) string {
   219  	digest := strings.Split(manifest.Config.Digest, ":")[1]
   220  	return filepath.Join(".flat", digest[0:2], digest)
   221  }
   222  
   223  // here is where in the FS we are going to store the singularity image
   224  func (img Image) GetSingularityPath() (string, error) {
   225  	manifest, err := img.GetManifest()
   226  	if err != nil {
   227  		LogE(err).Error("Error in getting the manifest to figureout the singularity path")
   228  		return "", err
   229  	}
   230  	return GetSingularityPathFromManifest(manifest), nil
   231  }
   232  
   233  type Singularity struct {
   234  	Image         *Image
   235  	TempDirectory string
   236  }
   237  
   238  func (img Image) DownloadSingularityDirectory(rootPath string) (sing Singularity, err error) {
   239  	dir, err := ioutil.TempDir(rootPath, "singularity_buffer")
   240  	if err != nil {
   241  		LogE(err).Error("Error in creating temporary directory for singularity")
   242  		return
   243  
   244  	}
   245  	singularityTempCache, err := ioutil.TempDir("", "tempDirSingularityCache")
   246  	if err != nil {
   247  		LogE(err).Error("Error in creating temporary directory for singularity cache")
   248  		return
   249  	}
   250  	defer os.RemoveAll(singularityTempCache)
   251  	err = ExecCommand("singularity", "build", "--sandbox", dir, img.GetSingularityLocation()).Env(
   252  		"SINGULARITY_CACHEDIR", singularityTempCache).Start()
   253  	if err != nil {
   254  		LogE(err).Error("Error in downloading the singularity image")
   255  		return
   256  	}
   257  
   258  	Log().Info("Successfully download the singularity image")
   259  	return Singularity{Image: &img, TempDirectory: dir}, nil
   260  }
   261  
   262  func (s Singularity) IngestIntoCVMFS(CVMFSRepo string) error {
   263  	symlinkPath := filepath.Join(s.Image.Registry, s.Image.Repository+":"+s.Image.GetSimpleReference())
   264  	singularityPath, err := s.Image.GetSingularityPath()
   265  	if err != nil {
   266  		LogE(err).Error(
   267  			"Error in ingesting singularity image into CVMFS, unable to get where save the image")
   268  		return err
   269  	}
   270  
   271  	err = IngestIntoCVMFS(CVMFSRepo, singularityPath, s.TempDirectory)
   272  	if err != nil {
   273  		// if there is an error ingest does not remove the folder.
   274  		// we do want to remove the folder anyway
   275  		os.RemoveAll(s.TempDirectory)
   276  		return err
   277  	}
   278  
   279  	for _, dir := range []string{
   280  		filepath.Dir(singularityPath),
   281  		singularityPath} {
   282  
   283  		err = CreateCatalogIntoDir(CVMFSRepo, dir)
   284  		if err != nil {
   285  			LogE(err).WithFields(log.Fields{
   286  				"directory": dir}).Error(
   287  				"Impossible to create subcatalog in super-directory.")
   288  		} else {
   289  			Log().WithFields(log.Fields{
   290  				"directory": dir}).Info(
   291  				"Created subcatalog in directory")
   292  		}
   293  
   294  	}
   295  
   296  	// lets create the symlink
   297  	err = CreateSymlinkIntoCVMFS(CVMFSRepo, symlinkPath, singularityPath)
   298  	if err != nil {
   299  		LogE(err).Error("Error in creating the symlink for the singularity Image")
   300  		return err
   301  	}
   302  	return nil
   303  }
   304  
   305  func (img Image) getByteManifest() ([]byte, error) {
   306  	pass, err := getPassword()
   307  	if err != nil {
   308  		LogE(err).Warning("Unable to retrieve the password, trying to get the manifest anonymously.")
   309  		return img.getAnonymousManifest()
   310  	}
   311  	return img.getManifestWithPassword(pass)
   312  }
   313  
   314  func (img Image) getAnonymousManifest() ([]byte, error) {
   315  	return getManifestWithUsernameAndPassword(img, "", "")
   316  }
   317  
   318  func (img Image) getManifestWithPassword(password string) ([]byte, error) {
   319  	return getManifestWithUsernameAndPassword(img, img.User, password)
   320  }
   321  
   322  func getManifestWithUsernameAndPassword(img Image, user, pass string) ([]byte, error) {
   323  
   324  	url := img.GetManifestUrl()
   325  
   326  	token, err := firstRequestForAuth(url, user, pass)
   327  	if err != nil {
   328  		LogE(err).Error("Error in getting the authentication token")
   329  		return nil, err
   330  	}
   331  
   332  	client := &http.Client{}
   333  	req, err := http.NewRequest("GET", url, nil)
   334  	if err != nil {
   335  		LogE(err).Error("Impossible to create a HTTP request")
   336  		return nil, err
   337  	}
   338  
   339  	req.Header.Set("Authorization", token)
   340  	req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
   341  
   342  	resp, err := client.Do(req)
   343  	if err != nil {
   344  		LogE(err).Error("Error in making the HTTP request")
   345  		return nil, err
   346  	}
   347  	defer resp.Body.Close()
   348  	body, err := ioutil.ReadAll(resp.Body)
   349  	if err != nil {
   350  		LogE(err).Error("Error in reading the second http response")
   351  		return nil, err
   352  	}
   353  	return body, nil
   354  }
   355  
   356  func firstRequestForAuth(url, user, pass string) (token string, err error) {
   357  	resp, err := http.Get(url)
   358  	if err != nil {
   359  		LogE(err).Error("Error in making the first request for auth")
   360  		return "", err
   361  	}
   362  	defer resp.Body.Close()
   363  	if resp.StatusCode != 401 {
   364  		log.WithFields(log.Fields{
   365  			"status code": resp.StatusCode,
   366  		}).Info("Expected status code 401, print body anyway.")
   367  		body, err := ioutil.ReadAll(resp.Body)
   368  		if err != nil {
   369  			LogE(err).Error("Error in reading the first http response")
   370  		}
   371  		fmt.Println(string(body))
   372  		return "", err
   373  	}
   374  	WwwAuthenticate := resp.Header["Www-Authenticate"][0]
   375  	token, err = requestAuthToken(WwwAuthenticate, user, pass)
   376  	if err != nil {
   377  		LogE(err).Error("Error in getting the authentication token")
   378  		return "", err
   379  	}
   380  	return token, nil
   381  
   382  }
   383  
   384  func getLayerUrl(img Image, layer da.Layer) string {
   385  	return fmt.Sprintf("%s://%s/v2/%s/blobs/%s",
   386  		img.Scheme, img.Registry, img.Repository, layer.Digest)
   387  }
   388  
   389  type downloadedLayer struct {
   390  	Name string
   391  	Path string
   392  }
   393  
   394  func (img Image) GetLayers(layersChan chan<- downloadedLayer, manifestChan chan<- string, stopGettingLayers <-chan bool, rootPath string) error {
   395  	defer close(layersChan)
   396  	defer close(manifestChan)
   397  
   398  	user := img.User
   399  	pass, err := getPassword()
   400  	if err != nil {
   401  		LogE(err).Warning("Unable to retrieve the password, trying to get the layers anonymously.")
   402  		user = ""
   403  		pass = ""
   404  	}
   405  
   406  	// then we try to get the manifest from our database
   407  	manifest, err := img.GetManifest()
   408  	if err != nil {
   409  		LogE(err).Warn("Error in getting the manifest")
   410  		return err
   411  	}
   412  
   413  	// A first request is used to get the authentication
   414  	firstLayer := manifest.Layers[0]
   415  	layerUrl := getLayerUrl(img, firstLayer)
   416  	token, err := firstRequestForAuth(layerUrl, user, pass)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	var wg sync.WaitGroup
   422  	defer wg.Wait()
   423  	// at this point we iterate each layer and we download it.
   424  	for _, layer := range manifest.Layers {
   425  		wg.Add(1)
   426  		go func(layer da.Layer) {
   427  			defer wg.Done()
   428  			Log().WithFields(log.Fields{"layer": layer.Digest}).Info("Start working on layer")
   429  			toSend, err := img.downloadLayer(layer, token, rootPath)
   430  			if err != nil {
   431  				LogE(err).Error("Error in downloading a layer")
   432  				return
   433  			}
   434  			layersChan <- toSend
   435  		}(layer)
   436  	}
   437  
   438  	// finally we marshal the manifest and store it into a file
   439  	manifestBytes, err := json.Marshal(manifest)
   440  	if err != nil {
   441  		LogE(err).Error("Error in marshaling the manifest")
   442  		return err
   443  	}
   444  	manifestPath := filepath.Join(rootPath, "manifest.json")
   445  	err = ioutil.WriteFile(manifestPath, manifestBytes, 0666)
   446  	if err != nil {
   447  		LogE(err).Error("Error in writing the manifest to file")
   448  		return err
   449  	}
   450  	// ship the manifest file
   451  	manifestChan <- manifestPath
   452  	return nil
   453  }
   454  
   455  func (img Image) downloadLayer(layer da.Layer, token, rootPath string) (toSend downloadedLayer, err error) {
   456  	user := img.User
   457  	pass, err := getPassword()
   458  	if err != nil {
   459  		LogE(err).Warning("Unable to retrieve the password, trying to get the layers anonymously.")
   460  		user = ""
   461  		pass = ""
   462  	}
   463  	layerUrl := getLayerUrl(img, layer)
   464  	if token == "" {
   465  		token, err = firstRequestForAuth(layerUrl, user, pass)
   466  		if err != nil {
   467  			return
   468  		}
   469  	}
   470  	for i := 0; i <= 5; i++ {
   471  		err = nil
   472  		client := &http.Client{}
   473  		req, err := http.NewRequest("GET", layerUrl, nil)
   474  		if err != nil {
   475  			LogE(err).Error("Impossible to create the HTTP request.")
   476  			break
   477  		}
   478  		req.Header.Set("Authorization", token)
   479  		resp, err := client.Do(req)
   480  		Log().WithFields(log.Fields{"layer": layer.Digest}).Info("Make request for layer")
   481  		if err != nil {
   482  			break
   483  		}
   484  		if 200 <= resp.StatusCode && resp.StatusCode < 300 {
   485  
   486  			gread, err := gzip.NewReader(resp.Body)
   487  			if err != nil {
   488  				LogE(err).Warning("Error in creating the zip to unzip the layer")
   489  				continue
   490  			}
   491  
   492  			tmpFile, err := ioutil.TempFile(rootPath, "layer.*.tar")
   493  			if err != nil {
   494  				LogE(err).Warning("Error in creating buffer temp layer")
   495  				continue
   496  			}
   497  			defer tmpFile.Close()
   498  
   499  			_, err = io.Copy(tmpFile, gread)
   500  			if err != nil {
   501  				LogE(err).Warning("Error in copying the layer into the temp file")
   502  				os.Remove(tmpFile.Name())
   503  				continue
   504  			}
   505  
   506  			toSend = downloadedLayer{Name: layer.Digest, Path: tmpFile.Name()}
   507  			return toSend, nil
   508  
   509  		} else {
   510  			Log().Warning("Received status code ", resp.StatusCode)
   511  			err = fmt.Errorf("Layer not received, status code: %s", resp.StatusCode)
   512  		}
   513  	}
   514  	return
   515  
   516  }
   517  
   518  func parseBearerToken(token string) (realm string, options map[string]string, err error) {
   519  	options = make(map[string]string)
   520  	args := token[7:]
   521  	keyValue := strings.Split(args, ",")
   522  	for _, kv := range keyValue {
   523  		splitted := strings.Split(kv, "=")
   524  		if len(splitted) != 2 {
   525  			err = fmt.Errorf("Wrong formatting of the token")
   526  			return
   527  		}
   528  		splitted[1] = strings.Trim(splitted[1], `"`)
   529  		if splitted[0] == "realm" {
   530  			realm = splitted[1]
   531  		} else {
   532  			options[splitted[0]] = splitted[1]
   533  		}
   534  	}
   535  	return
   536  }
   537  
   538  func requestAuthToken(token, user, pass string) (authToken string, err error) {
   539  	realm, options, err := parseBearerToken(token)
   540  	if err != nil {
   541  		return
   542  	}
   543  	req, err := http.NewRequest("GET", realm, nil)
   544  	if err != nil {
   545  		return
   546  	}
   547  
   548  	query := req.URL.Query()
   549  	for k, v := range options {
   550  		query.Add(k, v)
   551  	}
   552  	if user != "" && pass != "" {
   553  		query.Add("offline_token", "true")
   554  		req.SetBasicAuth(user, pass)
   555  	}
   556  	req.URL.RawQuery = query.Encode()
   557  
   558  	client := &http.Client{}
   559  	resp, err := client.Do(req)
   560  	defer resp.Body.Close()
   561  
   562  	if resp.StatusCode >= 400 {
   563  		err = fmt.Errorf("Authorization error %s", resp.Status)
   564  		return
   565  	}
   566  
   567  	var jsonResp map[string]interface{}
   568  	err = json.NewDecoder(resp.Body).Decode(&jsonResp)
   569  	if err != nil {
   570  		return
   571  	}
   572  	authTokenInterface, ok := jsonResp["token"]
   573  	if ok {
   574  		authToken = "Bearer " + authTokenInterface.(string)
   575  	} else {
   576  		err = fmt.Errorf("Didn't get the token key from the server")
   577  		return
   578  	}
   579  	return
   580  }