github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/client/inspect_image.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  
     7  	"github.com/Masterminds/semver"
     8  	"github.com/buildpacks/lifecycle/buildpack"
     9  	"github.com/buildpacks/lifecycle/launch"
    10  	"github.com/buildpacks/lifecycle/platform"
    11  	"github.com/buildpacks/lifecycle/platform/files"
    12  	"github.com/pkg/errors"
    13  
    14  	"github.com/buildpacks/pack/pkg/dist"
    15  	"github.com/buildpacks/pack/pkg/image"
    16  )
    17  
    18  // ImageInfo is a collection of metadata describing
    19  // an app image built using Cloud Native Buildpacks.
    20  type ImageInfo struct {
    21  	// Stack Identifier used when building this image
    22  	StackID string
    23  
    24  	// List of buildpacks that passed detection, ran their build
    25  	// phases and made a contribution to this image.
    26  	Buildpacks []buildpack.GroupElement
    27  
    28  	// List of extensions that passed detection, ran their generate
    29  	// phases and made a contribution to this image.
    30  	Extensions []buildpack.GroupElement
    31  
    32  	// Base includes two references to the run image,
    33  	// - the Run Image ID,
    34  	// - the hash of the last layer in the app image that belongs to the run image.
    35  	// A way to visualize this is given an image with n layers:
    36  	//
    37  	// last layer in run image
    38  	//          v
    39  	// [1, ..., k, k+1, ..., n]
    40  	//              ^
    41  	//   first layer added by buildpacks
    42  	//
    43  	// the first 1 to k layers all belong to the run image,
    44  	// the last k+1 to n layers are added by buildpacks.
    45  	// the sum of all of these is our app image.
    46  	Base files.RunImageForRebase
    47  
    48  	// BOM or Bill of materials, contains dependency and
    49  	// version information provided by each buildpack.
    50  	BOM []buildpack.BOMEntry
    51  
    52  	// Stack includes the run image name, and a list of image mirrors,
    53  	// where the run image is hosted.
    54  	Stack files.Stack
    55  
    56  	// Processes lists all processes contributed by buildpacks.
    57  	Processes ProcessDetails
    58  
    59  	// If the image can be rebased
    60  	Rebasable bool
    61  }
    62  
    63  // ProcessDetails is a collection of all start command metadata
    64  // on an image.
    65  type ProcessDetails struct {
    66  	// An Images default start command.
    67  	DefaultProcess *launch.Process
    68  
    69  	// List of all start commands contributed by buildpacks.
    70  	OtherProcesses []launch.Process
    71  }
    72  
    73  // Deserialize just the subset of fields we need to avoid breaking changes
    74  type layersMetadata struct {
    75  	RunImage files.RunImageForRebase `json:"runImage" toml:"run-image"`
    76  	Stack    files.Stack             `json:"stack" toml:"stack"`
    77  }
    78  
    79  const (
    80  	platformAPIEnv            = "CNB_PLATFORM_API"
    81  	cnbProcessEnv             = "CNB_PROCESS_TYPE"
    82  	launcherEntrypoint        = "/cnb/lifecycle/launcher"
    83  	windowsLauncherEntrypoint = `c:\cnb\lifecycle\launcher.exe`
    84  	entrypointPrefix          = "/cnb/process/"
    85  	windowsEntrypointPrefix   = `c:\cnb\process\`
    86  	defaultProcess            = "web"
    87  	fallbackPlatformAPI       = "0.3"
    88  	windowsPrefix             = "c:"
    89  )
    90  
    91  // InspectImage reads the Label metadata of an image. It initializes a ImageInfo object
    92  // using this metadata, and returns it.
    93  // If daemon is true, first the local registry will be searched for the image.
    94  // Otherwise it assumes the image is remote.
    95  func (c *Client) InspectImage(name string, daemon bool) (*ImageInfo, error) {
    96  	img, err := c.imageFetcher.Fetch(context.Background(), name, image.FetchOptions{Daemon: daemon, PullPolicy: image.PullNever})
    97  	if err != nil {
    98  		if errors.Cause(err) == image.ErrNotFound {
    99  			return nil, nil
   100  		}
   101  		return nil, err
   102  	}
   103  
   104  	var layersMd layersMetadata
   105  	if _, err := dist.GetLabel(img, platform.LifecycleMetadataLabel, &layersMd); err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	var buildMD files.BuildMetadata
   110  	if _, err := dist.GetLabel(img, platform.BuildMetadataLabel, &buildMD); err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	minimumBaseImageReferenceVersion := semver.MustParse("0.5.0")
   115  	actualLauncherVersion, err := semver.NewVersion(buildMD.Launcher.Version)
   116  
   117  	if err == nil && actualLauncherVersion.LessThan(minimumBaseImageReferenceVersion) {
   118  		layersMd.RunImage.Reference = ""
   119  	}
   120  
   121  	stackID, err := img.Label(platform.StackIDLabel)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	rebasable, err := getRebasableLabel(img)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	platformAPI, err := img.Env(platformAPIEnv)
   132  	if err != nil {
   133  		return nil, errors.Wrap(err, "reading platform api")
   134  	}
   135  
   136  	if platformAPI == "" {
   137  		platformAPI = fallbackPlatformAPI
   138  	}
   139  
   140  	platformAPIVersion, err := semver.NewVersion(platformAPI)
   141  	if err != nil {
   142  		return nil, errors.Wrap(err, "parsing platform api version")
   143  	}
   144  
   145  	var defaultProcessType string
   146  	if platformAPIVersion.LessThan(semver.MustParse("0.4")) {
   147  		defaultProcessType, err = img.Env(cnbProcessEnv)
   148  		if err != nil || defaultProcessType == "" {
   149  			defaultProcessType = defaultProcess
   150  		}
   151  	} else {
   152  		entrypoint, err := img.Entrypoint()
   153  		if err != nil {
   154  			return nil, errors.Wrap(err, "reading entrypoint")
   155  		}
   156  
   157  		if len(entrypoint) > 0 && entrypoint[0] != launcherEntrypoint && entrypoint[0] != windowsLauncherEntrypoint {
   158  			process := entrypoint[0]
   159  			if strings.HasPrefix(process, windowsPrefix) {
   160  				process = strings.TrimPrefix(process, windowsEntrypointPrefix)
   161  				process = strings.TrimSuffix(process, ".exe") // Trim .exe for Windows support
   162  			} else {
   163  				process = strings.TrimPrefix(process, entrypointPrefix)
   164  			}
   165  
   166  			defaultProcessType = process
   167  		}
   168  	}
   169  
   170  	workingDir, err := img.WorkingDir()
   171  	if err != nil {
   172  		return nil, errors.Wrap(err, "reading WorkingDir")
   173  	}
   174  
   175  	var processDetails ProcessDetails
   176  	for _, proc := range buildMD.Processes {
   177  		proc := proc
   178  		if proc.WorkingDirectory == "" {
   179  			proc.WorkingDirectory = workingDir
   180  		}
   181  		if proc.Type == defaultProcessType {
   182  			processDetails.DefaultProcess = &proc
   183  			continue
   184  		}
   185  		processDetails.OtherProcesses = append(processDetails.OtherProcesses, proc)
   186  	}
   187  
   188  	var stackCompat files.Stack
   189  	if layersMd.RunImage.Image != "" {
   190  		stackCompat = layersMd.RunImage.ToStack()
   191  	} else {
   192  		stackCompat = layersMd.Stack
   193  	}
   194  
   195  	if buildMD.Extensions != nil {
   196  		return &ImageInfo{
   197  			StackID:    stackID,
   198  			Stack:      stackCompat,
   199  			Base:       layersMd.RunImage,
   200  			BOM:        buildMD.BOM,
   201  			Buildpacks: buildMD.Buildpacks,
   202  			Extensions: buildMD.Extensions,
   203  			Processes:  processDetails,
   204  			Rebasable:  rebasable,
   205  		}, nil
   206  	}
   207  
   208  	return &ImageInfo{
   209  		StackID:    stackID,
   210  		Stack:      stackCompat,
   211  		Base:       layersMd.RunImage,
   212  		BOM:        buildMD.BOM,
   213  		Buildpacks: buildMD.Buildpacks,
   214  		Processes:  processDetails,
   215  		Rebasable:  rebasable,
   216  	}, nil
   217  }
   218  
   219  func getRebasableLabel(labeled dist.Labeled) (bool, error) {
   220  	var rebasableOutput bool
   221  	isPresent, err := dist.GetLabel(labeled, platform.RebasableLabel, &rebasableOutput)
   222  	if err != nil {
   223  		return false, err
   224  	}
   225  
   226  	if !isPresent {
   227  		rebasableOutput = true
   228  	}
   229  
   230  	return rebasableOutput, nil
   231  }