images.go raw

   1  package linodego
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"io"
   7  	"time"
   8  
   9  	"github.com/go-resty/resty/v2"
  10  	"github.com/linode/linodego/internal/parseabletime"
  11  )
  12  
  13  // ImageStatus represents the status of an Image.
  14  type ImageStatus string
  15  
  16  // ImageStatus options start with ImageStatus and include all Image statuses
  17  const (
  18  	ImageStatusCreating      ImageStatus = "creating"
  19  	ImageStatusPendingUpload ImageStatus = "pending_upload"
  20  	ImageStatusAvailable     ImageStatus = "available"
  21  )
  22  
  23  // ImageRegionStatus represents the status of an Image's replica.
  24  type ImageRegionStatus string
  25  
  26  // ImageRegionStatus options start with ImageRegionStatus and
  27  // include all Image replica statuses
  28  const (
  29  	ImageRegionStatusAvailable          ImageRegionStatus = "available"
  30  	ImageRegionStatusCreating           ImageRegionStatus = "creating"
  31  	ImageRegionStatusPending            ImageRegionStatus = "pending"
  32  	ImageRegionStatusPendingReplication ImageRegionStatus = "pending replication"
  33  	ImageRegionStatusPendingDeletion    ImageRegionStatus = "pending deletion"
  34  	ImageRegionStatusReplicating        ImageRegionStatus = "replicating"
  35  )
  36  
  37  // ImageRegion represents the status of an Image object in a given Region.
  38  type ImageRegion struct {
  39  	Region string            `json:"region"`
  40  	Status ImageRegionStatus `json:"status"`
  41  }
  42  
  43  // Image represents a deployable Image object for use with Linode Instances
  44  type Image struct {
  45  	ID           string      `json:"id"`
  46  	CreatedBy    string      `json:"created_by"`
  47  	Capabilities []string    `json:"capabilities"`
  48  	Label        string      `json:"label"`
  49  	Description  string      `json:"description"`
  50  	Type         string      `json:"type"`
  51  	Vendor       string      `json:"vendor"`
  52  	Status       ImageStatus `json:"status"`
  53  	Size         int         `json:"size"`
  54  	TotalSize    int         `json:"total_size"`
  55  	IsPublic     bool        `json:"is_public"`
  56  
  57  	// NOTE: IsShared may not currently be available to all users and can only be used with v4beta.
  58  	IsShared bool `json:"is_shared"`
  59  
  60  	Deprecated bool          `json:"deprecated"`
  61  	Regions    []ImageRegion `json:"regions"`
  62  	Tags       []string      `json:"tags"`
  63  
  64  	Updated *time.Time `json:"-"`
  65  	Created *time.Time `json:"-"`
  66  	Expiry  *time.Time `json:"-"`
  67  	EOL     *time.Time `json:"-"`
  68  
  69  	// NOTE: ImageSharing may not currently be available to all users and can only be used with v4beta.
  70  	ImageSharing ImageSharing `json:"image_sharing"`
  71  }
  72  
  73  type ImageSharing struct {
  74  	SharedWith *ImageSharingSharedWith `json:"shared_with"`
  75  	SharedBy   *ImageSharingSharedBy   `json:"shared_by"`
  76  }
  77  
  78  type ImageSharingSharedWith struct {
  79  	ShareGroupCount   int    `json:"sharegroup_count"`
  80  	ShareGroupListURL string `json:"sharegroup_list_url"`
  81  }
  82  
  83  type ImageSharingSharedBy struct {
  84  	ShareGroupID    int     `json:"sharegroup_id"`
  85  	ShareGroupUUID  string  `json:"sharegroup_uuid"`
  86  	ShareGroupLabel string  `json:"sharegroup_label"`
  87  	SourceImageID   *string `json:"source_image_id"`
  88  }
  89  
  90  // ImageShareEntry represents a shared image entry for an ImageShareGroup
  91  type ImageShareEntry struct {
  92  	ID           string        `json:"id"`
  93  	CreatedBy    *string       `json:"created_by"`
  94  	Capabilities []string      `json:"capabilities"`
  95  	Label        string        `json:"label"`
  96  	Description  string        `json:"description"`
  97  	Type         string        `json:"type"`
  98  	Vendor       *string       `json:"vendor"`
  99  	Status       ImageStatus   `json:"status"`
 100  	Size         int           `json:"size"`
 101  	TotalSize    int           `json:"total_size"`
 102  	IsPublic     bool          `json:"is_public"`
 103  	IsShared     *bool         `json:"is_shared"`
 104  	Deprecated   bool          `json:"deprecated"`
 105  	Regions      []ImageRegion `json:"regions"`
 106  	Tags         []string      `json:"tags"`
 107  
 108  	Updated *time.Time `json:"-"`
 109  	Created *time.Time `json:"-"`
 110  	Expiry  *time.Time `json:"-"`
 111  	EOL     *time.Time `json:"-"`
 112  
 113  	ImageSharing ImageSharing `json:"image_sharing"`
 114  }
 115  
 116  // ImageCreateOptions fields are those accepted by CreateImage
 117  type ImageCreateOptions struct {
 118  	DiskID      int       `json:"disk_id"`
 119  	Label       string    `json:"label"`
 120  	Description string    `json:"description,omitempty"`
 121  	CloudInit   bool      `json:"cloud_init,omitempty"`
 122  	Tags        *[]string `json:"tags,omitempty"`
 123  }
 124  
 125  // ImageUpdateOptions fields are those accepted by UpdateImage
 126  type ImageUpdateOptions struct {
 127  	Label       string    `json:"label,omitempty"`
 128  	Description *string   `json:"description,omitempty"`
 129  	Tags        *[]string `json:"tags,omitempty"`
 130  }
 131  
 132  // ImageReplicateOptions represents the options accepted by the
 133  // ReplicateImage(...) function.
 134  type ImageReplicateOptions struct {
 135  	Regions []string `json:"regions"`
 136  }
 137  
 138  // ImageCreateUploadResponse fields are those returned by CreateImageUpload
 139  type ImageCreateUploadResponse struct {
 140  	Image    *Image `json:"image"`
 141  	UploadTo string `json:"upload_to"`
 142  }
 143  
 144  // ImageCreateUploadOptions fields are those accepted by CreateImageUpload
 145  type ImageCreateUploadOptions struct {
 146  	Region      string    `json:"region"`
 147  	Label       string    `json:"label"`
 148  	Description string    `json:"description,omitempty"`
 149  	CloudInit   bool      `json:"cloud_init,omitempty"`
 150  	Tags        *[]string `json:"tags,omitempty"`
 151  }
 152  
 153  // ImageUploadOptions fields are those accepted by UploadImage
 154  type ImageUploadOptions struct {
 155  	Region      string    `json:"region"`
 156  	Label       string    `json:"label"`
 157  	Description string    `json:"description,omitempty"`
 158  	CloudInit   bool      `json:"cloud_init"`
 159  	Tags        *[]string `json:"tags,omitempty"`
 160  	Image       io.Reader
 161  }
 162  
 163  // UnmarshalJSON implements the json.Unmarshaler interface
 164  func (i *Image) UnmarshalJSON(b []byte) error {
 165  	type Mask Image
 166  
 167  	p := struct {
 168  		*Mask
 169  
 170  		Updated *parseabletime.ParseableTime `json:"updated"`
 171  		Created *parseabletime.ParseableTime `json:"created"`
 172  		Expiry  *parseabletime.ParseableTime `json:"expiry"`
 173  		EOL     *parseabletime.ParseableTime `json:"eol"`
 174  	}{
 175  		Mask: (*Mask)(i),
 176  	}
 177  
 178  	if err := json.Unmarshal(b, &p); err != nil {
 179  		return err
 180  	}
 181  
 182  	i.Updated = (*time.Time)(p.Updated)
 183  	i.Created = (*time.Time)(p.Created)
 184  	i.Expiry = (*time.Time)(p.Expiry)
 185  	i.EOL = (*time.Time)(p.EOL)
 186  
 187  	return nil
 188  }
 189  
 190  // UnmarshalJSON implements the json.Unmarshaler interface
 191  func (ise *ImageShareEntry) UnmarshalJSON(b []byte) error {
 192  	type Mask ImageShareEntry
 193  
 194  	p := struct {
 195  		*Mask
 196  
 197  		Updated *parseabletime.ParseableTime `json:"updated"`
 198  		Created *parseabletime.ParseableTime `json:"created"`
 199  		Expiry  *parseabletime.ParseableTime `json:"expiry"`
 200  		EOL     *parseabletime.ParseableTime `json:"eol"`
 201  	}{
 202  		Mask: (*Mask)(ise),
 203  	}
 204  
 205  	if err := json.Unmarshal(b, &p); err != nil {
 206  		return err
 207  	}
 208  
 209  	ise.Updated = (*time.Time)(p.Updated)
 210  	ise.Created = (*time.Time)(p.Created)
 211  	ise.Expiry = (*time.Time)(p.Expiry)
 212  	ise.EOL = (*time.Time)(p.EOL)
 213  
 214  	return nil
 215  }
 216  
 217  // GetUpdateOptions converts an Image to ImageUpdateOptions for use in UpdateImage
 218  func (i Image) GetUpdateOptions() (iu ImageUpdateOptions) {
 219  	iu.Label = i.Label
 220  	iu.Description = copyString(&i.Description)
 221  
 222  	return iu
 223  }
 224  
 225  // ListImages lists Images.
 226  func (c *Client) ListImages(ctx context.Context, opts *ListOptions) ([]Image, error) {
 227  	return getPaginatedResults[Image](
 228  		ctx,
 229  		c,
 230  		"images",
 231  		opts,
 232  	)
 233  }
 234  
 235  // GetImage gets the Image with the provided ID.
 236  func (c *Client) GetImage(ctx context.Context, imageID string) (*Image, error) {
 237  	return doGETRequest[Image](
 238  		ctx,
 239  		c,
 240  		formatAPIPath("images/%s", imageID),
 241  	)
 242  }
 243  
 244  // CreateImage creates an Image.
 245  func (c *Client) CreateImage(ctx context.Context, opts ImageCreateOptions) (*Image, error) {
 246  	return doPOSTRequest[Image](
 247  		ctx,
 248  		c,
 249  		"images",
 250  		opts,
 251  	)
 252  }
 253  
 254  // UpdateImage updates the Image with the specified id.
 255  func (c *Client) UpdateImage(ctx context.Context, imageID string, opts ImageUpdateOptions) (*Image, error) {
 256  	return doPUTRequest[Image](
 257  		ctx,
 258  		c,
 259  		formatAPIPath("images/%s", imageID),
 260  		opts,
 261  	)
 262  }
 263  
 264  // ReplicateImage replicates an image to a given set of regions.
 265  func (c *Client) ReplicateImage(ctx context.Context, imageID string, opts ImageReplicateOptions) (*Image, error) {
 266  	return doPOSTRequest[Image](
 267  		ctx,
 268  		c,
 269  		formatAPIPath("images/%s/regions", imageID),
 270  		opts,
 271  	)
 272  }
 273  
 274  // DeleteImage deletes the Image with the specified id.
 275  func (c *Client) DeleteImage(ctx context.Context, imageID string) error {
 276  	return doDELETERequest(
 277  		ctx,
 278  		c,
 279  		formatAPIPath("images/%s", imageID),
 280  	)
 281  }
 282  
 283  // CreateImageUpload creates an Image and an upload URL.
 284  func (c *Client) CreateImageUpload(ctx context.Context, opts ImageCreateUploadOptions) (*Image, string, error) {
 285  	result, err := doPOSTRequest[ImageCreateUploadResponse](
 286  		ctx,
 287  		c,
 288  		"images/upload",
 289  		opts,
 290  	)
 291  	if err != nil {
 292  		return nil, "", err
 293  	}
 294  
 295  	return result.Image, result.UploadTo, nil
 296  }
 297  
 298  // UploadImageToURL uploads the given image to the given upload URL.
 299  func (c *Client) UploadImageToURL(ctx context.Context, uploadURL string, image io.Reader) error {
 300  	// Linode-specific headers do not need to be sent to this endpoint
 301  	req := resty.New().SetDebug(c.resty.Debug).R().
 302  		SetContext(ctx).
 303  		SetContentLength(true).
 304  		SetHeader("Content-Type", "application/octet-stream").
 305  		SetBody(image)
 306  
 307  	_, err := coupleAPIErrors(req.
 308  		Put(uploadURL))
 309  
 310  	return err
 311  }
 312  
 313  // UploadImage creates and uploads an image.
 314  func (c *Client) UploadImage(ctx context.Context, opts ImageUploadOptions) (*Image, error) {
 315  	image, uploadURL, err := c.CreateImageUpload(ctx, ImageCreateUploadOptions{
 316  		Label:       opts.Label,
 317  		Region:      opts.Region,
 318  		Description: opts.Description,
 319  		CloudInit:   opts.CloudInit,
 320  		Tags:        opts.Tags,
 321  	})
 322  	if err != nil {
 323  		return nil, err
 324  	}
 325  
 326  	return image, c.UploadImageToURL(ctx, uploadURL, opts.Image)
 327  }
 328