Skip to content

Commit

Permalink
Merge pull request #194 from LBGarber/upload-images
Browse files Browse the repository at this point in the history
Add Machine Images Endpoints
  • Loading branch information
0xch4z authored May 5, 2021
2 parents 8392825 + 6cec6f6 commit bc69cc6
Show file tree
Hide file tree
Showing 4 changed files with 409 additions and 13 deletions.
119 changes: 108 additions & 11 deletions images.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,37 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"time"

"github.com/go-resty/resty/v2"
"github.com/linode/linodego/internal/parseabletime"
)

// ImageStatus represents the status of an Image.
type ImageStatus string

// ImageStatus options start with ImageStatus and include all Image statuses
const (
ImageStatusCreating ImageStatus = "creating"
ImageStatusPendingUpload ImageStatus = "pending_upload"
ImageStatusAvailable ImageStatus = "available"
)

// Image represents a deployable Image object for use with Linode Instances
type Image struct {
ID string `json:"id"`
CreatedBy string `json:"created_by"`
Label string `json:"label"`
Description string `json:"description"`
Type string `json:"type"`
Vendor string `json:"vendor"`
Size int `json:"size"`
IsPublic bool `json:"is_public"`
Deprecated bool `json:"deprecated"`
Created *time.Time `json:"-"`
Expiry *time.Time `json:"-"`
ID string `json:"id"`
CreatedBy string `json:"created_by"`
Label string `json:"label"`
Description string `json:"description"`
Type string `json:"type"`
Vendor string `json:"vendor"`
Status ImageStatus `json:"status"`
Size int `json:"size"`
IsPublic bool `json:"is_public"`
Deprecated bool `json:"deprecated"`
Created *time.Time `json:"-"`
Expiry *time.Time `json:"-"`
}

// ImageCreateOptions fields are those accepted by CreateImage
Expand All @@ -37,6 +50,27 @@ type ImageUpdateOptions struct {
Description *string `json:"description,omitempty"`
}

// ImageCreateUploadResponse fields are those returned by CreateImageUpload
type ImageCreateUploadResponse struct {
Image *Image `json:"image"`
UploadTo string `json:"upload_to"`
}

// ImageCreateUploadOptions fields are those accepted by CreateImageUpload
type ImageCreateUploadOptions struct {
Region string `json:"region"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
}

// ImageUploadOptions fields are those accepted by UploadImage
type ImageUploadOptions struct {
Region string `json:"region"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
Image io.Reader
}

