github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/artifactory/utils/docker/buildinfo.go (about)

     1  package docker
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"path"
     9  	"strings"
    10  
    11  	buildutils "github.com/jfrog/jfrog-cli-go/artifactory/utils"
    12  	"github.com/jfrog/jfrog-client-go/artifactory"
    13  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
    14  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    15  	"github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    16  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    17  	"github.com/jfrog/jfrog-client-go/utils/log"
    18  )
    19  
    20  const (
    21  	Pull                      CommandType = "pull"
    22  	Push                      CommandType = "push"
    23  	ForeignLayerMediaType     string      = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
    24  	ImageNotFoundErrorMessage string      = "Could not find docker image in Artifactory, expecting image ID: %s"
    25  )
    26  
    27  // Docker image build info builder.
    28  type Builder interface {
    29  	Build(module string) (*buildinfo.BuildInfo, error)
    30  }
    31  
    32  // Create instance of docker build info builder.
    33  func BuildInfoBuilder(image Image, repository, buildName, buildNumber string, serviceManager *artifactory.ArtifactoryServicesManager, commandType CommandType) Builder {
    34  	builder := &buildInfoBuilder{}
    35  	builder.image = image
    36  	builder.repository = repository
    37  	builder.buildName = buildName
    38  	builder.buildNumber = buildNumber
    39  	builder.serviceManager = serviceManager
    40  	builder.commandType = commandType
    41  	return builder
    42  }
    43  
    44  type buildInfoBuilder struct {
    45  	image          Image
    46  	repository     string
    47  	buildName      string
    48  	buildNumber    string
    49  	serviceManager *artifactory.ArtifactoryServicesManager
    50  
    51  	// internal fields
    52  	imageId      string
    53  	layers       []utils.ResultItem
    54  	artifacts    []buildinfo.Artifact
    55  	dependencies []buildinfo.Dependency
    56  	commandType  CommandType
    57  }
    58  
    59  // Create build info for docker image.
    60  func (builder *buildInfoBuilder) Build(module string) (*buildinfo.BuildInfo, error) {
    61  	var err error
    62  	builder.imageId, err = builder.image.Id()
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	err = builder.updateArtifactsAndDependencies()
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	// Set build properties only when pushing image.
    73  	if builder.commandType == Push {
    74  		_, err = builder.setBuildProperties()
    75  		if err != nil {
    76  			return nil, err
    77  		}
    78  	}
    79  
    80  	return builder.createBuildInfo(module)
    81  }
    82  
    83  // Search, validate and create artifacts and dependencies of docker image.
    84  func (builder *buildInfoBuilder) updateArtifactsAndDependencies() error {
    85  	// Search for all the image layer to get the local path inside Artifactory (supporting virtual repos).
    86  	searchResults, err := builder.getImageLayersFromArtifactory()
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	manifest, manifestArtifact, manifestDependency, err := getManifest(builder.imageId, searchResults, builder.serviceManager)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	configLayer, configLayerArtifact, configLayerDependency, err := getConfigLayer(builder.imageId, searchResults, builder.serviceManager)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	if builder.commandType == Push {
   102  		return builder.handlePush(manifestArtifact, configLayerArtifact, manifest, configLayer, searchResults)
   103  	}
   104  
   105  	return builder.handlePull(manifestDependency, configLayerDependency, manifest, searchResults)
   106  }
   107  
   108  // First we will try to get assuming using a reverse proxy (sub domain or port methods).
   109  // If fails, we will try the repository path (proxy-less).
   110  func (builder *buildInfoBuilder) getImageLayersFromArtifactory() (map[string]utils.ResultItem, error) {
   111  	var searchResults map[string]utils.ResultItem
   112  	imagePath := builder.image.Path()
   113  
   114  	// Search layers - assuming reverse proxy.
   115  	searchResults, err := searchImageLayers(builder, path.Join(builder.repository, imagePath, "*"), builder.serviceManager)
   116  	if err != nil || searchResults != nil {
   117  		return searchResults, err
   118  	}
   119  
   120  	// Search layers - assuming proxy-less (repository path).
   121  	// Need to remove the "/" from the image path.
   122  	searchResults, err = searchImageLayers(builder, path.Join(imagePath[1:], "*"), builder.serviceManager)
   123  	if err != nil || searchResults != nil {
   124  		return searchResults, err
   125  	}
   126  
   127  	if builder.commandType == Push {
   128  		return nil, errorutils.CheckError(errors.New(fmt.Sprintf(ImageNotFoundErrorMessage, builder.imageId)))
   129  	}
   130  
   131  	// If image path includes more than 3 slashes, Artifactory doesn't store this image under 'library',
   132  	// thus we should not look further.
   133  	if strings.Count(imagePath, "/") > 3 {
   134  		return nil, errorutils.CheckError(errors.New(fmt.Sprintf(ImageNotFoundErrorMessage, builder.imageId)))
   135  	}
   136  
   137  	// Assume reverse proxy - this time with 'library' as part of the path.
   138  	searchResults, err = searchImageLayers(builder, path.Join(builder.repository, "library", imagePath, "*"), builder.serviceManager)
   139  	if err != nil || searchResults != nil {
   140  		return searchResults, err
   141  	}
   142  
   143  	// Assume proxy-less - this time with 'library' as part of the path.
   144  	searchResults, err = searchImageLayers(builder, path.Join(builder.buildReverseProxyPathWithLibrary(), "*"), builder.serviceManager)
   145  	if err != nil || searchResults != nil {
   146  		return searchResults, err
   147  	}
   148  
   149  	// Image layers not found.
   150  	return nil, errorutils.CheckError(errors.New(fmt.Sprintf(ImageNotFoundErrorMessage, builder.imageId)))
   151  }
   152  
   153  func (builder *buildInfoBuilder) buildReverseProxyPathWithLibrary() string {
   154  	endOfRepoNameIndex := strings.Index(builder.image.Path()[1:], "/")
   155  	return path.Join(builder.repository, "library", builder.image.Path()[endOfRepoNameIndex+1:])
   156  }
   157  
   158  func (builder *buildInfoBuilder) handlePull(manifestDependency, configLayerDependency buildinfo.Dependency, imageManifest *manifest, searchResults map[string]utils.ResultItem) error {
   159  	// Add dependencies.
   160  	builder.dependencies = append(builder.dependencies, manifestDependency)
   161  	builder.dependencies = append(builder.dependencies, configLayerDependency)
   162  
   163  	// Add image layers as dependencies.
   164  	for i := 0; i < len(imageManifest.Layers); i++ {
   165  		layerFileName := digestToLayer(imageManifest.Layers[i].Digest)
   166  		item, layerExists := searchResults[layerFileName]
   167  		if !layerExists {
   168  			// Check if layer marker exists in Artifactory.
   169  			item, layerExists = searchResults[layerFileName+".marker"]
   170  			if !layerExists {
   171  				err := builder.handleMissingLayer(imageManifest.Layers[i].MediaType, layerFileName)
   172  				if err != nil {
   173  					return err
   174  				}
   175  				continue
   176  			}
   177  		}
   178  		builder.dependencies = append(builder.dependencies, item.ToDependency())
   179  	}
   180  	return nil
   181  }
   182  
   183  func (builder *buildInfoBuilder) handlePush(manifestArtifact, configLayerArtifact buildinfo.Artifact, imageManifest *manifest, configurationLayer *configLayer, searchResults map[string]utils.ResultItem) error {
   184  	// Add artifacts
   185  	builder.artifacts = append(builder.artifacts, manifestArtifact)
   186  	builder.artifacts = append(builder.artifacts, configLayerArtifact)
   187  	// Add layers
   188  	builder.layers = append(builder.layers, searchResults["manifest.json"])
   189  	builder.layers = append(builder.layers, searchResults[digestToLayer(builder.imageId)])
   190  
   191  	// Add image layers as artifacts and dependencies.
   192  	for i := 0; i < configurationLayer.getNumberLayers(); i++ {
   193  		layerFileName := digestToLayer(imageManifest.Layers[i].Digest)
   194  		item, layerExists := searchResults[layerFileName]
   195  		if !layerExists {
   196  			err := builder.handleMissingLayer(imageManifest.Layers[i].MediaType, layerFileName)
   197  			if err != nil {
   198  				return err
   199  			}
   200  			continue
   201  		}
   202  		// Decide if the layer is also a dependency.
   203  		if i < configurationLayer.getNumberOfDependentLayers() {
   204  			builder.dependencies = append(builder.dependencies, item.ToDependency())
   205  		}
   206  
   207  		builder.artifacts = append(builder.artifacts, item.ToArtifact())
   208  		builder.layers = append(builder.layers, item)
   209  	}
   210  	return nil
   211  }
   212  
   213  func (builder *buildInfoBuilder) handleMissingLayer(layerMediaType, layerFileName string) error {
   214  	// Allow missing layer to be of a foreign type.
   215  	if layerMediaType == ForeignLayerMediaType {
   216  		log.Info(fmt.Sprintf("Foreign layer: %s is missing in Artifactory and therefore will not be added to the build-info.", layerFileName))
   217  		return nil
   218  	}
   219  
   220  	return errorutils.CheckError(errors.New("Could not find layer: " + layerFileName + " in Artifactory"))
   221  }
   222  
   223  // Set build properties on docker image layers in Artifactory.
   224  func (builder *buildInfoBuilder) setBuildProperties() (int, error) {
   225  	props, err := buildutils.CreateBuildProperties(builder.buildName, builder.buildNumber)
   226  	if err != nil {
   227  		return 0, err
   228  	}
   229  	return builder.serviceManager.SetProps(services.PropsParams{Items: builder.layers, Props: props})
   230  }
   231  
   232  // Create docker build info
   233  func (builder *buildInfoBuilder) createBuildInfo(module string) (*buildinfo.BuildInfo, error) {
   234  	imageProperties := map[string]string{}
   235  	imageProperties["docker.image.id"] = builder.imageId
   236  	imageProperties["docker.image.tag"] = builder.image.Tag()
   237  
   238  	parentId, err := builder.image.ParentId()
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	if parentId != "" {
   243  		imageProperties["docker.image.parent"] = parentId
   244  	}
   245  
   246  	if module == "" {
   247  		module = builder.image.Name()
   248  	}
   249  	buildInfo := &buildinfo.BuildInfo{Modules: []buildinfo.Module{{
   250  		Id:           module,
   251  		Properties:   imageProperties,
   252  		Artifacts:    builder.artifacts,
   253  		Dependencies: builder.dependencies,
   254  	}}}
   255  	return buildInfo, nil
   256  }
   257  
   258  // Download and read the manifest from Artifactory.
   259  // Returned values:
   260  // imageManifest - pointer to the manifest struct, retrieved from Artifactory.
   261  // artifact - manifest as buildinfo.Artifact object.
   262  // dependency - manifest as buildinfo.Dependency object.
   263  func getManifest(imageId string, searchResults map[string]utils.ResultItem, serviceManager *artifactory.ArtifactoryServicesManager) (imageManifest *manifest, artifact buildinfo.Artifact, dependency buildinfo.Dependency, err error) {
   264  	item := searchResults["manifest.json"]
   265  	ioReaderCloser, err := serviceManager.ReadRemoteFile(item.GetItemRelativePath())
   266  	if err != nil {
   267  		return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err
   268  	}
   269  	defer ioReaderCloser.Close()
   270  	content, err := ioutil.ReadAll(ioReaderCloser)
   271  	if err != nil {
   272  		return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err
   273  	}
   274  
   275  	imageManifest = new(manifest)
   276  	err = json.Unmarshal(content, &imageManifest)
   277  	if errorutils.CheckError(err) != nil {
   278  		return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err
   279  	}
   280  
   281  	// Check that the manifest ID is the right one.
   282  	if imageManifest.Config.Digest != imageId {
   283  		return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, errorutils.CheckError(errors.New("Found incorrect manifest.json file, expecting image ID: " + imageId))
   284  	}
   285  
   286  	artifact = buildinfo.Artifact{Name: "manifest.json", Type: "json", Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}}
   287  	dependency = buildinfo.Dependency{Id: "manifest.json", Type: "json", Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}}
   288  	return
   289  }
   290  
   291  // Download and read the config layer from Artifactory.
   292  // Returned values:
   293  // configurationLayer - pointer to the configuration layer struct, retrieved from Artifactory.
   294  // artifact - configuration layer as buildinfo.Artifact object.
   295  // dependency - configuration layer as buildinfo.Dependency object.
   296  func getConfigLayer(imageId string, searchResults map[string]utils.ResultItem, serviceManager *artifactory.ArtifactoryServicesManager) (configurationLayer *configLayer, artifact buildinfo.Artifact, dependency buildinfo.Dependency, err error) {
   297  	item := searchResults[digestToLayer(imageId)]
   298  	ioReaderCloser, err := serviceManager.ReadRemoteFile(item.GetItemRelativePath())
   299  	if err != nil {
   300  		return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err
   301  	}
   302  	defer ioReaderCloser.Close()
   303  	content, err := ioutil.ReadAll(ioReaderCloser)
   304  	if err != nil {
   305  		return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err
   306  	}
   307  
   308  	configurationLayer = new(configLayer)
   309  	err = json.Unmarshal(content, &configurationLayer)
   310  	if err != nil {
   311  		return nil, buildinfo.Artifact{}, buildinfo.Dependency{}, err
   312  	}
   313  
   314  	artifact = buildinfo.Artifact{Name: digestToLayer(imageId), Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}}
   315  	dependency = buildinfo.Dependency{Id: digestToLayer(imageId), Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}}
   316  	return
   317  }
   318  
   319  // Search for image layers in Artifactory.
   320  func searchImageLayers(builder *buildInfoBuilder, imagePathPattern string, serviceManager *artifactory.ArtifactoryServicesManager) (map[string]utils.ResultItem, error) {
   321  	resultMap, err := searchImageHandler(builder, imagePathPattern, serviceManager)
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  
   326  	// Validate image ID layer exists.
   327  	if _, ok := resultMap[digestToLayer(builder.imageId)]; !ok {
   328  		// In case of a fat-manifest, Artifactory will create two folders.
   329  		// One folder named as the image tag, which contains the fat manifest.
   330  		// The second folder, named as image's manifest digest, contains the image layers and the image's manifest.
   331  		if _, ok := resultMap["list.manifest.json"]; ok {
   332  			v, err := builder.image.Manifest()
   333  			if err != nil {
   334  				log.Error("Fail to run docker inspect " + builder.image.Tag() + " \nError: " + err.Error())
   335  				return nil, err
   336  			}
   337  			var listManifest []Manifest
   338  			err = json.Unmarshal([]byte(v), &listManifest)
   339  			if err != nil {
   340  				log.Error("json.Unmarshal failed with " + err.Error() + "\n" + v)
   341  				return nil, err
   342  			}
   343  			result := ""
   344  			for _, manifest := range listManifest {
   345  				if *manifest.SchemaV2Manifest.Config.Digest == builder.imageId {
   346  					result = *manifest.Descriptor.Digest
   347  					break
   348  				}
   349  			}
   350  			if result != "" {
   351  				// Remove the tag from the pattern, and place the manifest digest instead.
   352  				imagePathPattern = strings.Replace(imagePathPattern, "/*", "", 1)
   353  				imagePathPattern = path.Join(imagePathPattern[:strings.LastIndex(imagePathPattern, "/")], strings.Replace(result, ":", "__", 1), "*")
   354  				return searchImageHandler(builder, imagePathPattern, serviceManager)
   355  			}
   356  		}
   357  		return nil, nil
   358  	}
   359  	return resultMap, nil
   360  }
   361  
   362  func searchImageHandler(builder *buildInfoBuilder, imagePathPattern string, serviceManager *artifactory.ArtifactoryServicesManager) (map[string]utils.ResultItem, error) {
   363  	searchParams := services.NewSearchParams()
   364  	searchParams.ArtifactoryCommonParams = &utils.ArtifactoryCommonParams{}
   365  	searchParams.Pattern = imagePathPattern
   366  	results, err := serviceManager.SearchFiles(searchParams)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	resultMap := map[string]utils.ResultItem{}
   371  	for _, v := range results {
   372  		resultMap[v.Name] = v
   373  	}
   374  	return resultMap, nil
   375  }
   376  
   377  // Digest of type sha256:30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e to
   378  // sha256__30daa5c11544632449b01f450bebfef6b89644e9e683258ed05797abe7c32a6e
   379  func digestToLayer(digest string) string {
   380  	return strings.Replace(digest, ":", "__", 1)
   381  }
   382  
   383  // Get the total number of layers from the config.
   384  func (configLayer *configLayer) getNumberLayers() int {
   385  	layersNum := len(configLayer.History)
   386  	for i := len(configLayer.History) - 1; i >= 0; i-- {
   387  		if configLayer.History[i].EmptyLayer {
   388  			layersNum--
   389  		}
   390  	}
   391  	return layersNum
   392  }
   393  
   394  // Get the number of dependencies layers from the config.
   395  func (configLayer *configLayer) getNumberOfDependentLayers() int {
   396  	layersNum := len(configLayer.History)
   397  	newImageLayers := true
   398  	for i := len(configLayer.History) - 1; i >= 0; i-- {
   399  		if newImageLayers {
   400  			layersNum--
   401  		}
   402  
   403  		if !newImageLayers && configLayer.History[i].EmptyLayer {
   404  			layersNum--
   405  		}
   406  
   407  		createdBy := configLayer.History[i].CreatedBy
   408  		if strings.Contains(createdBy, "ENTRYPOINT") || strings.Contains(createdBy, "MAINTAINER") {
   409  			newImageLayers = false
   410  		}
   411  	}
   412  	return layersNum
   413  }
   414  
   415  // To unmarshal config layer file
   416  type configLayer struct {
   417  	History []history `json:"history,omitempty"`
   418  }
   419  
   420  type history struct {
   421  	Created    string `json:"created,omitempty"`
   422  	CreatedBy  string `json:"created_by,omitempty"`
   423  	EmptyLayer bool   `json:"empty_layer,omitempty"`
   424  }
   425  
   426  // To unmarshal manifest.json file
   427  type manifest struct {
   428  	Config manifestConfig `json:"config,omitempty"`
   429  	Layers []layer        `json:"layers,omitempty"`
   430  }
   431  
   432  type manifestConfig struct {
   433  	Digest string `json:"digest,omitempty"`
   434  }
   435  
   436  type layer struct {
   437  	Digest    string `json:"digest,omitempty"`
   438  	MediaType string `json:"mediaType,omitempty"`
   439  }
   440  
   441  type CommandType string