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 }