// UnmarshalJSON implements the json.Unmarshaler interface
func (i *Image) UnmarshalJSON(b []byte) error {
type Mask Image
Expand Down Expand Up @@ -175,3 +209,66 @@ func (c *Client) DeleteImage(ctx context.Context, id string) error {
_, err = coupleAPIErrors(c.R(ctx).Delete(e))
return err
}

// CreateImageUpload creates an Image and an upload URL
func (c *Client) CreateImageUpload(ctx context.Context, createOpts ImageCreateUploadOptions) (image *Image, uploadURL string, err error) {
var body string

e, err := c.Images.Endpoint()
if err != nil {
return nil, "", err
}

e = fmt.Sprintf("%s/upload", e)

req := c.R(ctx).SetResult(&ImageCreateUploadResponse{})

if bodyData, err := json.Marshal(createOpts); err == nil {
body = string(bodyData)
} else {
return nil, "", NewError(err)
}

r, err := coupleAPIErrors(req.
SetBody(body).
Post(e))
if err != nil {
return nil, "", err
}

result, ok := r.Result().(*ImageCreateUploadResponse)
if !ok {
return nil, "", fmt.Errorf("failed to parse result")
}

return result.Image, result.UploadTo, nil
}

// UploadImageToURL uploads the given image to the given upload URL
func (c *Client) UploadImageToURL(ctx context.Context, uploadURL string, image io.Reader) error {
// We currently need to create a new resty instance in order to bypass the global transport.
// This is due to the S3 rejecting requests with Authorization headers, which are injected
// by the client and test suite.
req := resty.New().SetDebug(c.resty.Debug).R().
SetHeader("Content-Type", "application/octet-stream").
SetBody(image)

_, err := coupleAPIErrors(req.
Put(uploadURL))

return err
}

// UploadImage creates and uploads an image
func (c *Client) UploadImage(ctx context.Context, options ImageUploadOptions) (*Image, error) {
image, uploadURL, err := c.CreateImageUpload(ctx, ImageCreateUploadOptions{
Label: options.Label,
Region: options.Region,
Description: options.Description,
})
if err != nil {
return nil, err
}

return image, c.UploadImageToURL(ctx, uploadURL, options.Image)
}
227 changes: 227 additions & 0 deletions test/integration/fixtures/TestUploadImage.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
---
version: 1
interactions:
- request:
body: '{"region":"us-southeast","label":"linodego-test-image","description":"An image that does stuff."}'
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- linodego https://github.com/linode/linodego
url: https://api.linode.com/v4beta/images/upload
method: POST
response:
body: '{"upload_to": "https://us-east-1.linodeobjects.com:443/linode-production-machine-images-uploads/12056370?Signature=iiRvVN6J0LbQjK4AiIpK7l3Pvpw%3D&Expires=1620318538&AWSAccessKeyID=SANITIZED", "image": {"id": "private/12056370", "label": "linodego-test-image", "description": "An image that does stuff.", "created": "2018-01-02T03:04:05", "updated": "2018-01-02T03:04:05", "size": 0, "created_by": "LGarber", "type": "manual", "is_public": false, "deprecated": false, "vendor": null, "expiry": null, "eol": null, "status": "pending_upload"}}'
headers:
Access-Control-Allow-Credentials:
- "true"
Access-Control-Allow-Headers:
- Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter
Access-Control-Allow-Methods:
- HEAD, GET, OPTIONS, POST, PUT, DELETE
Access-Control-Allow-Origin:
- '*'
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Cache-Control:
- private, max-age=60, s-maxage=60
Content-Length:
- "551"
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Server:
- nginx
Strict-Transport-Security:
- max-age=31536000
Vary:
- Authorization, X-Filter
X-Accepted-Oauth-Scopes:
- images:read_write
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
- DENY
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "800"
X-Xss-Protection:
- 1; mode=block
status: 200 OK
code: 200
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- linodego https://github.com/linode/linodego
url: https://api.linode.com/v4beta/images/private/12056370
method: GET
response:
body: '{"id": "private/12056370", "label": "linodego-test-image", "description": "An image that does stuff.", "created": "2018-01-02T03:04:05", "updated": "2018-01-02T03:04:05", "size": 0, "created_by": "LGarber", "type": "manual", "is_public": false, "deprecated": false, "vendor": null, "expiry": null, "eol": null, "status": "pending_upload"}'
headers:
Access-Control-Allow-Credentials:
- "true"
Access-Control-Allow-Headers:
- Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter
Access-Control-Allow-Methods:
- HEAD, GET, OPTIONS, POST, PUT, DELETE
Access-Control-Allow-Origin:
- '*'
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Cache-Control:
- private, max-age=0, s-maxage=0, no-cache, no-store
- private, max-age=60, s-maxage=60
Content-Length:
- "338"
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Server:
- nginx
Strict-Transport-Security:
- max-age=31536000
Vary:
- Authorization, X-Filter
- Authorization, X-Filter
X-Accepted-Oauth-Scopes:
- images:read_only
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
- DENY
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "800"
X-Xss-Protection:
- 1; mode=block
status: 200 OK
code: 200
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- linodego https://github.com/linode/linodego
url: https://api.linode.com/v4beta/images/private/12056370
method: GET
response:
body: '{"id": "private/12056370", "label": "linodego-test-image", "description": "An image that does stuff.", "created": "2018-01-02T03:04:05", "updated": "2018-01-02T03:04:05", "size": 1, "created_by": "LGarber", "type": "manual", "is_public": false, "deprecated": false, "vendor": null, "expiry": null, "eol": null, "status": "available"}'
headers:
Access-Control-Allow-Credentials:
- "true"
Access-Control-Allow-Headers:
- Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter
Access-Control-Allow-Methods:
- HEAD, GET, OPTIONS, POST, PUT, DELETE
Access-Control-Allow-Origin:
- '*'
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Cache-Control:
- private, max-age=0, s-maxage=0, no-cache, no-store
- private, max-age=60, s-maxage=60
Content-Length:
- "333"
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Server:
- nginx
Strict-Transport-Security:
- max-age=31536000
Vary:
- Authorization, X-Filter
- Authorization, X-Filter
X-Accepted-Oauth-Scopes:
- images:read_only
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
- DENY
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "800"
X-Xss-Protection:
- 1; mode=block
status: 200 OK
code: 200
duration: ""
- request:
body: ""
form: {}
headers:
Accept:
- application/json
Content-Type:
- application/json
User-Agent:
- linodego https://github.com/linode/linodego
url: https://api.linode.com/v4beta/images/private/12056370
method: DELETE
response:
body: '{}'
headers:
Access-Control-Allow-Credentials:
- "true"
Access-Control-Allow-Headers:
- Authorization, Origin, X-Requested-With, Content-Type, Accept, X-Filter
Access-Control-Allow-Methods:
- HEAD, GET, OPTIONS, POST, PUT, DELETE
Access-Control-Allow-Origin:
- '*'
Access-Control-Expose-Headers:
- X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Status
Cache-Control:
- private, max-age=60, s-maxage=60
Content-Length:
- "2"
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Server:
- nginx
Strict-Transport-Security:
- max-age=31536000
Vary:
- Authorization, X-Filter
X-Accepted-Oauth-Scopes:
- images:read_write
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- DENY
- DENY
X-Oauth-Scopes:
- '*'
X-Ratelimit-Limit:
- "800"
X-Xss-Protection:
- 1; mode=block
status: 200 OK
code: 200
duration: ""
Loading

0 comments on commit bc69cc6

Please sign in to comment.