github.com/containers/podman/v4@v4.9.4/pkg/bindings/manifests/manifests.go (about)

     1  package manifests
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/containers/common/libimage/define"
    15  	"github.com/containers/image/v5/manifest"
    16  	imageTypes "github.com/containers/image/v5/types"
    17  	"github.com/containers/podman/v4/pkg/auth"
    18  	"github.com/containers/podman/v4/pkg/bindings"
    19  	"github.com/containers/podman/v4/pkg/bindings/images"
    20  	"github.com/containers/podman/v4/pkg/domain/entities"
    21  	"github.com/containers/podman/v4/pkg/errorhandling"
    22  	jsoniter "github.com/json-iterator/go"
    23  )
    24  
    25  // Create creates a manifest for the given name.  Optional images to be associated with
    26  // the new manifest can also be specified.  The all boolean specifies to add all entries
    27  // of a list if the name provided is a manifest list.  The ID of the new manifest list
    28  // is returned as a string.
    29  func Create(ctx context.Context, name string, images []string, options *CreateOptions) (string, error) {
    30  	var idr entities.IDResponse
    31  	if options == nil {
    32  		options = new(CreateOptions)
    33  	}
    34  	conn, err := bindings.GetClient(ctx)
    35  	if err != nil {
    36  		return "", err
    37  	}
    38  	if len(name) < 1 {
    39  		return "", errors.New("creating a manifest requires at least one name argument")
    40  	}
    41  	params, err := options.ToParams()
    42  	if err != nil {
    43  		return "", err
    44  	}
    45  
    46  	for _, i := range images {
    47  		params.Add("images", i)
    48  	}
    49  
    50  	response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/manifests/%s", params, nil, name)
    51  	if err != nil {
    52  		return "", err
    53  	}
    54  	defer response.Body.Close()
    55  
    56  	return idr.ID, response.Process(&idr)
    57  }
    58  
    59  // Exists returns true if a given manifest list exists
    60  func Exists(ctx context.Context, name string, options *ExistsOptions) (bool, error) {
    61  	conn, err := bindings.GetClient(ctx)
    62  	if err != nil {
    63  		return false, err
    64  	}
    65  	response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/manifests/%s/exists", nil, nil, name)
    66  	if err != nil {
    67  		return false, err
    68  	}
    69  	defer response.Body.Close()
    70  
    71  	return response.IsSuccess(), nil
    72  }
    73  
    74  // Inspect returns a manifest list for a given name.
    75  func Inspect(ctx context.Context, name string, options *InspectOptions) (*manifest.Schema2List, error) {
    76  	conn, err := bindings.GetClient(ctx)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	if options == nil {
    81  		options = new(InspectOptions)
    82  	}
    83  
    84  	params, err := options.ToParams()
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	// SkipTLSVerify is special.  We need to delete the param added by
    89  	// ToParams() and change the key and flip the bool
    90  	if options.SkipTLSVerify != nil {
    91  		params.Del("SkipTLSVerify")
    92  		params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify()))
    93  	}
    94  
    95  	header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, "", "")
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  
   100  	response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/manifests/%s/json", params, header, name)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	defer response.Body.Close()
   105  
   106  	var list manifest.Schema2List
   107  	return &list, response.Process(&list)
   108  }
   109  
   110  // InspectListData returns a manifest list for a given name.
   111  // Contains exclusive field like `annotations` which is only
   112  // present in OCI spec and not in docker image spec.
   113  func InspectListData(ctx context.Context, name string, options *InspectOptions) (*define.ManifestListData, error) {
   114  	conn, err := bindings.GetClient(ctx)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	if options == nil {
   119  		options = new(InspectOptions)
   120  	}
   121  
   122  	params, err := options.ToParams()
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	// SkipTLSVerify is special.  We need to delete the param added by
   127  	// ToParams() and change the key and flip the bool
   128  	if options.SkipTLSVerify != nil {
   129  		params.Del("SkipTLSVerify")
   130  		params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify()))
   131  	}
   132  
   133  	header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, "", "")
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/manifests/%s/json", params, header, name)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	defer response.Body.Close()
   143  
   144  	var list define.ManifestListData
   145  	return &list, response.Process(&list)
   146  }
   147  
   148  // Add adds a manifest to a given manifest list.  Additional options for the manifest
   149  // can also be specified.  The ID of the new manifest list is returned as a string
   150  func Add(ctx context.Context, name string, options *AddOptions) (string, error) {
   151  	if options == nil {
   152  		options = new(AddOptions)
   153  	}
   154  
   155  	optionsv4 := ModifyOptions{
   156  		All:           options.All,
   157  		Annotations:   options.Annotation,
   158  		Arch:          options.Arch,
   159  		Features:      options.Features,
   160  		Images:        options.Images,
   161  		OS:            options.OS,
   162  		OSFeatures:    nil,
   163  		OSVersion:     options.OSVersion,
   164  		Variant:       options.Variant,
   165  		Username:      options.Username,
   166  		Password:      options.Password,
   167  		Authfile:      options.Authfile,
   168  		SkipTLSVerify: options.SkipTLSVerify,
   169  	}
   170  	optionsv4.WithOperation("update")
   171  	return Modify(ctx, name, options.Images, &optionsv4)
   172  }
   173  
   174  // Remove deletes a manifest entry from a manifest list.  Both name and the digest to be
   175  // removed are mandatory inputs.  The ID of the new manifest list is returned as a string.
   176  func Remove(ctx context.Context, name, digest string, _ *RemoveOptions) (string, error) {
   177  	optionsv4 := new(ModifyOptions).WithOperation("remove")
   178  	return Modify(ctx, name, []string{digest}, optionsv4)
   179  }
   180  
   181  // Delete removes specified manifest from local storage.
   182  func Delete(ctx context.Context, name string) (*entities.ManifestRemoveReport, error) {
   183  	var report entities.ManifestRemoveReport
   184  	conn, err := bindings.GetClient(ctx)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/manifests/%s", nil, nil, name)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	defer response.Body.Close()
   193  
   194  	if err := response.Process(&report); err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	return &report, errorhandling.JoinErrors(errorhandling.StringsToErrors(report.Errors))
   199  }
   200  
   201  // Push takes a manifest list and pushes to a destination.  If the destination is not specified,
   202  // the name will be used instead.  If the optional all boolean is specified, all images specified
   203  // in the list will be pushed as well.
   204  func Push(ctx context.Context, name, destination string, options *images.PushOptions) (string, error) {
   205  	if options == nil {
   206  		options = new(images.PushOptions)
   207  	}
   208  	if len(destination) < 1 {
   209  		destination = name
   210  	}
   211  	conn, err := bindings.GetClient(ctx)
   212  	if err != nil {
   213  		return "", err
   214  	}
   215  
   216  	header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
   217  	if err != nil {
   218  		return "", err
   219  	}
   220  
   221  	params, err := options.ToParams()
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  	// SkipTLSVerify is special.  It's not being serialized by ToParams()
   226  	// because we need to flip the boolean.
   227  	if options.SkipTLSVerify != nil {
   228  		params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify()))
   229  	}
   230  
   231  	response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/manifests/%s/registry/%s", params, header, name, destination)
   232  	if err != nil {
   233  		return "", err
   234  	}
   235  	defer response.Body.Close()
   236  
   237  	if !response.IsSuccess() {
   238  		return "", response.Process(err)
   239  	}
   240  
   241  	var writer io.Writer
   242  	if options.GetQuiet() {
   243  		writer = io.Discard
   244  	} else if progressWriter := options.GetProgressWriter(); progressWriter != nil {
   245  		writer = progressWriter
   246  	} else {
   247  		// Historically push writes status to stderr
   248  		writer = os.Stderr
   249  	}
   250  
   251  	dec := json.NewDecoder(response.Body)
   252  	for {
   253  		var report entities.ManifestPushReport
   254  		if err := dec.Decode(&report); err != nil {
   255  			return "", err
   256  		}
   257  
   258  		select {
   259  		case <-response.Request.Context().Done():
   260  			return "", context.Canceled
   261  		default:
   262  			// non-blocking select
   263  		}
   264  
   265  		switch {
   266  		case report.ID != "":
   267  			return report.ID, nil
   268  		case report.Stream != "":
   269  			fmt.Fprint(writer, report.Stream)
   270  		case report.Error != "":
   271  			// There can only be one error.
   272  			return "", errors.New(report.Error)
   273  		default:
   274  			return "", fmt.Errorf("failed to parse push results stream, unexpected input: %v", report)
   275  		}
   276  	}
   277  }
   278  
   279  // Modify modifies the given manifest list using options and the optional list of images
   280  func Modify(ctx context.Context, name string, images []string, options *ModifyOptions) (string, error) {
   281  	if options == nil || *options.Operation == "" {
   282  		return "", errors.New(`the field ModifyOptions.Operation must be set to either "update" or "remove"`)
   283  	}
   284  	options.WithImages(images)
   285  
   286  	conn, err := bindings.GetClient(ctx)
   287  	if err != nil {
   288  		return "", err
   289  	}
   290  	opts, err := jsoniter.MarshalToString(options)
   291  	if err != nil {
   292  		return "", err
   293  	}
   294  	reader := strings.NewReader(opts)
   295  
   296  	header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
   297  	if err != nil {
   298  		return "", err
   299  	}
   300  
   301  	params, err := options.ToParams()
   302  	if err != nil {
   303  		return "", err
   304  	}
   305  	// SkipTLSVerify is special.  It's not being serialized by ToParams()
   306  	// because we need to flip the boolean.
   307  	if options.SkipTLSVerify != nil {
   308  		params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify()))
   309  	}
   310  
   311  	response, err := conn.DoRequest(ctx, reader, http.MethodPut, "/manifests/%s", params, header, name)
   312  	if err != nil {
   313  		return "", err
   314  	}
   315  	defer response.Body.Close()
   316  
   317  	data, err := io.ReadAll(response.Body)
   318  	if err != nil {
   319  		return "", fmt.Errorf("unable to process API response: %w", err)
   320  	}
   321  
   322  	if response.IsSuccess() || response.IsRedirection() {
   323  		var report entities.ManifestModifyReport
   324  		if err = jsoniter.Unmarshal(data, &report); err != nil {
   325  			return "", fmt.Errorf("unable to decode API response: %w", err)
   326  		}
   327  
   328  		err = errorhandling.JoinErrors(report.Errors)
   329  		if err != nil {
   330  			errModel := errorhandling.ErrorModel{
   331  				Because:      errorhandling.Cause(err).Error(),
   332  				Message:      err.Error(),
   333  				ResponseCode: response.StatusCode,
   334  			}
   335  			return report.ID, &errModel
   336  		}
   337  		return report.ID, nil
   338  	}
   339  
   340  	errModel := errorhandling.ErrorModel{
   341  		ResponseCode: response.StatusCode,
   342  	}
   343  	if err = jsoniter.Unmarshal(data, &errModel); err != nil {
   344  		return "", fmt.Errorf("unable to decode API response: %w", err)
   345  	}
   346  	return "", &errModel
   347  }
   348  
   349  // Annotate modifies the given manifest list using options and the optional list of images
   350  //
   351  // As of 4.0.0
   352  func Annotate(ctx context.Context, name string, images []string, options *ModifyOptions) (string, error) {
   353  	options.WithOperation("annotate")
   354  	return Modify(ctx, name, images, options)
   355  }