github.com/Cloud-Foundations/Dominator@v0.3.4/imagebuilder/builder/image.go (about)

     1  package builder
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	stdlog "log"
    10  	"net/url"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"strings"
    15  	"syscall"
    16  	"time"
    17  
    18  	"github.com/Cloud-Foundations/Dominator/lib/filesystem"
    19  	"github.com/Cloud-Foundations/Dominator/lib/filesystem/util"
    20  	"github.com/Cloud-Foundations/Dominator/lib/filter"
    21  	"github.com/Cloud-Foundations/Dominator/lib/format"
    22  	"github.com/Cloud-Foundations/Dominator/lib/fsutil"
    23  	"github.com/Cloud-Foundations/Dominator/lib/gitutil"
    24  	"github.com/Cloud-Foundations/Dominator/lib/image"
    25  	libjson "github.com/Cloud-Foundations/Dominator/lib/json"
    26  	objectclient "github.com/Cloud-Foundations/Dominator/lib/objectserver/client"
    27  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    28  	"github.com/Cloud-Foundations/Dominator/lib/tags"
    29  	"github.com/Cloud-Foundations/Dominator/lib/triggers"
    30  	proto "github.com/Cloud-Foundations/Dominator/proto/imaginator"
    31  )
    32  
    33  type gitInfoType struct {
    34  	branch   string
    35  	commitId string
    36  	gitUrl   string
    37  }
    38  
    39  func (stream *imageStreamType) build(b *Builder, client srpc.ClientI,
    40  	request proto.BuildImageRequest, buildLog buildLogger) (
    41  	*image.Image, error) {
    42  	manifestDirectory, gitInfo, err := stream.getManifest(b, request.StreamName,
    43  		request.GitBranch, request.Variables, buildLog)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	defer os.RemoveAll(manifestDirectory)
    48  	img, err := buildImageFromManifest(client, manifestDirectory, request,
    49  		b.bindMounts, stream, gitInfo, b.mtimesCopyFilter, buildLog)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	return img, nil
    54  }
    55  
    56  func (stream *imageStreamType) getenv() map[string]string {
    57  	envTable := make(map[string]string, len(stream.Variables)+3)
    58  	for key, value := range stream.Variables {
    59  		envTable[key] = expandExpression(value, func(name string) string {
    60  			if name == "IMAGE_STREAM" {
    61  				return stream.name
    62  			}
    63  			return ""
    64  		})
    65  	}
    66  	envTable["IMAGE_STREAM"] = stream.name
    67  	envTable["IMAGE_STREAM_DIRECTORY_NAME"] = filepath.Dir(stream.name)
    68  	envTable["IMAGE_STREAM_LEAF_NAME"] = filepath.Base(stream.name)
    69  	return envTable
    70  }
    71  
    72  // getManifestLocation will expand variables and return the actual manifest
    73  // location. These data may include secrets (i.e. username and password).
    74  // If b is nil then secret variables are not expanded and thus the returned
    75  // data do not contain secrets but may be incorrect.
    76  func (stream *imageStreamType) getManifestLocation(b *Builder,
    77  	variables map[string]string) manifestLocationType {
    78  	var variableFunc func(string) string
    79  	if b == nil {
    80  		variableFunc = func(name string) string {
    81  			return stream.getenv()[name]
    82  		}
    83  	} else {
    84  		variableFunc = b.getVariableFunc(stream.getenv(), variables)
    85  	}
    86  	return manifestLocationType{
    87  		directory: expandExpression(stream.ManifestDirectory, variableFunc),
    88  		url:       expandExpression(stream.ManifestUrl, variableFunc),
    89  	}
    90  }
    91  
    92  func (stream *imageStreamType) getManifest(b *Builder, streamName string,
    93  	gitBranch string, variables map[string]string,
    94  	buildLog io.Writer) (string, *gitInfoType, error) {
    95  	if gitBranch == "" {
    96  		gitBranch = "master"
    97  	}
    98  	manifestRoot, err := makeTempDirectory("",
    99  		strings.Replace(streamName, "/", "_", -1)+".manifest")
   100  	if err != nil {
   101  		return "", nil, err
   102  	}
   103  	doCleanup := true
   104  	defer func() {
   105  		if doCleanup {
   106  			os.RemoveAll(manifestRoot)
   107  		}
   108  	}()
   109  	manifestLocation := stream.getManifestLocation(b, variables)
   110  	if rootDir, err := urlToLocal(manifestLocation.url); err != nil {
   111  		return "", nil, err
   112  	} else if rootDir != "" {
   113  		if gitBranch != "master" {
   114  			return "", nil,
   115  				fmt.Errorf("branch: %s is not master", gitBranch)
   116  		}
   117  		sourceTree := filepath.Join(rootDir, manifestLocation.directory)
   118  		fmt.Fprintf(buildLog, "Copying manifest tree: %s\n", sourceTree)
   119  		if err := fsutil.CopyTree(manifestRoot, sourceTree); err != nil {
   120  			return "", nil, fmt.Errorf("error copying manifest: %s", err)
   121  		}
   122  		doCleanup = false
   123  		return manifestRoot, nil, nil
   124  	}
   125  	var patterns []string
   126  	if manifestLocation.directory != "" {
   127  		patterns = append(patterns, manifestLocation.directory+"/*")
   128  	}
   129  	err = gitShallowClone(manifestRoot, manifestLocation.url,
   130  		stream.ManifestUrl, gitBranch, patterns, buildLog)
   131  	if err != nil {
   132  		return "", nil, err
   133  	}
   134  	gitDirectory := filepath.Join(manifestRoot, ".git")
   135  	var gitInfo *gitInfoType
   136  	// The specified branch/tag/commit will be in the "master" branch in the
   137  	// cloned repository.
   138  	commitId, err := gitutil.GetCommitIdOfRef(manifestRoot, "HEAD")
   139  	if err != nil {
   140  		return "", nil, err
   141  	} else {
   142  		gitInfo = &gitInfoType{
   143  			branch:   gitBranch,
   144  			commitId: commitId,
   145  			gitUrl:   manifestLocation.url,
   146  		}
   147  	}
   148  	if err := os.RemoveAll(gitDirectory); err != nil {
   149  		return "", nil, err
   150  	}
   151  	if manifestLocation.directory != "" {
   152  		// Move manifest directory into manifestRoot, remove anything else.
   153  		err := os.Rename(filepath.Join(manifestRoot,
   154  			manifestLocation.directory),
   155  			gitDirectory)
   156  		if err != nil {
   157  			return "", nil, err
   158  		}
   159  		filenames, err := listDirectory(manifestRoot)
   160  		if err != nil {
   161  			return "", nil, err
   162  		}
   163  		for _, filename := range filenames {
   164  			if filename == ".git" {
   165  				continue
   166  			}
   167  			err := os.RemoveAll(filepath.Join(manifestRoot, filename))
   168  			if err != nil {
   169  				return "", nil, err
   170  			}
   171  		}
   172  		filenames, err = listDirectory(gitDirectory)
   173  		if err != nil {
   174  			return "", nil, err
   175  		}
   176  		for _, filename := range filenames {
   177  			err := os.Rename(filepath.Join(gitDirectory, filename),
   178  				filepath.Join(manifestRoot, filename))
   179  			if err != nil {
   180  				return "", nil, err
   181  			}
   182  		}
   183  		if err := os.Remove(gitDirectory); err != nil {
   184  			return "", nil, err
   185  		}
   186  	}
   187  	doCleanup = false
   188  	return manifestRoot, gitInfo, nil
   189  }
   190  
   191  func (stream *imageStreamType) getSourceImage(b *Builder, buildLog io.Writer) (
   192  	string, string, *gitInfoType, []byte, *manifestConfigType, error) {
   193  	manifestDirectory, gitInfo, err := stream.getManifest(stream.builder,
   194  		stream.name, "", nil, buildLog)
   195  	if err != nil {
   196  		return "", "", nil, nil, nil, err
   197  	}
   198  	doRemove := true
   199  	defer func() {
   200  		if doRemove {
   201  			os.RemoveAll(manifestDirectory)
   202  		}
   203  	}()
   204  	manifestFilename := filepath.Join(manifestDirectory, "manifest")
   205  	manifestBytes, err := ioutil.ReadFile(manifestFilename)
   206  	if err != nil {
   207  		return "", "", nil, nil, nil, err
   208  	}
   209  	var manifest manifestConfigType
   210  	if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
   211  		return "", "", nil, nil, nil, err
   212  	}
   213  	sourceImageName := expandExpression(manifest.SourceImage,
   214  		func(name string) string {
   215  			return stream.getenv()[name]
   216  		})
   217  	doRemove = false
   218  	return manifestDirectory, sourceImageName, gitInfo, manifestBytes,
   219  		&manifest, nil
   220  }
   221  
   222  func listDirectory(directoryName string) ([]string, error) {
   223  	directory, err := os.Open(directoryName)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	defer directory.Close()
   228  	filenames, err := directory.Readdirnames(-1)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  	return filenames, nil
   233  }
   234  
   235  func runCommand(buildLog io.Writer, cwd string, args ...string) error {
   236  	cmd := exec.Command(args[0], args[1:]...)
   237  	cmd.Dir = cwd
   238  	cmd.Stdout = buildLog
   239  	cmd.Stderr = buildLog
   240  	return cmd.Run()
   241  }
   242  
   243  func buildImageFromManifest(client srpc.ClientI, manifestDir string,
   244  	request proto.BuildImageRequest, bindMounts []string,
   245  	envGetter environmentGetter, gitInfo *gitInfoType,
   246  	mtimesCopyFilter *filter.Filter, buildLog buildLogger) (
   247  	*image.Image, error) {
   248  	// First load all the various manifest files (fail early on error).
   249  	computedFilesList, addComputedFiles, err := loadComputedFiles(manifestDir)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	imageFilter, addFilter, err := loadFilter(manifestDir)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	tgs, err := loadTags(manifestDir)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	imageTriggers, addTriggers, err := loadTriggers(manifestDir)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	rootDir, err := makeTempDirectory("",
   266  		strings.Replace(request.StreamName, "/", "_", -1)+".root")
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	defer os.RemoveAll(rootDir)
   271  	fmt.Fprintf(buildLog, "Created image working directory: %s\n", rootDir)
   272  	vGetter := variablesGetter(envGetter.getenv()).copy()
   273  	vGetter.merge(request.Variables)
   274  	if gitInfo != nil {
   275  		vGetter.add("MANIFEST_GIT_COMMIT_ID", gitInfo.commitId)
   276  	}
   277  	vGetter.add("REQUESTED_GIT_BRANCH", request.GitBranch)
   278  	request.Variables = vGetter
   279  	manifest, err := unpackImageAndProcessManifest(client, manifestDir,
   280  		request.MaxSourceAge, rootDir, bindMounts, false, vGetter, buildLog)
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  	ctimeResolution, err := getCtimeResolution()
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	time.Sleep(ctimeResolution)
   289  	fmt.Fprintf(buildLog, "Waited %s (Ctime resolution)\n",
   290  		format.Duration(ctimeResolution))
   291  	if fi, err := os.Lstat(filepath.Join(manifestDir, "tests")); err == nil {
   292  		if fi.IsDir() {
   293  			testsDir := filepath.Join(rootDir, "tests", request.StreamName)
   294  			if err := os.MkdirAll(testsDir, fsutil.DirPerms); err != nil {
   295  				return nil, err
   296  			}
   297  			err := copyFiles(manifestDir, "tests", testsDir, buildLog)
   298  			if err != nil {
   299  				return nil, err
   300  			}
   301  		}
   302  	}
   303  	if addComputedFiles {
   304  		computedFilesList = util.MergeComputedFiles(
   305  			manifest.sourceImageInfo.computedFiles, computedFilesList)
   306  	}
   307  	if addFilter {
   308  		mergeableFilter := &filter.MergeableFilter{}
   309  		mergeableFilter.Merge(manifest.sourceImageInfo.filter)
   310  		mergeableFilter.Merge(imageFilter)
   311  		imageFilter = mergeableFilter.ExportFilter()
   312  	}
   313  	if addTriggers {
   314  		mergeableTriggers := &triggers.MergeableTriggers{}
   315  		mergeableTriggers.Merge(manifest.sourceImageInfo.triggers)
   316  		mergeableTriggers.Merge(imageTriggers)
   317  		imageTriggers = mergeableTriggers.ExportTriggers()
   318  	}
   319  	if manifest.mtimesCopyFilter != nil {
   320  		mtimesCopyFilter = manifest.mtimesCopyFilter
   321  	} else if manifest.mtimesCopyAddFilter != nil {
   322  		mf := &filter.MergeableFilter{}
   323  		mf.Merge(mtimesCopyFilter)
   324  		mf.Merge(manifest.mtimesCopyAddFilter)
   325  		mtimesCopyFilter = mf.ExportFilter()
   326  	}
   327  	img, err := packImage(nil, client, request, rootDir, manifest.filter,
   328  		manifest.sourceImageInfo.treeCache, computedFilesList, imageFilter,
   329  		tgs, imageTriggers, mtimesCopyFilter, buildLog)
   330  	if err != nil {
   331  		return nil, err
   332  	}
   333  	if gitInfo != nil {
   334  		img.BuildBranch = gitInfo.branch
   335  		img.BuildCommitId = gitInfo.commitId
   336  		img.BuildGitUrl = gitInfo.gitUrl
   337  	}
   338  	img.SourceImage = manifest.sourceImageInfo.imageName
   339  	return img, nil
   340  }
   341  
   342  func buildImageFromManifestAndUpload(client srpc.ClientI,
   343  	options BuildLocalOptions, streamName string, expiresIn time.Duration,
   344  	buildLog buildLogger) (*image.Image, string, error) {
   345  	request := proto.BuildImageRequest{
   346  		StreamName: streamName,
   347  		ExpiresIn:  expiresIn,
   348  	}
   349  	img, err := buildImageFromManifest(
   350  		client,
   351  		options.ManifestDirectory,
   352  		request,
   353  		options.BindMounts,
   354  		&imageStreamType{
   355  			name:      streamName,
   356  			Variables: options.Variables,
   357  		},
   358  		nil,
   359  		options.MtimesCopyFilter,
   360  		buildLog)
   361  	if err != nil {
   362  		return nil, "", err
   363  	}
   364  	name, err := addImage(client, request, img)
   365  	if err != nil {
   366  		return nil, "", err
   367  	}
   368  	return img, name, nil
   369  }
   370  
   371  func buildTreeCache(rootDir string, fs *filesystem.FileSystem,
   372  	buildLog io.Writer) (*treeCache, error) {
   373  	cache := treeCache{
   374  		inodeTable:  make(map[uint64]inodeData),
   375  		pathToInode: make(map[string]uint64),
   376  	}
   377  	filenameToInodeTable := fs.FilenameToInodeTable()
   378  	rootLength := len(rootDir)
   379  	startTime := time.Now()
   380  	err := filepath.Walk(rootDir,
   381  		func(path string, info os.FileInfo, err error) error {
   382  			if info.Mode()&os.ModeType != 0 {
   383  				return nil
   384  			}
   385  			rootedPath := path[rootLength:]
   386  			inum, ok := filenameToInodeTable[rootedPath]
   387  			if !ok {
   388  				return nil
   389  			}
   390  			gInode, ok := fs.InodeTable[inum]
   391  			if !ok {
   392  				return nil
   393  			}
   394  			rInode, ok := gInode.(*filesystem.RegularInode)
   395  			if !ok {
   396  				return nil
   397  			}
   398  			var stat syscall.Stat_t
   399  			if err := syscall.Stat(path, &stat); err != nil {
   400  				return err
   401  			}
   402  			cache.inodeTable[stat.Ino] = inodeData{
   403  				ctime: stat.Ctim,
   404  				hash:  rInode.Hash,
   405  				size:  uint64(stat.Size),
   406  			}
   407  			cache.pathToInode[path] = uint64(stat.Ino)
   408  			return nil
   409  		})
   410  	if err != nil {
   411  		return nil, err
   412  	}
   413  	fmt.Fprintf(buildLog, "Built tree cache in: %s\n",
   414  		format.Duration(time.Since(startTime)))
   415  	return &cache, nil
   416  }
   417  
   418  func buildTreeFromManifest(client srpc.ClientI, options BuildLocalOptions,
   419  	buildLog io.Writer) (string, error) {
   420  	rootDir, err := makeTempDirectory("", "tree")
   421  	if err != nil {
   422  		return "", err
   423  	}
   424  	_, err = unpackImageAndProcessManifest(client,
   425  		options.ManifestDirectory, 0, rootDir, options.BindMounts, true,
   426  		variablesGetter(options.Variables), buildLog)
   427  	if err != nil {
   428  		os.RemoveAll(rootDir)
   429  		return "", err
   430  	}
   431  	return rootDir, nil
   432  }
   433  
   434  func listComputedFiles(fs *filesystem.FileSystem) []util.ComputedFile {
   435  	var computedFiles []util.ComputedFile
   436  	fs.ForEachFile(
   437  		func(path string, _ uint64, inode filesystem.GenericInode) error {
   438  			if inode, ok := inode.(*filesystem.ComputedRegularInode); ok {
   439  				computedFiles = append(computedFiles, util.ComputedFile{
   440  					Filename: path,
   441  					Source:   inode.Source,
   442  				})
   443  			}
   444  			return nil
   445  		})
   446  	return computedFiles
   447  }
   448  
   449  func loadComputedFiles(manifestDir string) ([]util.ComputedFile, bool, error) {
   450  	computedFiles, err := util.LoadComputedFiles(filepath.Join(manifestDir,
   451  		"computed-files.json"))
   452  	if os.IsNotExist(err) {
   453  		computedFiles, err = util.LoadComputedFiles(
   454  			filepath.Join(manifestDir, "computed-files"))
   455  	}
   456  	if err != nil && !os.IsNotExist(err) {
   457  		return nil, false, err
   458  	}
   459  	haveComputedFiles := err == nil
   460  	addComputedFiles, err := util.LoadComputedFiles(
   461  		filepath.Join(manifestDir, "computed-files.add.json"))
   462  	if os.IsNotExist(err) {
   463  		addComputedFiles, err = util.LoadComputedFiles(
   464  			filepath.Join(manifestDir, "computed-files.add"))
   465  	}
   466  	if err != nil && !os.IsNotExist(err) {
   467  		return nil, false, err
   468  	}
   469  	haveAddComputedFiles := err == nil
   470  	if !haveComputedFiles && !haveAddComputedFiles {
   471  		return nil, false, nil
   472  	} else if haveComputedFiles && haveAddComputedFiles {
   473  		return nil, false, errors.New(
   474  			"computed-files and computed-files.add files both present")
   475  	} else if haveComputedFiles {
   476  		return computedFiles, false, nil
   477  	} else {
   478  		return addComputedFiles, true, nil
   479  	}
   480  }
   481  
   482  func loadFilter(manifestDir string) (*filter.Filter, bool, error) {
   483  	imageFilter, err := filter.Load(filepath.Join(manifestDir, "filter"))
   484  	if err != nil && !os.IsNotExist(err) {
   485  		return nil, false, err
   486  	}
   487  	addFilter, err := filter.Load(filepath.Join(manifestDir, "filter.add"))
   488  	if err != nil && !os.IsNotExist(err) {
   489  		return nil, false, err
   490  	}
   491  	if imageFilter == nil && addFilter == nil {
   492  		return nil, false, nil
   493  	} else if imageFilter != nil && addFilter != nil {
   494  		return nil, false, errors.New(
   495  			"filter and filter.add files both present")
   496  	} else if imageFilter != nil {
   497  		return imageFilter, false, nil
   498  	} else {
   499  		return addFilter, true, nil
   500  	}
   501  }
   502  
   503  func loadTags(manifestDir string) (tags.Tags, error) {
   504  	var tgs tags.Tags
   505  	err := libjson.ReadFromFile(filepath.Join(manifestDir, "tags.json"), &tgs)
   506  	if err != nil {
   507  		if os.IsNotExist(err) {
   508  			return nil, nil
   509  		}
   510  		return nil, err
   511  	}
   512  	if len(tgs) < 1 {
   513  		return nil, nil
   514  	}
   515  	return tgs, nil
   516  }
   517  
   518  func loadTriggers(manifestDir string) (*triggers.Triggers, bool, error) {
   519  	imageTriggers, err := triggers.Load(filepath.Join(manifestDir, "triggers"))
   520  	if err != nil && !os.IsNotExist(err) {
   521  		return nil, false, err
   522  	}
   523  	addTriggers, err := triggers.Load(filepath.Join(manifestDir,
   524  		"triggers.add"))
   525  	if err != nil && !os.IsNotExist(err) {
   526  		return nil, false, err
   527  	}
   528  	if imageTriggers == nil && addTriggers == nil {
   529  		return nil, false, nil
   530  	} else if imageTriggers != nil && addTriggers != nil {
   531  		return nil, false, errors.New(
   532  			"triggers and triggers.add files both present")
   533  	} else if imageTriggers != nil {
   534  		return imageTriggers, false, nil
   535  	} else {
   536  		return addTriggers, true, nil
   537  	}
   538  }
   539  
   540  func unpackImage(client srpc.ClientI, streamName, buildCommitId string,
   541  	sourceImageTagsToMatch tags.MatchTags, maxSourceAge time.Duration,
   542  	rootDir string, buildLog io.Writer) (*sourceImageInfoType, error) {
   543  	ctimeResolution, err := getCtimeResolution()
   544  	if err != nil {
   545  		return nil, err
   546  	}
   547  	imageName, sourceImage, err := getLatestImage(client, streamName,
   548  		buildCommitId, sourceImageTagsToMatch, buildLog)
   549  	if err != nil {
   550  		return nil, err
   551  	}
   552  	var specifiedStream string
   553  	if buildCommitId == "" {
   554  		specifiedStream = streamName
   555  	} else {
   556  		specifiedStream = streamName + "@gitCommitId:" + buildCommitId
   557  	}
   558  	if len(sourceImageTagsToMatch) > 0 {
   559  		specifiedStream += fmt.Sprintf("@tags:%v", sourceImageTagsToMatch)
   560  	}
   561  	if sourceImage == nil {
   562  		return nil, &buildErrorType{
   563  			error:                  "no source image: " + specifiedStream,
   564  			needSourceImage:        true,
   565  			sourceImage:            streamName,
   566  			sourceImageGitCommitId: buildCommitId,
   567  		}
   568  	}
   569  	if maxSourceAge > 0 && time.Since(sourceImage.CreatedOn) > maxSourceAge {
   570  		return nil, &buildErrorType{
   571  			error:                  "too old source image: " + specifiedStream,
   572  			needSourceImage:        true,
   573  			sourceImage:            streamName,
   574  			sourceImageGitCommitId: buildCommitId,
   575  		}
   576  	}
   577  	objClient := objectclient.AttachObjectClient(client)
   578  	defer objClient.Close()
   579  	err = util.Unpack(sourceImage.FileSystem, objClient, rootDir,
   580  		stdlog.New(buildLog, "", 0))
   581  	if err != nil {
   582  		return nil, err
   583  	}
   584  	fmt.Fprintf(buildLog, "Source image: %s\n", imageName)
   585  	treeCache, err := buildTreeCache(rootDir, sourceImage.FileSystem, buildLog)
   586  	if err != nil {
   587  		return nil, err
   588  	}
   589  	time.Sleep(ctimeResolution)
   590  	fmt.Fprintf(buildLog, "Waited %s (Ctime resolution)\n",
   591  		format.Duration(ctimeResolution))
   592  	return &sourceImageInfoType{
   593  		computedFiles: listComputedFiles(sourceImage.FileSystem),
   594  		filter:        sourceImage.Filter,
   595  		imageName:     imageName,
   596  		treeCache:     treeCache,
   597  		triggers:      sourceImage.Triggers,
   598  	}, nil
   599  }
   600  
   601  func urlToLocal(urlValue string) (string, error) {
   602  	if parsedUrl, err := url.Parse(urlValue); err == nil {
   603  		if parsedUrl.Scheme == "dir" {
   604  			if parsedUrl.Path[0] != '/' {
   605  				return "", fmt.Errorf("missing leading slash: %s",
   606  					parsedUrl.Path)
   607  			}
   608  			return parsedUrl.Path, nil
   609  		}
   610  	}
   611  	return "", nil
   612  }