From 444fcd12c981c202bc218df1065300a18736a2d7 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Wed, 21 Nov 2018 12:04:55 -0500 Subject: [PATCH 01/48] Update vendored godo. --- .../github.com/digitalocean/godo/CHANGELOG.md | 56 +++ .../digitalocean/godo/CONTRIBUTING.md | 1 + vendor/github.com/digitalocean/godo/README.md | 36 +- .../github.com/digitalocean/godo/account.go | 10 +- vendor/github.com/digitalocean/godo/action.go | 9 +- vendor/github.com/digitalocean/godo/cdn.go | 195 +++++++++ .../digitalocean/godo/certificates.go | 40 +- .../github.com/digitalocean/godo/domains.go | 55 ++- .../digitalocean/godo/droplet_actions.go | 62 ++- .../github.com/digitalocean/godo/droplets.go | 45 ++- .../github.com/digitalocean/godo/firewalls.go | 267 ++++++++++++ .../digitalocean/godo/floating_ips.go | 21 +- .../digitalocean/godo/floating_ips_actions.go | 13 +- vendor/github.com/digitalocean/godo/godo.go | 40 +- .../digitalocean/godo/image_actions.go | 13 +- vendor/github.com/digitalocean/godo/images.go | 78 +++- vendor/github.com/digitalocean/godo/keys.go | 21 +- .../digitalocean/godo/kubernetes.go | 380 ++++++++++++++++++ .../digitalocean/godo/load_balancers.go | 66 ++- .../github.com/digitalocean/godo/projects.go | 302 ++++++++++++++ .../github.com/digitalocean/godo/regions.go | 9 +- vendor/github.com/digitalocean/godo/sizes.go | 9 +- .../github.com/digitalocean/godo/snapshots.go | 13 +- .../github.com/digitalocean/godo/storage.go | 69 ++-- .../digitalocean/godo/storage_actions.go | 13 +- .../github.com/digitalocean/godo/strings.go | 10 + vendor/github.com/digitalocean/godo/tags.go | 43 +- vendor/vendor.json | 18 +- 28 files changed, 1639 insertions(+), 255 deletions(-) create mode 100644 vendor/github.com/digitalocean/godo/cdn.go create mode 100644 vendor/github.com/digitalocean/godo/firewalls.go create mode 100644 vendor/github.com/digitalocean/godo/kubernetes.go create mode 100644 vendor/github.com/digitalocean/godo/projects.go diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md index 71886a354..e01582aff 100644 --- a/vendor/github.com/digitalocean/godo/CHANGELOG.md +++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md @@ -1,5 +1,61 @@ # Change Log +## [v1.6.0] - 2018-10-16 + +- #185 Projects support [beta] - @mchitten + +## [v1.5.0] - 2018-10-01 + +- #181 Adding tagging images support - @hugocorbucci + +## [v1.4.2] - 2018-08-30 + +- #178 Allowing creating domain records with weight of 0 - @TFaga +- #177 Adding `VolumeLimit` to account - @lxfontes + +## [v1.4.1] - 2018-08-23 + +- #176 Fix cdn flush cache API endpoint - @sunny-b + +## [v1.4.0] - 2018-08-22 + +- #175 Add support for Spaces CDN - @sunny-b + +## [v1.3.0] - 2018-05-24 + +- #170 Add support for volume formatting - @adamwg + +## [v1.2.0] - 2018-05-08 + +- #166 Remove support for Go 1.6 - @iheanyi +- #165 Add support for Let's Encrypt Certificates - @viola + +## [v1.1.3] - 2018-03-07 + +- #156 Handle non-json errors from the API - @aknuds1 +- #158 Update droplet example to use latest instance type - @dan-v + +## [v1.1.2] - 2018-03-06 + +- #157 storage: list volumes should handle only name or only region params - @andrewsykim +- #154 docs: replace first example with fully-runnable example - @xmudrii +- #152 Handle flags & tag properties of domain record - @jaymecd + +## [v1.1.1] - 2017-09-29 + +- #151 Following user agent field recommendations - @joonas +- #148 AsRequest method to create load balancers requests - @lukegb + +## [v1.1.0] - 2017-06-06 + +### Added +- #145 Add FirewallsService for managing Firewalls with the DigitalOcean API. - @viola +- #139 Add TTL field to the Domains. - @xmudrii + +### Fixed +- #143 Fix oauth2.NoContext depreciation. - @jbowens +- #141 Fix DropletActions on tagged resources. - @xmudrii + ## [v1.0.0] - 2017-03-10 ### Added diff --git a/vendor/github.com/digitalocean/godo/CONTRIBUTING.md b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md index f27200a7a..7e16f89db 100644 --- a/vendor/github.com/digitalocean/godo/CONTRIBUTING.md +++ b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md @@ -12,6 +12,7 @@ Assuming your `$GOPATH` is set up according to your desires, run: ```sh go get github.com/digitalocean/godo +go get -u github.com/stretchr/testify/assert ``` ## Running tests diff --git a/vendor/github.com/digitalocean/godo/README.md b/vendor/github.com/digitalocean/godo/README.md index 4d5cdf83e..ffd3988b7 100644 --- a/vendor/github.com/digitalocean/godo/README.md +++ b/vendor/github.com/digitalocean/godo/README.md @@ -27,25 +27,37 @@ at the DigitalOcean Control Panel [Applications Page](https://cloud.digitalocean You can then use your token to create a new client: ```go -import "golang.org/x/oauth2" +package main + +import ( + "context" + "github.com/digitalocean/godo" + "golang.org/x/oauth2" +) + +const ( + pat = "mytoken" +) -pat := "mytoken" type TokenSource struct { - AccessToken string + AccessToken string } func (t *TokenSource) Token() (*oauth2.Token, error) { - token := &oauth2.Token{ - AccessToken: t.AccessToken, - } - return token, nil + token := &oauth2.Token{ + AccessToken: t.AccessToken, + } + return token, nil } -tokenSource := &TokenSource{ - AccessToken: pat, +func main() { + tokenSource := &TokenSource{ + AccessToken: pat, + } + + oauthClient := oauth2.NewClient(context.Background(), tokenSource) + client := godo.NewClient(oauthClient) } -oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) -client := godo.NewClient(oauthClient) ``` ## Examples @@ -59,7 +71,7 @@ dropletName := "super-cool-droplet" createRequest := &godo.DropletCreateRequest{ Name: dropletName, Region: "nyc3", - Size: "512mb", + Size: "s-1vcpu-1gb", Image: godo.DropletCreateImage{ Slug: "ubuntu-14-04-x64", }, diff --git a/vendor/github.com/digitalocean/godo/account.go b/vendor/github.com/digitalocean/godo/account.go index 18eed9712..7d3e105d3 100644 --- a/vendor/github.com/digitalocean/godo/account.go +++ b/vendor/github.com/digitalocean/godo/account.go @@ -1,6 +1,9 @@ package godo -import "context" +import ( + "context" + "net/http" +) // AccountService is an interface for interfacing with the Account // endpoints of the DigitalOcean API @@ -21,6 +24,7 @@ var _ AccountService = &AccountServiceOp{} type Account struct { DropletLimit int `json:"droplet_limit,omitempty"` FloatingIPLimit int `json:"floating_ip_limit,omitempty"` + VolumeLimit int `json:"volume_limit,omitempty"` Email string `json:"email,omitempty"` UUID string `json:"uuid,omitempty"` EmailVerified bool `json:"email_verified,omitempty"` @@ -41,13 +45,13 @@ func (s *AccountServiceOp) Get(ctx context.Context) (*Account, *Response, error) path := "v2/account" - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(accountRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/action.go b/vendor/github.com/digitalocean/godo/action.go index 9baef2147..67ef3ab8d 100644 --- a/vendor/github.com/digitalocean/godo/action.go +++ b/vendor/github.com/digitalocean/godo/action.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) const ( @@ -60,13 +61,13 @@ func (s *ActionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Action return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -84,13 +85,13 @@ func (s *ActionsServiceOp) Get(ctx context.Context, id int) (*Action, *Response, } path := fmt.Sprintf("%s/%d", actionsBasePath, id) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/cdn.go b/vendor/github.com/digitalocean/godo/cdn.go new file mode 100644 index 000000000..217d90e52 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/cdn.go @@ -0,0 +1,195 @@ +package godo + +import ( + "context" + "fmt" + "net/http" + "time" +) + +const cdnBasePath = "v2/cdn/endpoints" + +// CDNService is an interface for managing Spaces CDN with the DigitalOcean API. +type CDNService interface { + List(context.Context, *ListOptions) ([]CDN, *Response, error) + Get(context.Context, string) (*CDN, *Response, error) + Create(context.Context, *CDNCreateRequest) (*CDN, *Response, error) + UpdateTTL(context.Context, string, *CDNUpdateRequest) (*CDN, *Response, error) + FlushCache(context.Context, string, *CDNFlushCacheRequest) (*Response, error) + Delete(context.Context, string) (*Response, error) +} + +// CDNServiceOp handles communication with the CDN related methods of the +// DigitalOcean API. +type CDNServiceOp struct { + client *Client +} + +var _ CDNService = &CDNServiceOp{} + +// CDN represents a DigitalOcean CDN +type CDN struct { + ID string `json:"id"` + Origin string `json:"origin"` + Endpoint string `json:"endpoint"` + CreatedAt time.Time `json:"created_at"` + TTL uint32 `json:"ttl"` +} + +// CDNRoot represents a response from the DigitalOcean API +type cdnRoot struct { + Endpoint *CDN `json:"endpoint"` +} + +type cdnsRoot struct { + Endpoints []CDN `json:"endpoints"` + Links *Links `json:"links"` +} + +// CDNCreateRequest represents a request to create a CDN. +type CDNCreateRequest struct { + Origin string `json:"origin"` + TTL uint32 `json:"ttl"` +} + +// CDNUpdateRequest represents a request to update the ttl of a CDN. +type CDNUpdateRequest struct { + TTL uint32 `json:"ttl"` +} + +// CDNFlushCacheRequest represents a request to flush cache of a CDN. +type CDNFlushCacheRequest struct { + Files []string `json:"files"` +} + +// List all CDN endpoints +func (c CDNServiceOp) List(ctx context.Context, opt *ListOptions) ([]CDN, *Response, error) { + path, err := addOptions(cdnBasePath, opt) + if err != nil { + return nil, nil, err + } + + req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(cdnsRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Endpoints, resp, err +} + +// Get individual CDN. It requires a non-empty cdn id. +func (c CDNServiceOp) Get(ctx context.Context, id string) (*CDN, *Response, error) { + if len(id) == 0 { + return nil, nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(cdnRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Endpoint, resp, err +} + +// Create a new CDN +func (c CDNServiceOp) Create(ctx context.Context, createRequest *CDNCreateRequest) (*CDN, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := c.client.NewRequest(ctx, http.MethodPost, cdnBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(cdnRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Endpoint, resp, err +} + +// UpdateTTL updates the ttl of individual CDN +func (c CDNServiceOp) UpdateTTL(ctx context.Context, id string, updateRequest *CDNUpdateRequest) (*CDN, *Response, error) { + if updateRequest == nil { + return nil, nil, NewArgError("updateRequest", "cannot be nil") + } + + if len(id) == 0 { + return nil, nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodPut, path, updateRequest) + if err != nil { + return nil, nil, err + } + + root := new(cdnRoot) + resp, err := c.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Endpoint, resp, err +} + +// FlushCache flushes the cache of an individual CDN. Requires a non-empty slice of file paths and/or wildcards +func (c CDNServiceOp) FlushCache(ctx context.Context, id string, flushCacheRequest *CDNFlushCacheRequest) (*Response, error) { + if flushCacheRequest == nil { + return nil, NewArgError("flushCacheRequest", "cannot be nil") + } + + if len(id) == 0 { + return nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s/cache", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodDelete, path, flushCacheRequest) + if err != nil { + return nil, err + } + + resp, err := c.client.Do(ctx, req, nil) + + return resp, err +} + +// Delete an individual CDN +func (c CDNServiceOp) Delete(ctx context.Context, id string) (*Response, error) { + if len(id) == 0 { + return nil, NewArgError("id", "cannot be an empty string") + } + + path := fmt.Sprintf("%s/%s", cdnBasePath, id) + + req, err := c.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + resp, err := c.client.Do(ctx, req, nil) + + return resp, err +} diff --git a/vendor/github.com/digitalocean/godo/certificates.go b/vendor/github.com/digitalocean/godo/certificates.go index b48e80ec3..cbc3e16ae 100644 --- a/vendor/github.com/digitalocean/godo/certificates.go +++ b/vendor/github.com/digitalocean/godo/certificates.go @@ -2,6 +2,7 @@ package godo import ( "context" + "net/http" "path" ) @@ -18,19 +19,24 @@ type CertificatesService interface { // Certificate represents a DigitalOcean certificate configuration. type Certificate struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - NotAfter string `json:"not_after,omitempty"` - SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"` - Created string `json:"created_at,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + DNSNames []string `json:"dns_names,omitempty"` + NotAfter string `json:"not_after,omitempty"` + SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"` + Created string `json:"created_at,omitempty"` + State string `json:"state,omitempty"` + Type string `json:"type,omitempty"` } // CertificateRequest represents configuration for a new certificate. type CertificateRequest struct { - Name string `json:"name,omitempty"` - PrivateKey string `json:"private_key,omitempty"` - LeafCertificate string `json:"leaf_certificate,omitempty"` - CertificateChain string `json:"certificate_chain,omitempty"` + Name string `json:"name,omitempty"` + DNSNames []string `json:"dns_names,omitempty"` + PrivateKey string `json:"private_key,omitempty"` + LeafCertificate string `json:"leaf_certificate,omitempty"` + CertificateChain string `json:"certificate_chain,omitempty"` + Type string `json:"type,omitempty"` } type certificateRoot struct { @@ -53,13 +59,13 @@ var _ CertificatesService = &CertificatesServiceOp{} func (c *CertificatesServiceOp) Get(ctx context.Context, cID string) (*Certificate, *Response, error) { urlStr := path.Join(certificatesBasePath, cID) - req, err := c.client.NewRequest(ctx, "GET", urlStr, nil) + req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil) if err != nil { return nil, nil, err } root := new(certificateRoot) - resp, err := c.client.Do(req, root) + resp, err := c.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -74,13 +80,13 @@ func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]C return nil, nil, err } - req, err := c.client.NewRequest(ctx, "GET", urlStr, nil) + req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil) if err != nil { return nil, nil, err } root := new(certificatesRoot) - resp, err := c.client.Do(req, root) + resp, err := c.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -93,13 +99,13 @@ func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]C // Create a new certificate with provided configuration. func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateRequest) (*Certificate, *Response, error) { - req, err := c.client.NewRequest(ctx, "POST", certificatesBasePath, cr) + req, err := c.client.NewRequest(ctx, http.MethodPost, certificatesBasePath, cr) if err != nil { return nil, nil, err } root := new(certificateRoot) - resp, err := c.client.Do(req, root) + resp, err := c.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -111,10 +117,10 @@ func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateReque func (c *CertificatesServiceOp) Delete(ctx context.Context, cID string) (*Response, error) { urlStr := path.Join(certificatesBasePath, cID) - req, err := c.client.NewRequest(ctx, "DELETE", urlStr, nil) + req, err := c.client.NewRequest(ctx, http.MethodDelete, urlStr, nil) if err != nil { return nil, err } - return c.client.Do(req, nil) + return c.client.Do(ctx, req, nil) } diff --git a/vendor/github.com/digitalocean/godo/domains.go b/vendor/github.com/digitalocean/godo/domains.go index fdc2a84c6..de266512e 100644 --- a/vendor/github.com/digitalocean/godo/domains.go +++ b/vendor/github.com/digitalocean/godo/domains.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) const domainsBasePath = "v2/domains" @@ -51,7 +52,7 @@ type domainsRoot struct { // DomainCreateRequest respresents a request to create a domain. type DomainCreateRequest struct { Name string `json:"name"` - IPAddress string `json:"ip_address"` + IPAddress string `json:"ip_address,omitempty"` } // DomainRecordRoot is the root of an individual Domain Record response @@ -71,9 +72,12 @@ type DomainRecord struct { Type string `json:"type,omitempty"` Name string `json:"name,omitempty"` Data string `json:"data,omitempty"` - Priority int `json:"priority,omitempty"` + Priority int `json:"priority"` Port int `json:"port,omitempty"` - Weight int `json:"weight,omitempty"` + TTL int `json:"ttl,omitempty"` + Weight int `json:"weight"` + Flags int `json:"flags"` + Tag string `json:"tag,omitempty"` } // DomainRecordEditRequest represents a request to update a domain record. @@ -81,15 +85,22 @@ type DomainRecordEditRequest struct { Type string `json:"type,omitempty"` Name string `json:"name,omitempty"` Data string `json:"data,omitempty"` - Priority int `json:"priority,omitempty"` + Priority int `json:"priority"` Port int `json:"port,omitempty"` - Weight int `json:"weight,omitempty"` + TTL int `json:"ttl,omitempty"` + Weight int `json:"weight"` + Flags int `json:"flags"` + Tag string `json:"tag,omitempty"` } func (d Domain) String() string { return Stringify(d) } +func (d Domain) URN() string { + return ToURN("Domain", d.Name) +} + // List all domains. func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) { path := domainsBasePath @@ -98,13 +109,13 @@ func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(domainsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -123,13 +134,13 @@ func (s *DomainsServiceOp) Get(ctx context.Context, name string) (*Domain, *Resp path := fmt.Sprintf("%s/%s", domainsBasePath, name) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(domainRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -145,13 +156,13 @@ func (s *DomainsServiceOp) Create(ctx context.Context, createRequest *DomainCrea path := domainsBasePath - req, err := s.client.NewRequest(ctx, "POST", path, createRequest) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest) if err != nil { return nil, nil, err } root := new(domainRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -166,12 +177,12 @@ func (s *DomainsServiceOp) Delete(ctx context.Context, name string) (*Response, path := fmt.Sprintf("%s/%s", domainsBasePath, name) - req, err := s.client.NewRequest(ctx, "DELETE", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } @@ -198,13 +209,13 @@ func (s *DomainsServiceOp) Records(ctx context.Context, domain string, opt *List return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(domainRecordsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -227,13 +238,13 @@ func (s *DomainsServiceOp) Record(ctx context.Context, domain string, id int) (* path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } record := new(domainRecordRoot) - resp, err := s.client.Do(req, record) + resp, err := s.client.Do(ctx, req, record) if err != nil { return nil, resp, err } @@ -253,12 +264,12 @@ func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id i path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id) - req, err := s.client.NewRequest(ctx, "DELETE", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } @@ -289,7 +300,7 @@ func (s *DomainsServiceOp) EditRecord(ctx context.Context, } d := new(DomainRecord) - resp, err := s.client.Do(req, d) + resp, err := s.client.Do(ctx, req, d) if err != nil { return nil, resp, err } @@ -310,14 +321,14 @@ func (s *DomainsServiceOp) CreateRecord(ctx context.Context, } path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain) - req, err := s.client.NewRequest(ctx, "POST", path, createRequest) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest) if err != nil { return nil, nil, err } d := new(domainRecordRoot) - resp, err := s.client.Do(req, d) + resp, err := s.client.Do(ctx, req, d) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/droplet_actions.go b/vendor/github.com/digitalocean/godo/droplet_actions.go index cd8b94e2f..ddeacfc86 100644 --- a/vendor/github.com/digitalocean/godo/droplet_actions.go +++ b/vendor/github.com/digitalocean/godo/droplet_actions.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" "net/url" ) @@ -14,32 +15,31 @@ type ActionRequest map[string]interface{} // See: https://developers.digitalocean.com/documentation/v2#droplet-actions type DropletActionsService interface { Shutdown(context.Context, int) (*Action, *Response, error) - ShutdownByTag(context.Context, string) (*Action, *Response, error) + ShutdownByTag(context.Context, string) ([]Action, *Response, error) PowerOff(context.Context, int) (*Action, *Response, error) - PowerOffByTag(context.Context, string) (*Action, *Response, error) + PowerOffByTag(context.Context, string) ([]Action, *Response, error) PowerOn(context.Context, int) (*Action, *Response, error) - PowerOnByTag(context.Context, string) (*Action, *Response, error) + PowerOnByTag(context.Context, string) ([]Action, *Response, error) PowerCycle(context.Context, int) (*Action, *Response, error) - PowerCycleByTag(context.Context, string) (*Action, *Response, error) + PowerCycleByTag(context.Context, string) ([]Action, *Response, error) Reboot(context.Context, int) (*Action, *Response, error) Restore(context.Context, int, int) (*Action, *Response, error) Resize(context.Context, int, string, bool) (*Action, *Response, error) Rename(context.Context, int, string) (*Action, *Response, error) Snapshot(context.Context, int, string) (*Action, *Response, error) - SnapshotByTag(context.Context, string, string) (*Action, *Response, error) + SnapshotByTag(context.Context, string, string) ([]Action, *Response, error) EnableBackups(context.Context, int) (*Action, *Response, error) - EnableBackupsByTag(context.Context, string) (*Action, *Response, error) + EnableBackupsByTag(context.Context, string) ([]Action, *Response, error) DisableBackups(context.Context, int) (*Action, *Response, error) - DisableBackupsByTag(context.Context, string) (*Action, *Response, error) + DisableBackupsByTag(context.Context, string) ([]Action, *Response, error) PasswordReset(context.Context, int) (*Action, *Response, error) RebuildByImageID(context.Context, int, int) (*Action, *Response, error) RebuildByImageSlug(context.Context, int, string) (*Action, *Response, error) ChangeKernel(context.Context, int, int) (*Action, *Response, error) EnableIPv6(context.Context, int) (*Action, *Response, error) - EnableIPv6ByTag(context.Context, string) (*Action, *Response, error) + EnableIPv6ByTag(context.Context, string) ([]Action, *Response, error) EnablePrivateNetworking(context.Context, int) (*Action, *Response, error) - EnablePrivateNetworkingByTag(context.Context, string) (*Action, *Response, error) - Upgrade(context.Context, int) (*Action, *Response, error) + EnablePrivateNetworkingByTag(context.Context, string) ([]Action, *Response, error) Get(context.Context, int, int) (*Action, *Response, error) GetByURI(context.Context, string) (*Action, *Response, error) } @@ -59,7 +59,7 @@ func (s *DropletActionsServiceOp) Shutdown(ctx context.Context, id int) (*Action } // ShutdownByTag shuts down Droplets matched by a Tag. -func (s *DropletActionsServiceOp) ShutdownByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) ShutdownByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "shutdown"} return s.doActionByTag(ctx, tag, request) } @@ -71,7 +71,7 @@ func (s *DropletActionsServiceOp) PowerOff(ctx context.Context, id int) (*Action } // PowerOffByTag powers off Droplets matched by a Tag. -func (s *DropletActionsServiceOp) PowerOffByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) PowerOffByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "power_off"} return s.doActionByTag(ctx, tag, request) } @@ -83,7 +83,7 @@ func (s *DropletActionsServiceOp) PowerOn(ctx context.Context, id int) (*Action, } // PowerOnByTag powers on Droplets matched by a Tag. -func (s *DropletActionsServiceOp) PowerOnByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) PowerOnByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "power_on"} return s.doActionByTag(ctx, tag, request) } @@ -95,7 +95,7 @@ func (s *DropletActionsServiceOp) PowerCycle(ctx context.Context, id int) (*Acti } // PowerCycleByTag power cycles Droplets matched by a Tag. -func (s *DropletActionsServiceOp) PowerCycleByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) PowerCycleByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "power_cycle"} return s.doActionByTag(ctx, tag, request) } @@ -148,7 +148,7 @@ func (s *DropletActionsServiceOp) Snapshot(ctx context.Context, id int, name str } // SnapshotByTag snapshots Droplets matched by a Tag. -func (s *DropletActionsServiceOp) SnapshotByTag(ctx context.Context, tag string, name string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) SnapshotByTag(ctx context.Context, tag string, name string) ([]Action, *Response, error) { requestType := "snapshot" request := &ActionRequest{ "type": requestType, @@ -164,7 +164,7 @@ func (s *DropletActionsServiceOp) EnableBackups(ctx context.Context, id int) (*A } // EnableBackupsByTag enables backups for Droplets matched by a Tag. -func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "enable_backups"} return s.doActionByTag(ctx, tag, request) } @@ -176,7 +176,7 @@ func (s *DropletActionsServiceOp) DisableBackups(ctx context.Context, id int) (* } // DisableBackupsByTag disables backups for Droplet matched by a Tag. -func (s *DropletActionsServiceOp) DisableBackupsByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) DisableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "disable_backups"} return s.doActionByTag(ctx, tag, request) } @@ -212,7 +212,7 @@ func (s *DropletActionsServiceOp) EnableIPv6(ctx context.Context, id int) (*Acti } // EnableIPv6ByTag enables IPv6 for Droplets matched by a Tag. -func (s *DropletActionsServiceOp) EnableIPv6ByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) EnableIPv6ByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "enable_ipv6"} return s.doActionByTag(ctx, tag, request) } @@ -224,17 +224,11 @@ func (s *DropletActionsServiceOp) EnablePrivateNetworking(ctx context.Context, i } // EnablePrivateNetworkingByTag enables private networking for Droplets matched by a Tag. -func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(ctx context.Context, tag string) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(ctx context.Context, tag string) ([]Action, *Response, error) { request := &ActionRequest{"type": "enable_private_networking"} return s.doActionByTag(ctx, tag, request) } -// Upgrade a Droplet. -func (s *DropletActionsServiceOp) Upgrade(ctx context.Context, id int) (*Action, *Response, error) { - request := &ActionRequest{"type": "upgrade"} - return s.doAction(ctx, id, request) -} - func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request *ActionRequest) (*Action, *Response, error) { if id < 1 { return nil, nil, NewArgError("id", "cannot be less than 1") @@ -246,13 +240,13 @@ func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request path := dropletActionPath(id) - req, err := s.client.NewRequest(ctx, "POST", path, request) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, request) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -260,7 +254,7 @@ func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request return root.Event, resp, err } -func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) ([]Action, *Response, error) { if tag == "" { return nil, nil, NewArgError("tag", "cannot be empty") } @@ -271,18 +265,18 @@ func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, path := dropletActionPathByTag(tag) - req, err := s.client.NewRequest(ctx, "POST", path, request) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, request) if err != nil { return nil, nil, err } - root := new(actionRoot) - resp, err := s.client.Do(req, root) + root := new(actionsRoot) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } - return root.Event, resp, err + return root.Actions, resp, err } // Get an action for a particular Droplet by id. @@ -311,13 +305,13 @@ func (s *DropletActionsServiceOp) GetByURI(ctx context.Context, rawurl string) ( } func (s *DropletActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) { - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/droplets.go b/vendor/github.com/digitalocean/godo/droplets.go index b145d9805..ab508f1c0 100644 --- a/vendor/github.com/digitalocean/godo/droplets.go +++ b/vendor/github.com/digitalocean/godo/droplets.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" ) const dropletBasePath = "v2/droplets" @@ -124,6 +125,10 @@ func (d Droplet) String() string { return Stringify(d) } +func (d Droplet) URN() string { + return ToURN("Droplet", d.ID) +} + // DropletRoot represents a Droplet root type dropletRoot struct { Droplet *Droplet `json:"droplet"` @@ -274,13 +279,13 @@ func (n NetworkV6) String() string { // Performs a list request given a path. func (s *DropletsServiceOp) list(ctx context.Context, path string) ([]Droplet, *Response, error) { - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(dropletsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -321,13 +326,13 @@ func (s *DropletsServiceOp) Get(ctx context.Context, dropletID int) (*Droplet, * path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(dropletRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -343,13 +348,13 @@ func (s *DropletsServiceOp) Create(ctx context.Context, createRequest *DropletCr path := dropletBasePath - req, err := s.client.NewRequest(ctx, "POST", path, createRequest) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest) if err != nil { return nil, nil, err } root := new(dropletRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -368,13 +373,13 @@ func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *D path := dropletBasePath - req, err := s.client.NewRequest(ctx, "POST", path, createRequest) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest) if err != nil { return nil, nil, err } root := new(dropletsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -387,12 +392,12 @@ func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *D // Performs a delete request given a path func (s *DropletsServiceOp) delete(ctx context.Context, path string) (*Response, error) { - req, err := s.client.NewRequest(ctx, "DELETE", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } @@ -431,13 +436,13 @@ func (s *DropletsServiceOp) Kernels(ctx context.Context, dropletID int, opt *Lis return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(kernelsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if l := root.Links; l != nil { resp.Links = l } @@ -457,13 +462,13 @@ func (s *DropletsServiceOp) Actions(ctx context.Context, dropletID int, opt *Lis return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -486,13 +491,13 @@ func (s *DropletsServiceOp) Backups(ctx context.Context, dropletID int, opt *Lis return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(backupsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -515,13 +520,13 @@ func (s *DropletsServiceOp) Snapshots(ctx context.Context, dropletID int, opt *L return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(dropletSnapshotsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -540,13 +545,13 @@ func (s *DropletsServiceOp) Neighbors(ctx context.Context, dropletID int) ([]Dro path := fmt.Sprintf("%s/%d/neighbors", dropletBasePath, dropletID) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(dropletsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/firewalls.go b/vendor/github.com/digitalocean/godo/firewalls.go new file mode 100644 index 000000000..c28cac03b --- /dev/null +++ b/vendor/github.com/digitalocean/godo/firewalls.go @@ -0,0 +1,267 @@ +package godo + +import ( + "context" + "net/http" + "path" + "strconv" +) + +const firewallsBasePath = "/v2/firewalls" + +// FirewallsService is an interface for managing Firewalls with the DigitalOcean API. +// See: https://developers.digitalocean.com/documentation/documentation/v2/#firewalls +type FirewallsService interface { + Get(context.Context, string) (*Firewall, *Response, error) + Create(context.Context, *FirewallRequest) (*Firewall, *Response, error) + Update(context.Context, string, *FirewallRequest) (*Firewall, *Response, error) + Delete(context.Context, string) (*Response, error) + List(context.Context, *ListOptions) ([]Firewall, *Response, error) + ListByDroplet(context.Context, int, *ListOptions) ([]Firewall, *Response, error) + AddDroplets(context.Context, string, ...int) (*Response, error) + RemoveDroplets(context.Context, string, ...int) (*Response, error) + AddTags(context.Context, string, ...string) (*Response, error) + RemoveTags(context.Context, string, ...string) (*Response, error) + AddRules(context.Context, string, *FirewallRulesRequest) (*Response, error) + RemoveRules(context.Context, string, *FirewallRulesRequest) (*Response, error) +} + +// FirewallsServiceOp handles communication with Firewalls methods of the DigitalOcean API. +type FirewallsServiceOp struct { + client *Client +} + +// Firewall represents a DigitalOcean Firewall configuration. +type Firewall struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + InboundRules []InboundRule `json:"inbound_rules"` + OutboundRules []OutboundRule `json:"outbound_rules"` + DropletIDs []int `json:"droplet_ids"` + Tags []string `json:"tags"` + Created string `json:"created_at"` + PendingChanges []PendingChange `json:"pending_changes"` +} + +// String creates a human-readable description of a Firewall. +func (fw Firewall) String() string { + return Stringify(fw) +} + +func (fw Firewall) URN() string { + return ToURN("Firewall", fw.ID) +} + +// FirewallRequest represents the configuration to be applied to an existing or a new Firewall. +type FirewallRequest struct { + Name string `json:"name"` + InboundRules []InboundRule `json:"inbound_rules"` + OutboundRules []OutboundRule `json:"outbound_rules"` + DropletIDs []int `json:"droplet_ids"` + Tags []string `json:"tags"` +} + +// FirewallRulesRequest represents rules configuration to be applied to an existing Firewall. +type FirewallRulesRequest struct { + InboundRules []InboundRule `json:"inbound_rules"` + OutboundRules []OutboundRule `json:"outbound_rules"` +} + +// InboundRule represents a DigitalOcean Firewall inbound rule. +type InboundRule struct { + Protocol string `json:"protocol,omitempty"` + PortRange string `json:"ports,omitempty"` + Sources *Sources `json:"sources"` +} + +// OutboundRule represents a DigitalOcean Firewall outbound rule. +type OutboundRule struct { + Protocol string `json:"protocol,omitempty"` + PortRange string `json:"ports,omitempty"` + Destinations *Destinations `json:"destinations"` +} + +// Sources represents a DigitalOcean Firewall InboundRule sources. +type Sources struct { + Addresses []string `json:"addresses,omitempty"` + Tags []string `json:"tags,omitempty"` + DropletIDs []int `json:"droplet_ids,omitempty"` + LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"` +} + +// PendingChange represents a DigitalOcean Firewall status details. +type PendingChange struct { + DropletID int `json:"droplet_id,omitempty"` + Removing bool `json:"removing,omitempty"` + Status string `json:"status,omitempty"` +} + +// Destinations represents a DigitalOcean Firewall OutboundRule destinations. +type Destinations struct { + Addresses []string `json:"addresses,omitempty"` + Tags []string `json:"tags,omitempty"` + DropletIDs []int `json:"droplet_ids,omitempty"` + LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"` +} + +var _ FirewallsService = &FirewallsServiceOp{} + +// Get an existing Firewall by its identifier. +func (fw *FirewallsServiceOp) Get(ctx context.Context, fID string) (*Firewall, *Response, error) { + path := path.Join(firewallsBasePath, fID) + + req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(firewallRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Firewall, resp, err +} + +// Create a new Firewall with a given configuration. +func (fw *FirewallsServiceOp) Create(ctx context.Context, fr *FirewallRequest) (*Firewall, *Response, error) { + req, err := fw.client.NewRequest(ctx, http.MethodPost, firewallsBasePath, fr) + if err != nil { + return nil, nil, err + } + + root := new(firewallRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Firewall, resp, err +} + +// Update an existing Firewall with new configuration. +func (fw *FirewallsServiceOp) Update(ctx context.Context, fID string, fr *FirewallRequest) (*Firewall, *Response, error) { + path := path.Join(firewallsBasePath, fID) + + req, err := fw.client.NewRequest(ctx, "PUT", path, fr) + if err != nil { + return nil, nil, err + } + + root := new(firewallRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Firewall, resp, err +} + +// Delete a Firewall by its identifier. +func (fw *FirewallsServiceOp) Delete(ctx context.Context, fID string) (*Response, error) { + path := path.Join(firewallsBasePath, fID) + return fw.createAndDoReq(ctx, http.MethodDelete, path, nil) +} + +// List Firewalls. +func (fw *FirewallsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Firewall, *Response, error) { + path, err := addOptions(firewallsBasePath, opt) + if err != nil { + return nil, nil, err + } + + return fw.listHelper(ctx, path) +} + +// ListByDroplet Firewalls. +func (fw *FirewallsServiceOp) ListByDroplet(ctx context.Context, dID int, opt *ListOptions) ([]Firewall, *Response, error) { + basePath := path.Join(dropletBasePath, strconv.Itoa(dID), "firewalls") + path, err := addOptions(basePath, opt) + if err != nil { + return nil, nil, err + } + + return fw.listHelper(ctx, path) +} + +// AddDroplets to a Firewall. +func (fw *FirewallsServiceOp) AddDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "droplets") + return fw.createAndDoReq(ctx, http.MethodPost, path, &dropletsRequest{IDs: dropletIDs}) +} + +// RemoveDroplets from a Firewall. +func (fw *FirewallsServiceOp) RemoveDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "droplets") + return fw.createAndDoReq(ctx, http.MethodDelete, path, &dropletsRequest{IDs: dropletIDs}) +} + +// AddTags to a Firewall. +func (fw *FirewallsServiceOp) AddTags(ctx context.Context, fID string, tags ...string) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "tags") + return fw.createAndDoReq(ctx, http.MethodPost, path, &tagsRequest{Tags: tags}) +} + +// RemoveTags from a Firewall. +func (fw *FirewallsServiceOp) RemoveTags(ctx context.Context, fID string, tags ...string) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "tags") + return fw.createAndDoReq(ctx, http.MethodDelete, path, &tagsRequest{Tags: tags}) +} + +// AddRules to a Firewall. +func (fw *FirewallsServiceOp) AddRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "rules") + return fw.createAndDoReq(ctx, http.MethodPost, path, rr) +} + +// RemoveRules from a Firewall. +func (fw *FirewallsServiceOp) RemoveRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) { + path := path.Join(firewallsBasePath, fID, "rules") + return fw.createAndDoReq(ctx, http.MethodDelete, path, rr) +} + +type dropletsRequest struct { + IDs []int `json:"droplet_ids"` +} + +type tagsRequest struct { + Tags []string `json:"tags"` +} + +type firewallRoot struct { + Firewall *Firewall `json:"firewall"` +} + +type firewallsRoot struct { + Firewalls []Firewall `json:"firewalls"` + Links *Links `json:"links"` +} + +func (fw *FirewallsServiceOp) createAndDoReq(ctx context.Context, method, path string, v interface{}) (*Response, error) { + req, err := fw.client.NewRequest(ctx, method, path, v) + if err != nil { + return nil, err + } + + return fw.client.Do(ctx, req, nil) +} + +func (fw *FirewallsServiceOp) listHelper(ctx context.Context, path string) ([]Firewall, *Response, error) { + req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(firewallsRoot) + resp, err := fw.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Firewalls, resp, err +} diff --git a/vendor/github.com/digitalocean/godo/floating_ips.go b/vendor/github.com/digitalocean/godo/floating_ips.go index 13e983261..4545e9037 100644 --- a/vendor/github.com/digitalocean/godo/floating_ips.go +++ b/vendor/github.com/digitalocean/godo/floating_ips.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) const floatingBasePath = "v2/floating_ips" @@ -36,6 +37,10 @@ func (f FloatingIP) String() string { return Stringify(f) } +func (f FloatingIP) URN() string { + return ToURN("FloatingIP", f.IP) +} + type floatingIPsRoot struct { FloatingIPs []FloatingIP `json:"floating_ips"` Links *Links `json:"links"` @@ -62,13 +67,13 @@ func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Fl return nil, nil, err } - req, err := f.client.NewRequest(ctx, "GET", path, nil) + req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(floatingIPsRoot) - resp, err := f.client.Do(req, root) + resp, err := f.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -83,13 +88,13 @@ func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Fl func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP, *Response, error) { path := fmt.Sprintf("%s/%s", floatingBasePath, ip) - req, err := f.client.NewRequest(ctx, "GET", path, nil) + req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(floatingIPRoot) - resp, err := f.client.Do(req, root) + resp, err := f.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -102,13 +107,13 @@ func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP, func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *FloatingIPCreateRequest) (*FloatingIP, *Response, error) { path := floatingBasePath - req, err := f.client.NewRequest(ctx, "POST", path, createRequest) + req, err := f.client.NewRequest(ctx, http.MethodPost, path, createRequest) if err != nil { return nil, nil, err } root := new(floatingIPRoot) - resp, err := f.client.Do(req, root) + resp, err := f.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -123,12 +128,12 @@ func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *Floati func (f *FloatingIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) { path := fmt.Sprintf("%s/%s", floatingBasePath, ip) - req, err := f.client.NewRequest(ctx, "DELETE", path, nil) + req, err := f.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := f.client.Do(req, nil) + resp, err := f.client.Do(ctx, req, nil) return resp, err } diff --git a/vendor/github.com/digitalocean/godo/floating_ips_actions.go b/vendor/github.com/digitalocean/godo/floating_ips_actions.go index 5afa051d7..74ad279f9 100644 --- a/vendor/github.com/digitalocean/godo/floating_ips_actions.go +++ b/vendor/github.com/digitalocean/godo/floating_ips_actions.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) // FloatingIPActionsService is an interface for interfacing with the @@ -56,13 +57,13 @@ func (s *FloatingIPActionsServiceOp) List(ctx context.Context, ip string, opt *L func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) { path := floatingIPActionPath(ip) - req, err := s.client.NewRequest(ctx, "POST", path, request) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, request) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -71,13 +72,13 @@ func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, re } func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) { - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -86,13 +87,13 @@ func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Act } func (s *FloatingIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) { - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index 0f9d7a91b..ab4f0884d 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -18,7 +18,7 @@ import ( ) const ( - libraryVersion = "1.0.0" + libraryVersion = "1.6.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" @@ -46,6 +46,7 @@ type Client struct { // Services used for communicating with the API Account AccountService Actions ActionsService + CDNs CDNService Domains DomainsService Droplets DropletsService DropletActions DropletActionsService @@ -62,6 +63,9 @@ type Client struct { Tags TagsService LoadBalancers LoadBalancersService Certificates CertificatesService + Firewalls FirewallsService + Projects ProjectsService + Kubernetes KubernetesService // Optional function called after every successful request made to the DO APIs onRequestCompleted RequestCompletionCallback @@ -156,22 +160,26 @@ func NewClient(httpClient *http.Client) *Client { c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent} c.Account = &AccountServiceOp{client: c} c.Actions = &ActionsServiceOp{client: c} + c.CDNs = &CDNServiceOp{client: c} + c.Certificates = &CertificatesServiceOp{client: c} c.Domains = &DomainsServiceOp{client: c} c.Droplets = &DropletsServiceOp{client: c} c.DropletActions = &DropletActionsServiceOp{client: c} + c.Firewalls = &FirewallsServiceOp{client: c} c.FloatingIPs = &FloatingIPsServiceOp{client: c} c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c} c.Images = &ImagesServiceOp{client: c} c.ImageActions = &ImageActionsServiceOp{client: c} c.Keys = &KeysServiceOp{client: c} + c.LoadBalancers = &LoadBalancersServiceOp{client: c} + c.Projects = &ProjectsServiceOp{client: c} c.Regions = &RegionsServiceOp{client: c} - c.Snapshots = &SnapshotsServiceOp{client: c} c.Sizes = &SizesServiceOp{client: c} + c.Snapshots = &SnapshotsServiceOp{client: c} c.Storage = &StorageServiceOp{client: c} c.StorageActions = &StorageActionsServiceOp{client: c} c.Tags = &TagsServiceOp{client: c} - c.LoadBalancers = &LoadBalancersServiceOp{client: c} - c.Certificates = &CertificatesServiceOp{client: c} + c.Kubernetes = &KubernetesServiceOp{client: c} return c } @@ -207,7 +215,7 @@ func SetBaseURL(bu string) ClientOpt { // SetUserAgent is a client option for setting the user agent. func SetUserAgent(ua string) ClientOpt { return func(c *Client) error { - c.UserAgent = fmt.Sprintf("%s+%s", ua, c.UserAgent) + c.UserAgent = fmt.Sprintf("%s %s", ua, c.UserAgent) return nil } } @@ -236,7 +244,6 @@ func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body int return nil, err } - req = req.WithContext(ctx) req.Header.Add("Content-Type", mediaType) req.Header.Add("Accept", mediaType) req.Header.Add("User-Agent", c.UserAgent) @@ -293,8 +300,8 @@ func (r *Response) populateRate() { // Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value // pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface, // the raw response will be written to v, without attempting to decode it. -func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { - resp, err := c.client.Do(req) +func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { + resp, err := DoRequestWithClient(ctx, c.client, req) if err != nil { return nil, err } @@ -332,6 +339,21 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { return response, err } + +// DoRequest submits an HTTP request. +func DoRequest(ctx context.Context, req *http.Request) (*http.Response, error) { + return DoRequestWithClient(ctx, http.DefaultClient, req) +} + +// DoRequestWithClient submits an HTTP request using the specified client. +func DoRequestWithClient( + ctx context.Context, + client *http.Client, + req *http.Request) (*http.Response, error) { + req = req.WithContext(ctx) + return client.Do(req) +} + func (r *ErrorResponse) Error() string { if r.RequestID != "" { return fmt.Sprintf("%v %v: %d (request %q) %v", @@ -354,7 +376,7 @@ func CheckResponse(r *http.Response) error { if err == nil && len(data) > 0 { err := json.Unmarshal(data, errorResponse) if err != nil { - return err + errorResponse.Message = string(data) } } diff --git a/vendor/github.com/digitalocean/godo/image_actions.go b/vendor/github.com/digitalocean/godo/image_actions.go index c4201c0de..976f7c687 100644 --- a/vendor/github.com/digitalocean/godo/image_actions.go +++ b/vendor/github.com/digitalocean/godo/image_actions.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) // ImageActionsService is an interface for interfacing with the image actions @@ -34,13 +35,13 @@ func (i *ImageActionsServiceOp) Transfer(ctx context.Context, imageID int, trans path := fmt.Sprintf("v2/images/%d/actions", imageID) - req, err := i.client.NewRequest(ctx, "POST", path, transferRequest) + req, err := i.client.NewRequest(ctx, http.MethodPost, path, transferRequest) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := i.client.Do(req, root) + resp, err := i.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -60,13 +61,13 @@ func (i *ImageActionsServiceOp) Convert(ctx context.Context, imageID int) (*Acti "type": "convert", } - req, err := i.client.NewRequest(ctx, "POST", path, convertRequest) + req, err := i.client.NewRequest(ctx, http.MethodPost, path, convertRequest) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := i.client.Do(req, root) + resp, err := i.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -86,13 +87,13 @@ func (i *ImageActionsServiceOp) Get(ctx context.Context, imageID, actionID int) path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID) - req, err := i.client.NewRequest(ctx, "GET", path, nil) + req, err := i.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := i.client.Do(req, root) + resp, err := i.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/images.go b/vendor/github.com/digitalocean/godo/images.go index c808af6ec..69de4c075 100644 --- a/vendor/github.com/digitalocean/godo/images.go +++ b/vendor/github.com/digitalocean/godo/images.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) const imageBasePath = "v2/images" @@ -15,8 +16,10 @@ type ImagesService interface { ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) + ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) GetByID(context.Context, int) (*Image, *Response, error) GetBySlug(context.Context, string) (*Image, *Response, error) + Create(context.Context, *CustomImageCreateRequest) (*Image, *Response, error) Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error) Delete(context.Context, int) (*Response, error) } @@ -31,15 +34,20 @@ var _ ImagesService = &ImagesServiceOp{} // Image represents a DigitalOcean Image type Image struct { - ID int `json:"id,float64,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Distribution string `json:"distribution,omitempty"` - Slug string `json:"slug,omitempty"` - Public bool `json:"public,omitempty"` - Regions []string `json:"regions,omitempty"` - MinDiskSize int `json:"min_disk_size,omitempty"` - Created string `json:"created_at,omitempty"` + ID int `json:"id,float64,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Distribution string `json:"distribution,omitempty"` + Slug string `json:"slug,omitempty"` + Public bool `json:"public,omitempty"` + Regions []string `json:"regions,omitempty"` + MinDiskSize int `json:"min_disk_size,omitempty"` + SizeGigaBytes float64 `json:"size_gigabytes,omitempty"` + Created string `json:"created_at,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Status string `json:"status,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` } // ImageUpdateRequest represents a request to update an image. @@ -47,6 +55,16 @@ type ImageUpdateRequest struct { Name string `json:"name"` } +// CustomImageCreateRequest represents a request to create a custom image. +type CustomImageCreateRequest struct { + Name string `json:"name"` + Url string `json:"url"` + Region string `json:"region"` + Distribution string `json:"distribution,omitempty"` + Description string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` +} + type imageRoot struct { Image *Image } @@ -59,6 +77,7 @@ type imagesRoot struct { type listImageOptions struct { Private bool `url:"private,omitempty"` Type string `url:"type,omitempty"` + Tag string `url:"tag_name,omitempty"` } func (i Image) String() string { @@ -88,6 +107,12 @@ func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Ima return s.list(ctx, opt, &listOpt) } +// ListByTag lists all images with a specific tag applied. +func (s *ImagesServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) { + listOpt := listImageOptions{Tag: tag} + return s.list(ctx, opt, &listOpt) +} + // GetByID retrieves an image by id. func (s *ImagesServiceOp) GetByID(ctx context.Context, imageID int) (*Image, *Response, error) { if imageID < 1 { @@ -106,6 +131,25 @@ func (s *ImagesServiceOp) GetBySlug(ctx context.Context, slug string) (*Image, * return s.get(ctx, interface{}(slug)) } +func (s *ImagesServiceOp) Create(ctx context.Context, createRequest *CustomImageCreateRequest) (*Image, *Response, error) { + if createRequest == nil { + return nil, nil, NewArgError("createRequest", "cannot be nil") + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, imageBasePath, createRequest) + if err != nil { + return nil, nil, err + } + + root := new(imageRoot) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Image, resp, err +} + // Update an image name. func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) { if imageID < 1 { @@ -117,13 +161,13 @@ func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest } path := fmt.Sprintf("%s/%d", imageBasePath, imageID) - req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest) + req, err := s.client.NewRequest(ctx, http.MethodPut, path, updateRequest) if err != nil { return nil, nil, err } root := new(imageRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -139,12 +183,12 @@ func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, e path := fmt.Sprintf("%s/%d", imageBasePath, imageID) - req, err := s.client.NewRequest(ctx, "DELETE", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } @@ -153,13 +197,13 @@ func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, e func (s *ImagesServiceOp) get(ctx context.Context, ID interface{}) (*Image, *Response, error) { path := fmt.Sprintf("%s/%v", imageBasePath, ID) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(imageRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -179,13 +223,13 @@ func (s *ImagesServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *l return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(imagesRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/keys.go b/vendor/github.com/digitalocean/godo/keys.go index 7b336c4ce..9695c21f4 100644 --- a/vendor/github.com/digitalocean/godo/keys.go +++ b/vendor/github.com/digitalocean/godo/keys.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) const keysBasePath = "v2/account/keys" @@ -69,13 +70,13 @@ func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Res return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(keysRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -88,13 +89,13 @@ func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Res // Performs a get given a path func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) { - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(keyRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -128,13 +129,13 @@ func (s *KeysServiceOp) Create(ctx context.Context, createRequest *KeyCreateRequ return nil, nil, NewArgError("createRequest", "cannot be nil") } - req, err := s.client.NewRequest(ctx, "POST", keysBasePath, createRequest) + req, err := s.client.NewRequest(ctx, http.MethodPost, keysBasePath, createRequest) if err != nil { return nil, nil, err } root := new(keyRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -159,7 +160,7 @@ func (s *KeysServiceOp) UpdateByID(ctx context.Context, keyID int, updateRequest } root := new(keyRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -184,7 +185,7 @@ func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint str } root := new(keyRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -194,12 +195,12 @@ func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint str // Delete key using a path func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) { - req, err := s.client.NewRequest(ctx, "DELETE", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } diff --git a/vendor/github.com/digitalocean/godo/kubernetes.go b/vendor/github.com/digitalocean/godo/kubernetes.go new file mode 100644 index 000000000..e5769de0e --- /dev/null +++ b/vendor/github.com/digitalocean/godo/kubernetes.go @@ -0,0 +1,380 @@ +package godo + +import ( + "bytes" + "context" + "fmt" + "net/http" + "time" +) + +const ( + kubernetesBasePath = "/v2/kubernetes" + kubernetesClustersPath = kubernetesBasePath + "/clusters" + kubernetesOptionsPath = kubernetesBasePath + "/options" +) + +// KubernetesService is an interface for interfacing with the kubernetes endpoints +// of the DigitalOcean API. +// See: https://developers.digitalocean.com/documentation/v2#kubernetes +type KubernetesService interface { + Create(context.Context, *KubernetesClusterCreateRequest) (*KubernetesCluster, *Response, error) + Get(context.Context, string) (*KubernetesCluster, *Response, error) + GetKubeConfig(context.Context, string) (*KubernetesClusterConfig, *Response, error) + List(context.Context, *ListOptions) ([]*KubernetesCluster, *Response, error) + Update(context.Context, string, *KubernetesClusterUpdateRequest) (*KubernetesCluster, *Response, error) + Delete(context.Context, string) (*Response, error) + + CreateNodePool(ctx context.Context, clusterID string, req *KubernetesNodePoolCreateRequest) (*KubernetesNodePool, *Response, error) + GetNodePool(ctx context.Context, clusterID, poolID string) (*KubernetesNodePool, *Response, error) + ListNodePools(ctx context.Context, clusterID string, opts *ListOptions) ([]*KubernetesNodePool, *Response, error) + UpdateNodePool(ctx context.Context, clusterID, poolID string, req *KubernetesNodePoolUpdateRequest) (*KubernetesNodePool, *Response, error) + RecycleNodePoolNodes(ctx context.Context, clusterID, poolID string, req *KubernetesNodePoolRecycleNodesRequest) (*Response, error) + DeleteNodePool(ctx context.Context, clusterID, poolID string) (*Response, error) + + GetOptions(context.Context) (*KubernetesOptions, *Response, error) +} + +var _ KubernetesService = &KubernetesServiceOp{} + +// KubernetesServiceOp handles communication with Kubernetes methods of the DigitalOcean API. +type KubernetesServiceOp struct { + client *Client +} + +// KubernetesClusterCreateRequest represents a request to create a Kubernetes cluster. +type KubernetesClusterCreateRequest struct { + Name string `json:"name,omitempty"` + RegionSlug string `json:"region,omitempty"` + VersionSlug string `json:"version,omitempty"` + Tags []string `json:"tags,omitempty"` + + NodePools []*KubernetesNodePoolCreateRequest `json:"node_pools,omitempty"` +} + +// KubernetesClusterUpdateRequest represents a request to update a Kubernetes cluster. +type KubernetesClusterUpdateRequest struct { + Name string `json:"name,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// KubernetesNodePoolCreateRequest represents a request to create a node pool for a +// Kubernetes cluster. +type KubernetesNodePoolCreateRequest struct { + Name string `json:"name,omitempty"` + Size string `json:"size,omitempty"` + Count int `json:"count,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// KubernetesNodePoolUpdateRequest represents a request to update a node pool in a +// Kubernetes cluster. +type KubernetesNodePoolUpdateRequest struct { + Name string `json:"name,omitempty"` + Count int `json:"count,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// KubernetesNodePoolRecycleNodesRequest represents a request to recycle a set of +// nodes in a node pool. This will recycle the nodes by ID. +type KubernetesNodePoolRecycleNodesRequest struct { + Nodes []string `json:"nodes,omitempty"` +} + +// KubernetesCluster represents a Kubernetes cluster. +type KubernetesCluster struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + RegionSlug string `json:"region,omitempty"` + VersionSlug string `json:"version,omitempty"` + ClusterSubnet string `json:"cluster_subnet,omitempty"` + ServiceSubnet string `json:"service_subnet,omitempty"` + IPv4 string `json:"ipv4,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + Tags []string `json:"tags,omitempty"` + + NodePools []*KubernetesNodePool `json:"node_pools,omitempty"` + + Status *KubernetesClusterStatus `json:"status,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + +// KubernetesClusterStatus describes the status of a cluster. +type KubernetesClusterStatus struct { + State string `json:"state,omitempty"` + Message string `json:"message,omitempty"` +} + +// KubernetesNodePool represents a node pool in a Kubernetes cluster. +type KubernetesNodePool struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Size string `json:"size,omitempty"` + Count int `json:"count,omitempty"` + Tags []string `json:"tags,omitempty"` + + Nodes []*KubernetesNode `json:"nodes,omitempty"` +} + +// KubernetesNode represents a Node in a node pool in a Kubernetes cluster. +type KubernetesNode struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Status *KubernetesNodeStatus `json:"status,omitempty"` + + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + +// KubernetesNodeStatus represents the status of a particular Node in a Kubernetes cluster. +type KubernetesNodeStatus struct { + State string `json:"state,omitempty"` + Message string `json:"message,omitempty"` +} + +// KubernetesOptions represents options available for creating Kubernetes clusters. +type KubernetesOptions struct { + Versions []*KubernetesVersion `json:"versions,omitempty"` +} + +// KubernetesVersion is a DigitalOcean Kubernetes release. +type KubernetesVersion struct { + Slug string `json:"slug,omitempty"` + KubernetesVersion string `json:"kubernetes_version,omitempty"` +} + +type kubernetesClustersRoot struct { + Clusters []*KubernetesCluster `json:"kubernetes_clusters,omitempty"` + Links *Links `json:"links,omitempty"` +} + +type kubernetesClusterRoot struct { + Cluster *KubernetesCluster `json:"kubernetes_cluster,omitempty"` +} + +type kubernetesNodePoolRoot struct { + NodePool *KubernetesNodePool `json:"node_pool,omitempty"` +} + +type kubernetesNodePoolsRoot struct { + NodePools []*KubernetesNodePool `json:"node_pools,omitempty"` + Links *Links `json:"links,omitempty"` +} + +// Get retrieves the details of a Kubernetes cluster. +func (svc *KubernetesServiceOp) Get(ctx context.Context, clusterID string) (*KubernetesCluster, *Response, error) { + path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(kubernetesClusterRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Cluster, resp, nil +} + +// Create creates a Kubernetes cluster. +func (svc *KubernetesServiceOp) Create(ctx context.Context, create *KubernetesClusterCreateRequest) (*KubernetesCluster, *Response, error) { + path := kubernetesClustersPath + req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create) + if err != nil { + return nil, nil, err + } + root := new(kubernetesClusterRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Cluster, resp, nil +} + +// Delete deletes a Kubernetes cluster. There is no way to recover a cluster +// once it has been destroyed. +func (svc *KubernetesServiceOp) Delete(ctx context.Context, clusterID string) (*Response, error) { + path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID) + req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + resp, err := svc.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + return resp, nil +} + +// List returns a list of the Kubernetes clusters visible with the caller's API token. +func (svc *KubernetesServiceOp) List(ctx context.Context, opts *ListOptions) ([]*KubernetesCluster, *Response, error) { + path := kubernetesClustersPath + path, err := addOptions(path, opts) + if err != nil { + return nil, nil, err + } + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(kubernetesClustersRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Clusters, resp, nil +} + +// KubernetesClusterConfig is the content of a Kubernetes config file, which can be +// used to interact with your Kubernetes cluster using `kubectl`. +// See: https://kubernetes.io/docs/tasks/tools/install-kubectl/ +type KubernetesClusterConfig struct { + KubeconfigYAML []byte +} + +// GetKubeConfig returns a Kubernetes config file for the specified cluster. +func (svc *KubernetesServiceOp) GetKubeConfig(ctx context.Context, clusterID string) (*KubernetesClusterConfig, *Response, error) { + path := fmt.Sprintf("%s/%s/kubeconfig", kubernetesClustersPath, clusterID) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + configBytes := bytes.NewBuffer(nil) + resp, err := svc.client.Do(ctx, req, configBytes) + if err != nil { + return nil, resp, err + } + res := &KubernetesClusterConfig{ + KubeconfigYAML: configBytes.Bytes(), + } + return res, resp, nil +} + +// Update updates a Kubernetes cluster's properties. +func (svc *KubernetesServiceOp) Update(ctx context.Context, clusterID string, update *KubernetesClusterUpdateRequest) (*KubernetesCluster, *Response, error) { + path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID) + req, err := svc.client.NewRequest(ctx, http.MethodPut, path, update) + if err != nil { + return nil, nil, err + } + root := new(kubernetesClusterRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Cluster, resp, nil +} + +// CreateNodePool creates a new node pool in an existing Kubernetes cluster. +func (svc *KubernetesServiceOp) CreateNodePool(ctx context.Context, clusterID string, create *KubernetesNodePoolCreateRequest) (*KubernetesNodePool, *Response, error) { + path := fmt.Sprintf("%s/%s/node_pools", kubernetesClustersPath, clusterID) + req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create) + if err != nil { + return nil, nil, err + } + root := new(kubernetesNodePoolRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.NodePool, resp, nil +} + +// GetNodePool retrieves an existing node pool in a Kubernetes cluster. +func (svc *KubernetesServiceOp) GetNodePool(ctx context.Context, clusterID, poolID string) (*KubernetesNodePool, *Response, error) { + path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(kubernetesNodePoolRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.NodePool, resp, nil +} + +// ListNodePools lists all the node pools found in a Kubernetes cluster. +func (svc *KubernetesServiceOp) ListNodePools(ctx context.Context, clusterID string, opts *ListOptions) ([]*KubernetesNodePool, *Response, error) { + path := fmt.Sprintf("%s/%s/node_pools", kubernetesClustersPath, clusterID) + path, err := addOptions(path, opts) + if err != nil { + return nil, nil, err + } + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(kubernetesNodePoolsRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.NodePools, resp, nil +} + +// UpdateNodePool updates the details of an existing node pool. +func (svc *KubernetesServiceOp) UpdateNodePool(ctx context.Context, clusterID, poolID string, update *KubernetesNodePoolUpdateRequest) (*KubernetesNodePool, *Response, error) { + path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID) + req, err := svc.client.NewRequest(ctx, http.MethodPut, path, update) + if err != nil { + return nil, nil, err + } + root := new(kubernetesNodePoolRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.NodePool, resp, nil +} + +// RecycleNodePoolNodes schedules nodes in a node pool for recycling. +func (svc *KubernetesServiceOp) RecycleNodePoolNodes(ctx context.Context, clusterID, poolID string, recycle *KubernetesNodePoolRecycleNodesRequest) (*Response, error) { + path := fmt.Sprintf("%s/%s/node_pools/%s/recycle", kubernetesClustersPath, clusterID, poolID) + req, err := svc.client.NewRequest(ctx, http.MethodPost, path, recycle) + if err != nil { + return nil, err + } + resp, err := svc.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + return resp, nil +} + +// DeleteNodePool deletes a node pool, and subsequently all the nodes in that pool. +func (svc *KubernetesServiceOp) DeleteNodePool(ctx context.Context, clusterID, poolID string) (*Response, error) { + path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID) + req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + resp, err := svc.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + return resp, nil +} + +type kubernetesOptionsRoot struct { + Options *KubernetesOptions `json:"options,omitempty"` + Links *Links `json:"links,omitempty"` +} + +// GetOptions returns options about the Kubernetes service, such as the versions available for +// cluster creation. +func (svc *KubernetesServiceOp) GetOptions(ctx context.Context) (*KubernetesOptions, *Response, error) { + path := kubernetesOptionsPath + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + root := new(kubernetesOptionsRoot) + resp, err := svc.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + return root.Options, resp, nil +} diff --git a/vendor/github.com/digitalocean/godo/load_balancers.go b/vendor/github.com/digitalocean/godo/load_balancers.go index 48b9c2654..1472fff0b 100644 --- a/vendor/github.com/digitalocean/godo/load_balancers.go +++ b/vendor/github.com/digitalocean/godo/load_balancers.go @@ -3,10 +3,12 @@ package godo import ( "context" "fmt" + "net/http" ) const loadBalancersBasePath = "/v2/load_balancers" const forwardingRulesPath = "forwarding_rules" + const dropletsPath = "droplets" // LoadBalancersService is an interface for managing load balancers with the DigitalOcean API. @@ -45,6 +47,36 @@ func (l LoadBalancer) String() string { return Stringify(l) } +func (l LoadBalancer) URN() string { + return ToURN("LoadBalancer", l.ID) +} + +// AsRequest creates a LoadBalancerRequest that can be submitted to Update with the current values of the LoadBalancer. +// Modifying the returned LoadBalancerRequest will not modify the original LoadBalancer. +func (l LoadBalancer) AsRequest() *LoadBalancerRequest { + r := LoadBalancerRequest{ + Name: l.Name, + Algorithm: l.Algorithm, + ForwardingRules: append([]ForwardingRule(nil), l.ForwardingRules...), + DropletIDs: append([]int(nil), l.DropletIDs...), + Tag: l.Tag, + RedirectHttpToHttps: l.RedirectHttpToHttps, + HealthCheck: l.HealthCheck, + } + if l.HealthCheck != nil { + r.HealthCheck = &HealthCheck{} + *r.HealthCheck = *l.HealthCheck + } + if l.StickySessions != nil { + r.StickySessions = &StickySessions{} + *r.StickySessions = *l.StickySessions + } + if l.Region != nil { + r.Region = l.Region.Slug + } + return &r +} + // ForwardingRule represents load balancer forwarding rules. type ForwardingRule struct { EntryProtocol string `json:"entry_protocol,omitempty"` @@ -142,13 +174,13 @@ var _ LoadBalancersService = &LoadBalancersServiceOp{} func (l *LoadBalancersServiceOp) Get(ctx context.Context, lbID string) (*LoadBalancer, *Response, error) { path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID) - req, err := l.client.NewRequest(ctx, "GET", path, nil) + req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(loadBalancerRoot) - resp, err := l.client.Do(req, root) + resp, err := l.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -163,13 +195,13 @@ func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([] return nil, nil, err } - req, err := l.client.NewRequest(ctx, "GET", path, nil) + req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(loadBalancersRoot) - resp, err := l.client.Do(req, root) + resp, err := l.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -182,13 +214,13 @@ func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([] // Create a new load balancer with a given configuration. func (l *LoadBalancersServiceOp) Create(ctx context.Context, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) { - req, err := l.client.NewRequest(ctx, "POST", loadBalancersBasePath, lbr) + req, err := l.client.NewRequest(ctx, http.MethodPost, loadBalancersBasePath, lbr) if err != nil { return nil, nil, err } root := new(loadBalancerRoot) - resp, err := l.client.Do(req, root) + resp, err := l.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -206,7 +238,7 @@ func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *L } root := new(loadBalancerRoot) - resp, err := l.client.Do(req, root) + resp, err := l.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -218,58 +250,58 @@ func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *L func (l *LoadBalancersServiceOp) Delete(ctx context.Context, ldID string) (*Response, error) { path := fmt.Sprintf("%s/%s", loadBalancersBasePath, ldID) - req, err := l.client.NewRequest(ctx, "DELETE", path, nil) + req, err := l.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - return l.client.Do(req, nil) + return l.client.Do(ctx, req, nil) } // AddDroplets adds droplets to a load balancer. func (l *LoadBalancersServiceOp) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) { path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath) - req, err := l.client.NewRequest(ctx, "POST", path, &dropletIDsRequest{IDs: dropletIDs}) + req, err := l.client.NewRequest(ctx, http.MethodPost, path, &dropletIDsRequest{IDs: dropletIDs}) if err != nil { return nil, err } - return l.client.Do(req, nil) + return l.client.Do(ctx, req, nil) } // RemoveDroplets removes droplets from a load balancer. func (l *LoadBalancersServiceOp) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) { path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath) - req, err := l.client.NewRequest(ctx, "DELETE", path, &dropletIDsRequest{IDs: dropletIDs}) + req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &dropletIDsRequest{IDs: dropletIDs}) if err != nil { return nil, err } - return l.client.Do(req, nil) + return l.client.Do(ctx, req, nil) } // AddForwardingRules adds forwarding rules to a load balancer. func (l *LoadBalancersServiceOp) AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) { path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath) - req, err := l.client.NewRequest(ctx, "POST", path, &forwardingRulesRequest{Rules: rules}) + req, err := l.client.NewRequest(ctx, http.MethodPost, path, &forwardingRulesRequest{Rules: rules}) if err != nil { return nil, err } - return l.client.Do(req, nil) + return l.client.Do(ctx, req, nil) } // RemoveForwardingRules removes forwarding rules from a load balancer. func (l *LoadBalancersServiceOp) RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) { path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath) - req, err := l.client.NewRequest(ctx, "DELETE", path, &forwardingRulesRequest{Rules: rules}) + req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &forwardingRulesRequest{Rules: rules}) if err != nil { return nil, err } - return l.client.Do(req, nil) + return l.client.Do(ctx, req, nil) } diff --git a/vendor/github.com/digitalocean/godo/projects.go b/vendor/github.com/digitalocean/godo/projects.go new file mode 100644 index 000000000..52291a1e0 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/projects.go @@ -0,0 +1,302 @@ +package godo + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "path" +) + +const ( + // DefaultProject is the ID you should use if you are working with your + // default project. + DefaultProject = "default" + + projectsBasePath = "/v2/projects" +) + +// ProjectsService is an interface for creating and managing Projects with the DigitalOcean API. +// See: https://developers.digitalocean.com/documentation/documentation/v2/#projects +type ProjectsService interface { + List(context.Context, *ListOptions) ([]Project, *Response, error) + GetDefault(context.Context) (*Project, *Response, error) + Get(context.Context, string) (*Project, *Response, error) + Create(context.Context, *CreateProjectRequest) (*Project, *Response, error) + Update(context.Context, string, *UpdateProjectRequest) (*Project, *Response, error) + Delete(context.Context, string) (*Response, error) + + ListResources(context.Context, string, *ListOptions) ([]ProjectResource, *Response, error) + AssignResources(context.Context, string, ...interface{}) ([]ProjectResource, *Response, error) +} + +// ProjectsServiceOp handles communication with Projects methods of the DigitalOcean API. +type ProjectsServiceOp struct { + client *Client +} + +// Project represents a DigitalOcean Project configuration. +type Project struct { + ID string `json:"id"` + OwnerUUID string `json:"owner_uuid"` + OwnerID uint64 `json:"owner_id"` + Name string `json:"name"` + Description string `json:"description"` + Purpose string `json:"purpose"` + Environment string `json:"environment"` + IsDefault bool `json:"is_default"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// String creates a human-readable description of a Project. +func (p Project) String() string { + return Stringify(p) +} + +// CreateProjectRequest represents the request to create a new project. +type CreateProjectRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Purpose string `json:"purpose"` + Environment string `json:"environment"` +} + +// UpdateProjectRequest represents the request to update project information. +// This type expects certain attribute types, but is built this way to allow +// nil values as well. See `updateProjectRequest` for the "real" types. +type UpdateProjectRequest struct { + Name interface{} + Description interface{} + Purpose interface{} + Environment interface{} + IsDefault interface{} +} + +type updateProjectRequest struct { + Name *string `json:"name"` + Description *string `json:"description"` + Purpose *string `json:"purpose"` + Environment *string `json:"environment"` + IsDefault *bool `json:"is_default"` +} + +// MarshalJSON takes an UpdateRequest and converts it to the "typed" request +// which is sent to the projects API. This is a PATCH request, which allows +// partial attributes, so `null` values are OK. +func (upr *UpdateProjectRequest) MarshalJSON() ([]byte, error) { + d := &updateProjectRequest{} + if str, ok := upr.Name.(string); ok { + d.Name = &str + } + if str, ok := upr.Description.(string); ok { + d.Description = &str + } + if str, ok := upr.Purpose.(string); ok { + d.Purpose = &str + } + if str, ok := upr.Environment.(string); ok { + d.Environment = &str + } + if val, ok := upr.IsDefault.(bool); ok { + d.IsDefault = &val + } + + return json.Marshal(d) +} + +type assignResourcesRequest struct { + Resources []string `json:"resources"` +} + +// ProjectResource is the projects API's representation of a resource. +type ProjectResource struct { + URN string `json:"urn"` + AssignedAt string `json:"assigned_at"` + Links *ProjectResourceLinks `json:"links"` + Status string `json:"status,omitempty"` +} + +// ProjetResourceLinks specify the link for more information about the resource. +type ProjectResourceLinks struct { + Self string `json:"self"` +} + +type projectsRoot struct { + Projects []Project `json:"projects"` + Links *Links `json:"links"` +} + +type projectRoot struct { + Project *Project `json:"project"` +} + +type projectResourcesRoot struct { + Resources []ProjectResource `json:"resources"` + Links *Links `json:"links,omitempty"` +} + +var _ ProjectsService = &ProjectsServiceOp{} + +// List Projects. +func (p *ProjectsServiceOp) List(ctx context.Context, opts *ListOptions) ([]Project, *Response, error) { + path, err := addOptions(projectsBasePath, opts) + if err != nil { + return nil, nil, err + } + + req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(projectsRoot) + resp, err := p.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Projects, resp, err +} + +// GetDefault project. +func (p *ProjectsServiceOp) GetDefault(ctx context.Context) (*Project, *Response, error) { + return p.getHelper(ctx, "default") +} + +// Get retrieves a single project by its ID. +func (p *ProjectsServiceOp) Get(ctx context.Context, projectID string) (*Project, *Response, error) { + return p.getHelper(ctx, projectID) +} + +// Create a new project. +func (p *ProjectsServiceOp) Create(ctx context.Context, cr *CreateProjectRequest) (*Project, *Response, error) { + req, err := p.client.NewRequest(ctx, http.MethodPost, projectsBasePath, cr) + if err != nil { + return nil, nil, err + } + + root := new(projectRoot) + resp, err := p.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Project, resp, err +} + +// Update an existing project. +func (p *ProjectsServiceOp) Update(ctx context.Context, projectID string, ur *UpdateProjectRequest) (*Project, *Response, error) { + path := path.Join(projectsBasePath, projectID) + req, err := p.client.NewRequest(ctx, http.MethodPatch, path, ur) + if err != nil { + return nil, nil, err + } + + root := new(projectRoot) + resp, err := p.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Project, resp, err +} + +// Delete an existing project. You cannot have any resources in a project +// before deleting it. See the API documentation for more details. +func (p *ProjectsServiceOp) Delete(ctx context.Context, projectID string) (*Response, error) { + path := path.Join(projectsBasePath, projectID) + req, err := p.client.NewRequest(ctx, http.MethodDelete, path, nil) + if err != nil { + return nil, err + } + + return p.client.Do(ctx, req, nil) +} + +// ListResources lists all resources in a project. +func (p *ProjectsServiceOp) ListResources(ctx context.Context, projectID string, opts *ListOptions) ([]ProjectResource, *Response, error) { + basePath := path.Join(projectsBasePath, projectID, "resources") + path, err := addOptions(basePath, opts) + if err != nil { + return nil, nil, err + } + + req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(projectResourcesRoot) + resp, err := p.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Resources, resp, err +} + +// AssignResources assigns one or more resources to a project. AssignResources +// accepts resources in two possible formats: + +// 1. The resource type, like `&Droplet{ID: 1}` or `&FloatingIP{IP: "1.2.3.4"}` +// 2. A valid DO URN as a string, like "do:droplet:1234" +// +// There is no unassign. To move a resource to another project, just assign +// it to that other project. +func (p *ProjectsServiceOp) AssignResources(ctx context.Context, projectID string, resources ...interface{}) ([]ProjectResource, *Response, error) { + path := path.Join(projectsBasePath, projectID, "resources") + + ar := &assignResourcesRequest{ + Resources: make([]string, len(resources)), + } + + for i, resource := range resources { + switch resource.(type) { + case ResourceWithURN: + ar.Resources[i] = resource.(ResourceWithURN).URN() + case string: + ar.Resources[i] = resource.(string) + default: + return nil, nil, fmt.Errorf("%T must either be a string or have a valid URN method", resource) + } + } + req, err := p.client.NewRequest(ctx, http.MethodPost, path, ar) + if err != nil { + return nil, nil, err + } + + root := new(projectResourcesRoot) + resp, err := p.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + if l := root.Links; l != nil { + resp.Links = l + } + + return root.Resources, resp, err +} + +func (p *ProjectsServiceOp) getHelper(ctx context.Context, projectID string) (*Project, *Response, error) { + path := path.Join(projectsBasePath, projectID) + + req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(projectRoot) + resp, err := p.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Project, resp, err +} diff --git a/vendor/github.com/digitalocean/godo/regions.go b/vendor/github.com/digitalocean/godo/regions.go index ccfe02926..409959d95 100644 --- a/vendor/github.com/digitalocean/godo/regions.go +++ b/vendor/github.com/digitalocean/godo/regions.go @@ -1,6 +1,9 @@ package godo -import "context" +import ( + "context" + "net/http" +) // RegionsService is an interface for interfacing with the regions // endpoints of the DigitalOcean API @@ -43,13 +46,13 @@ func (s *RegionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Region return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(regionsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/sizes.go b/vendor/github.com/digitalocean/godo/sizes.go index b69579278..da8207c08 100644 --- a/vendor/github.com/digitalocean/godo/sizes.go +++ b/vendor/github.com/digitalocean/godo/sizes.go @@ -1,6 +1,9 @@ package godo -import "context" +import ( + "context" + "net/http" +) // SizesService is an interface for interfacing with the size // endpoints of the DigitalOcean API @@ -47,13 +50,13 @@ func (s *SizesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Size, *R return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(sizesRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/snapshots.go b/vendor/github.com/digitalocean/godo/snapshots.go index 5a9e5c5d8..181188db2 100644 --- a/vendor/github.com/digitalocean/godo/snapshots.go +++ b/vendor/github.com/digitalocean/godo/snapshots.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) const snapshotBasePath = "v2/snapshots" @@ -81,12 +82,12 @@ func (s *SnapshotsServiceOp) Get(ctx context.Context, snapshotID string) (*Snaps func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Response, error) { path := fmt.Sprintf("%s/%s", snapshotBasePath, snapshotID) - req, err := s.client.NewRequest(ctx, "DELETE", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } @@ -95,13 +96,13 @@ func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Re func (s *SnapshotsServiceOp) get(ctx context.Context, ID string) (*Snapshot, *Response, error) { path := fmt.Sprintf("%s/%s", snapshotBasePath, ID) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(snapshotRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -121,13 +122,13 @@ func (s *SnapshotsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(snapshotsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/storage.go b/vendor/github.com/digitalocean/godo/storage.go index 00d5157d6..a79332a79 100644 --- a/vendor/github.com/digitalocean/godo/storage.go +++ b/vendor/github.com/digitalocean/godo/storage.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" "time" ) @@ -43,19 +44,25 @@ var _ StorageService = &StorageServiceOp{} // Volume represents a Digital Ocean block store volume. type Volume struct { - ID string `json:"id"` - Region *Region `json:"region"` - Name string `json:"name"` - SizeGigaBytes int64 `json:"size_gigabytes"` - Description string `json:"description"` - DropletIDs []int `json:"droplet_ids"` - CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + Region *Region `json:"region"` + Name string `json:"name"` + SizeGigaBytes int64 `json:"size_gigabytes"` + Description string `json:"description"` + DropletIDs []int `json:"droplet_ids"` + CreatedAt time.Time `json:"created_at"` + FilesystemType string `json:"filesystem_type"` + FilesystemLabel string `json:"filesystem_label"` } func (f Volume) String() string { return Stringify(f) } +func (f Volume) URN() string { + return ToURN("Volume", f.ID) +} + type storageVolumesRoot struct { Volumes []Volume `json:"volumes"` Links *Links `json:"links"` @@ -69,11 +76,13 @@ type storageVolumeRoot struct { // VolumeCreateRequest represents a request to create a block store // volume. type VolumeCreateRequest struct { - Region string `json:"region"` - Name string `json:"name"` - Description string `json:"description"` - SizeGigaBytes int64 `json:"size_gigabytes"` - SnapshotID string `json:"snapshot_id"` + Region string `json:"region"` + Name string `json:"name"` + Description string `json:"description"` + SizeGigaBytes int64 `json:"size_gigabytes"` + SnapshotID string `json:"snapshot_id"` + FilesystemType string `json:"filesystem_type"` + FilesystemLabel string `json:"filesystem_label"` } // ListVolumes lists all storage volumes. @@ -82,6 +91,10 @@ func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolume if params != nil { if params.Region != "" && params.Name != "" { path = fmt.Sprintf("%s?name=%s®ion=%s", path, params.Name, params.Region) + } else if params.Region != "" { + path = fmt.Sprintf("%s?region=%s", path, params.Region) + } else if params.Name != "" { + path = fmt.Sprintf("%s?name=%s", path, params.Name) } if params.ListOptions != nil { @@ -93,13 +106,13 @@ func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolume } } - req, err := svc.client.NewRequest(ctx, "GET", path, nil) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(storageVolumesRoot) - resp, err := svc.client.Do(req, root) + resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -115,13 +128,13 @@ func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolume func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *VolumeCreateRequest) (*Volume, *Response, error) { path := storageAllocPath - req, err := svc.client.NewRequest(ctx, "POST", path, createRequest) + req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest) if err != nil { return nil, nil, err } root := new(storageVolumeRoot) - resp, err := svc.client.Do(req, root) + resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -132,13 +145,13 @@ func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *Vo func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume, *Response, error) { path := fmt.Sprintf("%s/%s", storageAllocPath, id) - req, err := svc.client.NewRequest(ctx, "GET", path, nil) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(storageVolumeRoot) - resp, err := svc.client.Do(req, root) + resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -150,11 +163,11 @@ func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume, func (svc *StorageServiceOp) DeleteVolume(ctx context.Context, id string) (*Response, error) { path := fmt.Sprintf("%s/%s", storageAllocPath, id) - req, err := svc.client.NewRequest(ctx, "DELETE", path, nil) + req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - return svc.client.Do(req, nil) + return svc.client.Do(ctx, req, nil) } // SnapshotCreateRequest represents a request to create a block store @@ -173,13 +186,13 @@ func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string, return nil, nil, err } - req, err := svc.client.NewRequest(ctx, "GET", path, nil) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(snapshotsRoot) - resp, err := svc.client.Do(req, root) + resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -195,13 +208,13 @@ func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string, func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) { path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.VolumeID) - req, err := svc.client.NewRequest(ctx, "POST", path, createRequest) + req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest) if err != nil { return nil, nil, err } root := new(snapshotRoot) - resp, err := svc.client.Do(req, root) + resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -212,13 +225,13 @@ func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest * func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snapshot, *Response, error) { path := fmt.Sprintf("%s/%s", storageSnapPath, id) - req, err := svc.client.NewRequest(ctx, "GET", path, nil) + req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(snapshotRoot) - resp, err := svc.client.Do(req, root) + resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -230,9 +243,9 @@ func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snaps func (svc *StorageServiceOp) DeleteSnapshot(ctx context.Context, id string) (*Response, error) { path := fmt.Sprintf("%s/%s", storageSnapPath, id) - req, err := svc.client.NewRequest(ctx, "DELETE", path, nil) + req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - return svc.client.Do(req, nil) + return svc.client.Do(ctx, req, nil) } diff --git a/vendor/github.com/digitalocean/godo/storage_actions.go b/vendor/github.com/digitalocean/godo/storage_actions.go index 21f82659f..948897568 100644 --- a/vendor/github.com/digitalocean/godo/storage_actions.go +++ b/vendor/github.com/digitalocean/godo/storage_actions.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) // StorageActionsService is an interface for interfacing with the @@ -76,13 +77,13 @@ func (s *StorageActionsServiceOp) Resize(ctx context.Context, volumeID string, s func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string, request *ActionRequest) (*Action, *Response, error) { path := storageAllocationActionPath(volumeID) - req, err := s.client.NewRequest(ctx, "POST", path, request) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, request) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -91,13 +92,13 @@ func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string, } func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) { - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -106,13 +107,13 @@ func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action } func (s *StorageActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) { - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(actionsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } diff --git a/vendor/github.com/digitalocean/godo/strings.go b/vendor/github.com/digitalocean/godo/strings.go index 4a8bfb636..4d5c0ad22 100644 --- a/vendor/github.com/digitalocean/godo/strings.go +++ b/vendor/github.com/digitalocean/godo/strings.go @@ -5,10 +5,20 @@ import ( "fmt" "io" "reflect" + "strings" ) var timestampType = reflect.TypeOf(Timestamp{}) +type ResourceWithURN interface { + URN() string +} + +// ToURN converts the resource type and ID to a valid DO API URN. +func ToURN(resourceType string, id interface{}) string { + return fmt.Sprintf("%s:%s:%v", "do", strings.ToLower(resourceType), id) +} + // Stringify attempts to create a string representation of DigitalOcean types func Stringify(message interface{}) string { var buf bytes.Buffer diff --git a/vendor/github.com/digitalocean/godo/tags.go b/vendor/github.com/digitalocean/godo/tags.go index 391864434..6a9c4dae3 100644 --- a/vendor/github.com/digitalocean/godo/tags.go +++ b/vendor/github.com/digitalocean/godo/tags.go @@ -3,6 +3,7 @@ package godo import ( "context" "fmt" + "net/http" ) const tagsBasePath = "v2/tags" @@ -34,6 +35,8 @@ type ResourceType string const ( //DropletResourceType holds the string representing our ResourceType of Droplet. DropletResourceType ResourceType = "droplet" + //ImageResourceType holds the string representing our ResourceType of Image. + ImageResourceType ResourceType = "image" ) // Resource represent a single resource for associating/disassociating with tags @@ -44,13 +47,23 @@ type Resource struct { // TaggedResources represent the set of resources a tag is attached to type TaggedResources struct { - Droplets *TaggedDropletsResources `json:"droplets,omitempty"` + Count int `json:"count"` + LastTaggedURI string `json:"last_tagged_uri,omitempty"` + Droplets *TaggedDropletsResources `json:"droplets,omitempty"` + Images *TaggedImagesResources `json:"images"` } // TaggedDropletsResources represent the droplet resources a tag is attached to type TaggedDropletsResources struct { - Count int `json:"count,float64,omitempty"` - LastTagged *Droplet `json:"last_tagged,omitempty"` + Count int `json:"count,float64,omitempty"` + LastTagged *Droplet `json:"last_tagged,omitempty"` + LastTaggedURI string `json:"last_tagged_uri,omitempty"` +} + +// TaggedImagesResources represent the image resources a tag is attached to +type TaggedImagesResources struct { + Count int `json:"count,float64,omitempty"` + LastTaggedURI string `json:"last_tagged_uri,omitempty"` } // Tag represent DigitalOcean tag @@ -92,13 +105,13 @@ func (s *TagsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Tag, *Res return nil, nil, err } - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(tagsRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -113,13 +126,13 @@ func (s *TagsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Tag, *Res func (s *TagsServiceOp) Get(ctx context.Context, name string) (*Tag, *Response, error) { path := fmt.Sprintf("%s/%s", tagsBasePath, name) - req, err := s.client.NewRequest(ctx, "GET", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(tagRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -133,13 +146,13 @@ func (s *TagsServiceOp) Create(ctx context.Context, createRequest *TagCreateRequ return nil, nil, NewArgError("createRequest", "cannot be nil") } - req, err := s.client.NewRequest(ctx, "POST", tagsBasePath, createRequest) + req, err := s.client.NewRequest(ctx, http.MethodPost, tagsBasePath, createRequest) if err != nil { return nil, nil, err } root := new(tagRoot) - resp, err := s.client.Do(req, root) + resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } @@ -154,12 +167,12 @@ func (s *TagsServiceOp) Delete(ctx context.Context, name string) (*Response, err } path := fmt.Sprintf("%s/%s", tagsBasePath, name) - req, err := s.client.NewRequest(ctx, "DELETE", path, nil) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } @@ -175,12 +188,12 @@ func (s *TagsServiceOp) TagResources(ctx context.Context, name string, tagReques } path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name) - req, err := s.client.NewRequest(ctx, "POST", path, tagRequest) + req, err := s.client.NewRequest(ctx, http.MethodPost, path, tagRequest) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } @@ -196,12 +209,12 @@ func (s *TagsServiceOp) UntagResources(ctx context.Context, name string, untagRe } path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name) - req, err := s.client.NewRequest(ctx, "DELETE", path, untagRequest) + req, err := s.client.NewRequest(ctx, http.MethodDelete, path, untagRequest) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + resp, err := s.client.Do(ctx, req, nil) return resp, err } diff --git a/vendor/vendor.json b/vendor/vendor.json index 17d9bde1f..c250956c7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -717,11 +717,11 @@ "revisionTime": "2018-03-08T23:13:08Z" }, { - "checksumSHA1": "W1LGm0UNirwMDVCMFv5vZrOpUJI=", + "checksumSHA1": "u6kWbz1BRsZT433AooftjLNNtdw=", "comment": "v0.9.0-24-g6ca5b77", "path": "github.com/digitalocean/godo", - "revision": "4c04abe183f449bd9ede285f0e5c7ee575d0dbe4", - "revisionTime": "2017-04-07T15:15:42Z" + "revision": "c4ae66932b5d6fb727d54e9ded9ee722f67f3ca9", + "revisionTime": "2018-11-13T19:55:25Z" }, { "checksumSHA1": "1n5MBJthemxmfqU2gN3qLCd8s04=", @@ -1487,18 +1487,18 @@ "revision": "2d205ac6ec17a839a94bdbfd16d2fa6c6dada2e0", "revisionTime": "2016-03-31T20:48:55Z" }, - { - "checksumSHA1": "6JP37UqrI0H80Gpk0Y2P+KXgn5M=", - "path": "github.com/ryanuber/go-glob", - "revision": "256dc444b735e061061cf46c809487313d5b0065", - "revisionTime": "2017-01-28T01:21:29Z" - }, { "checksumSHA1": "4gVuzkHbQoznf+lCSJhIJnvS5tc=", "path": "github.com/rwtodd/Go.Sed/sed", "revision": "d6d5d585814e4c3560c684f52e3d8aeed721313d", "revisionTime": "2017-05-07T04:53:31Z" }, + { + "checksumSHA1": "6JP37UqrI0H80Gpk0Y2P+KXgn5M=", + "path": "github.com/ryanuber/go-glob", + "revision": "256dc444b735e061061cf46c809487313d5b0065", + "revisionTime": "2017-01-28T01:21:29Z" + }, { "checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=", "path": "github.com/satori/go.uuid", From 76d1bf567e1118cfc24512ed71db8770dedd305e Mon Sep 17 00:00:00 2001 From: Kevin Bulebush Date: Sun, 25 Nov 2018 14:30:31 -0500 Subject: [PATCH 02/48] Replaced compute/v2/images with imageservice/v2/images. Removed usage of a deprecated API. Standardized clientService naming. --- builder/openstack/artifact.go | 2 +- builder/openstack/builder.go | 7 ++++++- builder/openstack/step_create_image.go | 20 ++++++++++++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/builder/openstack/artifact.go b/builder/openstack/artifact.go index 4be50ab35..5492a0df3 100644 --- a/builder/openstack/artifact.go +++ b/builder/openstack/artifact.go @@ -5,7 +5,7 @@ import ( "log" "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack/compute/v2/images" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" ) // Artifact is an artifact implementation that contains built images. diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go index 7ab5fa6c3..15ad9da05 100644 --- a/builder/openstack/builder.go +++ b/builder/openstack/builder.go @@ -71,6 +71,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, fmt.Errorf("Error initializing compute client: %s", err) } + imageClient, err := b.config.imageV2Client() + if err != nil { + return nil, fmt.Errorf("Error initializing image client: %s", err) + } + // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", &b.config) @@ -164,7 +169,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe artifact := &Artifact{ ImageId: state.Get("image").(string), BuilderIdValue: BuilderId, - Client: computeClient, + Client: imageClient, } return artifact, nil diff --git a/builder/openstack/step_create_image.go b/builder/openstack/step_create_image.go index a4bed481a..608a4a818 100644 --- a/builder/openstack/step_create_image.go +++ b/builder/openstack/step_create_image.go @@ -9,8 +9,8 @@ import ( "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions" "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack/compute/v2/images" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" ) @@ -25,13 +25,21 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi ui := state.Get("ui").(packer.Ui) // We need the v2 compute client - client, err := config.computeV2Client() + computeClient, err := config.computeV2Client() if err != nil { err = fmt.Errorf("Error initializing compute client: %s", err) state.Put("error", err) return multistep.ActionHalt } + // We need the v2 image client + imageClient, err := config.imageV2Client() + if err != nil { + err = fmt.Errorf("Error initializing image service client: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + // Create the image. // Image source depends on the type of the Compute instance. It can be // Block Storage service volume or regular Compute service local volume. @@ -58,7 +66,7 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi } imageId = image.ImageID } else { - imageId, err = servers.CreateImage(client, server.ID, servers.CreateImageOpts{ + imageId, err = servers.CreateImage(computeClient, server.ID, servers.CreateImageOpts{ Name: config.ImageName, Metadata: config.ImageMetadata, }).ExtractImageID() @@ -76,7 +84,7 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi // Wait for the image to become ready ui.Say(fmt.Sprintf("Waiting for image %s (image id: %s) to become ready...", config.ImageName, imageId)) - if err := WaitForImage(client, imageId); err != nil { + if err := WaitForImage(imageClient, imageId); err != nil { err := fmt.Errorf("Error waiting for image: %s", err) state.Put("error", err) ui.Error(err.Error()) @@ -113,11 +121,11 @@ func WaitForImage(client *gophercloud.ServiceClient, imageId string) error { return err } - if image.Status == "ACTIVE" { + if image.Status == "active" { return nil } - log.Printf("Waiting for image creation status: %s (%d%%)", image.Status, image.Progress) + log.Printf("Waiting for image creation status: %s", image.Status) time.Sleep(2 * time.Second) } } From c0c5c6afacff5d9a4b72e959f3d53894526cb93b Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Thu, 22 Nov 2018 21:33:34 -0500 Subject: [PATCH 03/48] Add DigitalOcean post-processor. --- builder/digitalocean/artifact.go | 16 +- builder/digitalocean/builder.go | 8 +- builder/digitalocean/step_snapshot.go | 2 +- builder/digitalocean/wait.go | 4 +- command/plugin.go | 2 + .../digitalocean-import/post-processor.go | 359 ++++++++++++++++++ .../post-processor_test.go | 32 ++ .../digitalocean-import.html.md | 95 +++++ website/source/layouts/docs.erb | 3 + 9 files changed, 506 insertions(+), 15 deletions(-) create mode 100644 post-processor/digitalocean-import/post-processor.go create mode 100644 post-processor/digitalocean-import/post-processor_test.go create mode 100644 website/source/docs/post-processors/digitalocean-import.html.md diff --git a/builder/digitalocean/artifact.go b/builder/digitalocean/artifact.go index 54aed4786..d41f1522d 100644 --- a/builder/digitalocean/artifact.go +++ b/builder/digitalocean/artifact.go @@ -12,16 +12,16 @@ import ( type Artifact struct { // The name of the snapshot - snapshotName string + SnapshotName string // The ID of the image - snapshotId int + SnapshotId int // The name of the region - regionNames []string + RegionNames []string // The client for making API calls - client *godo.Client + Client *godo.Client } func (*Artifact) BuilderId() string { @@ -34,11 +34,11 @@ func (*Artifact) Files() []string { } func (a *Artifact) Id() string { - return fmt.Sprintf("%s:%s", strings.Join(a.regionNames[:], ","), strconv.FormatUint(uint64(a.snapshotId), 10)) + return fmt.Sprintf("%s:%s", strings.Join(a.RegionNames[:], ","), strconv.FormatUint(uint64(a.SnapshotId), 10)) } func (a *Artifact) String() string { - return fmt.Sprintf("A snapshot was created: '%v' (ID: %v) in regions '%v'", a.snapshotName, a.snapshotId, strings.Join(a.regionNames[:], ",")) + return fmt.Sprintf("A snapshot was created: '%v' (ID: %v) in regions '%v'", a.SnapshotName, a.SnapshotId, strings.Join(a.RegionNames[:], ",")) } func (a *Artifact) State(name string) interface{} { @@ -46,7 +46,7 @@ func (a *Artifact) State(name string) interface{} { } func (a *Artifact) Destroy() error { - log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName) - _, err := a.client.Images.Delete(context.TODO(), a.snapshotId) + log.Printf("Destroying image: %d (%s)", a.SnapshotId, a.SnapshotName) + _, err := a.Client.Images.Delete(context.TODO(), a.SnapshotId) return err } diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index 6af14df46..002d008b5 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -113,10 +113,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe } artifact := &Artifact{ - snapshotName: state.Get("snapshot_name").(string), - snapshotId: state.Get("snapshot_image_id").(int), - regionNames: state.Get("regions").([]string), - client: client, + SnapshotName: state.Get("snapshot_name").(string), + SnapshotId: state.Get("snapshot_image_id").(int), + RegionNames: state.Get("regions").([]string), + Client: client, } return artifact, nil diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go index 773626fa1..ed1cd7faf 100644 --- a/builder/digitalocean/step_snapshot.go +++ b/builder/digitalocean/step_snapshot.go @@ -91,7 +91,7 @@ func (s *stepSnapshot) Run(_ context.Context, state multistep.StateBag) multiste return multistep.ActionHalt } ui.Say(fmt.Sprintf("transferring Snapshot ID: %d", imageTransfer.ID)) - if err := waitForImageState(godo.ActionCompleted, imageTransfer.ID, action.ID, + if err := WaitForImageState(godo.ActionCompleted, imageTransfer.ID, action.ID, client, 20*time.Minute); err != nil { // If we get an error the first time, actually report it err := fmt.Errorf("Error waiting for snapshot transfer: %s", err) diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go index 684955853..dfbe8962a 100644 --- a/builder/digitalocean/wait.go +++ b/builder/digitalocean/wait.go @@ -158,9 +158,9 @@ func waitForActionState( } } -// waitForImageState simply blocks until the image action is in +// WaitForImageState simply blocks until the image action is in // a state we expect, while eventually timing out. -func waitForImageState( +func WaitForImageState( desiredState string, imageId, actionId int, client *godo.Client, timeout time.Duration) error { done := make(chan struct{}) diff --git a/command/plugin.go b/command/plugin.go index 71db5843e..f3e938d3e 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -51,6 +51,7 @@ import ( artificepostprocessor "github.com/hashicorp/packer/post-processor/artifice" checksumpostprocessor "github.com/hashicorp/packer/post-processor/checksum" compresspostprocessor "github.com/hashicorp/packer/post-processor/compress" + digitaloceanimportpostprocessor "github.com/hashicorp/packer/post-processor/digitalocean-import" dockerimportpostprocessor "github.com/hashicorp/packer/post-processor/docker-import" dockerpushpostprocessor "github.com/hashicorp/packer/post-processor/docker-push" dockersavepostprocessor "github.com/hashicorp/packer/post-processor/docker-save" @@ -142,6 +143,7 @@ var PostProcessors = map[string]packer.PostProcessor{ "artifice": new(artificepostprocessor.PostProcessor), "checksum": new(checksumpostprocessor.PostProcessor), "compress": new(compresspostprocessor.PostProcessor), + "digitalocean-import": new(digitaloceanimportpostprocessor.PostProcessor), "docker-import": new(dockerimportpostprocessor.PostProcessor), "docker-push": new(dockerpushpostprocessor.PostProcessor), "docker-save": new(dockersavepostprocessor.PostProcessor), diff --git a/post-processor/digitalocean-import/post-processor.go b/post-processor/digitalocean-import/post-processor.go new file mode 100644 index 000000000..ac456a4c9 --- /dev/null +++ b/post-processor/digitalocean-import/post-processor.go @@ -0,0 +1,359 @@ +package digitaloceanimport + +import ( + "context" + "fmt" + "golang.org/x/oauth2" + "log" + "os" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/digitalocean/godo" + + "github.com/hashicorp/packer/builder/digitalocean" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" +) + +const BuilderId = "packer.post-processor.digitalocean-import" + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + APIToken string `mapstructure:"api_token"` + SpacesKey string `mapstructure:"spaces_key"` + SpacesSecret string `mapstructure:"spaces_secret"` + + SpacesRegion string `mapstructure:"spaces_region"` + SpaceName string `mapstructure:"space_name"` + ObjectName string `mapstructure:"space_object_name"` + SkipClean bool `mapstructure:"skip_clean"` + Tags []string `mapstructure:"image_tags"` + Name string `mapstructure:"image_name"` + Description string `mapstructure:"image_description"` + Distribution string `mapstructure:"image_distribution"` + ImageRegions []string `mapstructure:"image_regions"` + Timeout time.Duration `mapstructure:"timeout"` + + ctx interpolate.Context +} + +type PostProcessor struct { + config Config +} + +type apiTokenSource struct { + AccessToken string +} + +type logger struct { + logger *log.Logger +} + +func (t *apiTokenSource) Token() (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: t.AccessToken, + }, nil +} + +func (l logger) Log(args ...interface{}) { + l.logger.Println(args...) +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &p.config.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{"space_object_name"}, + }, + }, raws...) + if err != nil { + return err + } + + if p.config.SpacesKey == "" { + p.config.SpacesKey = os.Getenv("SPACES_ACCESS_KEY") + } + + if p.config.SpacesSecret == "" { + p.config.SpacesSecret = os.Getenv("SPACES_SECRET_KEY") + } + + if p.config.APIToken == "" { + p.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN") + } + + if p.config.ObjectName == "" { + p.config.ObjectName = "packer-import-{{timestamp}}" + } + + if p.config.Distribution == "" { + p.config.Distribution = "Unkown" + } + + if p.config.Timeout == 0 { + p.config.Timeout = 20 * time.Minute + } + + errs := new(packer.MultiError) + + if err = interpolate.Validate(p.config.ObjectName, &p.config.ctx); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error parsing space_object_name template: %s", err)) + } + + templates := map[string]*string{ + "api_token": &p.config.APIToken, + "spaces_key": &p.config.SpacesKey, + "spaces_secret": &p.config.SpacesSecret, + "spaces_region": &p.config.SpacesRegion, + "space_name": &p.config.SpaceName, + "image_name": &p.config.Name, + "image_regions": &p.config.ImageRegions[0], + } + for key, ptr := range templates { + if *ptr == "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("%s must be set", key)) + } + } + + if len(errs.Errors) > 0 { + return errs + } + + packer.LogSecretFilter.Set(p.config.SpacesKey, p.config.SpacesSecret, p.config.APIToken) + log.Println(p.config) + return nil +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + var err error + + p.config.ObjectName, err = interpolate.Render(p.config.ObjectName, &p.config.ctx) + if err != nil { + return nil, false, fmt.Errorf("Error rendering space_object_name template: %s", err) + } + log.Printf("Rendered space_object_name as %s", p.config.ObjectName) + + log.Println("Looking for image in artifact") + source := "" + validSuffix := []string{"raw", "img", "qcow2", "vhdx", "vdi", "vmdk", "bz2", "tar.xz", "tar.gz"} + for _, path := range artifact.Files() { + for _, suffix := range validSuffix { + if strings.HasSuffix(path, suffix) { + source = path + break + } + } + if source != "" { + break + } + } + if source == "" { + return nil, false, fmt.Errorf("Image file not found") + } + + spacesCreds := credentials.NewStaticCredentials(p.config.SpacesKey, p.config.SpacesSecret, "") + spacesEndpoint := fmt.Sprintf("https://%s.digitaloceanspaces.com", p.config.SpacesRegion) + spacesConfig := &aws.Config{ + Credentials: spacesCreds, + Endpoint: aws.String(spacesEndpoint), + Region: aws.String(p.config.SpacesRegion), + LogLevel: aws.LogLevel(aws.LogDebugWithSigning), + Logger: &logger{ + logger: log.New(os.Stderr, "", log.LstdFlags), + }, + } + sess := session.New(spacesConfig) + + ui.Message(fmt.Sprintf("Uploading %s to spaces://%s/%s", source, p.config.SpaceName, p.config.ObjectName)) + err = uploadImageToSpaces(source, p, sess) + if err != nil { + return nil, false, err + } + ui.Message(fmt.Sprintf("Completed upload of %s to spaces://%s/%s", source, p.config.SpaceName, p.config.ObjectName)) + + client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{ + AccessToken: p.config.APIToken, + })) + + ui.Message(fmt.Sprintf("Started import of spaces://%s/%s", p.config.SpaceName, p.config.ObjectName)) + image, err := importImageFromSpaces(p, client) + if err != nil { + return nil, false, err + } + + ui.Message(fmt.Sprintf("Waiting for import of image %s to complete (may take a while)", p.config.Name)) + err = waitUntilImageAvailable(client, image.ID, p.config.Timeout) + if err != nil { + return nil, false, fmt.Errorf("Import of image %s failed with error: %s", p.config.Name, err) + } + ui.Message(fmt.Sprintf("Import of image %s complete", p.config.Name)) + + if len(p.config.ImageRegions) > 1 { + // Remove the first region from the slice as the image is already there. + regions := p.config.ImageRegions + regions[0] = regions[len(regions)-1] + regions[len(regions)-1] = "" + regions = regions[:len(regions)-1] + + ui.Message(fmt.Sprintf("Distributing image %s to additional regions: %v", p.config.Name, regions)) + err = distributeImageToRegions(client, image.ID, regions, p.config.Timeout) + if err != nil { + return nil, false, err + } + } + + log.Printf("Adding created image ID %v to output artifacts", image.ID) + artifact = &digitalocean.Artifact{ + SnapshotName: image.Name, + SnapshotId: image.ID, + RegionNames: p.config.ImageRegions, + Client: client, + } + + if !p.config.SkipClean { + ui.Message(fmt.Sprintf("Deleting import source spaces://%s/%s", p.config.SpaceName, p.config.ObjectName)) + err = deleteImageFromSpaces(p, sess) + if err != nil { + return nil, false, err + } + } + + return artifact, false, nil +} + +func uploadImageToSpaces(source string, p *PostProcessor, s *session.Session) (err error) { + file, err := os.Open(source) + if err != nil { + return fmt.Errorf("Failed to open %s: %s", source, err) + } + + uploader := s3manager.NewUploader(s) + _, err = uploader.Upload(&s3manager.UploadInput{ + Body: file, + Bucket: &p.config.SpaceName, + Key: &p.config.ObjectName, + ACL: aws.String("public-read"), + }) + if err != nil { + return fmt.Errorf("Failed to upload %s: %s", source, err) + } + + file.Close() + + return nil +} + +func importImageFromSpaces(p *PostProcessor, client *godo.Client) (image *godo.Image, err error) { + log.Printf("Importing custom image from spaces://%s/%s", p.config.SpaceName, p.config.ObjectName) + + url := fmt.Sprintf("https://%s.%s.digitaloceanspaces.com/%s", p.config.SpaceName, p.config.SpacesRegion, p.config.ObjectName) + createRequest := &godo.CustomImageCreateRequest{ + Name: p.config.Name, + Url: url, + Region: p.config.ImageRegions[0], + Distribution: p.config.Distribution, + Description: p.config.Description, + Tags: p.config.Tags, + } + + image, _, err = client.Images.Create(context.TODO(), createRequest) + if err != nil { + return image, fmt.Errorf("Failed to import from spaces://%s/%s: %s", p.config.SpaceName, p.config.ObjectName, err) + } + + return image, nil +} + +func waitUntilImageAvailable(client *godo.Client, imageId int, timeout time.Duration) (err error) { + done := make(chan struct{}) + defer close(done) + + result := make(chan error, 1) + go func() { + attempts := 0 + for { + attempts += 1 + + log.Printf("Waiting for image to become available... (attempt: %d)", attempts) + image, _, err := client.Images.GetByID(context.TODO(), imageId) + if err != nil { + result <- err + return + } + + if image.Status == "available" { + result <- nil + return + } + + if image.ErrorMessage != "" { + result <- fmt.Errorf("%v", image.ErrorMessage) + return + } + + time.Sleep(3 * time.Second) + + select { + case <-done: + return + default: + } + } + }() + + log.Printf("Waiting for up to %d seconds for image to become available", timeout/time.Second) + select { + case err := <-result: + return err + case <-time.After(timeout): + err := fmt.Errorf("Timeout while waiting to for action to become available") + return err + } +} + +func distributeImageToRegions(client *godo.Client, imageId int, regions []string, timeout time.Duration) (err error) { + for _, region := range regions { + transferRequest := &godo.ActionRequest{ + "type": "transfer", + "region": region, + } + log.Printf("Transferring image to %s", region) + action, _, err := client.ImageActions.Transfer(context.TODO(), imageId, transferRequest) + if err != nil { + return fmt.Errorf("Error transferring image: %s", err) + } + + if err := digitalocean.WaitForImageState(godo.ActionCompleted, imageId, action.ID, client, timeout); err != nil { + if err != nil { + return fmt.Errorf("Error transferring image: %s", err) + } + } + } + + return nil +} + +func deleteImageFromSpaces(p *PostProcessor, s *session.Session) (err error) { + s3conn := s3.New(s) + _, err = s3conn.DeleteObject(&s3.DeleteObjectInput{ + Bucket: &p.config.SpaceName, + Key: &p.config.ObjectName, + }) + if err != nil { + return fmt.Errorf("Failed to delete spaces://%s/%s: %s", p.config.SpaceName, p.config.ObjectName, err) + } + + return nil +} diff --git a/post-processor/digitalocean-import/post-processor_test.go b/post-processor/digitalocean-import/post-processor_test.go new file mode 100644 index 000000000..45e7a5b32 --- /dev/null +++ b/post-processor/digitalocean-import/post-processor_test.go @@ -0,0 +1,32 @@ +package digitaloceanimport + +import ( + "bytes" + "testing" + + "github.com/hashicorp/packer/packer" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{} +} + +func testPP(t *testing.T) *PostProcessor { + var p PostProcessor + if err := p.Configure(testConfig()); err != nil { + t.Fatalf("err: %s", err) + } + + return &p +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } +} + +func TestPostProcessor_ImplementsPostProcessor(t *testing.T) { + var _ packer.PostProcessor = new(PostProcessor) +} diff --git a/website/source/docs/post-processors/digitalocean-import.html.md b/website/source/docs/post-processors/digitalocean-import.html.md new file mode 100644 index 000000000..d4ef4c1c6 --- /dev/null +++ b/website/source/docs/post-processors/digitalocean-import.html.md @@ -0,0 +1,95 @@ +--- +description: | + The Packer DigitalOcean Import post-processor takes an image artifact + from various builders and imports it to DigitalOcean. +layout: docs +page_title: 'DigitalOcean Import - Post-Processors' +sidebar_current: 'docs-post-processors-digitalocean-import' +--- + +# DigitalOcean Import Post-Processor + +Type: `digitalocean-import` + +The Packer DigitalOcean Import post-processor takes an image artifact from +various builders and imports it to DigitalOcean. + +## How Does it Work? + +The import process operates uploading a temporary copy of the image to +DigitalOcean Spaces and then importing it as a custom image via the +DigialOcean API. The temporary copy in Spaces can be discarded after the +import is complete. + +For information about the requirements to use an image for a DigitalOcean +Droplet, see DigitalOcean's [Custom Images documentation](https://www.digitalocean.com/docs/images/custom-images/overview/). + +## Configuration + +There are some configuration options available for the post-processor. + +Required: + +- `api_token` (string) - A personal access token used to communicate with + the DigitalOcean v2 API. + +- `spaces_key` (string) - The access key used to communicate with Spaces. + +- `spaces_secret` (string) - The secret key used to communicate with Spaces. + +- `spaces_region` (string) - The name of the region, such as `nyc3`, in which + to upload the image to Spaces. + +- `space_name` (string) - The name of the specific Space where the image file + will be copied to for import. This Space must exist when the + post-processor is run. + +- `image_name` (string) - The name to be used for the resulting DigitalOcean + custom image. + +- `image_regions` (array of string) - A list of DigitalOcean regions, such + as `nyc3`, where the resulting image will be available for use in creating + Droplets. + +Optional: + +- `image_description` (string) - The description to set for the resulting + imported image. + +- `image_distribution` (string) - The name of the distribution to set for + the resulting imported image. + +- `image_tags` (array of strings) - A list of tags to apply to the resulting + imported image. + +- `skip_clean` (boolean) - Whether we should skip removing the image file + uploaded to Spaces after the import process has completed. "true" means + that we should leave it in the Space, "false" means to clean it out. + Defaults to `false`. + +- `space_object_name` (string) - The name of the key used in the Space where + the image file will be copied to for import. If not specified, this will default to "packer-import-{{timestamp}}". + +- `timeout` (number) - The length of time in minutes to wait for individual + steps in the process to successfully complete. This includes both importing + the image from Spaces as well as distributing the resulting image to + additional regions. If not specified, this will default to 20. + +## Basic Example + +Here is a basic example: + +``` json +{ + "type": "digitalocean-import", + "api_token": "{{user `token`}}", + "spaces_key": "{{user `key`}}", + "spaces_secret": "{{user `secret`}}", + "spaces_region": "nyc3", + "space_name": "import-bucket", + "image_name": "ubuntu-18.10-minimal-amd64", + "image_description": "Packer import {{timestamp}}", + "image_regions": ["nyc3", "nyc2"], + "image_tags": ["custom", "packer"] +} +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index eb400223b..952e93a8a 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -260,6 +260,9 @@ > Checksum + > + DigitalOcean Import + > Docker Import From f5b13e3cb54ab2735e0a842ae1a059f628d42fb1 Mon Sep 17 00:00:00 2001 From: xinau Date: Sun, 20 Jan 2019 15:43:47 +0000 Subject: [PATCH 04/48] added inspec.io provisioner --- Makefile | 2 +- command/plugin.go | 2 + provisioner/inspec/adapter.go | 285 +++++++++ provisioner/inspec/adapter_test.go | 116 ++++ provisioner/inspec/provisioner.go | 563 ++++++++++++++++++ provisioner/inspec/provisioner_test.go | 293 +++++++++ provisioner/inspec/test-fixtures/exit1 | 3 + website/source/community-plugins.html.md | 1 + .../source/docs/provisioners/inspec.html.md | 135 +++++ website/source/layouts/docs.erb | 3 + 10 files changed, 1402 insertions(+), 1 deletion(-) create mode 100644 provisioner/inspec/adapter.go create mode 100644 provisioner/inspec/adapter_test.go create mode 100644 provisioner/inspec/provisioner.go create mode 100644 provisioner/inspec/provisioner_test.go create mode 100755 provisioner/inspec/test-fixtures/exit1 create mode 100644 website/source/docs/provisioners/inspec.html.md diff --git a/Makefile b/Makefile index b65b6f500..042c46f2f 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ GOPATH=$(shell go env GOPATH) # gofmt UNFORMATTED_FILES=$(shell find . -not -path "./vendor/*" -name "*.go" | xargs gofmt -s -l) -EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/ansible/test-fixtures/exit1') +EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/(ansible|inspec)/test-fixtures/exit1') # Get the git commit GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true) diff --git a/command/plugin.go b/command/plugin.go index 825f06683..3bbe8965e 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -71,6 +71,7 @@ import ( chefsoloprovisioner "github.com/hashicorp/packer/provisioner/chef-solo" convergeprovisioner "github.com/hashicorp/packer/provisioner/converge" fileprovisioner "github.com/hashicorp/packer/provisioner/file" + inspecprovisioner "github.com/hashicorp/packer/provisioner/inspec" powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell" puppetmasterlessprovisioner "github.com/hashicorp/packer/provisioner/puppet-masterless" puppetserverprovisioner "github.com/hashicorp/packer/provisioner/puppet-server" @@ -130,6 +131,7 @@ var Provisioners = map[string]packer.Provisioner{ "chef-solo": new(chefsoloprovisioner.Provisioner), "converge": new(convergeprovisioner.Provisioner), "file": new(fileprovisioner.Provisioner), + "inspec": new(inspecprovisioner.Provisioner), "powershell": new(powershellprovisioner.Provisioner), "puppet-masterless": new(puppetmasterlessprovisioner.Provisioner), "puppet-server": new(puppetserverprovisioner.Provisioner), diff --git a/provisioner/inspec/adapter.go b/provisioner/inspec/adapter.go new file mode 100644 index 000000000..ebd52d0a3 --- /dev/null +++ b/provisioner/inspec/adapter.go @@ -0,0 +1,285 @@ +package inspec + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "net" + + "github.com/hashicorp/packer/packer" + "golang.org/x/crypto/ssh" +) + +// An adapter satisfies SSH requests (from an Inspec client) by delegating SSH +// exec and subsystem commands to a packer.Communicator. +type adapter struct { + done <-chan struct{} + l net.Listener + config *ssh.ServerConfig + ui packer.Ui + comm packer.Communicator +} + +func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, ui packer.Ui, comm packer.Communicator) *adapter { + return &adapter{ + done: done, + l: l, + config: config, + ui: ui, + comm: comm, + } +} + +func (c *adapter) Serve() { + log.Printf("SSH proxy: serving on %s", c.l.Addr()) + + for { + // Accept will return if either the underlying connection is closed or if a connection is made. + // after returning, check to see if c.done can be received. If so, then Accept() returned because + // the connection has been closed. + conn, err := c.l.Accept() + select { + case <-c.done: + return + default: + if err != nil { + c.ui.Error(fmt.Sprintf("listen.Accept failed: %v", err)) + continue + } + go func(conn net.Conn) { + if err := c.Handle(conn, c.ui); err != nil { + c.ui.Error(err.Error()) + } + }(conn) + } + } +} + +func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error { + log.Print("SSH proxy: accepted connection") + _, chans, reqs, err := ssh.NewServerConn(conn, c.config) + if err != nil { + return errors.New("failed to handshake") + } + + // discard all global requests + go ssh.DiscardRequests(reqs) + + // Service the incoming NewChannels + for newChannel := range chans { + if newChannel.ChannelType() != "session" { + newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + continue + } + + go func(ch ssh.NewChannel) { + if err := c.handleSession(ch); err != nil { + c.ui.Error(err.Error()) + } + }(newChannel) + } + + return nil +} + +func (c *adapter) handleSession(newChannel ssh.NewChannel) error { + channel, requests, err := newChannel.Accept() + if err != nil { + return err + } + defer channel.Close() + + done := make(chan struct{}) + + // Sessions have requests such as "pty-req", "shell", "env", and "exec". + // see RFC 4254, section 6 + go func(in <-chan *ssh.Request) { + env := make([]envRequestPayload, 4) + for req := range in { + switch req.Type { + case "pty-req": + log.Println("inspec provisioner pty-req request") + // accept pty-req requests, but don't actually do anything. Necessary for OpenSSH and sudo. + req.Reply(true, nil) + + case "env": + req, err := newEnvRequest(req) + if err != nil { + c.ui.Error(err.Error()) + req.Reply(false, nil) + continue + } + env = append(env, req.Payload) + log.Printf("new env request: %s", req.Payload) + req.Reply(true, nil) + case "exec": + req, err := newExecRequest(req) + if err != nil { + c.ui.Error(err.Error()) + req.Reply(false, nil) + close(done) + continue + } + + log.Printf("new exec request: %s", req.Payload) + + if len(req.Payload) == 0 { + req.Reply(false, nil) + close(done) + return + } + + go func(channel ssh.Channel) { + exit := c.exec(string(req.Payload), channel, channel, channel.Stderr()) + + exitStatus := make([]byte, 4) + binary.BigEndian.PutUint32(exitStatus, uint32(exit)) + channel.SendRequest("exit-status", false, exitStatus) + close(done) + }(channel) + req.Reply(true, nil) + case "subsystem": + req, err := newSubsystemRequest(req) + if err != nil { + c.ui.Error(err.Error()) + req.Reply(false, nil) + continue + } + + log.Printf("new subsystem request: %s", req.Payload) + + c.ui.Error(fmt.Sprintf("unsupported subsystem requested: %s", req.Payload)) + req.Reply(false, nil) + default: + log.Printf("rejecting %s request", req.Type) + req.Reply(false, nil) + } + } + }(requests) + + <-done + return nil +} + +func (c *adapter) Shutdown() { + c.l.Close() +} + +func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int { + var exitStatus int + exitStatus = c.remoteExec(command, in, out, err) + return exitStatus +} + +func (c *adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int { + cmd := &packer.RemoteCmd{ + Stdin: in, + Stdout: out, + Stderr: err, + Command: command, + } + + if err := c.comm.Start(cmd); err != nil { + c.ui.Error(err.Error()) + return cmd.ExitStatus + } + + cmd.Wait() + + return cmd.ExitStatus +} + +type envRequest struct { + *ssh.Request + Payload envRequestPayload +} + +type envRequestPayload struct { + Name string + Value string +} + +func (p envRequestPayload) String() string { + return fmt.Sprintf("%s=%s", p.Name, p.Value) +} + +func newEnvRequest(raw *ssh.Request) (*envRequest, error) { + r := new(envRequest) + r.Request = raw + + if err := ssh.Unmarshal(raw.Payload, &r.Payload); err != nil { + return nil, err + } + + return r, nil +} + +func sshString(buf io.Reader) (string, error) { + var size uint32 + err := binary.Read(buf, binary.BigEndian, &size) + if err != nil { + return "", err + } + + b := make([]byte, size) + err = binary.Read(buf, binary.BigEndian, b) + if err != nil { + return "", err + } + return string(b), nil +} + +type execRequest struct { + *ssh.Request + Payload execRequestPayload +} + +type execRequestPayload string + +func (p execRequestPayload) String() string { + return string(p) +} + +func newExecRequest(raw *ssh.Request) (*execRequest, error) { + r := new(execRequest) + r.Request = raw + buf := bytes.NewReader(r.Request.Payload) + + var err error + var payload string + if payload, err = sshString(buf); err != nil { + return nil, err + } + + r.Payload = execRequestPayload(payload) + return r, nil +} + +type subsystemRequest struct { + *ssh.Request + Payload subsystemRequestPayload +} + +type subsystemRequestPayload string + +func (p subsystemRequestPayload) String() string { + return string(p) +} + +func newSubsystemRequest(raw *ssh.Request) (*subsystemRequest, error) { + r := new(subsystemRequest) + r.Request = raw + buf := bytes.NewReader(r.Request.Payload) + + var err error + var payload string + if payload, err = sshString(buf); err != nil { + return nil, err + } + + r.Payload = subsystemRequestPayload(payload) + return r, nil +} diff --git a/provisioner/inspec/adapter_test.go b/provisioner/inspec/adapter_test.go new file mode 100644 index 000000000..638cead58 --- /dev/null +++ b/provisioner/inspec/adapter_test.go @@ -0,0 +1,116 @@ +package inspec + +import ( + "errors" + "io" + "log" + "net" + "os" + "testing" + "time" + + "github.com/hashicorp/packer/packer" + + "golang.org/x/crypto/ssh" +) + +func TestAdapter_Serve(t *testing.T) { + + // done signals the adapter that the provisioner is done + done := make(chan struct{}) + + acceptC := make(chan struct{}) + l := listener{done: make(chan struct{}), acceptC: acceptC} + + config := &ssh.ServerConfig{} + + ui := new(packer.NoopUi) + + sut := newAdapter(done, &l, config, newUi(ui), communicator{}) + go func() { + i := 0 + for range acceptC { + i++ + if i == 4 { + close(done) + l.Close() + } + } + }() + + sut.Serve() +} + +type listener struct { + done chan struct{} + acceptC chan<- struct{} + i int +} + +func (l *listener) Accept() (net.Conn, error) { + log.Println("Accept() called") + l.acceptC <- struct{}{} + select { + case <-l.done: + log.Println("done, serving an error") + return nil, errors.New("listener is closed") + + case <-time.After(10 * time.Millisecond): + l.i++ + + if l.i%2 == 0 { + c1, c2 := net.Pipe() + + go func(c net.Conn) { + <-time.After(100 * time.Millisecond) + log.Println("closing c") + c.Close() + }(c1) + + return c2, nil + } + } + + return nil, errors.New("accept error") +} + +func (l *listener) Close() error { + close(l.done) + return nil +} + +func (l *listener) Addr() net.Addr { + return addr{} +} + +type addr struct{} + +func (a addr) Network() string { + return a.String() +} + +func (a addr) String() string { + return "test" +} + +type communicator struct{} + +func (c communicator) Start(*packer.RemoteCmd) error { + return errors.New("communicator not supported") +} + +func (c communicator) Upload(string, io.Reader, *os.FileInfo) error { + return errors.New("communicator not supported") +} + +func (c communicator) UploadDir(dst string, src string, exclude []string) error { + return errors.New("communicator not supported") +} + +func (c communicator) Download(string, io.Writer) error { + return errors.New("communicator not supported") +} + +func (c communicator) DownloadDir(src string, dst string, exclude []string) error { + return errors.New("communicator not supported") +} diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go new file mode 100644 index 000000000..b520c1a66 --- /dev/null +++ b/provisioner/inspec/provisioner.go @@ -0,0 +1,563 @@ +package inspec + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "os/user" + "regexp" + "strconv" + "strings" + "sync" + "unicode" + + "golang.org/x/crypto/ssh" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" +) + +var SupportedBackends = map[string]bool{"docker": true, "local": true, "ssh": true, "winrm": true} + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + ctx interpolate.Context + + // The command to run inspec + Command string + SubCommand string + + // Extra options to pass to the inspec command + ExtraArguments []string `mapstructure:"extra_arguments"` + InspecEnvVars []string `mapstructure:"inspec_env_vars"` + + // The profile to execute. + Profile string `mapstructure:"profile"` + AttributesDirectory string `mapstructure:"attributes_directory"` + AttributesFiles []string `mapstructure:"attributes"` + Backend string `mapstructure:"backend"` + User string `mapstructure:"user"` + Host string `mapstructure:"host"` + LocalPort string `mapstructure:"local_port"` + SSHHostKeyFile string `mapstructure:"ssh_host_key_file"` + SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"` +} + +type Provisioner struct { + config Config + adapter *adapter + done chan struct{} + inspecVersion string + inspecMajVersion uint +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + p.done = make(chan struct{}) + + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &p.config.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{}, + }, + }, raws...) + if err != nil { + return err + } + + // Defaults + if p.config.Command == "" { + p.config.Command = "inspec" + } + + if p.config.SubCommand == "" { + p.config.SubCommand = "exec" + } + + var errs *packer.MultiError + err = validateProfileConfig(p.config.Profile) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + + // Check that the authorized key file exists + if len(p.config.SSHAuthorizedKeyFile) > 0 { + err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true) + if err != nil { + log.Println(p.config.SSHAuthorizedKeyFile, "does not exist") + errs = packer.MultiErrorAppend(errs, err) + } + } + if len(p.config.SSHHostKeyFile) > 0 { + err = validateFileConfig(p.config.SSHHostKeyFile, "ssh_host_key_file", true) + if err != nil { + log.Println(p.config.SSHHostKeyFile, "does not exist") + errs = packer.MultiErrorAppend(errs, err) + } + } + + if _, ok := SupportedBackends[p.config.Backend]; !ok { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend)) + } + + if p.config.Backend == "" { + p.config.Backend = "ssh" + } + + if p.config.Host == "" { + p.config.Host = "127.0.0.1" + } + + if len(p.config.LocalPort) > 0 { + if _, err := strconv.ParseUint(p.config.LocalPort, 10, 16); err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %s must be a valid port", p.config.LocalPort)) + } + } else { + p.config.LocalPort = "0" + } + + if len(p.config.AttributesDirectory) > 0 { + err = validateDirectoryConfig(p.config.AttributesDirectory, "attrs") + if err != nil { + log.Println(p.config.AttributesDirectory, "does not exist") + errs = packer.MultiErrorAppend(errs, err) + } + } + + if p.config.User == "" { + usr, err := user.Current() + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } else { + p.config.User = usr.Username + } + } + if p.config.User == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment.")) + } + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + return nil +} + +func (p *Provisioner) getVersion() error { + out, err := exec.Command(p.config.Command, "version").Output() + if err != nil { + return fmt.Errorf( + "Error running \"%s version\": %s", p.config.Command, err.Error()) + } + + versionRe := regexp.MustCompile(`\w (\d+\.\d+[.\d+]*)`) + matches := versionRe.FindStringSubmatch(string(out)) + if matches == nil { + return fmt.Errorf( + "Could not find %s version in output:\n%s", p.config.Command, string(out)) + } + + version := matches[1] + log.Printf("%s version: %s", p.config.Command, version) + p.inspecVersion = version + + majVer, err := strconv.ParseUint(strings.Split(version, ".")[0], 10, 0) + if err != nil { + return fmt.Errorf("Could not parse major version from \"%s\".", version) + } + p.inspecMajVersion = uint(majVer) + + return nil +} + +func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + ui.Say("Provisioning with Inspec...") + + for i, envVar := range p.config.InspecEnvVars { + envVar, err := interpolate.Render(envVar, &p.config.ctx) + if err != nil { + return fmt.Errorf("Could not interpolate inspec env vars: %s", err) + } + p.config.InspecEnvVars[i] = envVar + } + + for i, arg := range p.config.ExtraArguments { + arg, err := interpolate.Render(arg, &p.config.ctx) + if err != nil { + return fmt.Errorf("Could not interpolate inspec extra arguments: %s", err) + } + p.config.ExtraArguments[i] = arg + } + + for i, arg := range p.config.AttributesFiles { + arg, err := interpolate.Render(arg, &p.config.ctx) + if err != nil { + return fmt.Errorf("Could not interpolate inspec attributes: %s", err) + } + p.config.AttributesFiles[i] = arg + } + + k, err := newUserKey(p.config.SSHAuthorizedKeyFile) + if err != nil { + return err + } + + hostSigner, err := newSigner(p.config.SSHHostKeyFile) + // Remove the private key file + if len(k.privKeyFile) > 0 { + defer os.Remove(k.privKeyFile) + } + + keyChecker := ssh.CertChecker{ + UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + if user := conn.User(); user != p.config.User { + return nil, errors.New(fmt.Sprintf("authentication failed: %s is not a valid user", user)) + } + + if !bytes.Equal(k.Marshal(), pubKey.Marshal()) { + return nil, errors.New("authentication failed: unauthorized key") + } + + return nil, nil + }, + } + + config := &ssh.ServerConfig{ + AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) { + log.Printf("authentication attempt from %s to %s as %s using %s", conn.RemoteAddr(), conn.LocalAddr(), conn.User(), method) + }, + PublicKeyCallback: keyChecker.Authenticate, + //NoClientAuth: true, + } + + config.AddHostKey(hostSigner) + + localListener, err := func() (net.Listener, error) { + port, err := strconv.ParseUint(p.config.LocalPort, 10, 16) + if err != nil { + return nil, err + } + + tries := 1 + if port != 0 { + tries = 10 + } + for i := 0; i < tries; i++ { + l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + port++ + if err != nil { + ui.Say(err.Error()) + continue + } + _, p.config.LocalPort, err = net.SplitHostPort(l.Addr().String()) + if err != nil { + ui.Say(err.Error()) + continue + } + return l, nil + } + return nil, errors.New("Error setting up SSH proxy connection") + }() + + if err != nil { + return err + } + + ui = newUi(ui) + p.adapter = newAdapter(p.done, localListener, config, ui, comm) + + defer func() { + log.Print("shutting down the SSH proxy") + close(p.done) + p.adapter.Shutdown() + }() + + go p.adapter.Serve() + + tf, err := ioutil.TempFile(p.config.AttributesDirectory, "packer-provisioner-inspec.*.yml") + if err != nil { + return fmt.Errorf("Error preparing packer attributes file: %s", err) + } + defer os.Remove(tf.Name()) + + w := bufio.NewWriter(tf) + w.WriteString(fmt.Sprintf("packer_build_name: %s\n", p.config.PackerBuildName)) + w.WriteString(fmt.Sprintf("packer_builder_type: %s\n", p.config.PackerBuilderType)) + + if err := w.Flush(); err != nil { + tf.Close() + return fmt.Errorf("Error preparing packer attributes file: %s", err) + } + tf.Close() + p.config.AttributesFiles = append(p.config.AttributesFiles, tf.Name()) + + if err := p.executeInspec(ui, comm, k.privKeyFile); err != nil { + return fmt.Errorf("Error executing Inspec: %s", err) + } + + return nil +} +func (p *Provisioner) Cancel() { + if p.done != nil { + close(p.done) + } + if p.adapter != nil { + p.adapter.Shutdown() + } + os.Exit(0) +} + +func (p *Provisioner) executeInspec(ui packer.Ui, comm packer.Communicator, privKeyFile string) error { + var envvars []string + + args := []string{p.config.SubCommand, p.config.Profile} + args = append(args, "--backend", p.config.Backend) + args = append(args, "--host", p.config.Host) + + if p.config.Backend == "ssh" { + if len(privKeyFile) > 0 { + args = append(args, "--key-files", privKeyFile) + } + args = append(args, "--user", p.config.User) + args = append(args, "--port", p.config.LocalPort) + } + + args = append(args, "--attrs") + args = append(args, p.config.AttributesFiles...) + args = append(args, p.config.ExtraArguments...) + + if len(p.config.InspecEnvVars) > 0 { + envvars = append(envvars, p.config.InspecEnvVars...) + } + + cmd := exec.Command(p.config.Command, args...) + + cmd.Env = os.Environ() + if len(envvars) > 0 { + cmd.Env = append(cmd.Env, envvars...) + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + + wg := sync.WaitGroup{} + repeat := func(r io.ReadCloser) { + reader := bufio.NewReader(r) + for { + line, err := reader.ReadString('\n') + if line != "" { + line = strings.TrimRightFunc(line, unicode.IsSpace) + ui.Message(line) + } + if err != nil { + if err == io.EOF { + break + } else { + ui.Error(err.Error()) + break + } + } + } + wg.Done() + } + wg.Add(2) + go repeat(stdout) + go repeat(stderr) + + ui.Say(fmt.Sprintf("Executing Inspec: %s", strings.Join(cmd.Args, " "))) + if err := cmd.Start(); err != nil { + return err + } + wg.Wait() + err = cmd.Wait() + if err != nil { + return fmt.Errorf("Non-zero exit status: %s", err) + } + + return nil +} + +func validateFileConfig(name string, config string, req bool) error { + if req { + if name == "" { + return fmt.Errorf("%s must be specified.", config) + } + } + info, err := os.Stat(name) + if err != nil { + return fmt.Errorf("%s: %s is invalid: %s", config, name, err) + } else if info.IsDir() { + return fmt.Errorf("%s: %s must point to a file", config, name) + } + return nil +} + +func validateProfileConfig(name string) error { + if name == "" { + return fmt.Errorf("profile must be specified.") + } + return nil +} + +func validateDirectoryConfig(name string, config string) error { + info, err := os.Stat(name) + if err != nil { + return fmt.Errorf("%s: %s is invalid: %s", config, name, err) + } else if !info.IsDir() { + return fmt.Errorf("%s: %s must point to a directory", config, name) + } + return nil +} + +type userKey struct { + ssh.PublicKey + privKeyFile string +} + +func newUserKey(pubKeyFile string) (*userKey, error) { + userKey := new(userKey) + if len(pubKeyFile) > 0 { + pubKeyBytes, err := ioutil.ReadFile(pubKeyFile) + if err != nil { + return nil, errors.New("Failed to read public key") + } + userKey.PublicKey, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyBytes) + if err != nil { + return nil, errors.New("Failed to parse authorized key") + } + + return userKey, nil + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, errors.New("Failed to generate key pair") + } + userKey.PublicKey, err = ssh.NewPublicKey(key.Public()) + if err != nil { + return nil, errors.New("Failed to extract public key from generated key pair") + } + + // To support Inspec calling back to us we need to write + // this file down + privateKeyDer := x509.MarshalPKCS1PrivateKey(key) + privateKeyBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privateKeyDer, + } + tf, err := ioutil.TempFile("", "packer-provisioner-inspec.*.key") + if err != nil { + return nil, errors.New("failed to create temp file for generated key") + } + _, err = tf.Write(pem.EncodeToMemory(&privateKeyBlock)) + if err != nil { + return nil, errors.New("failed to write private key to temp file") + } + + err = tf.Close() + if err != nil { + return nil, errors.New("failed to close private key temp file") + } + userKey.privKeyFile = tf.Name() + + return userKey, nil +} + +type signer struct { + ssh.Signer +} + +func newSigner(privKeyFile string) (*signer, error) { + signer := new(signer) + + if len(privKeyFile) > 0 { + privateBytes, err := ioutil.ReadFile(privKeyFile) + if err != nil { + return nil, errors.New("Failed to load private host key") + } + + signer.Signer, err = ssh.ParsePrivateKey(privateBytes) + if err != nil { + return nil, errors.New("Failed to parse private host key") + } + + return signer, nil + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, errors.New("Failed to generate server key pair") + } + + signer.Signer, err = ssh.NewSignerFromKey(key) + if err != nil { + return nil, errors.New("Failed to extract private key from generated key pair") + } + + return signer, nil +} + +// Ui provides concurrency-safe access to packer.Ui. +type Ui struct { + sem chan int + ui packer.Ui +} + +func newUi(ui packer.Ui) packer.Ui { + return &Ui{sem: make(chan int, 1), ui: ui} +} + +func (ui *Ui) Ask(s string) (string, error) { + ui.sem <- 1 + ret, err := ui.ui.Ask(s) + <-ui.sem + + return ret, err +} + +func (ui *Ui) Say(s string) { + ui.sem <- 1 + ui.ui.Say(s) + <-ui.sem +} + +func (ui *Ui) Message(s string) { + ui.sem <- 1 + ui.ui.Message(s) + <-ui.sem +} + +func (ui *Ui) Error(s string) { + ui.sem <- 1 + ui.ui.Error(s) + <-ui.sem +} + +func (ui *Ui) Machine(t string, args ...string) { + ui.sem <- 1 + ui.ui.Machine(t, args...) + <-ui.sem +} + +func (ui *Ui) ProgressBar() packer.ProgressBar { + return new(packer.NoopProgressBar) +} diff --git a/provisioner/inspec/provisioner_test.go b/provisioner/inspec/provisioner_test.go new file mode 100644 index 000000000..d38d1f0c6 --- /dev/null +++ b/provisioner/inspec/provisioner_test.go @@ -0,0 +1,293 @@ +package inspec + +import ( + "crypto/rand" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "strings" + "testing" + + "github.com/hashicorp/packer/packer" +) + +// Be sure to remove the InSpec stub file in each test with: +// defer os.Remove(config["command"].(string)) +func testConfig(t *testing.T) map[string]interface{} { + m := make(map[string]interface{}) + wd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + inspec_stub := path.Join(wd, "packer-inspec-stub.sh") + + err = ioutil.WriteFile(inspec_stub, []byte("#!/usr/bin/env bash\necho 2.2.16"), 0777) + if err != nil { + t.Fatalf("err: %s", err) + } + m["command"] = inspec_stub + + return m +} + +func TestProvisioner_Impl(t *testing.T) { + var raw interface{} + raw = &Provisioner{} + if _, ok := raw.(packer.Provisioner); !ok { + t.Fatalf("must be a Provisioner") + } +} + +func TestProvisionerPrepare_Defaults(t *testing.T) { + var p Provisioner + config := testConfig(t) + defer os.Remove(config["command"].(string)) + + err := p.Prepare(config) + if err == nil { + t.Fatalf("should have error") + } + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + profile_file, err := ioutil.TempFile("", "test") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(profile_file.Name()) + + config["ssh_host_key_file"] = hostkey_file.Name() + config["ssh_authorized_key_file"] = publickey_file.Name() + config["profile"] = profile_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(profile_file.Name()) + + err = os.Unsetenv("USER") + if err != nil { + t.Fatalf("err: %s", err) + } + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvisionerPrepare_ProfileFile(t *testing.T) { + var p Provisioner + config := testConfig(t) + defer os.Remove(config["command"].(string)) + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + config["ssh_host_key_file"] = hostkey_file.Name() + config["ssh_authorized_key_file"] = publickey_file.Name() + + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + profile_file, err := ioutil.TempFile("", "test") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(profile_file.Name()) + + config["profile"] = profile_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + test_dir, err := ioutil.TempDir("", "test") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(test_dir) + + config["profile"] = test_dir + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvisionerPrepare_HostKeyFile(t *testing.T) { + var p Provisioner + config := testConfig(t) + defer os.Remove(config["command"].(string)) + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + profile_file, err := ioutil.TempFile("", "test") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(profile_file.Name()) + + filename := make([]byte, 10) + n, err := io.ReadFull(rand.Reader, filename) + if n != len(filename) || err != nil { + t.Fatal("could not create random file name") + } + + config["ssh_host_key_file"] = fmt.Sprintf("%x", filename) + config["ssh_authorized_key_file"] = publickey_file.Name() + config["profile"] = profile_file.Name() + + err = p.Prepare(config) + if err == nil { + t.Fatal("should error if ssh_host_key_file does not exist") + } + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + config["ssh_host_key_file"] = hostkey_file.Name() + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvisionerPrepare_AuthorizedKeyFiles(t *testing.T) { + var p Provisioner + config := testConfig(t) + defer os.Remove(config["command"].(string)) + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + profile_file, err := ioutil.TempFile("", "test") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(profile_file.Name()) + + filename := make([]byte, 10) + n, err := io.ReadFull(rand.Reader, filename) + if n != len(filename) || err != nil { + t.Fatal("could not create random file name") + } + + config["ssh_host_key_file"] = hostkey_file.Name() + config["profile"] = profile_file.Name() + config["ssh_authorized_key_file"] = fmt.Sprintf("%x", filename) + + err = p.Prepare(config) + if err == nil { + t.Errorf("should error if ssh_authorized_key_file does not exist") + } + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + config["ssh_authorized_key_file"] = publickey_file.Name() + err = p.Prepare(config) + if err != nil { + t.Errorf("err: %s", err) + } +} + +func TestProvisionerPrepare_LocalPort(t *testing.T) { + var p Provisioner + config := testConfig(t) + defer os.Remove(config["command"].(string)) + + hostkey_file, err := ioutil.TempFile("", "hostkey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(hostkey_file.Name()) + + publickey_file, err := ioutil.TempFile("", "publickey") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(publickey_file.Name()) + + profile_file, err := ioutil.TempFile("", "test") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(profile_file.Name()) + + config["ssh_host_key_file"] = hostkey_file.Name() + config["ssh_authorized_key_file"] = publickey_file.Name() + config["profile"] = profile_file.Name() + + config["local_port"] = "65537" + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + config["local_port"] = "22222" + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestInspecGetVersion(t *testing.T) { + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1 and it requires InSpec to be installed") + } + + var p Provisioner + p.config.Command = "inspec exec" + err := p.getVersion() + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestInspecGetVersionError(t *testing.T) { + var p Provisioner + p.config.Command = "./test-fixtures/exit1" + err := p.getVersion() + if err == nil { + t.Fatal("Should return error") + } + if !strings.Contains(err.Error(), "./test-fixtures/exit1 version") { + t.Fatal("Error message should include command name") + } +} diff --git a/provisioner/inspec/test-fixtures/exit1 b/provisioner/inspec/test-fixtures/exit1 new file mode 100755 index 000000000..2bb8d868b --- /dev/null +++ b/provisioner/inspec/test-fixtures/exit1 @@ -0,0 +1,3 @@ +#!/bin/sh + +exit 1 diff --git a/website/source/community-plugins.html.md b/website/source/community-plugins.html.md index bb0cb2490..537cdc3ca 100644 --- a/website/source/community-plugins.html.md +++ b/website/source/community-plugins.html.md @@ -25,6 +25,7 @@ still distributed with Packer. ## Provisioners - File +- InSpec - PowerShell - Shell - Windows Restart diff --git a/website/source/docs/provisioners/inspec.html.md b/website/source/docs/provisioners/inspec.html.md new file mode 100644 index 000000000..809b33b5e --- /dev/null +++ b/website/source/docs/provisioners/inspec.html.md @@ -0,0 +1,135 @@ +--- +description: | + The inspec Packer provisioner allows inspec profiles to be run to test the + machine. +layout: docs +page_title: 'InSpec - Provisioners' +sidebar_current: 'docs-provisioners-inspec' +--- + +# InSpec Provisioner + +Type: `inspec` + +The `inspec` Packer provisioner runs InSpec profiles. It dynamically creates a +target configured to use SSH, runs an SSH server, executes `inspec exec`, and +marshals InSpec tests through the SSH server to the machine being provisioned +by Packer. + +## Basic Example + +This is a fully functional template that will test an image on DigitalOcean. +Replace the mock `api_token` value with your own. + +``` json +{ + "provisioners": [ + { + "type": "inspec", + "profile": "https://github.com/dev-sec/linux-baseline" + } + ], + + "builders": [ + { + "type": "digitalocean", + "api_token": "", + "image": "ubuntu-14-04-x64", + "region": "sfo1" + } + ] +} +``` + +## Configuration Reference + +Required Parameters: + +- `profile` - The profile to be executed by InSpec. + +Optional Parameters: + +- `inspec_env_vars` (array of strings) - Environment variables to set before + running InSpec. Usage example: + + ``` json + { + "inspec_env_vars": [ "FOO=bar" ] + } + ``` + +- `command` (string) - The command to invoke InSpec. Defaults to `inspec`. + +- `extra_arguments` (array of strings) - Extra arguments to pass to InSpec. + These arguments *will not* be passed through a shell and arguments should + not be quoted. Usage example: + + ``` json + { + "extra_arguments": [ "--sudo", "--reporter", "json" ] + } + ``` + +- `attributes` (array of strings) - Attribute Files used by InSpec which will + be passed to the `--attrs` argument of the `inspec` command when this + provisioner runs InSpec. Specify this if you want a different location. + Note using also `"--attrs"` in `extra_arguments` will override this + setting. + +- `attributes_directory` (string) - The directory in which to place the + temporary generated InSpec Attributes file. By default, this is the + system-specific temporary file location. The fully-qualified name of this + temporary file will be passed to the `--attrs` argument of the `inspec` + command when this provisioner runs InSpec. Specify this if you want a + different location. + +- `backend` (string) - Backend used by InSpec for connection. Defaults to + SSH. + +- `host` (string) - Host used for by InSpec for connection. Defaults to + localhost. + +- `local_port` (string) - The port on which to attempt to listen for SSH + connections. This value is a starting point. The provisioner will attempt to + listen for SSH connections on the first available of ten ports, starting at + `local_port`. A system-chosen port is used when `local_port` is missing or + empty. + +- `ssh_host_key_file` (string) - The SSH key that will be used to run the SSH + server on the host machine to forward commands to the target machine. + InSpec connects to this server and will validate the identity of the server + using the system known\_hosts. The default behavior is to generate and use + a onetime key. + +- `ssh_authorized_key_file` (string) - The SSH public key of the InSpec + `ssh_user`. The default behavior is to generate and use a onetime key. If + this key is generated, the corresponding private key is passed to `inspec` + command with the `-i inspec_ssh_private_key_file` option. + +- `user` (string) - The `--user` to use. Defaults to the user running Packer. + +## Default Extra Variables + +In addition to being able to specify extra arguments using the +`extra_arguments` configuration, the provisioner automatically defines certain +commonly useful InSpec Attributes: + +- `packer_build_name` is set to the name of the build that Packer is running. + This is most useful when Packer is making multiple builds and you want to + distinguish them slightly when using a common profile. + +- `packer_builder_type` is the type of the builder that was used to create + the machine that the script is running on. This is useful if you want to + run only certain parts of the profile on systems built with certain + builders. + +## Debugging + +To debug underlying issues with InSpec, add `"-l"` to `"extra_arguments"` to +enable verbose logging. + +``` json +{ + "extra_arguments": [ "-l", "debug" ] +} +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 5a9f4436d..9c6496594 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -216,6 +216,9 @@ > File + > + InSpec + > PowerShell From ad21367b21db0e780a796d11c1e36638f393de87 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 11 Jan 2019 14:06:15 -0800 Subject: [PATCH 05/48] vagrant builder --- builder/vagrant/artifact.go | 50 ++++ builder/vagrant/builder.go | 222 ++++++++++++++++++ builder/vagrant/driver.go | 80 +++++++ builder/vagrant/driver_2_2.go | 163 +++++++++++++ builder/vagrant/ssh.go | 19 ++ builder/vagrant/step_add_box.go | 83 +++++++ builder/vagrant/step_initialize_vagrant.go | 119 ++++++++++ .../vagrant/step_initialize_vagrant_test.go | 60 +++++ builder/vagrant/step_package.go | 44 ++++ builder/vagrant/step_ssh_config.go | 51 ++++ builder/vagrant/step_up.go | 57 +++++ command/plugin.go | 2 + .../source/docs/builders/alicloud-ecs.html.md | 45 ++-- .../docs/builders/amazon-chroot.html.md | 10 +- website/source/docs/builders/amazon.html.md | 6 +- website/source/docs/builders/azure.html.md | 33 +-- .../source/docs/builders/digitalocean.html.md | 6 +- website/source/docs/builders/docker.html.md | 6 +- .../docs/builders/googlecompute.html.md | 6 +- .../docs/builders/hetzner-cloud.html.md | 4 +- website/source/docs/builders/lxc.html.md | 2 +- website/source/docs/builders/lxd.html.md | 6 +- website/source/docs/builders/ncloud.html.md | 1 - .../source/docs/builders/oneandone.html.md | 2 +- .../source/docs/builders/openstack.html.md | 4 +- .../docs/builders/oracle-classic.html.md | 9 +- .../source/docs/builders/profitbricks.html.md | 2 +- website/source/docs/builders/scaleway.html.md | 4 +- website/source/docs/builders/triton.html.md | 2 +- website/source/docs/builders/vagrant.html.md | 66 ++++++ website/source/docs/commands/index.html.md | 2 +- .../docs/extending/custom-builders.html.md | 2 +- .../extending/custom-post-processors.html.md | 2 +- .../extending/custom-provisioners.html.md | 2 +- website/source/docs/extending/plugins.html.md | 7 +- .../post-processors/alicloud-import.html.md | 15 +- .../post-processors/amazon-import.html.md | 13 +- .../post-processors/docker-import.html.md | 7 +- .../googlecompute-import.html.md | 2 +- .../docs/post-processors/shell-local.html.md | 29 +-- .../docs/post-processors/vagrant.html.md | 4 +- .../post-processors/vsphere-template.html.md | 18 +- .../docs/provisioners/ansible-local.html.md | 1 + .../docs/provisioners/breakpoint.html.md | 24 +- .../provisioners/puppet-masterless.html.md | 9 +- .../docs/provisioners/shell-local.html.md | 27 +-- .../docs/provisioners/windows-restart.html.md | 20 +- .../docs/templates/communicator.html.md | 18 +- website/source/docs/templates/engine.html.md | 35 +-- 49 files changed, 1211 insertions(+), 190 deletions(-) create mode 100644 builder/vagrant/artifact.go create mode 100644 builder/vagrant/builder.go create mode 100644 builder/vagrant/driver.go create mode 100644 builder/vagrant/driver_2_2.go create mode 100644 builder/vagrant/ssh.go create mode 100644 builder/vagrant/step_add_box.go create mode 100644 builder/vagrant/step_initialize_vagrant.go create mode 100644 builder/vagrant/step_initialize_vagrant_test.go create mode 100644 builder/vagrant/step_package.go create mode 100644 builder/vagrant/step_ssh_config.go create mode 100644 builder/vagrant/step_up.go create mode 100644 website/source/docs/builders/vagrant.html.md diff --git a/builder/vagrant/artifact.go b/builder/vagrant/artifact.go new file mode 100644 index 000000000..ec5ed3282 --- /dev/null +++ b/builder/vagrant/artifact.go @@ -0,0 +1,50 @@ +package vagrant + +import ( + "fmt" + "path/filepath" + + "github.com/hashicorp/packer/packer" +) + +// This is the common builder ID to all of these artifacts. +const BuilderId = "vagrant" + +// Artifact is the result of running the vagrant builder, namely a set +// of files associated with the resulting machine. +type artifact struct { + OutputDir string + BoxName string +} + +// NewArtifact returns a vagrant artifact containing the .box file +func NewArtifact(dir string) (packer.Artifact, error) { + return &artifact{ + OutputDir: dir, + BoxName: "package.box", + }, nil +} + +func (*artifact) BuilderId() string { + return BuilderId +} + +func (a *artifact) Files() []string { + return []string{a.BoxName} +} + +func (a *artifact) Id() string { + return filepath.Join(a.OutputDir, a.BoxName) +} + +func (a *artifact) String() string { + return fmt.Sprintf("Vagrant box is %s", a.Id()) +} + +func (a *artifact) State(name string) interface{} { + return nil +} + +func (a *artifact) Destroy() error { + return nil +} diff --git a/builder/vagrant/builder.go b/builder/vagrant/builder.go new file mode 100644 index 000000000..fa2a94a76 --- /dev/null +++ b/builder/vagrant/builder.go @@ -0,0 +1,222 @@ +package vagrant + +import ( + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" +) + +// Builder implements packer.Builder and builds the actual VirtualBox +// images. +type Builder struct { + config *Config + runner multistep.Runner +} + +type SSHConfig struct { + Comm communicator.Config `mapstructure:",squash"` +} + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + common.HTTPConfig `mapstructure:",squash"` + common.ISOConfig `mapstructure:",squash"` + common.FloppyConfig `mapstructure:",squash"` + bootcommand.BootConfig `mapstructure:",squash"` + SSHConfig `mapstructure:",squash"` + + // This is the name of the new virtual machine. + // By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build. + OutputDir string `mapstructure:"output_dir"` + SourceBox string `mapstructure:"source_box"` + SourceBoxName string `mapstructure:"source_box_name"` + Provider string `mapstructure:"provider"` + + Communicator string `mapstructure:"communicator"` + + // What vagrantfile to use + VagrantfileTpl string `mapstructure:"vagrantfile_template"` + + // Whether to Halt, Suspend, or Destroy the box + TeardownMethod string `mapstructure:"teardown_method"` + + // Options for the "vagrant init" command + BoxVersion string `mapstructure:"box_version"` + Minimal bool `mapstructure:"init_minimal"` + Template string `mapstructure:"template"` + SyncedFolder string `mapstructure:"synced_folder"` + + // Options for the "vagrant box add" command + AddCACert string `mapstructure:"add_cacert"` + AddCAPath string `mapstructure:"add_capath"` + AddCert string `mapstructure:"add_cert"` + AddClean bool `mapstructure:"add_clean"` + AddForce bool `mapstructure:"add_force"` + AddInsecure bool `mapstructure:"add_insecure"` + + // Don't package the Vagrant box after build. + SkipPackage bool `mapstructure:"skip_package"` + OutputVagrantfile string `mapstructure:"output_vagrantfile"` + PackageInclude []string `mapstructure:"package_include"` + + ctx interpolate.Context +} + +// Prepare processes the build configuration parameters. +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + b.config = new(Config) + err := config.Decode(&b.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &b.config.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "boot_command", + }, + }, + }, raws...) + if err != nil { + return nil, err + } + + // Accumulate any errors and warnings + var errs *packer.MultiError + warnings := make([]string, 0) + + if b.config.OutputDir == "" { + b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) + } + + if b.config.Comm.SSHTimeout == 0 { + b.config.Comm.SSHTimeout = 10 * time.Minute + } + + if b.config.Comm.Type != "ssh" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf(`The Vagrant builder currently only supports the ssh communicator"`)) + } + + if b.config.TeardownMethod == "" { + b.config.TeardownMethod = "destroy" + } else { + matches := false + for _, name := range []string{"halt", "suspend", "destroy"} { + if strings.ToLower(b.config.TeardownMethod) == name { + matches = true + } + } + if !matches { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf(`TeardownMethod must be "halt", "suspend", or "destroy"`)) + } + } + + if errs != nil && len(errs.Errors) > 0 { + return warnings, errs + } + + return warnings, nil +} + +// Run executes a Packer build and returns a packer.Artifact representing +// a VirtualBox appliance. +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + // Create the driver that we'll use to communicate with VirtualBox + driver, err := NewDriver() + if err != nil { + return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err) + } + + // Set up the state. + state := new(multistep.BasicStateBag) + state.Put("config", b.config) + state.Put("debug", b.config.PackerDebug) + state.Put("driver", driver) + state.Put("cache", cache) + state.Put("hook", hook) + state.Put("ui", ui) + + // Build the steps. + steps := []multistep.Step{} + if !b.config.SkipPackage { + steps = append(steps, + &common.StepOutputDir{ + Force: b.config.PackerForce, + Path: b.config.OutputDir, + }) + } + steps = append(steps, + &StepInitializeVagrant{ + BoxName: b.config.SourceBoxName, + BoxVersion: b.config.BoxVersion, + Minimal: b.config.Minimal, + Template: b.config.Template, + SourceBox: b.config.SourceBox, + OutputDir: b.config.OutputDir, + }, + &StepAddBox{ + BoxVersion: b.config.BoxVersion, + CACert: b.config.AddCACert, + CAPath: b.config.AddCAPath, + DownloadCert: b.config.AddCert, + Clean: b.config.AddClean, + Force: b.config.AddForce, + Insecure: b.config.AddInsecure, + Provider: b.config.Provider, + SourceBox: b.config.SourceBox, + BoxName: b.config.SourceBoxName, + }, + &StepUp{ + b.config.TeardownMethod, + b.config.Provider, + }, + &StepSSHConfig{}, + &communicator.StepConnect{ + Config: &b.config.SSHConfig.Comm, + Host: CommHost(), + SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(), + }, + new(common.StepProvision), + &StepPackage{ + SkipPackage: b.config.SkipPackage, + Include: b.config.PackageInclude, + Vagrantfile: b.config.OutputVagrantfile, + }) + + // Run the steps. + b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state) + b.runner.Run(state) + + // Report any errors. + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + // If we were interrupted or cancelled, then just exit. + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return nil, errors.New("Build was cancelled.") + } + + if _, ok := state.GetOk(multistep.StateHalted); ok { + return nil, errors.New("Build was halted.") + } + + return NewArtifact(b.config.OutputDir) +} + +// Cancel. +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/vagrant/driver.go b/builder/vagrant/driver.go new file mode 100644 index 000000000..08c31b8c8 --- /dev/null +++ b/builder/vagrant/driver.go @@ -0,0 +1,80 @@ +package vagrant + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// A driver is able to talk to Vagrant and perform certain +// operations with it. + +type VagrantDriver interface { + // Calls "vagrant init" + Init([]string) error + + // Calls "vagrant add" + Add([]string) error + + // Calls "vagrant up" + Up([]string) (string, string, error) + + // Calls "vagrant halt" + Halt() error + + // Calls "vagrant suspend" + Suspend() error + + SSHConfig() (*VagrantSSHConfig, error) + + // Calls "vagrant destroy" + Destroy() error + + // Calls "vagrant package"[ + Package([]string) error + + // Verify checks to make sure that this driver should function + // properly. If there is any indication the driver can't function, + // this will return an error. + Verify() error + + // Version reads the version of VirtualBox that is installed. + Version() (string, error) +} + +func NewDriver() (VagrantDriver, error) { + // Hardcode path for now while I'm developing. Obviously this path needs + // to be discovered based on OS. + vagrantBinary := "vagrant" + if runtime.GOOS == "windows" { + vagrantBinary = "vagrant.exe" + } + + if _, err := exec.LookPath(vagrantBinary); err != nil { + return nil, fmt.Errorf("Error: Packer cannot find Vagrant in the path: %s", err.Error()) + } + + driver := &Vagrant_2_2_Driver{ + vagrantBinary: vagrantBinary, + } + + if err := driver.Verify(); err != nil { + return nil, err + } + + return driver, nil +} + +func findVBoxManageWindows(paths string) string { + for _, path := range strings.Split(paths, ";") { + path = filepath.Join(path, "VBoxManage.exe") + if _, err := os.Stat(path); err == nil { + return path + } + } + + return "" +} diff --git a/builder/vagrant/driver_2_2.go b/builder/vagrant/driver_2_2.go new file mode 100644 index 000000000..13a87dd59 --- /dev/null +++ b/builder/vagrant/driver_2_2.go @@ -0,0 +1,163 @@ +package vagrant + +import ( + "bytes" + "fmt" + "log" + "os" + "os/exec" + "regexp" + "strings" +) + +type Vagrant_2_2_Driver struct { + vagrantBinary string +} + +// Calls "vagrant init" +func (d *Vagrant_2_2_Driver) Init(args []string) error { + _, _, err := d.vagrantCmd(append([]string{"init"}, args...)...) + return err +} + +// Calls "vagrant add" +func (d *Vagrant_2_2_Driver) Add(args []string) error { + // vagrant box add partyvm ubuntu-14.04.vmware.box + _, _, err := d.vagrantCmd(append([]string{"box", "add"}, args...)...) + return err +} + +// Calls "vagrant up" +func (d *Vagrant_2_2_Driver) Up(args []string) (string, string, error) { + stdout, stderr, err := d.vagrantCmd(append([]string{"up"}, args...)...) + return stdout, stderr, err +} + +// Calls "vagrant halt" +func (d *Vagrant_2_2_Driver) Halt() error { + _, _, err := d.vagrantCmd([]string{"halt"}...) + return err +} + +// Calls "vagrant suspend" +func (d *Vagrant_2_2_Driver) Suspend() error { + _, _, err := d.vagrantCmd([]string{"suspend"}...) + return err +} + +// Calls "vagrant destroy" +func (d *Vagrant_2_2_Driver) Destroy() error { + _, _, err := d.vagrantCmd([]string{"destroy", "-f"}...) + return err +} + +// Calls "vagrant package" +func (d *Vagrant_2_2_Driver) Package(args []string) error { + _, _, err := d.vagrantCmd([]string{"package"}...) + return err +} + +// Verify makes sure that Vagrant exists at the given path +func (d *Vagrant_2_2_Driver) Verify() error { + vagrantPath, err := exec.LookPath(d.vagrantBinary) + if err != nil { + return fmt.Errorf("Can't find Vagrant binary!") + } + _, err = os.Stat(vagrantPath) + if err != nil { + return fmt.Errorf("Can't find Vagrant binary.") + } + return nil +} + +type VagrantSSHConfig struct { + Hostname string + User string + Port string + UserKnownHostsFile string + StrictHostKeyChecking bool + PasswordAuthentication bool + IdentityFile string + IdentitiesOnly bool + LogLevel string +} + +func parseSSHConfig(lines []string, value string) string { + out := "" + for _, line := range lines { + if index := strings.Index(line, value); index != -1 { + out = line[index+len(value):] + } + } + return out +} + +func yesno(yn string) bool { + if yn == "no" { + return false + } + return true +} + +func (d *Vagrant_2_2_Driver) SSHConfig() (*VagrantSSHConfig, error) { + // vagrant ssh-config --host 8df7860 + stdout, _, err := d.vagrantCmd([]string{"ssh-config"}...) + sshConf := &VagrantSSHConfig{} + + lines := strings.Split(stdout, "\n") + sshConf.Hostname = parseSSHConfig(lines, "HostName ") + sshConf.User = parseSSHConfig(lines, "User ") + sshConf.Port = parseSSHConfig(lines, "Port ") + sshConf.UserKnownHostsFile = parseSSHConfig(lines, "UserKnownHostsFile ") + sshConf.IdentityFile = parseSSHConfig(lines, "IdentityFile ") + sshConf.LogLevel = parseSSHConfig(lines, "LogLevel ") + + // handle the booleans + sshConf.StrictHostKeyChecking = yesno(parseSSHConfig(lines, "StrictHostKeyChecking ")) + sshConf.PasswordAuthentication = yesno(parseSSHConfig(lines, "PasswordAuthentication ")) + sshConf.IdentitiesOnly = yesno((parseSSHConfig(lines, "IdentitiesOnly "))) + + return sshConf, err +} + +// Version reads the version of VirtualBox that is installed. +func (d *Vagrant_2_2_Driver) Version() (string, error) { + stdoutString, _, err := d.vagrantCmd([]string{"version"}...) + // Example stdout: + + // Installed Version: 2.2.3 + // + // Vagrant was unable to check for the latest version of Vagrant. + // Please check manually at https://www.vagrantup.com + + // Use regex to find version + reg := regexp.MustCompile(`(\d+\.)?(\d+\.)?(\*|\d+)`) + version := reg.FindString(stdoutString) + if version == "" { + return "", err + } + + return version, nil +} + +func (d *Vagrant_2_2_Driver) vagrantCmd(args ...string) (string, string, error) { + var stdout, stderr bytes.Buffer + + log.Printf("Calling Vagrant CLI: %#v", args) + cmd := exec.Command(d.vagrantBinary, args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + stdoutString := strings.TrimSpace(stdout.String()) + stderrString := strings.TrimSpace(stderr.String()) + + if _, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf("Vagrant error: %s", stderrString) + } + + log.Printf("[vagrant driver] stdout: %s", stdoutString) + log.Printf("[vagrant driver] stderr: %s", stderrString) + + return stdoutString, stderrString, err +} diff --git a/builder/vagrant/ssh.go b/builder/vagrant/ssh.go new file mode 100644 index 000000000..d4a56cb17 --- /dev/null +++ b/builder/vagrant/ssh.go @@ -0,0 +1,19 @@ +package vagrant + +import ( + "github.com/hashicorp/packer/helper/multistep" +) + +func CommHost() func(multistep.StateBag) (string, error) { + return func(state multistep.StateBag) (string, error) { + config := state.Get("config").(*Config) + return config.Comm.SSHHost, nil + } +} + +func SSHPort() func(multistep.StateBag) (int, error) { + return func(state multistep.StateBag) (int, error) { + config := state.Get("config").(*Config) + return config.Comm.SSHPort, nil + } +} diff --git a/builder/vagrant/step_add_box.go b/builder/vagrant/step_add_box.go new file mode 100644 index 000000000..cdbfe3518 --- /dev/null +++ b/builder/vagrant/step_add_box.go @@ -0,0 +1,83 @@ +package vagrant + +import ( + "context" + "log" + "strings" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type StepAddBox struct { + BoxVersion string + CACert string + CAPath string + DownloadCert string + Clean bool + Force bool + Insecure bool + Provider string + SourceBox string + BoxName string +} + +func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(VagrantDriver) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Adding box using vagrant box add..") + addArgs := []string{} + + if strings.HasSuffix(s.SourceBox, ".box") { + // The box isn't a namespace like you'd pull from vagrant cloud + addArgs = append(addArgs, s.BoxName) + } + + addArgs = append(addArgs, s.SourceBox) + + if s.BoxVersion != "" { + addArgs = append(addArgs, "--box-version", s.BoxVersion) + } + + if s.CACert != "" { + addArgs = append(addArgs, "--cacert", s.CACert) + } + + if s.CAPath != "" { + addArgs = append(addArgs, "--capath", s.CAPath) + } + + if s.DownloadCert != "" { + addArgs = append(addArgs, "--cert", s.DownloadCert) + } + + if s.Clean { + addArgs = append(addArgs, "--clean") + } + + if s.Force { + addArgs = append(addArgs, "--force") + } + + if s.Insecure { + addArgs = append(addArgs, "--insecure") + } + + if s.Provider != "" { + addArgs = append(addArgs, "--provider", s.Provider) + } + + log.Printf("[vagrant] Calling box add with following args %s", strings.Join(addArgs, " ")) + // Call vagrant using prepared arguments + err := driver.Add(addArgs) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepAddBox) Cleanup(state multistep.StateBag) { +} diff --git a/builder/vagrant/step_initialize_vagrant.go b/builder/vagrant/step_initialize_vagrant.go new file mode 100644 index 000000000..0f6357a48 --- /dev/null +++ b/builder/vagrant/step_initialize_vagrant.go @@ -0,0 +1,119 @@ +package vagrant + +import ( + "context" + "fmt" + "os" + "path/filepath" + "text/template" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type StepInitializeVagrant struct { + BoxName string + BoxVersion string + Minimal bool + Template string + SourceBox string + OutputDir string + SyncedFolder string +} + +var DEFAULT_TEMPLATE = `Vagrant.configure("2") do |config| + config.vm.box = "{{.BoxName}}" + {{ if ne .SyncedFolder "" -}} + config.vm.synced_folder "{{.SyncedFolder}}", "/vagrant" + {{- else -}} + config.vm.synced_folder ".", "/vagrant", disabled: true + {{- end}} +end` + +type VagrantfileOptions struct { + SyncedFolder string + BoxName string +} + +func (s *StepInitializeVagrant) createInitializeCommand() (string, error) { + tplPath := filepath.Join(s.OutputDir, "packer-vagrantfile-template.erb") + templateFile, err := os.Create(tplPath) + templateFile.Chmod(0777) + if err != nil { + retErr := fmt.Errorf("Error creating vagrantfile %s", err.Error()) + return "", retErr + } + + var tpl *template.Template + if s.Template == "" { + // Generate vagrantfile template based on our default + tpl = template.Must(template.New("VagrantTpl").Parse(DEFAULT_TEMPLATE)) + } else { + // Read in the template from provided file. + tpl, err = template.ParseFiles(s.Template) + if err != nil { + return "", err + } + } + + opts := &VagrantfileOptions{ + SyncedFolder: s.SyncedFolder, + BoxName: s.SourceBox, + } + + err = tpl.Execute(templateFile, opts) + if err != nil { + return "", err + } + + abspath, err := filepath.Abs(tplPath) + if err != nil { + return "", err + } + + return abspath, nil +} +func (s *StepInitializeVagrant) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(VagrantDriver) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Initializing Vagrant in build directory...") + + // Prepare arguments + initArgs := []string{} + + if s.BoxName != "" { + initArgs = append(initArgs, s.BoxName) + } + + initArgs = append(initArgs, s.SourceBox) + + if s.BoxVersion != "" { + initArgs = append(initArgs, "--box-version", s.BoxVersion) + } + + if s.Minimal { + initArgs = append(initArgs, "-m") + } + + tplPath, err := s.createInitializeCommand() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + initArgs = append(initArgs, "--template", tplPath) + + os.Chdir(s.OutputDir) + // Call vagrant using prepared arguments + err = driver.Init(initArgs) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepInitializeVagrant) Cleanup(state multistep.StateBag) { +} diff --git a/builder/vagrant/step_initialize_vagrant_test.go b/builder/vagrant/step_initialize_vagrant_test.go new file mode 100644 index 000000000..158ec34af --- /dev/null +++ b/builder/vagrant/step_initialize_vagrant_test.go @@ -0,0 +1,60 @@ +package vagrant + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/hashicorp/packer/helper/multistep" +) + +func TestStepInitialize_Impl(t *testing.T) { + var raw interface{} + raw = new(StepInitializeVagrant) + if _, ok := raw.(multistep.Step); !ok { + t.Fatalf("initialize should be a step") + } +} + +func TestCreateFile(t *testing.T) { + testy := StepInitializeVagrant{ + OutputDir: "./", + SourceBox: "bananas", + } + templatePath, err := testy.createInitializeCommand() + if err != nil { + t.Fatalf(err.Error()) + } + contents, err := ioutil.ReadFile(templatePath) + actual := string(contents) + expected := `Vagrant.configure("2") do |config| + config.vm.box = "bananas" + config.vm.synced_folder ".", "/vagrant", disabled: true +end` + if ok := strings.Compare(actual, expected); ok != 0 { + t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual) + } + os.Remove(templatePath) +} + +func TestCreateFile_customSync(t *testing.T) { + testy := StepInitializeVagrant{ + OutputDir: "./", + SyncedFolder: "myfolder/foldertimes", + } + templatePath, err := testy.createInitializeCommand() + if err != nil { + t.Fatalf(err.Error()) + } + contents, err := ioutil.ReadFile(templatePath) + actual := string(contents) + expected := `Vagrant.configure("2") do |config| + config.vm.box = "" + config.vm.synced_folder "myfolder/foldertimes", "/vagrant" +end` + if ok := strings.Compare(actual, expected); ok != 0 { + t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual) + } + os.Remove(templatePath) +} diff --git a/builder/vagrant/step_package.go b/builder/vagrant/step_package.go new file mode 100644 index 000000000..d61b5c1ce --- /dev/null +++ b/builder/vagrant/step_package.go @@ -0,0 +1,44 @@ +package vagrant + +import ( + "context" + "strings" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type StepPackage struct { + SkipPackage bool + Include []string + Vagrantfile string +} + +func (s *StepPackage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(VagrantDriver) + ui := state.Get("ui").(packer.Ui) + + if s.SkipPackage { + ui.Say("skip_package flag set; not going to call Vagrant package on this box.") + return multistep.ActionContinue + } + ui.Say("Packaging box...") + packageArgs := []string{} + if len(s.Include) > 0 { + packageArgs = append(packageArgs, "--include", strings.Join(s.Include, ",")) + } + if s.Vagrantfile != "" { + packageArgs = append(packageArgs, "--vagrantfile", s.Vagrantfile) + } + + err := driver.Package(packageArgs) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepPackage) Cleanup(state multistep.StateBag) { +} diff --git a/builder/vagrant/step_ssh_config.go b/builder/vagrant/step_ssh_config.go new file mode 100644 index 000000000..175224727 --- /dev/null +++ b/builder/vagrant/step_ssh_config.go @@ -0,0 +1,51 @@ +package vagrant + +import ( + "context" + "strconv" + + "github.com/hashicorp/packer/helper/multistep" +) + +// Vagrant already sets up ssh on the guests; our job is to find out what +// it did. We can do that with the ssh-config command. Example output: + +// $ vagrant ssh-config +// Host default +// HostName 172.16.41.194 +// User vagrant +// Port 22 +// UserKnownHostsFile /dev/null +// StrictHostKeyChecking no +// PasswordAuthentication no +// IdentityFile /Users/mmarsh/Projects/vagrant-boxes/ubuntu/.vagrant/machines/default/vmware_fusion/private_key +// IdentitiesOnly yes +// LogLevel FATAL + +type StepSSHConfig struct{} + +func (s *StepSSHConfig) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(VagrantDriver) + config := state.Get("config").(*Config) + + sshConfig, err := driver.SSHConfig() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + config.Comm.SSHPrivateKeyFile = sshConfig.IdentityFile + config.Comm.SSHUsername = sshConfig.User + config.Comm.SSHHost = sshConfig.Hostname + port, err := strconv.Atoi(sshConfig.Port) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + config.Comm.SSHPort = port + + return multistep.ActionContinue +} + +func (s *StepSSHConfig) Cleanup(state multistep.StateBag) { +} diff --git a/builder/vagrant/step_up.go b/builder/vagrant/step_up.go new file mode 100644 index 000000000..93aad8b63 --- /dev/null +++ b/builder/vagrant/step_up.go @@ -0,0 +1,57 @@ +package vagrant + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type StepUp struct { + TeardownMethod string + Provider string +} + +func (s *StepUp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(VagrantDriver) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Calling Vagrant Up...") + + var args []string + if s.Provider != "" { + args = append(args, fmt.Sprintf("--provider=%s", s.Provider)) + } + + _, _, err := driver.Up(args) + + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepUp) Cleanup(state multistep.StateBag) { + driver := state.Get("driver").(VagrantDriver) + ui := state.Get("ui").(packer.Ui) + + ui.Say(fmt.Sprintf("%sing Vagrant box...", s.TeardownMethod)) + + var err error + if s.TeardownMethod == "halt" { + err = driver.Halt() + } else if s.TeardownMethod == "suspend" { + err = driver.Suspend() + } else if s.TeardownMethod == "destroy" { + err = driver.Destroy() + } else { + // Should never get here because of template validation + state.Put("error", fmt.Errorf("Invalid teardown method selected; must be either halt, suspend, or destory.")) + } + if err != nil { + state.Put("error", fmt.Errorf("Error halting Vagrant machine; please try to do this manually")) + } +} diff --git a/command/plugin.go b/command/plugin.go index 2d2272640..4311a55c9 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -43,6 +43,7 @@ import ( scalewaybuilder "github.com/hashicorp/packer/builder/scaleway" tencentcloudcvmbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm" tritonbuilder "github.com/hashicorp/packer/builder/triton" + vagrantbuilder "github.com/hashicorp/packer/builder/vagrant" virtualboxisobuilder "github.com/hashicorp/packer/builder/virtualbox/iso" virtualboxovfbuilder "github.com/hashicorp/packer/builder/virtualbox/ovf" vmwareisobuilder "github.com/hashicorp/packer/builder/vmware/iso" @@ -116,6 +117,7 @@ var Builders = map[string]packer.Builder{ "scaleway": new(scalewaybuilder.Builder), "tencentcloud-cvm": new(tencentcloudcvmbuilder.Builder), "triton": new(tritonbuilder.Builder), + "vagrant": new(vagrantbuilder.Builder), "virtualbox-iso": new(virtualboxisobuilder.Builder), "virtualbox-ovf": new(virtualboxovfbuilder.Builder), "vmware-iso": new(vmwareisobuilder.Builder), diff --git a/website/source/docs/builders/alicloud-ecs.html.md b/website/source/docs/builders/alicloud-ecs.html.md index 8585cee65..945b95090 100644 --- a/website/source/docs/builders/alicloud-ecs.html.md +++ b/website/source/docs/builders/alicloud-ecs.html.md @@ -147,28 +147,30 @@ builder. Snapshots from on or before July 15, 2013 cannot be used to create a disk. - -- `image_ignore_data_disks`(boolean) - If this value is true, the image created - will not include any snapshot of data disks. This option would be useful for - any circumstance that default data disks with instance types are not concerned. - The default value is false. -- `wait_snapshot_ready_timeout`(number) - Timeout of creating snapshot(s). The - default timeout is 3600 seconds if this option is not set or is set to 0. For - those disks containing lots of data, it may require a higher timeout value. +- `image_ignore_data_disks`(boolean) - If this value is true, the image + created will not include any snapshot of data disks. This option would be + useful for any circumstance that default data disks with instance types are + not concerned. The default value is false. + +- `wait_snapshot_ready_timeout`(number) - Timeout of creating snapshot(s). + The default timeout is 3600 seconds if this option is not set or is set + to 0. For those disks containing lots of data, it may require a higher + timeout value. - `image_force_delete` (boolean) - If this value is true, when the target - image names including those copied are duplicated with existing images, - it will delete the existing images and then create the target images, - otherwise, the creation will fail. The default value is false. Check - `image_name` and `image_copy_names` options for names of target images. - If [-force](https://packer.io/docs/commands/build.html#force) option is provided - in `build` command, this option can be omitted and taken as true. + image names including those copied are duplicated with existing images, it + will delete the existing images and then create the target images, + otherwise, the creation will fail. The default value is false. Check + `image_name` and `image_copy_names` options for names of target images. If + [-force](https://packer.io/docs/commands/build.html#force) option is + provided in `build` command, this option can be omitted and taken as true. - `image_force_delete_snapshots` (boolean) - If this value is true, when delete the duplicated existing images, the source snapshots of those images - will be delete either. If [-force](https://packer.io/docs/commands/build.html#force) - option is provided in `build` command, this option can be omitted and taken as true. + will be delete either. If + [-force](https://packer.io/docs/commands/build.html#force) option is + provided in `build` command, this option can be omitted and taken as true. - `image_share_account` (array of string) - The IDs of to-be-added Aliyun accounts to which the image is shared. The number of accounts is 1 to 10. @@ -299,10 +301,13 @@ Here is a basic example for Alicloud. \~> Note: Images can become deprecated after a while; run `aliyun ecs DescribeImages` to find one that exists. -\~> Note: Since WinRM is closed by default in the system image. If you are planning -to use Windows as the base image, you need enable it by userdata in order to connect to -the instance, check [alicloud_windows.json](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/alicloud_windows.json) -and [winrm_enable_userdata.ps1](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/winrm_enable_userdata.ps1) for details. +\~> Note: Since WinRM is closed by default in the system image. If you are +planning to use Windows as the base image, you need enable it by userdata in +order to connect to the instance, check +[alicloud\_windows.json](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/alicloud_windows.json) +and +[winrm\_enable\_userdata.ps1](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/winrm_enable_userdata.ps1) +for details. See the [examples/alicloud](https://github.com/hashicorp/packer/tree/master/examples/alicloud) diff --git a/website/source/docs/builders/amazon-chroot.html.md b/website/source/docs/builders/amazon-chroot.html.md index 24622e049..acbed1997 100644 --- a/website/source/docs/builders/amazon-chroot.html.md +++ b/website/source/docs/builders/amazon-chroot.html.md @@ -24,7 +24,7 @@ builder is able to build an EBS-backed AMI without launching a new EC2 instance. This can dramatically speed up AMI builds for organizations who need the extra fast build. -~> **This is an advanced builder** If you're just getting started with +\~> **This is an advanced builder** If you're just getting started with Packer, we recommend starting with the [amazon-ebs builder](/docs/builders/amazon-ebs.html), which is much easier to use. @@ -154,8 +154,8 @@ each category, the available configuration keys are alphabetized. associated with AMIs, which have been deregistered by `force_deregister`. Default `false`. -- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS verification of - the AWS EC2 endpoint. The default is `false`. +- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS + verification of the AWS EC2 endpoint. The default is `false`. - `kms_key_id` (string) - ID, alias or ARN of the KMS key to use for boot volume encryption. This only applies to the main `region`, other regions @@ -457,8 +457,8 @@ services: ### Ansible provisioner -Running ansible against `amazon-chroot` requires changing the Ansible connection -to chroot and running Ansible as root/sudo. +Running ansible against `amazon-chroot` requires changing the Ansible +connection to chroot and running Ansible as root/sudo. ### Using Instances with NVMe block devices. diff --git a/website/source/docs/builders/amazon.html.md b/website/source/docs/builders/amazon.html.md index 931ff86ae..f722cd7d3 100644 --- a/website/source/docs/builders/amazon.html.md +++ b/website/source/docs/builders/amazon.html.md @@ -218,9 +218,9 @@ fail. If that's the case, you might see an error like this: ==> amazon-ebs: Error querying AMI: AuthFailure: AWS was not able to validate the provided access credentials If you suspect your system's date is wrong, you can compare it against -. On Linux/OS X, you can run the `date` command to get -the current time. If you're on Linux, you can try setting the time with ntp by -running `sudo ntpd -q`. +http://www.time.gov/. On +Linux/OS X, you can run the `date` command to get the current time. If you're +on Linux, you can try setting the time with ntp by running `sudo ntpd -q`. ### `exceeded wait attempts` while waiting for tasks to complete diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index 8caf44848..a4b86d2c7 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -206,8 +206,8 @@ Providing `temp_resource_group_name` or `location` in combination with - `os_disk_size_gb` (number) Specify the size of the OS disk in GB (gigabytes). Values of zero or less than zero are ignored. -- `disk_caching_type` (string) Specify the disk caching type. Valid values are None, ReadOnly, - and ReadWrite. The default value is ReadWrite. +- `disk_caching_type` (string) Specify the disk caching type. Valid values + are None, ReadOnly, and ReadWrite. The default value is ReadWrite. - `disk_additional_size` (array of integers) - The size(s) of any additional hard disks for the VM in gigabytes. If this is not specified then the VM @@ -325,13 +325,14 @@ Providing `temp_resource_group_name` or `location` in combination with value and defaults to false. **Important** Setting this true means that your builds are faster, however any failed deletes are not reported. -- `managed_image_os_disk_snapshot_name` (string) If managed\_image\_os\_disk\_snapshot\_name - is set, a snapshot of the OS disk is created with the same name as this value before the - VM is captured. +- `managed_image_os_disk_snapshot_name` (string) If + managed\_image\_os\_disk\_snapshot\_name is set, a snapshot of the OS disk + is created with the same name as this value before the VM is captured. -- `managed_image_data_disk_snapshot_prefix` (string) If managed\_image\_data\_disk\_snapshot\_prefix - is set, snapshot of the data disk(s) is created with the same prefix as this value before the VM - is captured. +- `managed_image_data_disk_snapshot_prefix` (string) If + managed\_image\_data\_disk\_snapshot\_prefix is set, snapshot of the data + disk(s) is created with the same prefix as this value before the VM is + captured. ## Basic Example @@ -403,14 +404,14 @@ here](https://technet.microsoft.com/en-us/library/hh824815.aspx) } ``` -The Windows Guest Agent participates in the Sysprep process. The -agent must be fully installed before the VM can be sysprep'ed. To -ensure this is true all agent services must be running before -executing sysprep.exe. The above JSON snippet shows one way to do this -in the PowerShell provisioner. This snippet is **only** required if -the VM is configured to install the agent, which is the default. To -learn more about disabling the Windows Guest Agent please see [Install the VM Agent](https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/agent-windows#install-the-vm-agent). - +The Windows Guest Agent participates in the Sysprep process. The agent must be +fully installed before the VM can be sysprep'ed. To ensure this is true all +agent services must be running before executing sysprep.exe. The above JSON +snippet shows one way to do this in the PowerShell provisioner. This snippet is +**only** required if the VM is configured to install the agent, which is the +default. To learn more about disabling the Windows Guest Agent please see +[Install the VM +Agent](https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/agent-windows#install-the-vm-agent). ### Linux diff --git a/website/source/docs/builders/digitalocean.html.md b/website/source/docs/builders/digitalocean.html.md index 1d382869d..b7a4f298d 100644 --- a/website/source/docs/builders/digitalocean.html.md +++ b/website/source/docs/builders/digitalocean.html.md @@ -41,17 +41,17 @@ builder. - `image` (string) - The name (or slug) of the base image to use. This is the image that will be used to launch a new droplet and provision it. See - https://developers.digitalocean.com/documentation/v2/#list-all-images + https://developers.digitalocean.com/documentation/v2/\#list-all-images for details on how to get a list of the accepted image names/slugs. - `region` (string) - The name (or slug) of the region to launch the droplet in. Consequently, this is the region where the snapshot will be available. See - https://developers.digitalocean.com/documentation/v2/#list-all-regions + https://developers.digitalocean.com/documentation/v2/\#list-all-regions for the accepted region names/slugs. - `size` (string) - The name (or slug) of the droplet size to use. See - https://developers.digitalocean.com/documentation/v2/#list-all-sizes + https://developers.digitalocean.com/documentation/v2/\#list-all-sizes for the accepted size names/slugs. ### Optional: diff --git a/website/source/docs/builders/docker.html.md b/website/source/docs/builders/docker.html.md index 6612bb464..e5e1879ba 100644 --- a/website/source/docs/builders/docker.html.md +++ b/website/source/docs/builders/docker.html.md @@ -185,9 +185,9 @@ You must specify (only) one of `commit`, `discard`, or `export_path`. information see the [section on ECR](#amazon-ec2-container-registry). - `exec_user` (string) - Username or UID (format: - <name\\\|uid>\[:<group\\\|gid>\]) to run remote commands with. - You may need this if you get permission errors trying to run the `shell` or - other provisioners. + <name\\\\\|uid>\[:<group\\\\\|gid>\]) to run remote commands + with. You may need this if you get permission errors trying to run the + `shell` or other provisioners. - `login` (boolean) - Defaults to false. If true, the builder will login in order to pull the image. The builder only logs in for the duration of the diff --git a/website/source/docs/builders/googlecompute.html.md b/website/source/docs/builders/googlecompute.html.md index f1d672adb..24a83b05f 100644 --- a/website/source/docs/builders/googlecompute.html.md +++ b/website/source/docs/builders/googlecompute.html.md @@ -19,8 +19,8 @@ based on existing images. It is possible to build images from scratch, but not with the `googlecompute` Packer builder. The process is recommended only for advanced users, please see \[Building GCE Images from Scratch\] -() and the -[Google Compute Import +(https://cloud.google.com/compute/docs/tutorials/building-images) +and the [Google Compute Import Post-Processor](/docs/post-processors/googlecompute-import.html) for more information. @@ -139,7 +139,7 @@ using the gcloud command. gcloud compute firewall-rules create allow-winrm --allow tcp:5986 Or alternatively by navigating to -. +https://console.cloud.google.com/networking/firewalls/list. Once this is set up, the following is a complete working packer config after setting a valid `account_file` and `project_id`: diff --git a/website/source/docs/builders/hetzner-cloud.html.md b/website/source/docs/builders/hetzner-cloud.html.md index 96e945521..fe886dba9 100644 --- a/website/source/docs/builders/hetzner-cloud.html.md +++ b/website/source/docs/builders/hetzner-cloud.html.md @@ -75,7 +75,9 @@ builder. - `ssh_keys` (array of strings) - List of SSH keys by name or id to be added to image on launch. -- `rescue` (string) - Enable and boot in to the specified rescue system. This enables simple installation of custom operating systems. `linux64` `linux32` or `freebsd64` +- `rescue` (string) - Enable and boot in to the specified rescue system. This + enables simple installation of custom operating systems. `linux64` + `linux32` or `freebsd64` ## Basic Example diff --git a/website/source/docs/builders/lxc.html.md b/website/source/docs/builders/lxc.html.md index f17b20e5f..c94042856 100644 --- a/website/source/docs/builders/lxc.html.md +++ b/website/source/docs/builders/lxc.html.md @@ -19,7 +19,7 @@ as a tar.gz of the root file system. The LXC builder requires a modern linux kernel and the `lxc` or `lxc1` package. This builder does not work with LXD. -~> Note: to build Centos images on a Debian family host, you will need the +\~> Note: to build Centos images on a Debian family host, you will need the `yum` package installed.
Some provisioners such as `ansible-local` get confused when running in a container of a different family. E.G. it will attempt to use `apt-get` to install packages, when running in a Centos diff --git a/website/source/docs/builders/lxd.html.md b/website/source/docs/builders/lxd.html.md index 2523f1541..45a2937a9 100644 --- a/website/source/docs/builders/lxd.html.md +++ b/website/source/docs/builders/lxd.html.md @@ -47,7 +47,7 @@ Below is a fully functioning example. container. This can be a (local or remote) image (name or fingerprint). E.G. `my-base-image`, `ubuntu-daily:x`, `08fababf6f27`, ... - ~> Note: The builder may appear to pause if required to download a + \~> Note: The builder may appear to pause if required to download a remote image, as they are usually 100-200MB. `/var/log/lxd/lxd.log` will mention starting such downloads. @@ -68,8 +68,8 @@ Below is a fully functioning example. - `publish_properties` (map\[string\]string) - Pass key values to the publish step to be set as properties on the output image. This is most helpful to set the description, but can be used to set anything needed. See - for more - properties. + https://stgraber.org/2016/03/30/lxd-2-0-image-management-512/ + for more properties. - `launch_config` (map\[string\]string) - List of key/value pairs you wish to pass to `lxc launch` via `--config`. Defaults to empty. diff --git a/website/source/docs/builders/ncloud.html.md b/website/source/docs/builders/ncloud.html.md index 1aacf633f..ddc91b257 100644 --- a/website/source/docs/builders/ncloud.html.md +++ b/website/source/docs/builders/ncloud.html.md @@ -90,7 +90,6 @@ Here is a basic example for windows server. ] } - Here is a basic example for linux server. { diff --git a/website/source/docs/builders/oneandone.html.md b/website/source/docs/builders/oneandone.html.md index dca56dc85..5b9007823 100644 --- a/website/source/docs/builders/oneandone.html.md +++ b/website/source/docs/builders/oneandone.html.md @@ -44,7 +44,7 @@ builder. while waiting for the build to complete. Default value "600". - `url` (string) - Endpoint for the 1&1 REST API. Default URL - "" + "https://cloudpanel-api.1and1.com/v1" ## Example diff --git a/website/source/docs/builders/openstack.html.md b/website/source/docs/builders/openstack.html.md index c67460b4c..837409e9f 100644 --- a/website/source/docs/builders/openstack.html.md +++ b/website/source/docs/builders/openstack.html.md @@ -278,8 +278,8 @@ builder. isn't specified, the default enforced by your OpenStack cluster will be used. -- `volume_size` (int) - Size of the Block Storage service volume in GB. If this - isn't specified, it is set to source image min disk value (if set) or +- `volume_size` (int) - Size of the Block Storage service volume in GB. If + this isn't specified, it is set to source image min disk value (if set) or calculated from the source image bytes size. Note that in some cases this needs to be specified, if `use_blockstorage_volume` is true. diff --git a/website/source/docs/builders/oracle-classic.html.md b/website/source/docs/builders/oracle-classic.html.md index e917abeda..1fef81830 100644 --- a/website/source/docs/builders/oracle-classic.html.md +++ b/website/source/docs/builders/oracle-classic.html.md @@ -141,9 +141,10 @@ If this is set, a few more options become available. - `builder_communicator` (communicator) - This represents an [`ssh communicator`](/docs/templates/communicator.html#ssh-communicator), and can be configured as such. If you use a different builder image, you - may need to change the `ssh_username`, for example. That might look like this: + may need to change the `ssh_username`, for example. That might look like + this: - ```json + ``` json { "builders": [ { @@ -170,7 +171,7 @@ If this is set, a few more options become available. amount of memory, and other resources allocated to the builder instance. Default: `oc3`. -* `builder_upload_image_command` (string) - The command to run to upload the +- `builder_upload_image_command` (string) - The command to run to upload the image to Object Storage Classic. This is for advanced users only, and you should consult the default in code to decide on the changes to make. For most users the default should suffice. @@ -272,7 +273,7 @@ attributes file: Here is an example using a persistent volume. Note the `persistent_volume_size` setting. -```json +``` json { "variables": { "opc_username": "{{ env `OPC_USERNAME`}}", diff --git a/website/source/docs/builders/profitbricks.html.md b/website/source/docs/builders/profitbricks.html.md index 79fdb2a84..3762a9d97 100644 --- a/website/source/docs/builders/profitbricks.html.md +++ b/website/source/docs/builders/profitbricks.html.md @@ -60,7 +60,7 @@ builder. - `snapshot_password` (string) - Password for the snapshot. - `url` (string) - Endpoint for the ProfitBricks REST API. Default URL - "" + "https://api.profitbricks.com/rest/v2" ## Example diff --git a/website/source/docs/builders/scaleway.html.md b/website/source/docs/builders/scaleway.html.md index e2ce32676..f22043e0d 100644 --- a/website/source/docs/builders/scaleway.html.md +++ b/website/source/docs/builders/scaleway.html.md @@ -48,8 +48,8 @@ builder. - `image` (string) - The UUID of the base image to use. This is the image that will be used to launch a new server and provision it. See - get the complete list of the - accepted image UUID. + https://api-marketplace.scaleway.com/images + get the complete list of the accepted image UUID. - `region` (string) - The name of the region to launch the server in (`par1` or `ams1`). Consequently, this is the region where the snapshot will be diff --git a/website/source/docs/builders/triton.html.md b/website/source/docs/builders/triton.html.md index df5173022..e3b003190 100644 --- a/website/source/docs/builders/triton.html.md +++ b/website/source/docs/builders/triton.html.md @@ -30,7 +30,7 @@ This reusable image can then be used to launch new machines. The builder does *not* manage images. Once it creates an image, it is up to you to use it or delete it. -~> **Private installations of Triton must have custom images enabled!** To +\~> **Private installations of Triton must have custom images enabled!** To use the Triton builder with a private/on-prem installation of Joyent's Triton software, you'll need an operator to manually [enable custom images](https://docs.joyent.com/private-cloud/install/image-management) after diff --git a/website/source/docs/builders/vagrant.html.md b/website/source/docs/builders/vagrant.html.md new file mode 100644 index 000000000..f5d1829c0 --- /dev/null +++ b/website/source/docs/builders/vagrant.html.md @@ -0,0 +1,66 @@ +The Vagrant builder is intended for building new boxes from already-existing +boxes. Your source should be a URL or path to a .box file or a Vagrant Cloud +box name such as `hashicorp/precise64`. This builder is not currently intended +to work with an already-set-up Vagrant environment. + +Packer will not install vagrant, nor will it install the underlying +virtualization platforms or extra providers; We expect when you run this +builder that you have already installed what you need. + +Required: + +`source_box` (string) - URL of the vagrant box to use, or the name of the +vagrant box. For example, `hashicorp/precise64` or +`https://boxes.company.com/my-company.box` are valid source boxes. If using a URL like the latter example above, you will also need to provide a `box_name`. + +Optional: + +`output_dir` (string) - The directory to create that will contain +your output box. We always create this directory and run from inside of it to +prevent Vagrant init collisions. If unset, it will be set to `packer-` plus +your buildname. + +`box_name` (string) - if your source\_box is a boxfile that we need to add to +Vagrant, this is the name to give it. + +`vagrantfile_template` (string) - a path to an ERB template to use for the +vagrantfile when calling `vagrant init`. See the blog post +[here](https://www.hashicorp.com/blog/hashicorp-vagrant-2-0-2#customized-vagrantfile-templates) +for some more details on how this works. Available variables are `box_name`, +`box_url`, and `box_version`. + +`teardown_method` (string) - Whether to halt, suspend, or destroy the box when +the build has completed. Defaults to "halt" + +`box_version` (string) - What box version to use when initializing Vagrant. + +`init_minimal` (bool) - If true, will add the --minimal flag to the Vagrant +init command, creating a minimal vagrantfile instead of one filled with helpful +comments. + +`add_cacert` (string) - Equivalent to setting the +[`--cacert`](https://www.vagrantup.com/docs/cli/box.html#cacert-certfile) +option in `vagrant add`; defaults to unset. + +`add_capath` (string) - Equivalent to setting the +[`--capath`](https://www.vagrantup.com/docs/cli/box.html#capath-certdir) option +in `vagrant add`; defaults to unset. + +`add_cert` (string) - Equivalent to setting the +[`--cert`](https://www.vagrantup.com/docs/cli/box.html#cert-certfile) option in +`vagrant add`; defaults to unset. + +`add_clean` (bool) - Equivalent to setting the +[`--clean`](https://www.vagrantup.com/docs/cli/box.html#clean) flag in +`vagrant add`; defaults to unset. + +`add_force` (bool) - Equivalent to setting the +[`--force`](https://www.vagrantup.com/docs/cli/box.html#force) flag in +`vagrant add`; defaults to unset. + +`add_insecure` (bool) - Equivalent to setting the +[`--force`](https://www.vagrantup.com/docs/cli/box.html#insecure) flag in +`vagrant add`; defaults to unset. + +`skip_package` (bool) - if true, Packer will not call `vagrant package` to +package your base box into its own standalone .box file. diff --git a/website/source/docs/commands/index.html.md b/website/source/docs/commands/index.html.md index 8f71e386c..2aeecde6f 100644 --- a/website/source/docs/commands/index.html.md +++ b/website/source/docs/commands/index.html.md @@ -58,7 +58,7 @@ The format will be covered in more detail later. But as you can see, the output immediately becomes machine-friendly. Try some other commands with the `-machine-readable` flag to see! -~> The `-machine-readable` flag is designed for automated environments and +\~> The `-machine-readable` flag is designed for automated environments and is mutually-exclusive with the `-debug` flag, which is designed for interactive environments. diff --git a/website/source/docs/extending/custom-builders.html.md b/website/source/docs/extending/custom-builders.html.md index 6d1cbe03a..302401533 100644 --- a/website/source/docs/extending/custom-builders.html.md +++ b/website/source/docs/extending/custom-builders.html.md @@ -19,7 +19,7 @@ plugin interface, and this page documents how to do that. Prior to reading this page, it is assumed you have read the page on [plugin development basics](/docs/extending/plugins.html). -~> **Warning!** This is an advanced topic. If you're new to Packer, we +\~> **Warning!** This is an advanced topic. If you're new to Packer, we recommend getting a bit more comfortable before you dive into writing plugins. ## The Interface diff --git a/website/source/docs/extending/custom-post-processors.html.md b/website/source/docs/extending/custom-post-processors.html.md index b177cf0a3..0ec5f71a5 100644 --- a/website/source/docs/extending/custom-post-processors.html.md +++ b/website/source/docs/extending/custom-post-processors.html.md @@ -24,7 +24,7 @@ development basics](/docs/extending/plugins.html). Post-processor plugins implement the `packer.PostProcessor` interface and are served using the `plugin.ServePostProcessor` function. -~> **Warning!** This is an advanced topic. If you're new to Packer, we +\~> **Warning!** This is an advanced topic. If you're new to Packer, we recommend getting a bit more comfortable before you dive into writing plugins. ## The Interface diff --git a/website/source/docs/extending/custom-provisioners.html.md b/website/source/docs/extending/custom-provisioners.html.md index 75f412e4f..3d3a1721d 100644 --- a/website/source/docs/extending/custom-provisioners.html.md +++ b/website/source/docs/extending/custom-provisioners.html.md @@ -23,7 +23,7 @@ development basics](/docs/extending/plugins.html). Provisioner plugins implement the `packer.Provisioner` interface and are served using the `plugin.ServeProvisioner` function. -~> **Warning!** This is an advanced topic. If you're new to Packer, we +\~> **Warning!** This is an advanced topic. If you're new to Packer, we recommend getting a bit more comfortable before you dive into writing plugins. ## The Interface diff --git a/website/source/docs/extending/plugins.html.md b/website/source/docs/extending/plugins.html.md index 1a027c4b4..901bfc91b 100644 --- a/website/source/docs/extending/plugins.html.md +++ b/website/source/docs/extending/plugins.html.md @@ -56,7 +56,8 @@ later, it will take precedence over one found earlier. 3. The `%APPDATA%/packer.d/plugins` if `%APPDATA%` is defined (windows) -4. The `%USERPROFILE%/packer.d/plugins` if `%USERPROFILE%` is defined (windows) +4. The `%USERPROFILE%/packer.d/plugins` if `%USERPROFILE%` is defined + (windows) 5. The current working directory. @@ -83,7 +84,7 @@ assumed that you're familiar with the language. This page will not be a Go language tutorial. Thankfully, if you are familiar with Go, the Go toolchain provides many conveniences to help to develop Packer plugins. -~> **Warning!** This is an advanced topic. If you're new to Packer, we +\~> **Warning!** This is an advanced topic. If you're new to Packer, we recommend getting a bit more comfortable before you dive into writing plugins. ### Plugin System Architecture @@ -159,7 +160,7 @@ using standard installation procedures. The specifics of how to implement each type of interface are covered in the relevant subsections available in the navigation to the left. -~> **Lock your dependencies!** Using `govendor` is highly recommended since +\~> **Lock your dependencies!** Using `govendor` is highly recommended since the Packer codebase will continue to improve, potentially breaking APIs along the way until there is a stable release. By locking your dependencies, your plugins will continue to work with the version of Packer you lock to. diff --git a/website/source/docs/post-processors/alicloud-import.html.md b/website/source/docs/post-processors/alicloud-import.html.md index c421883d7..56d68f501 100644 --- a/website/source/docs/post-processors/alicloud-import.html.md +++ b/website/source/docs/post-processors/alicloud-import.html.md @@ -42,7 +42,8 @@ are two categories: required and optional parameters. - `image_name` (string) - The name of the user-defined image, \[2, 128\] English or Chinese characters. It must begin with an uppercase/lowercase letter or a Chinese character, and may contain numbers, `_` or `-`. It - cannot begin with or . + cannot begin with http:// or + https://. - `oss_bucket_name` (string) - The name of the OSS bucket where the RAW or VHD file will be copied to for import. If the Bucket isn't exist, @@ -52,7 +53,7 @@ are two categories: required and optional parameters. - `image_platform` (string) - platform such `CentOS` -- `image_architecture` (string) - Platform type of the image system:i386 \| +- `image_architecture` (string) - Platform type of the image system:i386 \\\| x86\_64 - `format` (string) - The format of the image for import, now alicloud only @@ -70,7 +71,9 @@ are two categories: required and optional parameters. - `image_description` (string) - The description of the image, with a length limit of 0 to 256 characters. Leaving it blank means null, which is the - default value. It cannot begin with or . + default value. It cannot begin with + http:// or + https://. - `image_force_delete` (boolean) - If this value is true, when the target image name is duplicated with an existing image, it will delete the @@ -79,9 +82,9 @@ are two categories: required and optional parameters. - `image_system_size` (number) - Size of the system disk, in GB, values range: - - cloud - 5 ~ 2000 - - cloud\_efficiency - 20 ~ 2048 - - cloud\_ssd - 20 ~ 2048 + - cloud - 5 \~ 2000 + - cloud\_efficiency - 20 \~ 2048 + - cloud\_ssd - 20 \~ 2048 ## Basic Example diff --git a/website/source/docs/post-processors/amazon-import.html.md b/website/source/docs/post-processors/amazon-import.html.md index 700542a40..d1dceed8c 100644 --- a/website/source/docs/post-processors/amazon-import.html.md +++ b/website/source/docs/post-processors/amazon-import.html.md @@ -14,7 +14,7 @@ Type: `amazon-import` The Packer Amazon Import post-processor takes an OVA artifact from various builders and imports it to an AMI available to Amazon Web Services EC2. -~> This post-processor is for advanced users. It depends on specific IAM +\~> This post-processor is for advanced users. It depends on specific IAM roles inside AWS and is best used with images that operate with the EC2 configuration model (eg, cloud-init for Linux systems). Please ensure you read the [prerequisites for @@ -85,12 +85,13 @@ Optional: provider whose API is compatible with aws EC2. Specify another endpoint like this `https://ec2.custom.endpoint.com`. -- `format` (string) - One of: `ova`, `raw`, `vhd`, `vhdx`, or `vmdk`. This specifies - the format of the source virtual machine image. The resulting artifact from the builder - is assumed to have a file extension matching the format. This defaults to `ova`. +- `format` (string) - One of: `ova`, `raw`, `vhd`, `vhdx`, or `vmdk`. This + specifies the format of the source virtual machine image. The resulting + artifact from the builder is assumed to have a file extension matching the + format. This defaults to `ova`. -- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS verification of - the AWS EC2 endpoint. The default is `false`. +- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS + verification of the AWS EC2 endpoint. The default is `false`. - `license_type` (string) - The license type to be used for the Amazon Machine Image (AMI) after importing. Valid values: `AWS` or `BYOL` diff --git a/website/source/docs/post-processors/docker-import.html.md b/website/source/docs/post-processors/docker-import.html.md index 8802a18e7..4fb3b0fe3 100644 --- a/website/source/docs/post-processors/docker-import.html.md +++ b/website/source/docs/post-processors/docker-import.html.md @@ -38,7 +38,6 @@ is optional. commit. Example of instructions are `CMD`, `ENTRYPOINT`, `ENV`, and `EXPOSE`. Example: `[ "USER ubuntu", "WORKDIR /app", "EXPOSE 8080" ]` - ## Example An example is shown below, showing only the post-processor configuration: @@ -61,9 +60,9 @@ to a registry, if you want. ## Changing Metadata Below is an example using the changes argument of the post-processor. This -feature allows the tarball metadata to be changed when imported into the -Docker environment. It is derived from the `docker import --change` command -line [option to +feature allows the tarball metadata to be changed when imported into the Docker +environment. It is derived from the `docker import --change` command line +[option to Docker](https://docs.docker.com/engine/reference/commandline/import/). Example uses of all of the options, assuming one is building an NGINX image diff --git a/website/source/docs/post-processors/googlecompute-import.html.md b/website/source/docs/post-processors/googlecompute-import.html.md index 4676dfd3c..c7ff21f09 100644 --- a/website/source/docs/post-processors/googlecompute-import.html.md +++ b/website/source/docs/post-processors/googlecompute-import.html.md @@ -14,7 +14,7 @@ Type: `googlecompute-import` The Google Compute Image Import post-processor takes a compressed raw disk image and imports it to a GCE image available to Google Compute Engine. -~> This post-processor is for advanced users. Please ensure you read the +\~> This post-processor is for advanced users. Please ensure you read the [GCE import documentation](https://cloud.google.com/compute/docs/images/import-existing-image) before using this post-processor. diff --git a/website/source/docs/post-processors/shell-local.html.md b/website/source/docs/post-processors/shell-local.html.md index 279548b10..517c094bc 100644 --- a/website/source/docs/post-processors/shell-local.html.md +++ b/website/source/docs/post-processors/shell-local.html.md @@ -60,14 +60,13 @@ Optional parameters: Packer injects some environmental variables by default into the environment, as well, which are covered in the section below. -- `env_var_format` (string) - When we parse the environment_vars that you +- `env_var_format` (string) - When we parse the environment\_vars that you provide, this gives us a string template to use in order to make sure that we are setting the environment vars correctly. By default on Windows hosts - this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably + this format is `set %s=%s &&` and on Unix, it is `%s='%s'`. You probably won't need to change this format, but you can see usage examples for where it is necessary below. - - `execute_command` (array of strings) - The command used to execute the script. By default this is `["/bin/sh", "-c", "{{.Vars}}", "{{.Script}}"]` on unix and `["cmd", "/c", "{{.Vars}}", "{{.Script}}"]` on windows. This is @@ -250,10 +249,10 @@ are cleaned up. For a shell script, that means the script **must** exit with a zero code. You *must* be extra careful to `exit 0` when necessary. - ## Usage Examples: ### Windows Host + Example of running a .cmd file on windows: { @@ -295,7 +294,6 @@ Contents of "example\_bash.sh": Example of running a powershell script on windows: Required customizations: env\_var\_format and execute\_command - { "type": "shell-local", "environment_vars": ["SHELLLOCALTEST=ShellTest4"], @@ -317,6 +315,7 @@ customizations: env\_var\_format, tempfile\_extension, and execute\_command } ### Unix Host + Example of running a bash script on unix: { @@ -336,19 +335,15 @@ Example of running a bash "inline" on unix: Example of running a python script on unix: -``` - { - "type": "shell-local", - "script": "hello.py", - "environment_vars": ["HELLO_USER=packeruser"], - "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"] - } -``` + { + "type": "shell-local", + "script": "hello.py", + "environment_vars": ["HELLO_USER=packeruser"], + "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"] + } Where "hello.py" contains: -``` -import os + import os -print('Hello, %s!' % os.getenv("HELLO_USER")) -``` \ No newline at end of file + print('Hello, %s!' % os.getenv("HELLO_USER")) diff --git a/website/source/docs/post-processors/vagrant.html.md b/website/source/docs/post-processors/vagrant.html.md index c0ccf6241..253472ed3 100644 --- a/website/source/docs/post-processors/vagrant.html.md +++ b/website/source/docs/post-processors/vagrant.html.md @@ -144,5 +144,5 @@ The following Docker input artifacts are supported: ### QEMU/libvirt -The `libvirt` provider supports QEMU artifacts built using any these accelerators: none, -kvm, tcg, or hvf. +The `libvirt` provider supports QEMU artifacts built using any these +accelerators: none, kvm, tcg, or hvf. diff --git a/website/source/docs/post-processors/vsphere-template.html.md b/website/source/docs/post-processors/vsphere-template.html.md index 9057aca14..08fbf46c9 100644 --- a/website/source/docs/post-processors/vsphere-template.html.md +++ b/website/source/docs/post-processors/vsphere-template.html.md @@ -60,16 +60,16 @@ Optional: - `insecure` (boolean) - If it's true skip verification of server certificate. Default is false - -- `snapshot_enable` (boolean) - Create a snapshot before marking as a + +- `snapshot_enable` (boolean) - Create a snapshot before marking as a template. Default is false - -- `snapshot_name` (string) - Name for the snapshot. - Required when `snapshot_enable` is `true` - -- `snapshot_description` (string) - Description for the snapshot. - Required when `snapshot_enable` is `true` - + +- `snapshot_name` (string) - Name for the snapshot. Required when + `snapshot_enable` is `true` + +- `snapshot_description` (string) - Description for the snapshot. Required + when `snapshot_enable` is `true` + ## Using the vSphere Template with local builders Once the [vSphere](/docs/post-processors/vsphere.html) takes an artifact from diff --git a/website/source/docs/provisioners/ansible-local.html.md b/website/source/docs/provisioners/ansible-local.html.md index 5972cff1d..63cd15125 100644 --- a/website/source/docs/provisioners/ansible-local.html.md +++ b/website/source/docs/provisioners/ansible-local.html.md @@ -68,6 +68,7 @@ Optional: example: + "extra_arguments": [ "--extra-vars \"Region={{user `Region`}} Stage={{user `Stage`}}\"" ] - `inventory_groups` (string) - A comma-separated list of groups to which diff --git a/website/source/docs/provisioners/breakpoint.html.md b/website/source/docs/provisioners/breakpoint.html.md index edcc48405..d251e534b 100644 --- a/website/source/docs/provisioners/breakpoint.html.md +++ b/website/source/docs/provisioners/breakpoint.html.md @@ -1,8 +1,8 @@ --- description: | - The breakpoint provisioner will pause until the user presses "enter" to - resume the build. This is intended for debugging purposes, and allows you - to halt at a particular part of the provisioning process. + The breakpoint provisioner will pause until the user presses "enter" to resume + the build. This is intended for debugging purposes, and allows you to halt at a + particular part of the provisioning process. layout: docs page_title: 'breakpoint - Provisioners' sidebar_current: 'docs-provisioners-breakpoint' @@ -12,9 +12,9 @@ sidebar_current: 'docs-provisioners-breakpoint' Type: `breakpoint` -The breakpoint provisioner will pause until the user presses "enter" to -resume the build. This is intended for debugging purposes, and allows you -to halt at a particular part of the provisioning process. +The breakpoint provisioner will pause until the user presses "enter" to resume +the build. This is intended for debugging purposes, and allows you to halt at a +particular part of the provisioning process. This is independent of the `-debug` flag, which will instead halt at every step and between every provisioner. @@ -33,8 +33,8 @@ and between every provisioner. ### Optional - `disable` (boolean) - If `true`, skip the breakpoint. Useful for when you - have set multiple breakpoints and want to toggle them off or on. - Default: `false` + have set multiple breakpoints and want to toggle them off or on. Default: + `false` - `note` (string) - a string to include explaining the purpose or location of the breakpoint. For example, you may find it useful to number your @@ -48,10 +48,8 @@ output prompting you to press "enter" to continue the build when you are ready. For example: -``` -==> docker: Pausing at breakpoint provisioner with note "foo bar baz". -==> docker: Press enter to continue. -``` + ==> docker: Pausing at breakpoint provisioner with note "foo bar baz". + ==> docker: Press enter to continue. Once you press enter, the build will resume and run normally until it either -completes or errors. \ No newline at end of file +completes or errors. diff --git a/website/source/docs/provisioners/puppet-masterless.html.md b/website/source/docs/provisioners/puppet-masterless.html.md index e28a10e7d..41ced3fde 100644 --- a/website/source/docs/provisioners/puppet-masterless.html.md +++ b/website/source/docs/provisioners/puppet-masterless.html.md @@ -73,7 +73,8 @@ Optional parameters: ] - `facter` (object of key:value strings) - Additional - [facts](https://docs.puppet.com/facter/) to make available to the Puppet run. + [facts](https://docs.puppet.com/facter/) to make available to the Puppet + run. - `guest_os_type` (string) - The remote host's OS type ('windows' or 'unix') to tailor command-line and path separators. (default: unix). @@ -88,9 +89,9 @@ Optional parameters: This is useful if your main manifest uses imports, but the directory might not contain the `manifest_file` itself. -~> `manifest_dir` is passed to Puppet as `--manifestdir` option. This option -was deprecated in puppet 3.6, and removed in puppet 4.0. If you have multiple -manifests you should use `manifest_file` instead. +\~> `manifest_dir` is passed to Puppet as `--manifestdir` option. This +option was deprecated in puppet 3.6, and removed in puppet 4.0. If you have +multiple manifests you should use `manifest_file` instead. - `module_paths` (array of strings) - Array of local module directories to be uploaded. diff --git a/website/source/docs/provisioners/shell-local.html.md b/website/source/docs/provisioners/shell-local.html.md index cf9b6cc60..95f099d92 100644 --- a/website/source/docs/provisioners/shell-local.html.md +++ b/website/source/docs/provisioners/shell-local.html.md @@ -74,10 +74,10 @@ Optional parameters: this as an environment variable. For example: `"environment_vars": "WINRMPASS={{.WinRMPassword}}"` -- `env_var_format` (string) - When we parse the environment_vars that you +- `env_var_format` (string) - When we parse the environment\_vars that you provide, this gives us a string template to use in order to make sure that we are setting the environment vars correctly. By default on Windows hosts - this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably + this format is `set %s=%s &&` and on Unix, it is `%s='%s'`. You probably won't need to change this format, but you can see usage examples for where it is necessary below. @@ -230,6 +230,7 @@ For a shell script, that means the script **must** exit with a zero code. You ## Usage Examples: ### Windows Host + Example of running a .cmd file on windows: { @@ -271,7 +272,6 @@ Contents of "example\_bash.sh": Example of running a powershell script on windows: Required customizations: env\_var\_format and execute\_command - { "type": "shell-local", "environment_vars": ["SHELLLOCALTEST=ShellTest4"], @@ -293,6 +293,7 @@ customizations: env\_var\_format, tempfile\_extension, and execute\_command } ### Unix Host + Example of running a bash script on unix: { @@ -312,19 +313,15 @@ Example of running a bash "inline" on unix: Example of running a python script on unix: -``` - { - "type": "shell-local", - "script": "hello.py", - "environment_vars": ["HELLO_USER=packeruser"], - "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"] - } -``` + { + "type": "shell-local", + "script": "hello.py", + "environment_vars": ["HELLO_USER=packeruser"], + "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"] + } Where "hello.py" contains: -``` -import os + import os -print('Hello, %s!' % os.getenv("HELLO_USER")) -``` \ No newline at end of file + print('Hello, %s!' % os.getenv("HELLO_USER")) diff --git a/website/source/docs/provisioners/windows-restart.html.md b/website/source/docs/provisioners/windows-restart.html.md index b9ee04567..bb340efc8 100644 --- a/website/source/docs/provisioners/windows-restart.html.md +++ b/website/source/docs/provisioners/windows-restart.html.md @@ -41,10 +41,10 @@ Optional parameters: - `check_registry` (bool) - if `true`, checks for several registry keys that indicate that the system is going to reboot. This is useful if an installation kicks off a reboot and you want the provisioner to wait for - that reboot to complete before reconnecting. Please note that this option is - a beta feature, and we generally recommend that you finish installs that - auto-reboot (like windows updates) during your autounattend phase before our - winrm provisioner connects. + that reboot to complete before reconnecting. Please note that this option + is a beta feature, and we generally recommend that you finish installs that + auto-reboot (like windows updates) during your autounattend phase before + our winrm provisioner connects. - `registry_keys` (array of strings) - if `check-registry` is `true`, windows-restart will not reconnect until after all of the listed keys are @@ -52,13 +52,11 @@ Optional parameters: default: - ``` - var DefaultRegistryKeys = []string{ - "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending", - "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending", - "HKLM:Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress", - } - ``` + var DefaultRegistryKeys = []string{ + "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending", + "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending", + "HKLM:Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress", + } - `restart_command` (string) - The command to execute to initiate the restart. By default this is `shutdown /r /f /t 0 /c "packer restart"`. diff --git a/website/source/docs/templates/communicator.html.md b/website/source/docs/templates/communicator.html.md index b2133f825..86267d225 100644 --- a/website/source/docs/templates/communicator.html.md +++ b/website/source/docs/templates/communicator.html.md @@ -75,9 +75,9 @@ The SSH communicator has the following options: - `ssh_bastion_port` (number) - The port of the bastion host. Defaults to `22`. -- `ssh_bastion_private_key_file` (string) - Path to a PEM encoded private - key file to use to authenticate with the bastion host. The `~` can be used - in path and will be expanded to the home directory of current user. +- `ssh_bastion_private_key_file` (string) - Path to a PEM encoded private key + file to use to authenticate with the bastion host. The `~` can be used in + path and will be expanded to the home directory of current user. - `ssh_bastion_username` (string) - The username to connect to the bastion host. @@ -155,23 +155,25 @@ Packer supports the following ciphers: - arcfour128 - arcfour256 - arcfour -- -- +- +- And the following MACs: - hmac-sha1 - hmac-sha1-96 - hmac-sha2-256 -- +- ## WinRM Communicator The WinRM communicator has the following options. - `winrm_host` (string) - The address for WinRM to connect to. - - NOTE: If using an Amazon EBS builder, you can specify the interface WinRM connects to via [`ssh_interface`](https://www.packer.io/docs/builders/amazon-ebs.html#ssh_interface) + + NOTE: If using an Amazon EBS builder, you can specify the interface WinRM + connects to via + [`ssh_interface`](https://www.packer.io/docs/builders/amazon-ebs.html#ssh_interface) - `winrm_insecure` (boolean) - If `true`, do not check server certificate chain and host name. diff --git a/website/source/docs/templates/engine.html.md b/website/source/docs/templates/engine.html.md index aae419098..1cebe5fc5 100644 --- a/website/source/docs/templates/engine.html.md +++ b/website/source/docs/templates/engine.html.md @@ -44,7 +44,8 @@ Here is a full list of the available functions for reference. reference](/docs/templates/engine.html#isotime-function-format-reference). - `lower` - Lowercases the string. - `pwd` - The working directory while executing Packer. -- `sed` - Use [a golang implementation of sed](https://github.com/rwtodd/Go.Sed) to parse an input string. +- `sed` - Use [a golang implementation of + sed](https://github.com/rwtodd/Go.Sed) to parse an input string. - `split` - Split an input string using separator and return the requested substring. - `template_dir` - The directory to the template for the build. @@ -227,6 +228,7 @@ Formatting for the function `isotime` uses the magic reference date **Mon Jan 2 + *The values in parentheses are the abbreviated, or 24-hour clock values* Note that "-0700" is always formatted into "+0000" because `isotime` is always @@ -272,10 +274,10 @@ builder in this example. The function `split` takes an input string, a seperator string, and a numeric component value and returns the requested substring. -Please note that you cannot use the `split` function on user variables, -because we can't nest the functions currently. This function is indended to -be used on builder variables like build_name. If you need a split user -variable, the best way to do it is to create a separate variable. +Please note that you cannot use the `split` function on user variables, because +we can't nest the functions currently. This function is indended to be used on +builder variables like build\_name. If you need a split user variable, the best +way to do it is to create a separate variable. Here are some examples using the above options: @@ -310,16 +312,15 @@ this case, on the `fixed-string` value): # sed Function Format Reference -See the library documentation https://github.com/rwtodd/Go.Sed for notes about -the difference between this golang implementation of sed and the regexes you may -be used to. A very simple example of this functionality: +See the library documentation +https://github.com/rwtodd/Go.Sed +for notes about the difference between this golang implementation of sed and +the regexes you may be used to. A very simple example of this functionality: -``` - "provisioners": [ - { - "type": "shell-local", - "environment_vars": ["EXAMPLE={{ sed \"s/null/awesome/\" build_type}}"], - "inline": ["echo build_type is $EXAMPLE\n"] - } - ] -``` + "provisioners": [ + { + "type": "shell-local", + "environment_vars": ["EXAMPLE={{ sed \"s/null/awesome/\" build_type}}"], + "inline": ["echo build_type is $EXAMPLE\n"] + } + ] From 52892699ca81493f0adcc28fad17f7ea322831cc Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 24 Jan 2019 16:56:57 -0800 Subject: [PATCH 06/48] make it work with a local vagrant box --- builder/vagrant/builder.go | 56 +++++++++--- builder/vagrant/step_add_box.go | 6 ++ website/source/docs/builders/vagrant.html.md | 92 +++++++++++--------- 3 files changed, 102 insertions(+), 52 deletions(-) diff --git a/builder/vagrant/builder.go b/builder/vagrant/builder.go index fa2a94a76..959ed277a 100644 --- a/builder/vagrant/builder.go +++ b/builder/vagrant/builder.go @@ -37,10 +37,13 @@ type Config struct { // This is the name of the new virtual machine. // By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build. - OutputDir string `mapstructure:"output_dir"` - SourceBox string `mapstructure:"source_box"` - SourceBoxName string `mapstructure:"source_box_name"` - Provider string `mapstructure:"provider"` + OutputDir string `mapstructure:"output_dir"` + SourceBox string `mapstructure:"source_box"` + Checksum string `mapstructure:"checksum"` + ChecksumType string `mapstructure:"checksum_type"` + BoxName string `mapstructure:"box_name"` + + Provider string `mapstructure:"provider"` Communicator string `mapstructure:"communicator"` @@ -104,6 +107,26 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, fmt.Errorf(`The Vagrant builder currently only supports the ssh communicator"`)) } + // The box isn't a namespace like you'd pull from vagrant cloud + if b.config.BoxName == "" { + b.config.BoxName = fmt.Sprintf("packer_%s", b.config.PackerBuildName) + } + + if b.config.SourceBox == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required")) + } else { + if !strings.HasSuffix(b.config.SourceBox, ".box") { + b.config.SourceBox, err = common.ValidatedURL(b.config.SourceBox) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is invalid: %s", err)) + } + fileOK := common.FileExistsLocally(b.config.SourceBox) + if !fileOK { + packer.MultiErrorAppend(errs, + fmt.Errorf("Source file '%s' needs to exist at time of config validation!", b.config.SourceBox)) + } + } + } if b.config.TeardownMethod == "" { b.config.TeardownMethod = "destroy" @@ -147,21 +170,30 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps. steps := []multistep.Step{} - if !b.config.SkipPackage { - steps = append(steps, - &common.StepOutputDir{ - Force: b.config.PackerForce, - Path: b.config.OutputDir, - }) + // Download if source box isn't from vagrant cloud. + if !strings.HasSuffix(b.config.SourceBox, ".box") { + steps = append(steps, &common.StepDownload{ + Checksum: b.config.Checksum, + ChecksumType: b.config.ChecksumType, + Description: "Box", + Extension: "box", + ResultKey: "box_path", + Url: []string{b.config.SourceBox}, + }) } + steps = append(steps, + &common.StepOutputDir{ + Force: b.config.PackerForce, + Path: b.config.OutputDir, + }, &StepInitializeVagrant{ - BoxName: b.config.SourceBoxName, BoxVersion: b.config.BoxVersion, Minimal: b.config.Minimal, Template: b.config.Template, SourceBox: b.config.SourceBox, OutputDir: b.config.OutputDir, + BoxName: b.config.BoxName, }, &StepAddBox{ BoxVersion: b.config.BoxVersion, @@ -173,7 +205,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Insecure: b.config.AddInsecure, Provider: b.config.Provider, SourceBox: b.config.SourceBox, - BoxName: b.config.SourceBoxName, + BoxName: b.config.BoxName, }, &StepUp{ b.config.TeardownMethod, diff --git a/builder/vagrant/step_add_box.go b/builder/vagrant/step_add_box.go index cdbfe3518..26ec1816c 100644 --- a/builder/vagrant/step_add_box.go +++ b/builder/vagrant/step_add_box.go @@ -2,6 +2,7 @@ package vagrant import ( "context" + "fmt" "log" "strings" @@ -25,12 +26,17 @@ type StepAddBox struct { func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(VagrantDriver) ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) ui.Say("Adding box using vagrant box add..") addArgs := []string{} if strings.HasSuffix(s.SourceBox, ".box") { // The box isn't a namespace like you'd pull from vagrant cloud + if s.BoxName == "" { + s.BoxName = fmt.Sprintf("packer_%s", config.PackerBuildName) + } + addArgs = append(addArgs, s.BoxName) } diff --git a/website/source/docs/builders/vagrant.html.md b/website/source/docs/builders/vagrant.html.md index f5d1829c0..a7c90dded 100644 --- a/website/source/docs/builders/vagrant.html.md +++ b/website/source/docs/builders/vagrant.html.md @@ -9,58 +9,70 @@ builder that you have already installed what you need. Required: -`source_box` (string) - URL of the vagrant box to use, or the name of the -vagrant box. For example, `hashicorp/precise64` or -`https://boxes.company.com/my-company.box` are valid source boxes. If using a URL like the latter example above, you will also need to provide a `box_name`. +- `source_box` (string) - URL of the vagrant box to use, or the name of the + vagrant box. `hashicorp/precise64`, `./mylocalbox.box` and + `https://boxes.company.com/my-company.box` are all valid source boxes. If your + source is a .box file, whether locally or from a URL like the latter example + above, you will also need to provide a `box_name`. Optional: -`output_dir` (string) - The directory to create that will contain -your output box. We always create this directory and run from inside of it to -prevent Vagrant init collisions. If unset, it will be set to `packer-` plus -your buildname. +- `output_dir` (string) - The directory to create that will contain + your output box. We always create this directory and run from inside of it to + prevent Vagrant init collisions. If unset, it will be set to `packer-` plus + your buildname. -`box_name` (string) - if your source\_box is a boxfile that we need to add to -Vagrant, this is the name to give it. +- `box_name` (string) - if your source\_box is a boxfile that we need to add + to Vagrant, this is the name to give it. If left blank, will default to + "packer_" plus your buildname. -`vagrantfile_template` (string) - a path to an ERB template to use for the -vagrantfile when calling `vagrant init`. See the blog post -[here](https://www.hashicorp.com/blog/hashicorp-vagrant-2-0-2#customized-vagrantfile-templates) -for some more details on how this works. Available variables are `box_name`, -`box_url`, and `box_version`. +- `checksum` (string) - The checksum for the .box file. The type of the + checksum is specified with `checksum_type`, documented below. -`teardown_method` (string) - Whether to halt, suspend, or destroy the box when -the build has completed. Defaults to "halt" +- `checksum_type` (string) - The type of the checksum specified in `checksum`. + Valid values are `none`, `md5`, `sha1`, `sha256`, or `sha512`. Although the + checksum will not be verified when `checksum_type` is set to "none", this is + not recommended since OVA files can be very large and corruption does happen + from time to time. -`box_version` (string) - What box version to use when initializing Vagrant. +- `vagrantfile_template` (string) - a path to an ERB template to use for the + vagrantfile when calling `vagrant init`. See the blog post + [here](https://www.hashicorp.com/blog/hashicorp-vagrant-2-0-2#customized-vagrantfile-templates) + for some more details on how this works. Available variables are `box_name`, + `box_url`, and `box_version`. -`init_minimal` (bool) - If true, will add the --minimal flag to the Vagrant -init command, creating a minimal vagrantfile instead of one filled with helpful -comments. +- `teardown_method` (string) - Whether to halt, suspend, or destroy the box when + the build has completed. Defaults to "halt" -`add_cacert` (string) - Equivalent to setting the -[`--cacert`](https://www.vagrantup.com/docs/cli/box.html#cacert-certfile) -option in `vagrant add`; defaults to unset. +- `box_version` (string) - What box version to use when initializing Vagrant. -`add_capath` (string) - Equivalent to setting the -[`--capath`](https://www.vagrantup.com/docs/cli/box.html#capath-certdir) option -in `vagrant add`; defaults to unset. +- `init_minimal` (bool) - If true, will add the --minimal flag to the Vagrant + init command, creating a minimal vagrantfile instead of one filled with helpful + comments. -`add_cert` (string) - Equivalent to setting the -[`--cert`](https://www.vagrantup.com/docs/cli/box.html#cert-certfile) option in -`vagrant add`; defaults to unset. +- `add_cacert` (string) - Equivalent to setting the + [`--cacert`](https://www.vagrantup.com/docs/cli/box.html#cacert-certfile) + option in `vagrant add`; defaults to unset. -`add_clean` (bool) - Equivalent to setting the -[`--clean`](https://www.vagrantup.com/docs/cli/box.html#clean) flag in -`vagrant add`; defaults to unset. +- `add_capath` (string) - Equivalent to setting the + [`--capath`](https://www.vagrantup.com/docs/cli/box.html#capath-certdir) option + in `vagrant add`; defaults to unset. -`add_force` (bool) - Equivalent to setting the -[`--force`](https://www.vagrantup.com/docs/cli/box.html#force) flag in -`vagrant add`; defaults to unset. +- `add_cert` (string) - Equivalent to setting the + [`--cert`](https://www.vagrantup.com/docs/cli/box.html#cert-certfile) option in + `vagrant add`; defaults to unset. -`add_insecure` (bool) - Equivalent to setting the -[`--force`](https://www.vagrantup.com/docs/cli/box.html#insecure) flag in -`vagrant add`; defaults to unset. +- `add_clean` (bool) - Equivalent to setting the + [`--clean`](https://www.vagrantup.com/docs/cli/box.html#clean) flag in + `vagrant add`; defaults to unset. -`skip_package` (bool) - if true, Packer will not call `vagrant package` to -package your base box into its own standalone .box file. +- `add_force` (bool) - Equivalent to setting the + [`--force`](https://www.vagrantup.com/docs/cli/box.html#force) flag in + `vagrant add`; defaults to unset. + +- `add_insecure` (bool) - Equivalent to setting the + [`--force`](https://www.vagrantup.com/docs/cli/box.html#insecure) flag in + `vagrant add`; defaults to unset. + +- `skip_package` (bool) - if true, Packer will not call `vagrant package` to + package your base box into its own standalone .box file. From af7131b1698b78c29dc1a7c6bbc834c7b484881e Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 25 Jan 2019 12:32:44 -0800 Subject: [PATCH 07/48] tests, logic cleanup, docs for vagrant builder --- builder/vagrant/artifact.go | 2 +- builder/vagrant/artifact_test.go | 40 ++++++ builder/vagrant/builder.go | 39 ++++-- builder/vagrant/builder_test.go | 91 ++++++++++++++ builder/vagrant/driver.go | 22 +--- builder/vagrant/driver_2_2.go | 52 ++++++-- builder/vagrant/step_add_box.go | 28 +++-- builder/vagrant/step_add_box_test.go | 62 ++++++++++ builder/vagrant/step_initialize_vagrant.go | 31 +++-- .../vagrant/step_initialize_vagrant_test.go | 52 +++++++- builder/vagrant/step_package.go | 5 + builder/vagrant/step_ssh_config.go | 6 +- builder/vagrant/step_up.go | 11 +- website/source/docs/builders/amazon.html.md | 2 +- .../source/docs/builders/digitalocean.html.md | 6 +- website/source/docs/builders/docker.html.md | 12 +- .../docs/builders/googlecompute.html.md | 3 +- .../source/docs/builders/oneandone.html.md | 2 +- .../source/docs/builders/profitbricks.html.md | 2 +- website/source/docs/builders/scaleway.html.md | 2 +- website/source/docs/builders/vagrant.html.md | 115 +++++++++++------- .../post-processors/alicloud-import.html.md | 11 +- .../docs/post-processors/shell-local.html.md | 2 +- .../docs/provisioners/shell-local.html.md | 2 +- .../docs/templates/communicator.html.md | 6 +- 25 files changed, 472 insertions(+), 134 deletions(-) create mode 100644 builder/vagrant/artifact_test.go create mode 100644 builder/vagrant/builder_test.go create mode 100644 builder/vagrant/step_add_box_test.go diff --git a/builder/vagrant/artifact.go b/builder/vagrant/artifact.go index ec5ed3282..8a3c053b1 100644 --- a/builder/vagrant/artifact.go +++ b/builder/vagrant/artifact.go @@ -38,7 +38,7 @@ func (a *artifact) Id() string { } func (a *artifact) String() string { - return fmt.Sprintf("Vagrant box is %s", a.Id()) + return fmt.Sprintf("Vagrant box is %s", a.Id()) } func (a *artifact) State(name string) interface{} { diff --git a/builder/vagrant/artifact_test.go b/builder/vagrant/artifact_test.go new file mode 100644 index 000000000..794f5b1da --- /dev/null +++ b/builder/vagrant/artifact_test.go @@ -0,0 +1,40 @@ +package vagrant + +import ( + "strings" + "testing" + + "github.com/hashicorp/packer/packer" +) + +func TestArtifact_Impl(t *testing.T) { + var raw interface{} = &artifact{} + + if _, ok := raw.(packer.Artifact); !ok { + t.Fatalf("Artifact does not implement packer.Artifact") + } +} + +func TestArtifactId(t *testing.T) { + a := &artifact{ + OutputDir: "/my/dir", + BoxName: "package.box", + } + + expected := "/my/dir/package.box" + if strings.Compare(a.Id(), expected) != 0 { + t.Fatalf("artifact ID should match: expected: %s received: %s", expected, a.Id()) + } +} + +func TestArtifactString(t *testing.T) { + a := &artifact{ + OutputDir: "/my/dir", + BoxName: "package.box", + } + expected := "Vagrant box is /my/dir/package.box" + + if strings.Compare(a.String(), expected) != 0 { + t.Fatalf("artifact string should match: expected: %s received: %s", expected, a.String()) + } +} diff --git a/builder/vagrant/builder.go b/builder/vagrant/builder.go index 959ed277a..b44f4a56d 100644 --- a/builder/vagrant/builder.go +++ b/builder/vagrant/builder.go @@ -38,7 +38,8 @@ type Config struct { // This is the name of the new virtual machine. // By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build. OutputDir string `mapstructure:"output_dir"` - SourceBox string `mapstructure:"source_box"` + SourceBox string `mapstructure:"source_path"` + GlobalID string `mapstructure:"global_id"` Checksum string `mapstructure:"checksum"` ChecksumType string `mapstructure:"checksum_type"` BoxName string `mapstructure:"box_name"` @@ -60,6 +61,7 @@ type Config struct { SyncedFolder string `mapstructure:"synced_folder"` // Options for the "vagrant box add" command + SkipAdd bool `mapstructure:"skip_add"` AddCACert string `mapstructure:"add_cacert"` AddCAPath string `mapstructure:"add_capath"` AddCert string `mapstructure:"add_cert"` @@ -113,23 +115,35 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } if b.config.SourceBox == "" { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required")) + if b.config.GlobalID == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required unless you have set global_id")) + } } else { - if !strings.HasSuffix(b.config.SourceBox, ".box") { + if b.config.GlobalID != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("You may either set global_id or source_path but not both")) + } + if strings.HasSuffix(b.config.SourceBox, ".box") { b.config.SourceBox, err = common.ValidatedURL(b.config.SourceBox) if err != nil { errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is invalid: %s", err)) } fileOK := common.FileExistsLocally(b.config.SourceBox) if !fileOK { - packer.MultiErrorAppend(errs, + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Source file '%s' needs to exist at time of config validation!", b.config.SourceBox)) } } } if b.config.TeardownMethod == "" { - b.config.TeardownMethod = "destroy" + // If we're using a box that's already opened on the system, don't + // automatically destroy it. If we open the box ourselves, then go ahead + // and kill it by default. + if b.config.GlobalID != "" { + b.config.TeardownMethod = "halt" + } else { + b.config.TeardownMethod = "destroy" + } } else { matches := false for _, name := range []string{"halt", "suspend", "destroy"} { @@ -171,7 +185,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps. steps := []multistep.Step{} // Download if source box isn't from vagrant cloud. - if !strings.HasSuffix(b.config.SourceBox, ".box") { + if strings.HasSuffix(b.config.SourceBox, ".box") { steps = append(steps, &common.StepDownload{ Checksum: b.config.Checksum, ChecksumType: b.config.ChecksumType, @@ -181,7 +195,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Url: []string{b.config.SourceBox}, }) } - steps = append(steps, &common.StepOutputDir{ Force: b.config.PackerForce, @@ -194,6 +207,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe SourceBox: b.config.SourceBox, OutputDir: b.config.OutputDir, BoxName: b.config.BoxName, + GlobalID: b.config.GlobalID, }, &StepAddBox{ BoxVersion: b.config.BoxVersion, @@ -206,12 +220,16 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Provider: b.config.Provider, SourceBox: b.config.SourceBox, BoxName: b.config.BoxName, + GlobalID: b.config.GlobalID, }, &StepUp{ - b.config.TeardownMethod, - b.config.Provider, + TeardownMethod: b.config.TeardownMethod, + Provider: b.config.Provider, + GlobalID: b.config.GlobalID, + }, + &StepSSHConfig{ + b.config.GlobalID, }, - &StepSSHConfig{}, &communicator.StepConnect{ Config: &b.config.SSHConfig.Comm, Host: CommHost(), @@ -222,6 +240,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe SkipPackage: b.config.SkipPackage, Include: b.config.PackageInclude, Vagrantfile: b.config.OutputVagrantfile, + GlobalID: b.config.GlobalID, }) // Run the steps. diff --git a/builder/vagrant/builder_test.go b/builder/vagrant/builder_test.go new file mode 100644 index 000000000..65c2e5e9f --- /dev/null +++ b/builder/vagrant/builder_test.go @@ -0,0 +1,91 @@ +package vagrant + +import ( + "testing" + + "github.com/hashicorp/packer/packer" +) + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Fatalf("Builder should be a builder") + } +} + +func TestBuilder_Prepare_ValidateSource(t *testing.T) { + b := &Builder{} + type testCase struct { + config map[string]interface{} + errExpected bool + reason string + } + + cases := []testCase{ + { + config: map[string]interface{}{ + "global_id": "a3559ec", + }, + errExpected: true, + reason: "Need to set SSH communicator.", + }, + { + config: map[string]interface{}{ + "global_id": "a3559ec", + "communicator": "ssh", + }, + errExpected: false, + reason: "Shouldn't fail because we've set global_id", + }, + { + config: map[string]interface{}{ + "communicator": "ssh", + }, + errExpected: true, + reason: "Should fail because we must set source_path or global_id", + }, + { + config: map[string]interface{}{ + "source_path": "./mybox", + "communicator": "ssh", + }, + errExpected: false, + reason: "Source path is set; we should be fine", + }, + { + config: map[string]interface{}{ + "source_path": "./mybox", + "communicator": "ssh", + "global_id": "a3559ec", + }, + errExpected: true, + reason: "Both source path and global are set: we should error.", + }, + { + config: map[string]interface{}{ + "communicator": "ssh", + "global_id": "a3559ec", + "teardown_method": "suspend", + }, + errExpected: false, + reason: "Valid argument for teardown method", + }, + { + config: map[string]interface{}{ + "communicator": "ssh", + "global_id": "a3559ec", + "teardown_method": "surspernd", + }, + errExpected: true, + reason: "Inalid argument for teardown method", + }, + } + + for _, tc := range cases { + _, err := b.Prepare(tc.config) + if (err != nil) != tc.errExpected { + t.Fatalf("Unexpected behavior from test case %#v; %s.", tc.config, tc.reason) + } + } +} diff --git a/builder/vagrant/driver.go b/builder/vagrant/driver.go index 08c31b8c8..2318a0382 100644 --- a/builder/vagrant/driver.go +++ b/builder/vagrant/driver.go @@ -2,11 +2,8 @@ package vagrant import ( "fmt" - "os" "os/exec" - "path/filepath" "runtime" - "strings" ) // A driver is able to talk to Vagrant and perform certain @@ -23,15 +20,15 @@ type VagrantDriver interface { Up([]string) (string, string, error) // Calls "vagrant halt" - Halt() error + Halt(string) error // Calls "vagrant suspend" - Suspend() error + Suspend(string) error - SSHConfig() (*VagrantSSHConfig, error) + SSHConfig(string) (*VagrantSSHConfig, error) // Calls "vagrant destroy" - Destroy() error + Destroy(string) error // Calls "vagrant package"[ Package([]string) error @@ -67,14 +64,3 @@ func NewDriver() (VagrantDriver, error) { return driver, nil } - -func findVBoxManageWindows(paths string) string { - for _, path := range strings.Split(paths, ";") { - path = filepath.Join(path, "VBoxManage.exe") - if _, err := os.Stat(path); err == nil { - return path - } - } - - return "" -} diff --git a/builder/vagrant/driver_2_2.go b/builder/vagrant/driver_2_2.go index 13a87dd59..9886939b1 100644 --- a/builder/vagrant/driver_2_2.go +++ b/builder/vagrant/driver_2_2.go @@ -8,8 +8,12 @@ import ( "os/exec" "regexp" "strings" + + "github.com/hashicorp/go-version" ) +const VAGRANT_MIN_VERSION = ">= 2.0.2" + type Vagrant_2_2_Driver struct { vagrantBinary string } @@ -34,26 +38,38 @@ func (d *Vagrant_2_2_Driver) Up(args []string) (string, string, error) { } // Calls "vagrant halt" -func (d *Vagrant_2_2_Driver) Halt() error { - _, _, err := d.vagrantCmd([]string{"halt"}...) +func (d *Vagrant_2_2_Driver) Halt(id string) error { + args := []string{"halt"} + if id != "" { + args = append(args, id) + } + _, _, err := d.vagrantCmd(args...) return err } // Calls "vagrant suspend" -func (d *Vagrant_2_2_Driver) Suspend() error { - _, _, err := d.vagrantCmd([]string{"suspend"}...) +func (d *Vagrant_2_2_Driver) Suspend(id string) error { + args := []string{"suspend"} + if id != "" { + args = append(args, id) + } + _, _, err := d.vagrantCmd(args...) return err } // Calls "vagrant destroy" -func (d *Vagrant_2_2_Driver) Destroy() error { - _, _, err := d.vagrantCmd([]string{"destroy", "-f"}...) +func (d *Vagrant_2_2_Driver) Destroy(id string) error { + args := []string{"destroy", "-f"} + if id != "" { + args = append(args, id) + } + _, _, err := d.vagrantCmd(args...) return err } // Calls "vagrant package" func (d *Vagrant_2_2_Driver) Package(args []string) error { - _, _, err := d.vagrantCmd([]string{"package"}...) + _, _, err := d.vagrantCmd(append([]string{"package"}, args...)...) return err } @@ -67,6 +83,18 @@ func (d *Vagrant_2_2_Driver) Verify() error { if err != nil { return fmt.Errorf("Can't find Vagrant binary.") } + + constraints, err := version.NewConstraint(VAGRANT_MIN_VERSION) + vers, err := d.Version() + v, err := version.NewVersion(vers) + if err != nil { + return fmt.Errorf("Error figuring out Vagrant version.") + } + + if !constraints.Check(v) { + return fmt.Errorf("installed Vagrant version must be >=2.0.2") + } + return nil } @@ -99,9 +127,13 @@ func yesno(yn string) bool { return true } -func (d *Vagrant_2_2_Driver) SSHConfig() (*VagrantSSHConfig, error) { +func (d *Vagrant_2_2_Driver) SSHConfig(id string) (*VagrantSSHConfig, error) { // vagrant ssh-config --host 8df7860 - stdout, _, err := d.vagrantCmd([]string{"ssh-config"}...) + args := []string{"ssh-config"} + if id != "" { + args = append(args, id) + } + stdout, _, err := d.vagrantCmd(args...) sshConf := &VagrantSSHConfig{} lines := strings.Split(stdout, "\n") @@ -122,7 +154,7 @@ func (d *Vagrant_2_2_Driver) SSHConfig() (*VagrantSSHConfig, error) { // Version reads the version of VirtualBox that is installed. func (d *Vagrant_2_2_Driver) Version() (string, error) { - stdoutString, _, err := d.vagrantCmd([]string{"version"}...) + stdoutString, _, err := d.vagrantCmd([]string{"--version"}...) // Example stdout: // Installed Version: 2.2.3 diff --git a/builder/vagrant/step_add_box.go b/builder/vagrant/step_add_box.go index 26ec1816c..a1832969e 100644 --- a/builder/vagrant/step_add_box.go +++ b/builder/vagrant/step_add_box.go @@ -2,7 +2,6 @@ package vagrant import ( "context" - "fmt" "log" "strings" @@ -21,22 +20,14 @@ type StepAddBox struct { Provider string SourceBox string BoxName string + GlobalID string } -func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { - driver := state.Get("driver").(VagrantDriver) - ui := state.Get("ui").(packer.Ui) - config := state.Get("config").(*Config) +func (s *StepAddBox) generateAddArgs() []string { - ui.Say("Adding box using vagrant box add..") addArgs := []string{} if strings.HasSuffix(s.SourceBox, ".box") { - // The box isn't a namespace like you'd pull from vagrant cloud - if s.BoxName == "" { - s.BoxName = fmt.Sprintf("packer_%s", config.PackerBuildName) - } - addArgs = append(addArgs, s.BoxName) } @@ -74,6 +65,21 @@ func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep. addArgs = append(addArgs, "--provider", s.Provider) } + return addArgs +} + +func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(VagrantDriver) + ui := state.Get("ui").(packer.Ui) + + if s.GlobalID != "" { + ui.Say("Using a global-id; skipping Vagrant add command...") + return multistep.ActionContinue + } + + ui.Say("Adding box using vagrant box add..") + addArgs := s.generateAddArgs() + log.Printf("[vagrant] Calling box add with following args %s", strings.Join(addArgs, " ")) // Call vagrant using prepared arguments err := driver.Add(addArgs) diff --git a/builder/vagrant/step_add_box_test.go b/builder/vagrant/step_add_box_test.go new file mode 100644 index 000000000..29feca5d1 --- /dev/null +++ b/builder/vagrant/step_add_box_test.go @@ -0,0 +1,62 @@ +package vagrant + +import ( + "strings" + "testing" + + "github.com/hashicorp/packer/helper/multistep" +) + +func TestStepAdd_Impl(t *testing.T) { + var raw interface{} + raw = new(StepAddBox) + if _, ok := raw.(multistep.Step); !ok { + t.Fatalf("initialize should be a step") + } +} + +func TestPrepAddArgs(t *testing.T) { + type testArgs struct { + Step StepAddBox + Expected []string + } + addTests := []testArgs{ + { + Step: StepAddBox{ + SourceBox: "my_source_box.box", + BoxName: "AWESOME BOX", + }, + Expected: []string{"AWESOME BOX", "my_source_box.box"}, + }, + { + Step: StepAddBox{ + SourceBox: "my_source_box", + BoxName: "AWESOME BOX", + }, + Expected: []string{"my_source_box"}, + }, + { + Step: StepAddBox{ + BoxVersion: "eleventyone", + CACert: "adfasdf", + CAPath: "adfasdf", + DownloadCert: "adfasdf", + Clean: true, + Force: true, + Insecure: true, + Provider: "virtualbox", + SourceBox: "bananabox.box", + BoxName: "bananas", + }, + Expected: []string{"bananas", "bananabox.box", "--box-version", "eleventyone", "--cacert", "adfasdf", "--capath", "adfasdf", "--cert", "adfasdf", "--clean", "--force", "--insecure", "--provider", "virtualbox"}, + }, + } + for _, addTest := range addTests { + addArgs := addTest.Step.generateAddArgs() + for i, val := range addTest.Expected { + if strings.Compare(addArgs[i], val) != 0 { + t.Fatalf("expected %#v but received %#v", addTest.Expected, addArgs) + } + } + } +} diff --git a/builder/vagrant/step_initialize_vagrant.go b/builder/vagrant/step_initialize_vagrant.go index 0f6357a48..43a824090 100644 --- a/builder/vagrant/step_initialize_vagrant.go +++ b/builder/vagrant/step_initialize_vagrant.go @@ -19,6 +19,7 @@ type StepInitializeVagrant struct { SourceBox string OutputDir string SyncedFolder string + GlobalID string } var DEFAULT_TEMPLATE = `Vagrant.configure("2") do |config| @@ -73,12 +74,8 @@ func (s *StepInitializeVagrant) createInitializeCommand() (string, error) { return abspath, nil } -func (s *StepInitializeVagrant) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { - driver := state.Get("driver").(VagrantDriver) - ui := state.Get("ui").(packer.Ui) - - ui.Say("Initializing Vagrant in build directory...") +func (s *StepInitializeVagrant) prepInitArgs() ([]string, error) { // Prepare arguments initArgs := []string{} @@ -98,12 +95,32 @@ func (s *StepInitializeVagrant) Run(_ context.Context, state multistep.StateBag) tplPath, err := s.createInitializeCommand() if err != nil { - state.Put("error", err) - return multistep.ActionHalt + return initArgs, err } initArgs = append(initArgs, "--template", tplPath) + return initArgs, nil +} + +func (s *StepInitializeVagrant) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(VagrantDriver) + ui := state.Get("ui").(packer.Ui) + + // Skip the initialize step if we're trying to launch from a global ID. + if s.GlobalID != "" { + ui.Say("Using a global-id; skipping Vagrant init in this directory...") + return multistep.ActionContinue + } + + ui.Say("Initializing Vagrant in build directory...") + + initArgs, err := s.prepInitArgs() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + os.Chdir(s.OutputDir) // Call vagrant using prepared arguments err = driver.Init(initArgs) diff --git a/builder/vagrant/step_initialize_vagrant_test.go b/builder/vagrant/step_initialize_vagrant_test.go index 158ec34af..9f91b0631 100644 --- a/builder/vagrant/step_initialize_vagrant_test.go +++ b/builder/vagrant/step_initialize_vagrant_test.go @@ -44,6 +44,7 @@ func TestCreateFile_customSync(t *testing.T) { SyncedFolder: "myfolder/foldertimes", } templatePath, err := testy.createInitializeCommand() + defer os.Remove(templatePath) if err != nil { t.Fatalf(err.Error()) } @@ -56,5 +57,54 @@ end` if ok := strings.Compare(actual, expected); ok != 0 { t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual) } - os.Remove(templatePath) +} + +func TestPrepInitArgs(t *testing.T) { + type testArgs struct { + Step StepInitializeVagrant + Expected []string + } + initTests := []testArgs{ + { + Step: StepInitializeVagrant{ + SourceBox: "my_source_box.box", + }, + Expected: []string{"my_source_box.box", "--template"}, + }, + { + Step: StepInitializeVagrant{ + SourceBox: "my_source_box", + BoxName: "My Box", + }, + Expected: []string{"My Box", "my_source_box", "--template"}, + }, + { + Step: StepInitializeVagrant{ + SourceBox: "my_source_box", + BoxName: "My Box", + BoxVersion: "42", + }, + Expected: []string{"My Box", "my_source_box", "--box-version", "42", "--template"}, + }, + { + Step: StepInitializeVagrant{ + SourceBox: "my_source_box", + BoxName: "My Box", + Minimal: true, + }, + Expected: []string{"My Box", "my_source_box", "-m", "--template"}, + }, + } + for _, initTest := range initTests { + initArgs, err := initTest.Step.prepInitArgs() + defer os.Remove(initArgs[len(initArgs)-1]) + if err != nil { + t.Fatalf(err.Error()) + } + for i, val := range initTest.Expected { + if strings.Compare(initArgs[i], val) != 0 { + t.Fatalf("expected %#v but received %#v", initTest.Expected, initArgs[:len(initArgs)-1]) + } + } + } } diff --git a/builder/vagrant/step_package.go b/builder/vagrant/step_package.go index d61b5c1ce..8200296b4 100644 --- a/builder/vagrant/step_package.go +++ b/builder/vagrant/step_package.go @@ -12,6 +12,7 @@ type StepPackage struct { SkipPackage bool Include []string Vagrantfile string + GlobalID string } func (s *StepPackage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { @@ -24,6 +25,10 @@ func (s *StepPackage) Run(_ context.Context, state multistep.StateBag) multistep } ui.Say("Packaging box...") packageArgs := []string{} + if s.GlobalID != "" { + packageArgs = append(packageArgs, s.GlobalID) + } + if len(s.Include) > 0 { packageArgs = append(packageArgs, "--include", strings.Join(s.Include, ",")) } diff --git a/builder/vagrant/step_ssh_config.go b/builder/vagrant/step_ssh_config.go index 175224727..68fc13aa3 100644 --- a/builder/vagrant/step_ssh_config.go +++ b/builder/vagrant/step_ssh_config.go @@ -22,13 +22,15 @@ import ( // IdentitiesOnly yes // LogLevel FATAL -type StepSSHConfig struct{} +type StepSSHConfig struct { + GlobalID string +} func (s *StepSSHConfig) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(VagrantDriver) config := state.Get("config").(*Config) - sshConfig, err := driver.SSHConfig() + sshConfig, err := driver.SSHConfig(s.GlobalID) if err != nil { state.Put("error", err) return multistep.ActionHalt diff --git a/builder/vagrant/step_up.go b/builder/vagrant/step_up.go index 93aad8b63..6f2584c5f 100644 --- a/builder/vagrant/step_up.go +++ b/builder/vagrant/step_up.go @@ -11,6 +11,7 @@ import ( type StepUp struct { TeardownMethod string Provider string + GlobalID string } func (s *StepUp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { @@ -20,6 +21,10 @@ func (s *StepUp) Run(_ context.Context, state multistep.StateBag) multistep.Step ui.Say("Calling Vagrant Up...") var args []string + if s.GlobalID != "" { + args = append(args, s.GlobalID) + } + if s.Provider != "" { args = append(args, fmt.Sprintf("--provider=%s", s.Provider)) } @@ -42,11 +47,11 @@ func (s *StepUp) Cleanup(state multistep.StateBag) { var err error if s.TeardownMethod == "halt" { - err = driver.Halt() + err = driver.Halt(s.GlobalID) } else if s.TeardownMethod == "suspend" { - err = driver.Suspend() + err = driver.Suspend(s.GlobalID) } else if s.TeardownMethod == "destroy" { - err = driver.Destroy() + err = driver.Destroy(s.GlobalID) } else { // Should never get here because of template validation state.Put("error", fmt.Errorf("Invalid teardown method selected; must be either halt, suspend, or destory.")) diff --git a/website/source/docs/builders/amazon.html.md b/website/source/docs/builders/amazon.html.md index f722cd7d3..9e3fb1ba9 100644 --- a/website/source/docs/builders/amazon.html.md +++ b/website/source/docs/builders/amazon.html.md @@ -218,7 +218,7 @@ fail. If that's the case, you might see an error like this: ==> amazon-ebs: Error querying AMI: AuthFailure: AWS was not able to validate the provided access credentials If you suspect your system's date is wrong, you can compare it against -http://www.time.gov/. On +. On Linux/OS X, you can run the `date` command to get the current time. If you're on Linux, you can try setting the time with ntp by running `sudo ntpd -q`. diff --git a/website/source/docs/builders/digitalocean.html.md b/website/source/docs/builders/digitalocean.html.md index b7a4f298d..1d382869d 100644 --- a/website/source/docs/builders/digitalocean.html.md +++ b/website/source/docs/builders/digitalocean.html.md @@ -41,17 +41,17 @@ builder. - `image` (string) - The name (or slug) of the base image to use. This is the image that will be used to launch a new droplet and provision it. See - https://developers.digitalocean.com/documentation/v2/\#list-all-images + https://developers.digitalocean.com/documentation/v2/#list-all-images for details on how to get a list of the accepted image names/slugs. - `region` (string) - The name (or slug) of the region to launch the droplet in. Consequently, this is the region where the snapshot will be available. See - https://developers.digitalocean.com/documentation/v2/\#list-all-regions + https://developers.digitalocean.com/documentation/v2/#list-all-regions for the accepted region names/slugs. - `size` (string) - The name (or slug) of the droplet size to use. See - https://developers.digitalocean.com/documentation/v2/\#list-all-sizes + https://developers.digitalocean.com/documentation/v2/#list-all-sizes for the accepted size names/slugs. ### Optional: diff --git a/website/source/docs/builders/docker.html.md b/website/source/docs/builders/docker.html.md index e5e1879ba..a0dddfc78 100644 --- a/website/source/docs/builders/docker.html.md +++ b/website/source/docs/builders/docker.html.md @@ -184,10 +184,10 @@ You must specify (only) one of `commit`, `discard`, or `export_path`. `login_username`, and `login_password` will be ignored. For more information see the [section on ECR](#amazon-ec2-container-registry). -- `exec_user` (string) - Username or UID (format: - <name\\\\\|uid>\[:<group\\\\\|gid>\]) to run remote commands - with. You may need this if you get permission errors trying to run the - `shell` or other provisioners. +- `exec_user` (string) - Username (UID) to run remote commands with. You can + also set the group name/ID if you want: (UID or UID:GID). + You may need this if you get permission errors trying to run the `shell` or + other provisioners. - `login` (boolean) - Defaults to false. If true, the builder will login in order to pull the image. The builder only logs in for the duration of the @@ -380,8 +380,8 @@ portable provisioning scripts. ## Overriding the host directory -By default, Packer creates a temporary folder under your home directory, and -uses that to stage files for uploading into the container. If you would like to +By default, Packer creates a temporary folder under your home directory, and +uses that to stage files for uploading into the container. If you would like to change the path to this temporary folder, you can set the `PACKER_TMP_DIR`. This can be useful, for example, if you have your home directory permissions set up to disallow access from the docker daemon. diff --git a/website/source/docs/builders/googlecompute.html.md b/website/source/docs/builders/googlecompute.html.md index 24a83b05f..681fad15e 100644 --- a/website/source/docs/builders/googlecompute.html.md +++ b/website/source/docs/builders/googlecompute.html.md @@ -18,8 +18,7 @@ based on existing images. It is possible to build images from scratch, but not with the `googlecompute` Packer builder. The process is recommended only for advanced users, please see -\[Building GCE Images from Scratch\] -(https://cloud.google.com/compute/docs/tutorials/building-images) +[Building GCE Images from Scratch](https://cloud.google.com/compute/docs/tutorials/building-images) and the [Google Compute Import Post-Processor](/docs/post-processors/googlecompute-import.html) for more information. diff --git a/website/source/docs/builders/oneandone.html.md b/website/source/docs/builders/oneandone.html.md index 5b9007823..dca56dc85 100644 --- a/website/source/docs/builders/oneandone.html.md +++ b/website/source/docs/builders/oneandone.html.md @@ -44,7 +44,7 @@ builder. while waiting for the build to complete. Default value "600". - `url` (string) - Endpoint for the 1&1 REST API. Default URL - "https://cloudpanel-api.1and1.com/v1" + "" ## Example diff --git a/website/source/docs/builders/profitbricks.html.md b/website/source/docs/builders/profitbricks.html.md index 3762a9d97..79fdb2a84 100644 --- a/website/source/docs/builders/profitbricks.html.md +++ b/website/source/docs/builders/profitbricks.html.md @@ -60,7 +60,7 @@ builder. - `snapshot_password` (string) - Password for the snapshot. - `url` (string) - Endpoint for the ProfitBricks REST API. Default URL - "https://api.profitbricks.com/rest/v2" + "" ## Example diff --git a/website/source/docs/builders/scaleway.html.md b/website/source/docs/builders/scaleway.html.md index f22043e0d..4678b4222 100644 --- a/website/source/docs/builders/scaleway.html.md +++ b/website/source/docs/builders/scaleway.html.md @@ -48,7 +48,7 @@ builder. - `image` (string) - The UUID of the base image to use. This is the image that will be used to launch a new server and provision it. See - https://api-marketplace.scaleway.com/images + [the images list](https://api-marketplace.scaleway.com/images) get the complete list of the accepted image UUID. - `region` (string) - The name of the region to launch the server in (`par1` diff --git a/website/source/docs/builders/vagrant.html.md b/website/source/docs/builders/vagrant.html.md index a7c90dded..2782975d0 100644 --- a/website/source/docs/builders/vagrant.html.md +++ b/website/source/docs/builders/vagrant.html.md @@ -1,30 +1,53 @@ The Vagrant builder is intended for building new boxes from already-existing boxes. Your source should be a URL or path to a .box file or a Vagrant Cloud -box name such as `hashicorp/precise64`. This builder is not currently intended -to work with an already-set-up Vagrant environment. +box name such as `hashicorp/precise64`. Packer will not install vagrant, nor will it install the underlying virtualization platforms or extra providers; We expect when you run this builder that you have already installed what you need. +By default, this builder will initialize a new Vagrant workspace, launch your +box from that workspace, provision it, call `vagrant package` to package it +into a new box, and then destroy the original box. Please note that vagrant +will _not_ remove the box file from your system (we don't call +`vagrant box remove`). + +You can change the behavior so that the builder doesn't destroy the box by +setting the `teardown_method` option. You can change the behavior so the builder +doesn't package it (not all provisioners support the `vagrant package` command) +by setting the `skip package` option. You can also change the behavior so that +rather than inititalizing a new Vagrant workspace, you use an already defined +one, by using `global_id` instead of `source_box`. + Required: -- `source_box` (string) - URL of the vagrant box to use, or the name of the - vagrant box. `hashicorp/precise64`, `./mylocalbox.box` and - `https://boxes.company.com/my-company.box` are all valid source boxes. If your - source is a .box file, whether locally or from a URL like the latter example - above, you will also need to provide a `box_name`. +- `source_box` (string) - URL of the vagrant box to use, or the name of the + vagrant box. `hashicorp/precise64`, `./mylocalbox.box` and + `https://example.com/my-box.box` are all valid source boxes. If your + source is a .box file, whether locally or from a URL like the latter example + above, you will also need to provide a `box_name`. This option is required, + unless you set `global_id`. You may only set one or the other, not both. + + or + +- `global_id` (string) - the global id of a Vagrant box already added to Vagrant + on your system. You can find the global id of your Vagrant boxes using the + command `vagrant global-status`; your global_id will be a 7-digit number and + letter comination that you'll find in the leftmost column of the + global-status output. If you choose to use `global_id` instead of + `source_box`, Packer will skip the Vagrant initialize and add steps, and + simply launch the box directly using the global id. Optional: -- `output_dir` (string) - The directory to create that will contain - your output box. We always create this directory and run from inside of it to - prevent Vagrant init collisions. If unset, it will be set to `packer-` plus - your buildname. +- `output_dir` (string) - The directory to create that will contain + your output box. We always create this directory and run from inside of it to + prevent Vagrant init collisions. If unset, it will be set to `packer-` plus + your buildname. - `box_name` (string) - if your source\_box is a boxfile that we need to add - to Vagrant, this is the name to give it. If left blank, will default to - "packer_" plus your buildname. + to Vagrant, this is the name to give it. If left blank, will default to + "packer_" plus your buildname. - `checksum` (string) - The checksum for the .box file. The type of the checksum is specified with `checksum_type`, documented below. @@ -35,44 +58,48 @@ Optional: not recommended since OVA files can be very large and corruption does happen from time to time. -- `vagrantfile_template` (string) - a path to an ERB template to use for the - vagrantfile when calling `vagrant init`. See the blog post - [here](https://www.hashicorp.com/blog/hashicorp-vagrant-2-0-2#customized-vagrantfile-templates) - for some more details on how this works. Available variables are `box_name`, - `box_url`, and `box_version`. +- `vagrantfile_template` (string) - a path to an ERB template to use for the + vagrantfile when calling `vagrant init`. See the blog post + [here](https://www.hashicorp.com/blog/hashicorp-vagrant-2-0-2#customized-vagrantfile-templates) + for some more details on how this works. Available variables are `box_name`, + `box_url`, and `box_version`. -- `teardown_method` (string) - Whether to halt, suspend, or destroy the box when - the build has completed. Defaults to "halt" +- `skip_add` (string) - Don't call "vagrant add" to add the box to your local + environment; this is necesasry if you want to launch a box that is already + added to your vagrant environment. -- `box_version` (string) - What box version to use when initializing Vagrant. +- `teardown_method` (string) - Whether to halt, suspend, or destroy the box when + the build has completed. Defaults to "halt" -- `init_minimal` (bool) - If true, will add the --minimal flag to the Vagrant - init command, creating a minimal vagrantfile instead of one filled with helpful - comments. +- `box_version` (string) - What box version to use when initializing Vagrant. -- `add_cacert` (string) - Equivalent to setting the - [`--cacert`](https://www.vagrantup.com/docs/cli/box.html#cacert-certfile) - option in `vagrant add`; defaults to unset. +- `init_minimal` (bool) - If true, will add the --minimal flag to the Vagrant + init command, creating a minimal vagrantfile instead of one filled with helpful + comments. -- `add_capath` (string) - Equivalent to setting the - [`--capath`](https://www.vagrantup.com/docs/cli/box.html#capath-certdir) option - in `vagrant add`; defaults to unset. +- `add_cacert` (string) - Equivalent to setting the + [`--cacert`](https://www.vagrantup.com/docs/cli/box.html#cacert-certfile) + option in `vagrant add`; defaults to unset. -- `add_cert` (string) - Equivalent to setting the - [`--cert`](https://www.vagrantup.com/docs/cli/box.html#cert-certfile) option in - `vagrant add`; defaults to unset. +- `add_capath` (string) - Equivalent to setting the + [`--capath`](https://www.vagrantup.com/docs/cli/box.html#capath-certdir) option + in `vagrant add`; defaults to unset. -- `add_clean` (bool) - Equivalent to setting the - [`--clean`](https://www.vagrantup.com/docs/cli/box.html#clean) flag in - `vagrant add`; defaults to unset. +- `add_cert` (string) - Equivalent to setting the + [`--cert`](https://www.vagrantup.com/docs/cli/box.html#cert-certfile) option in + `vagrant add`; defaults to unset. -- `add_force` (bool) - Equivalent to setting the - [`--force`](https://www.vagrantup.com/docs/cli/box.html#force) flag in - `vagrant add`; defaults to unset. +- `add_clean` (bool) - Equivalent to setting the + [`--clean`](https://www.vagrantup.com/docs/cli/box.html#clean) flag in + `vagrant add`; defaults to unset. -- `add_insecure` (bool) - Equivalent to setting the - [`--force`](https://www.vagrantup.com/docs/cli/box.html#insecure) flag in - `vagrant add`; defaults to unset. +- `add_force` (bool) - Equivalent to setting the + [`--force`](https://www.vagrantup.com/docs/cli/box.html#force) flag in + `vagrant add`; defaults to unset. -- `skip_package` (bool) - if true, Packer will not call `vagrant package` to - package your base box into its own standalone .box file. +- `add_insecure` (bool) - Equivalent to setting the + [`--force`](https://www.vagrantup.com/docs/cli/box.html#insecure) flag in + `vagrant add`; defaults to unset. + +- `skip_package` (bool) - if true, Packer will not call `vagrant package` to + package your base box into its own standalone .box file. diff --git a/website/source/docs/post-processors/alicloud-import.html.md b/website/source/docs/post-processors/alicloud-import.html.md index 56d68f501..7f26c42d7 100644 --- a/website/source/docs/post-processors/alicloud-import.html.md +++ b/website/source/docs/post-processors/alicloud-import.html.md @@ -42,8 +42,7 @@ are two categories: required and optional parameters. - `image_name` (string) - The name of the user-defined image, \[2, 128\] English or Chinese characters. It must begin with an uppercase/lowercase letter or a Chinese character, and may contain numbers, `_` or `-`. It - cannot begin with http:// or - https://. + cannot begin with `http://` or `https://` - `oss_bucket_name` (string) - The name of the OSS bucket where the RAW or VHD file will be copied to for import. If the Bucket isn't exist, @@ -53,8 +52,8 @@ are two categories: required and optional parameters. - `image_platform` (string) - platform such `CentOS` -- `image_architecture` (string) - Platform type of the image system:i386 \\\| - x86\_64 +- `image_architecture` (string) - Platform type of the image system: `i386` or + `x86_64` - `format` (string) - The format of the image for import, now alicloud only support RAW and VHD. @@ -71,9 +70,7 @@ are two categories: required and optional parameters. - `image_description` (string) - The description of the image, with a length limit of 0 to 256 characters. Leaving it blank means null, which is the - default value. It cannot begin with - http:// or - https://. + default value. It cannot begin with `http://` or `https://`. - `image_force_delete` (boolean) - If this value is true, when the target image name is duplicated with an existing image, it will delete the diff --git a/website/source/docs/post-processors/shell-local.html.md b/website/source/docs/post-processors/shell-local.html.md index 517c094bc..7599febf2 100644 --- a/website/source/docs/post-processors/shell-local.html.md +++ b/website/source/docs/post-processors/shell-local.html.md @@ -63,7 +63,7 @@ Optional parameters: - `env_var_format` (string) - When we parse the environment\_vars that you provide, this gives us a string template to use in order to make sure that we are setting the environment vars correctly. By default on Windows hosts - this format is `set %s=%s &&` and on Unix, it is `%s='%s'`. You probably + this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably won't need to change this format, but you can see usage examples for where it is necessary below. diff --git a/website/source/docs/provisioners/shell-local.html.md b/website/source/docs/provisioners/shell-local.html.md index 95f099d92..3dc81fe66 100644 --- a/website/source/docs/provisioners/shell-local.html.md +++ b/website/source/docs/provisioners/shell-local.html.md @@ -77,7 +77,7 @@ Optional parameters: - `env_var_format` (string) - When we parse the environment\_vars that you provide, this gives us a string template to use in order to make sure that we are setting the environment vars correctly. By default on Windows hosts - this format is `set %s=%s &&` and on Unix, it is `%s='%s'`. You probably + this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably won't need to change this format, but you can see usage examples for where it is necessary below. diff --git a/website/source/docs/templates/communicator.html.md b/website/source/docs/templates/communicator.html.md index 86267d225..c8054f623 100644 --- a/website/source/docs/templates/communicator.html.md +++ b/website/source/docs/templates/communicator.html.md @@ -155,15 +155,15 @@ Packer supports the following ciphers: - arcfour128 - arcfour256 - arcfour -- -- +- `es128-gcm@openssh.com` +- `acha20-poly1305@openssh.com` And the following MACs: - hmac-sha1 - hmac-sha1-96 - hmac-sha2-256 -- +- `hmac-sha2-256-etm@openssh.com` ## WinRM Communicator From 6b41a1663a392af3fb7d7851e6e11f49bd5ea0c4 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 4 Feb 2019 15:13:20 -0800 Subject: [PATCH 08/48] updated the amazon ebs docs to include vault integration --- .../source/docs/builders/amazon-ebs.html.md | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md index 35e0112c9..00a19d77c 100644 --- a/website/source/docs/builders/amazon-ebs.html.md +++ b/website/source/docs/builders/amazon-ebs.html.md @@ -46,7 +46,8 @@ builder. ### Required: - `access_key` (string) - The access key used to communicate with AWS. [Learn - how to set this](amazon.html#specifying-amazon-credentials) + how to set this](amazon.html#specifying-amazon-credentials). This is not + required if you are using `use_vault_aws_engine` for authentication instead. - `ami_name` (string) - The name of the resulting AMI that will appear when managing AMIs in the AWS console or via APIs. This must be unique. To help @@ -60,7 +61,8 @@ builder. to launch the EC2 instance to create the AMI. - `secret_key` (string) - The secret key used to communicate with AWS. [Learn - how to set this](amazon.html#specifying-amazon-credentials) + how to set this](amazon.html#specifying-amazon-credentials). This is not + required if you are using `use_vault_aws_engine` for authentication instead. - `source_ami` (string) - The initial AMI used as a base for the newly created machine. `source_ami_filter` may be used instead to populate this @@ -505,6 +507,33 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. +- `use_vault_aws_engine` (bool) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault docs.] + (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this + flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `role_arn` (string)- The ARN of the role to assume if credential_type on + the Vault role is assumed_role. Must match one of the allowed role ARNs + in the Vault role. Optional if the Vault role only allows a single AWS + role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This is + specified as a string with a duration suffix. Valid only when + credential_type is assumed_role or federation_token. When not specified, + the default_sts_ttl set for the role will be used. If that is also not + set, then the default value of 3600s will be used. AWS places limits on + the maximum TTL allowed. See the AWS documentation on the DurationSeconds + parameter for AssumeRole (for assumed_role credential types) and + GetFederationToken (for federation_token credential types) for more + details. + + Please note that because credentials that are not supported by an STS + token are eventually consistent, Packer will pause for ten seconds after + retrieving the credentials before continuing with the build. + - `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID in order to create a temporary security group within the VPC. Requires `subnet_id` to be set. If this field is left blank, Packer will try to get From 7a78b47e832dce8a4e7723df876d396c17933565 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 4 Feb 2019 15:29:45 -0800 Subject: [PATCH 09/48] make vault engine stuff into a little struct for easier management --- builder/amazon/common/access_config.go | 15 +++++++++++++++ website/source/docs/builders/amazon-ebs.html.md | 12 +++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go index d29845d3d..94c74121a 100644 --- a/builder/amazon/common/access_config.go +++ b/builder/amazon/common/access_config.go @@ -18,6 +18,12 @@ import ( "github.com/hashicorp/packer/template/interpolate" ) +type VaultAWSEngineOptions struct { + Name string `mapstructure:"name"` + RoleARN string `mapstructure:"role_arn"` + TTL string `mapstructure:"ttl"` +} + // AccessConfig is for common configuration related to AWS access type AccessConfig struct { AccessKey string `mapstructure:"access_key"` @@ -32,6 +38,7 @@ type AccessConfig struct { SkipMetadataApiCheck bool `mapstructure:"skip_metadata_api_check"` Token string `mapstructure:"token"` session *session.Session + VaultAWSEngine VaultAWSEngineOptions `mapstructure:"vault_aws_engine"` getEC2Connection func() ec2iface.EC2API } @@ -44,6 +51,7 @@ func (c *AccessConfig) Session() (*session.Session, error) { } config := aws.NewConfig().WithCredentialsChainVerboseErrors(true) + staticCreds := credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token) if _, err := staticCreds.Get(); err != credentials.ErrStaticCredentialsEmpty { config.WithCredentials(staticCreds) @@ -130,6 +138,13 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { } // Either both access and secret key must be set or neither of them should // be. + if c.VaultAWSEngine != nil { + if len(c.AccessKey) > 0 { + errs = append(errs, + fmt.Errorf("If you have set vault_aws_engine, you must not set"+ + " the access_key or secret_key.")) + } + } if (len(c.AccessKey) > 0) != (len(c.SecretKey) > 0) { errs = append(errs, fmt.Errorf("`access_key` and `secret_key` must both be either set or not set.")) diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md index 00a19d77c..6d0f180bc 100644 --- a/website/source/docs/builders/amazon-ebs.html.md +++ b/website/source/docs/builders/amazon-ebs.html.md @@ -507,7 +507,7 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. -- `use_vault_aws_engine` (bool) - Get credentials from Hashicorp Vault's aws +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws secrets engine. You must already have created a role to use. For more information about generating credentials via the Vault engine, see the [Vault docs.] @@ -534,6 +534,16 @@ builder. token are eventually consistent, Packer will pause for ten seconds after retrieving the credentials before continuing with the build. + ``` json + { + "vault_aws_engine": { + "name": "myrole" + "role_arn": "myarn" + "ttl": "3600s" + } + } + ``` + - `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID in order to create a temporary security group within the VPC. Requires `subnet_id` to be set. If this field is left blank, Packer will try to get From 8add176ab7f7c21f046ce3a282d7c1df669cd446 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 4 Feb 2019 15:54:14 -0800 Subject: [PATCH 10/48] finish first pass at vault code; needs testing and cleanup of error messages --- builder/amazon/common/access_config.go | 58 +++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go index 94c74121a..23d8dc3b4 100644 --- a/builder/amazon/common/access_config.go +++ b/builder/amazon/common/access_config.go @@ -16,12 +16,19 @@ import ( cleanhttp "github.com/hashicorp/go-cleanhttp" commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/template/interpolate" + vaultapi "github.com/hashicorp/vault/api" ) type VaultAWSEngineOptions struct { - Name string `mapstructure:"name"` - RoleARN string `mapstructure:"role_arn"` - TTL string `mapstructure:"ttl"` + Name string `mapstructure:"name"` + RoleARN string `mapstructure:"role_arn"` + TTL string `mapstructure:"ttl"` + EngineName string `mapstructure:"engine_name"` +} + +func (v *VaultAWSEngineOptions) Empty() bool { + return len(v.Name) == 0 && len(v.RoleARN) == 0 && + len(v.EngineName) == 0 && len(v.TTL) == 0 } // AccessConfig is for common configuration related to AWS access @@ -130,26 +137,65 @@ func (c *AccessConfig) IsChinaCloud() bool { return strings.HasPrefix(c.SessionRegion(), "cn-") } +func (c *AccessConfig) GetCredsFromVault() error { + // const EnvVaultAddress = "VAULT_ADDR" + // const EnvVaultToken = "VAULT_TOKEN" + vaultConfig := vaultapi.DefaultConfig() + cli, err := vaultapi.NewClient(vaultConfig) + if err != nil { + return fmt.Errorf("Error getting Vault client: %s", err) + } + path := fmt.Sprintf("/%s/creds/%s", c.VaultAWSEngine.EngineName, + c.VaultAWSEngine.Name) + secret, err := cli.Logical().Read(path) + if err != nil { + return fmt.Errorf("Error reading vault secret: %s", err) + } + if secret == nil { + return fmt.Errorf("Vault Secret does not exist at the given path.") + } + + data, _ := secret.Data["data"] + unpacked := data.(map[string]interface{}) + c.AccessKey = unpacked["access_key"].(string) + c.SecretKey = unpacked["secret_key"].(string) + c.Token = unpacked["security_token"].(string) + + return nil +} + func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { var errs []error if c.SkipMetadataApiCheck { log.Println("(WARN) skip_metadata_api_check ignored.") } - // Either both access and secret key must be set or neither of them should - // be. - if c.VaultAWSEngine != nil { + + // Make sure it's obvious from the config how we're getting credentials: + // Vault, Packer config, or environemnt. + if !c.VaultAWSEngine.Empty() { if len(c.AccessKey) > 0 { errs = append(errs, fmt.Errorf("If you have set vault_aws_engine, you must not set"+ " the access_key or secret_key.")) } + // Go ahead and grab those credentials from Vault now, so we can set + // the keys and token now. + err := c.GetCredsFromVault() + if err != nil { + errs = append(errs, err) + } } + if (len(c.AccessKey) > 0) != (len(c.SecretKey) > 0) { errs = append(errs, fmt.Errorf("`access_key` and `secret_key` must both be either set or not set.")) } + // abort build early so I can test more quickly + errs = append(errs, + fmt.Errorf("Megan remove this error to continue with build: \n\nAccess: %s, \n\nSecret: %s, \n\nToken: %s", c.AccessKey, c.SecretKey, c.Token)) + return errs } From a7d9d62996ed9c39a12196eab662334cf8a2b3c5 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 4 Feb 2019 16:11:25 -0800 Subject: [PATCH 11/48] fix tests on windows --- builder/vagrant/artifact_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/builder/vagrant/artifact_test.go b/builder/vagrant/artifact_test.go index 794f5b1da..63e1db1ec 100644 --- a/builder/vagrant/artifact_test.go +++ b/builder/vagrant/artifact_test.go @@ -1,6 +1,7 @@ package vagrant import ( + "runtime" "strings" "testing" @@ -22,6 +23,9 @@ func TestArtifactId(t *testing.T) { } expected := "/my/dir/package.box" + if runtime.GOOS == "windows" { + expected = strings.Replace(expected, "/", "\\", -1) + } if strings.Compare(a.Id(), expected) != 0 { t.Fatalf("artifact ID should match: expected: %s received: %s", expected, a.Id()) } @@ -33,6 +37,9 @@ func TestArtifactString(t *testing.T) { BoxName: "package.box", } expected := "Vagrant box is /my/dir/package.box" + if runtime.GOOS == "windows" { + expected = strings.Replace(expected, "/", "\\", -1) + } if strings.Compare(a.String(), expected) != 0 { t.Fatalf("artifact string should match: expected: %s received: %s", expected, a.String()) From e8c655bf157867f8403776ce00e8a17ebd471dfa Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Tue, 5 Feb 2019 09:31:38 -0800 Subject: [PATCH 12/48] Update website/source/docs/builders/vagrant.html.md Co-Authored-By: SwampDragons --- website/source/docs/builders/vagrant.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/builders/vagrant.html.md b/website/source/docs/builders/vagrant.html.md index 2782975d0..ae661629f 100644 --- a/website/source/docs/builders/vagrant.html.md +++ b/website/source/docs/builders/vagrant.html.md @@ -21,7 +21,7 @@ one, by using `global_id` instead of `source_box`. Required: -- `source_box` (string) - URL of the vagrant box to use, or the name of the +- `source_path` (string) - URL of the vagrant box to use, or the name of the vagrant box. `hashicorp/precise64`, `./mylocalbox.box` and `https://example.com/my-box.box` are all valid source boxes. If your source is a .box file, whether locally or from a URL like the latter example From 8d8b880ac48bbe77c9f249c3c339406d40cb9d7f Mon Sep 17 00:00:00 2001 From: pauloj95 <37109548+pauloj95@users.noreply.github.com> Date: Tue, 5 Feb 2019 17:47:00 +0000 Subject: [PATCH 13/48] add force to powershell move command this will allow powershell move in provisioners and replace file if existing found... issue example: https://github.com/hashicorp/packer/issues/7280 --- provisioner/guest_commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/guest_commands.go b/provisioner/guest_commands.go index 3a361a208..ca2b238a4 100644 --- a/provisioner/guest_commands.go +++ b/provisioner/guest_commands.go @@ -30,7 +30,7 @@ var guestOSTypeCommands = map[string]guestOSTypeCommand{ mkdir: "powershell.exe -Command \"New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path %s\"", removeDir: "powershell.exe -Command \"rm %s -recurse -force\"", statPath: "powershell.exe -Command { if (test-path %s) { exit 0 } else { exit 1 } }", - mv: "powershell.exe -Command \"mv %s %s\"", + mv: "powershell.exe -Command \"mv %s %s -force\"", }, } From 3704a053d02654404fec6b384fa86524a4b6e65c Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 5 Feb 2019 14:07:04 -0800 Subject: [PATCH 14/48] move region validation and credential wait into step pre validate --- builder/amazon/chroot/builder.go | 2 + builder/amazon/common/access_config.go | 20 ++++---- builder/amazon/common/step_pre_validate.go | 50 +++++++++++++++++++ builder/amazon/ebs/builder.go | 10 ++-- builder/amazon/ebssurrogate/builder.go | 2 + builder/amazon/ebsvolume/builder.go | 1 + builder/amazon/instance/builder.go | 2 + .../source/docs/builders/amazon-ebs.html.md | 3 ++ 8 files changed, 74 insertions(+), 16 deletions(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 6ce5e2c16..3b1b4609e 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -205,6 +205,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", &b.config) + state.Put("access_config", &b.config.AccessConfig) + state.Put("ami_config", &b.config.AMIConfig) state.Put("ec2", ec2conn) state.Put("awsSession", session) state.Put("hook", hook) diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go index 23d8dc3b4..ed6910b34 100644 --- a/builder/amazon/common/access_config.go +++ b/builder/amazon/common/access_config.go @@ -145,6 +145,9 @@ func (c *AccessConfig) GetCredsFromVault() error { if err != nil { return fmt.Errorf("Error getting Vault client: %s", err) } + if c.VaultAWSEngine.EngineName == "" { + c.VaultAWSEngine.EngineName = "aws" + } path := fmt.Sprintf("/%s/creds/%s", c.VaultAWSEngine.EngineName, c.VaultAWSEngine.Name) secret, err := cli.Logical().Read(path) @@ -155,11 +158,14 @@ func (c *AccessConfig) GetCredsFromVault() error { return fmt.Errorf("Vault Secret does not exist at the given path.") } - data, _ := secret.Data["data"] - unpacked := data.(map[string]interface{}) - c.AccessKey = unpacked["access_key"].(string) - c.SecretKey = unpacked["secret_key"].(string) - c.Token = unpacked["security_token"].(string) + c.AccessKey = secret.Data["access_key"].(string) + c.SecretKey = secret.Data["secret_key"].(string) + token := secret.Data["security_token"] + if token != nil { + c.Token = token.(string) + } else { + c.Token = "" + } return nil } @@ -192,10 +198,6 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error { fmt.Errorf("`access_key` and `secret_key` must both be either set or not set.")) } - // abort build early so I can test more quickly - errs = append(errs, - fmt.Errorf("Megan remove this error to continue with build: \n\nAccess: %s, \n\nSecret: %s, \n\nToken: %s", c.AccessKey, c.SecretKey, c.Token)) - return errs } diff --git a/builder/amazon/common/step_pre_validate.go b/builder/amazon/common/step_pre_validate.go index 73b4022b7..018457b21 100644 --- a/builder/amazon/common/step_pre_validate.go +++ b/builder/amazon/common/step_pre_validate.go @@ -3,9 +3,12 @@ package common import ( "context" "fmt" + "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" + retry "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" ) @@ -20,6 +23,53 @@ type StepPreValidate struct { func (s *StepPreValidate) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) + + if accessConfig, ok := state.GetOk("access_config"); ok { + accessconf := accessConfig.(*AccessConfig) + if !accessconf.VaultAWSEngine.Empty() { + // loop over the authentication a few times to give vault-created creds + // time to become eventually-consistent + ui.Say("You're using Vault-generated AWS credentials. It may take a " + + "few moments for them to become available on AWS. Waiting...") + err := retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) { + ec2conn, err := accessconf.NewEC2Connection() + if err != nil { + return true, err + } + _, err = listEC2Regions(ec2conn) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "AuthFailure" { + log.Printf("Waiting for Vault-generated AWS credentials" + + " to pass authentication... trying again.") + return false, nil + } + } else { + return true, nil + } + + return true, err + }) + + if err != nil { + state.Put("error", fmt.Errorf("Was unable to Authenticate to AWS using Vault-"+ + "Generated Credentials within the retry timeout.")) + return multistep.ActionHalt + } + } + + if amiConfig, ok := state.GetOk("ami_config"); ok { + amiconf := amiConfig.(*AMIConfig) + if !amiconf.AMISkipRegionValidation { + regionsToValidate := append(amiconf.AMIRegions, accessconf.RawRegion) + err := accessconf.ValidateRegion(regionsToValidate...) + if err != nil { + state.Put("error", fmt.Errorf("error validating regions: %v", err)) + return multistep.ActionHalt + } + } + } + } + if s.ForceDeregister { ui.Say("Force Deregister flag found, skipping prevalidating AMI Name") return multistep.ActionContinue diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index c1a36a71b..4734995b5 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -92,13 +92,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe if err != nil { return nil, err } - if !b.config.AMISkipRegionValidation { - regionsToValidate := append(b.config.AMIRegions, b.config.RawRegion) - err := b.config.AccessConfig.ValidateRegion(regionsToValidate...) - if err != nil { - return nil, fmt.Errorf("error validating regions: %v", err) - } - } + ec2conn := ec2.New(session, &aws.Config{ HTTPClient: commonhelper.HttpClientWithEnvironmentProxy(), }) @@ -106,6 +100,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", &b.config) + state.Put("access_config", &b.config.AccessConfig) + state.Put("ami_config", &b.config.AMIConfig) state.Put("ec2", ec2conn) state.Put("awsSession", session) state.Put("hook", hook) diff --git a/builder/amazon/ebssurrogate/builder.go b/builder/amazon/ebssurrogate/builder.go index 464ebfa31..82dd58125 100644 --- a/builder/amazon/ebssurrogate/builder.go +++ b/builder/amazon/ebssurrogate/builder.go @@ -114,6 +114,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", &b.config) + state.Put("access_config", &b.config.AccessConfig) + state.Put("ami_config", &b.config.AMIConfig) state.Put("ec2", ec2conn) state.Put("awsSession", session) state.Put("hook", hook) diff --git a/builder/amazon/ebsvolume/builder.go b/builder/amazon/ebsvolume/builder.go index 8a75c05ce..95801c80e 100644 --- a/builder/amazon/ebsvolume/builder.go +++ b/builder/amazon/ebsvolume/builder.go @@ -103,6 +103,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", &b.config) + state.Put("access_config", &b.config.AccessConfig) state.Put("ec2", ec2conn) state.Put("hook", hook) state.Put("ui", ui) diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index 1d9e29b20..5eef95538 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -184,6 +184,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) state.Put("config", &b.config) + state.Put("access_config", &b.config.AccessConfig) + state.Put("ami_config", &b.config.AMIConfig) state.Put("ec2", ec2conn) state.Put("awsSession", session) state.Put("hook", hook) diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md index 6d0f180bc..f0e7d97ce 100644 --- a/website/source/docs/builders/amazon-ebs.html.md +++ b/website/source/docs/builders/amazon-ebs.html.md @@ -516,6 +516,9 @@ builder. flag, you must also set the below options: - `name` (string) - Required. Specifies the name of the role to generate credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the Vault + docs, this is normally referred to as "aws", and Packer will default to + "aws" if `engine_name` is not set. - `role_arn` (string)- The ARN of the role to assume if credential_type on the Vault role is assumed_role. Must match one of the allowed role ARNs in the Vault role. Optional if the Vault role only allows a single AWS From 90baa006dab0b62742bc153d9d0558b0ebcebb43 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 5 Feb 2019 14:19:24 -0800 Subject: [PATCH 15/48] add docs on all builders. --- .../docs/builders/amazon-chroot.html.md | 37 ++++++++++++++++++ .../source/docs/builders/amazon-ebs.html.md | 8 +--- .../docs/builders/amazon-ebssurrogate.html.md | 36 ++++++++++++++++++ .../docs/builders/amazon-ebsvolume.html.md | 37 ++++++++++++++++++ .../docs/builders/amazon-instance.html.md | 38 +++++++++++++++++++ 5 files changed, 150 insertions(+), 6 deletions(-) diff --git a/website/source/docs/builders/amazon-chroot.html.md b/website/source/docs/builders/amazon-chroot.html.md index 24622e049..0bc4934ae 100644 --- a/website/source/docs/builders/amazon-chroot.html.md +++ b/website/source/docs/builders/amazon-chroot.html.md @@ -362,6 +362,43 @@ each category, the available configuration keys are alphabetized. [template engine](/docs/templates/engine.html), see [Build template data](#build-template-data) for more information. +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault docs.] + (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this + flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the Vault + docs, this is normally referred to as "aws", and Packer will default to + "aws" if `engine_name` is not set. + - `role_arn` (string)- The ARN of the role to assume if credential_type on + the Vault role is assumed_role. Must match one of the allowed role ARNs + in the Vault role. Optional if the Vault role only allows a single AWS + role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This is + specified as a string with a duration suffix. Valid only when + credential_type is assumed_role or federation_token. When not specified, + the default_sts_ttl set for the role will be used. If that is also not + set, then the default value of 3600s will be used. AWS places limits on + the maximum TTL allowed. See the AWS documentation on the DurationSeconds + parameter for AssumeRole (for assumed_role credential types) and + GetFederationToken (for federation_token credential types) for more + details. + + Example: + ``` json + { + "vault_aws_engine": { + "name": "myrole", + "role_arn": "myarn", + "ttl": "3600s" + } + } + ``` + ## Basic Example Here is a basic example. It is completely valid except for the access keys: diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md index f0e7d97ce..58bcd9b9f 100644 --- a/website/source/docs/builders/amazon-ebs.html.md +++ b/website/source/docs/builders/amazon-ebs.html.md @@ -533,15 +533,11 @@ builder. GetFederationToken (for federation_token credential types) for more details. - Please note that because credentials that are not supported by an STS - token are eventually consistent, Packer will pause for ten seconds after - retrieving the credentials before continuing with the build. - ``` json { "vault_aws_engine": { - "name": "myrole" - "role_arn": "myarn" + "name": "myrole", + "role_arn": "myarn", "ttl": "3600s" } } diff --git a/website/source/docs/builders/amazon-ebssurrogate.html.md b/website/source/docs/builders/amazon-ebssurrogate.html.md index becffc4f4..126b770a5 100644 --- a/website/source/docs/builders/amazon-ebssurrogate.html.md +++ b/website/source/docs/builders/amazon-ebssurrogate.html.md @@ -497,6 +497,42 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. + +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault docs.] + (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the Vault + docs, this is normally referred to as "aws", and Packer will default to + "aws" if `engine_name` is not set. + - `role_arn` (string)- The ARN of the role to assume if credential_type on + the Vault role is assumed_role. Must match one of the allowed role ARNs + in the Vault role. Optional if the Vault role only allows a single AWS + role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This is + specified as a string with a duration suffix. Valid only when + credential_type is assumed_role or federation_token. When not specified, + the default_sts_ttl set for the role will be used. If that is also not + set, then the default value of 3600s will be used. AWS places limits on + the maximum TTL allowed. See the AWS documentation on the DurationSeconds + parameter for AssumeRole (for assumed_role credential types) and + GetFederationToken (for federation_token credential types) for more + details. + + Example: + ``` json + { + "vault_aws_engine": { + "name": "myrole", + "role_arn": "myarn", + "ttl": "3600s" + } + } + ``` - `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID in order to create a temporary security group within the VPC. Requires `subnet_id` to be set. If this field is left blank, Packer will try to get diff --git a/website/source/docs/builders/amazon-ebsvolume.html.md b/website/source/docs/builders/amazon-ebsvolume.html.md index 9cd438a8a..18b1ebadd 100644 --- a/website/source/docs/builders/amazon-ebsvolume.html.md +++ b/website/source/docs/builders/amazon-ebsvolume.html.md @@ -407,6 +407,43 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault docs.] + (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this + flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the Vault + docs, this is normally referred to as "aws", and Packer will default to + "aws" if `engine_name` is not set. + - `role_arn` (string)- The ARN of the role to assume if credential_type on + the Vault role is assumed_role. Must match one of the allowed role ARNs + in the Vault role. Optional if the Vault role only allows a single AWS + role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This is + specified as a string with a duration suffix. Valid only when + credential_type is assumed_role or federation_token. When not specified, + the default_sts_ttl set for the role will be used. If that is also not + set, then the default value of 3600s will be used. AWS places limits on + the maximum TTL allowed. See the AWS documentation on the DurationSeconds + parameter for AssumeRole (for assumed_role credential types) and + GetFederationToken (for federation_token credential types) for more + details. + + Example: + ``` json + { + "vault_aws_engine": { + "name": "myrole", + "role_arn": "myarn", + "ttl": "3600s" + } + } + ``` + - `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID in order to create a temporary security group within the VPC. Requires `subnet_id` to be set. If this field is left blank, Packer will try to get diff --git a/website/source/docs/builders/amazon-instance.html.md b/website/source/docs/builders/amazon-instance.html.md index 4ce4a0255..0f74a6455 100644 --- a/website/source/docs/builders/amazon-instance.html.md +++ b/website/source/docs/builders/amazon-instance.html.md @@ -489,6 +489,44 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. + +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault docs.] + (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this + flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the Vault + docs, this is normally referred to as "aws", and Packer will default to + "aws" if `engine_name` is not set. + - `role_arn` (string)- The ARN of the role to assume if credential_type on + the Vault role is assumed_role. Must match one of the allowed role ARNs + in the Vault role. Optional if the Vault role only allows a single AWS + role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This is + specified as a string with a duration suffix. Valid only when + credential_type is assumed_role or federation_token. When not specified, + the default_sts_ttl set for the role will be used. If that is also not + set, then the default value of 3600s will be used. AWS places limits on + the maximum TTL allowed. See the AWS documentation on the DurationSeconds + parameter for AssumeRole (for assumed_role credential types) and + GetFederationToken (for federation_token credential types) for more + details. + + Example: + ``` json + { + "vault_aws_engine": { + "name": "myrole", + "role_arn": "myarn", + "ttl": "3600s" + } + } + ``` + - `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID in order to create a temporary security group within the VPC. Requires `subnet_id` to be set. If this field is left blank, Packer will try to get From 608b7cb7a32dff09a9400ec82217855df8f0b38f Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 5 Feb 2019 15:16:47 -0800 Subject: [PATCH 16/48] review comments --- builder/vagrant/step_initialize_vagrant.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/builder/vagrant/step_initialize_vagrant.go b/builder/vagrant/step_initialize_vagrant.go index 43a824090..51b4710f6 100644 --- a/builder/vagrant/step_initialize_vagrant.go +++ b/builder/vagrant/step_initialize_vagrant.go @@ -36,10 +36,9 @@ type VagrantfileOptions struct { BoxName string } -func (s *StepInitializeVagrant) createInitializeCommand() (string, error) { +func (s *StepInitializeVagrant) getVagrantfileTemplate() (string, error) { tplPath := filepath.Join(s.OutputDir, "packer-vagrantfile-template.erb") templateFile, err := os.Create(tplPath) - templateFile.Chmod(0777) if err != nil { retErr := fmt.Errorf("Error creating vagrantfile %s", err.Error()) return "", retErr @@ -93,7 +92,7 @@ func (s *StepInitializeVagrant) prepInitArgs() ([]string, error) { initArgs = append(initArgs, "-m") } - tplPath, err := s.createInitializeCommand() + tplPath, err := s.getVagrantfileTemplate() if err != nil { return initArgs, err } From e56d7f723420946b37620706096ac5224829426e Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Tue, 5 Feb 2019 15:34:59 -0800 Subject: [PATCH 17/48] fix tests --- builder/vagrant/step_initialize_vagrant_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/vagrant/step_initialize_vagrant_test.go b/builder/vagrant/step_initialize_vagrant_test.go index 9f91b0631..4df7284f4 100644 --- a/builder/vagrant/step_initialize_vagrant_test.go +++ b/builder/vagrant/step_initialize_vagrant_test.go @@ -22,7 +22,7 @@ func TestCreateFile(t *testing.T) { OutputDir: "./", SourceBox: "bananas", } - templatePath, err := testy.createInitializeCommand() + templatePath, err := testy.getVagrantfileTemplate() if err != nil { t.Fatalf(err.Error()) } @@ -43,7 +43,7 @@ func TestCreateFile_customSync(t *testing.T) { OutputDir: "./", SyncedFolder: "myfolder/foldertimes", } - templatePath, err := testy.createInitializeCommand() + templatePath, err := testy.getVagrantfileTemplate() defer os.Remove(templatePath) if err != nil { t.Fatalf(err.Error()) From d7b93aa34378c4bc4ff5771269badd12724c2c16 Mon Sep 17 00:00:00 2001 From: pauloj95 <37109548+pauloj95@users.noreply.github.com> Date: Wed, 6 Feb 2019 10:27:45 +0000 Subject: [PATCH 18/48] add force to guestcommands test too --- provisioner/guest_commands_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/guest_commands_test.go b/provisioner/guest_commands_test.go index 08baca058..27499740d 100644 --- a/provisioner/guest_commands_test.go +++ b/provisioner/guest_commands_test.go @@ -183,7 +183,7 @@ func TestMovePath(t *testing.T) { t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType) } cmd = guestCmd.MovePath("C:\\Temp\\SomeDir", "C:\\Temp\\NewDir") - if cmd != "powershell.exe -Command \"mv C:\\Temp\\SomeDir C:\\Temp\\NewDir\"" { + if cmd != "powershell.exe -Command \"mv C:\\Temp\\SomeDir C:\\Temp\\NewDir\ -force"" { t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) } From b4d1c77cc05b9d49c264857d60957cc895b81ffe Mon Sep 17 00:00:00 2001 From: pauloj95 <37109548+pauloj95@users.noreply.github.com> Date: Wed, 6 Feb 2019 10:30:37 +0000 Subject: [PATCH 19/48] Update guest_commands_test.go --- provisioner/guest_commands_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/guest_commands_test.go b/provisioner/guest_commands_test.go index 27499740d..bdea019cc 100644 --- a/provisioner/guest_commands_test.go +++ b/provisioner/guest_commands_test.go @@ -183,7 +183,7 @@ func TestMovePath(t *testing.T) { t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType) } cmd = guestCmd.MovePath("C:\\Temp\\SomeDir", "C:\\Temp\\NewDir") - if cmd != "powershell.exe -Command \"mv C:\\Temp\\SomeDir C:\\Temp\\NewDir\ -force"" { + if cmd != "powershell.exe -Command \"mv C:\\Temp\\SomeDir C:\\Temp\\NewDir -force\"" { t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) } From b1fffee614d6fc60bbe232dd9a425387996f3f81 Mon Sep 17 00:00:00 2001 From: pauloj95 <37109548+pauloj95@users.noreply.github.com> Date: Wed, 6 Feb 2019 10:50:29 +0000 Subject: [PATCH 20/48] Update guest_commands_test.go --- provisioner/guest_commands_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/guest_commands_test.go b/provisioner/guest_commands_test.go index bdea019cc..ffbad4bbd 100644 --- a/provisioner/guest_commands_test.go +++ b/provisioner/guest_commands_test.go @@ -189,7 +189,7 @@ func TestMovePath(t *testing.T) { // Windows OS w/ space in path cmd = guestCmd.MovePath("C:\\Temp\\Some Dir", "C:\\Temp\\New Dir") - if cmd != "powershell.exe -Command \"mv C:\\Temp\\Some` Dir C:\\Temp\\New` Dir\"" { + if cmd != "powershell.exe -Command \"mv C:\\Temp\\Some` Dir C:\\Temp\\New` Dir -force\"" { t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) } } From dc848ea5d75ed896ed08599560d691a5a3626c59 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 7 Feb 2019 12:39:56 -0800 Subject: [PATCH 21/48] just make vagrantfile instead of calling init --- builder/vagrant/builder.go | 15 +-- ..._vagrant.go => step_create_vagrantfile.go} | 55 ++------- .../vagrant/step_create_vagrantfile_test.go | 60 ++++++++++ .../vagrant/step_initialize_vagrant_test.go | 110 ------------------ website/source/docs/builders/vagrant.html.md | 14 +-- 5 files changed, 80 insertions(+), 174 deletions(-) rename builder/vagrant/{step_initialize_vagrant.go => step_create_vagrantfile.go} (60%) create mode 100644 builder/vagrant/step_create_vagrantfile_test.go delete mode 100644 builder/vagrant/step_initialize_vagrant_test.go diff --git a/builder/vagrant/builder.go b/builder/vagrant/builder.go index b44f4a56d..d14f6df8e 100644 --- a/builder/vagrant/builder.go +++ b/builder/vagrant/builder.go @@ -56,7 +56,6 @@ type Config struct { // Options for the "vagrant init" command BoxVersion string `mapstructure:"box_version"` - Minimal bool `mapstructure:"init_minimal"` Template string `mapstructure:"template"` SyncedFolder string `mapstructure:"synced_folder"` @@ -200,14 +199,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Force: b.config.PackerForce, Path: b.config.OutputDir, }, - &StepInitializeVagrant{ - BoxVersion: b.config.BoxVersion, - Minimal: b.config.Minimal, - Template: b.config.Template, - SourceBox: b.config.SourceBox, - OutputDir: b.config.OutputDir, - BoxName: b.config.BoxName, - GlobalID: b.config.GlobalID, + &StepCreateVagrantfile{ + Template: b.config.Template, + SyncedFolder: b.config.SyncedFolder, + SourceBox: b.config.SourceBox, + OutputDir: b.config.OutputDir, + GlobalID: b.config.GlobalID, }, &StepAddBox{ BoxVersion: b.config.BoxVersion, diff --git a/builder/vagrant/step_initialize_vagrant.go b/builder/vagrant/step_create_vagrantfile.go similarity index 60% rename from builder/vagrant/step_initialize_vagrant.go rename to builder/vagrant/step_create_vagrantfile.go index 51b4710f6..375b9b423 100644 --- a/builder/vagrant/step_initialize_vagrant.go +++ b/builder/vagrant/step_create_vagrantfile.go @@ -3,6 +3,7 @@ package vagrant import ( "context" "fmt" + "log" "os" "path/filepath" "text/template" @@ -11,10 +12,7 @@ import ( "github.com/hashicorp/packer/packer" ) -type StepInitializeVagrant struct { - BoxName string - BoxVersion string - Minimal bool +type StepCreateVagrantfile struct { Template string SourceBox string OutputDir string @@ -36,8 +34,8 @@ type VagrantfileOptions struct { BoxName string } -func (s *StepInitializeVagrant) getVagrantfileTemplate() (string, error) { - tplPath := filepath.Join(s.OutputDir, "packer-vagrantfile-template.erb") +func (s *StepCreateVagrantfile) createVagrantfile() (string, error) { + tplPath := filepath.Join(s.OutputDir, "Vagrantfile") templateFile, err := os.Create(tplPath) if err != nil { retErr := fmt.Errorf("Error creating vagrantfile %s", err.Error()) @@ -74,36 +72,7 @@ func (s *StepInitializeVagrant) getVagrantfileTemplate() (string, error) { return abspath, nil } -func (s *StepInitializeVagrant) prepInitArgs() ([]string, error) { - // Prepare arguments - initArgs := []string{} - - if s.BoxName != "" { - initArgs = append(initArgs, s.BoxName) - } - - initArgs = append(initArgs, s.SourceBox) - - if s.BoxVersion != "" { - initArgs = append(initArgs, "--box-version", s.BoxVersion) - } - - if s.Minimal { - initArgs = append(initArgs, "-m") - } - - tplPath, err := s.getVagrantfileTemplate() - if err != nil { - return initArgs, err - } - - initArgs = append(initArgs, "--template", tplPath) - - return initArgs, nil -} - -func (s *StepInitializeVagrant) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { - driver := state.Get("driver").(VagrantDriver) +func (s *StepCreateVagrantfile) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) // Skip the initialize step if we're trying to launch from a global ID. @@ -112,24 +81,18 @@ func (s *StepInitializeVagrant) Run(_ context.Context, state multistep.StateBag) return multistep.ActionContinue } - ui.Say("Initializing Vagrant in build directory...") - - initArgs, err := s.prepInitArgs() + ui.Say("Creating a Vagrantfile in the build directory...") + vagrantfilePath, err := s.createVagrantfile() if err != nil { state.Put("error", err) return multistep.ActionHalt } + log.Printf("Created vagrantfile at %s", vagrantfilePath) os.Chdir(s.OutputDir) - // Call vagrant using prepared arguments - err = driver.Init(initArgs) - if err != nil { - state.Put("error", err) - return multistep.ActionHalt - } return multistep.ActionContinue } -func (s *StepInitializeVagrant) Cleanup(state multistep.StateBag) { +func (s *StepCreateVagrantfile) Cleanup(state multistep.StateBag) { } diff --git a/builder/vagrant/step_create_vagrantfile_test.go b/builder/vagrant/step_create_vagrantfile_test.go new file mode 100644 index 000000000..d3bcf21b9 --- /dev/null +++ b/builder/vagrant/step_create_vagrantfile_test.go @@ -0,0 +1,60 @@ +package vagrant + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/hashicorp/packer/helper/multistep" +) + +func TestStepCreateVagrantfile_Impl(t *testing.T) { + var raw interface{} + raw = new(StepCreateVagrantfile) + if _, ok := raw.(multistep.Step); !ok { + t.Fatalf("initialize should be a step") + } +} + +func TestCreateFile(t *testing.T) { + testy := StepCreateVagrantfile{ + OutputDir: "./", + SourceBox: "bananas", + } + templatePath, err := testy.createVagrantfile() + if err != nil { + t.Fatalf(err.Error()) + } + defer os.Remove(templatePath) + contents, err := ioutil.ReadFile(templatePath) + actual := string(contents) + expected := `Vagrant.configure("2") do |config| + config.vm.box = "bananas" + config.vm.synced_folder ".", "/vagrant", disabled: true +end` + if ok := strings.Compare(actual, expected); ok != 0 { + t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual) + } +} + +func TestCreateFile_customSync(t *testing.T) { + testy := StepCreateVagrantfile{ + OutputDir: "./", + SyncedFolder: "myfolder/foldertimes", + } + templatePath, err := testy.createVagrantfile() + if err != nil { + t.Fatalf(err.Error()) + } + defer os.Remove(templatePath) + contents, err := ioutil.ReadFile(templatePath) + actual := string(contents) + expected := `Vagrant.configure("2") do |config| + config.vm.box = "" + config.vm.synced_folder "myfolder/foldertimes", "/vagrant" +end` + if ok := strings.Compare(actual, expected); ok != 0 { + t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual) + } +} diff --git a/builder/vagrant/step_initialize_vagrant_test.go b/builder/vagrant/step_initialize_vagrant_test.go deleted file mode 100644 index 4df7284f4..000000000 --- a/builder/vagrant/step_initialize_vagrant_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package vagrant - -import ( - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/hashicorp/packer/helper/multistep" -) - -func TestStepInitialize_Impl(t *testing.T) { - var raw interface{} - raw = new(StepInitializeVagrant) - if _, ok := raw.(multistep.Step); !ok { - t.Fatalf("initialize should be a step") - } -} - -func TestCreateFile(t *testing.T) { - testy := StepInitializeVagrant{ - OutputDir: "./", - SourceBox: "bananas", - } - templatePath, err := testy.getVagrantfileTemplate() - if err != nil { - t.Fatalf(err.Error()) - } - contents, err := ioutil.ReadFile(templatePath) - actual := string(contents) - expected := `Vagrant.configure("2") do |config| - config.vm.box = "bananas" - config.vm.synced_folder ".", "/vagrant", disabled: true -end` - if ok := strings.Compare(actual, expected); ok != 0 { - t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual) - } - os.Remove(templatePath) -} - -func TestCreateFile_customSync(t *testing.T) { - testy := StepInitializeVagrant{ - OutputDir: "./", - SyncedFolder: "myfolder/foldertimes", - } - templatePath, err := testy.getVagrantfileTemplate() - defer os.Remove(templatePath) - if err != nil { - t.Fatalf(err.Error()) - } - contents, err := ioutil.ReadFile(templatePath) - actual := string(contents) - expected := `Vagrant.configure("2") do |config| - config.vm.box = "" - config.vm.synced_folder "myfolder/foldertimes", "/vagrant" -end` - if ok := strings.Compare(actual, expected); ok != 0 { - t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual) - } -} - -func TestPrepInitArgs(t *testing.T) { - type testArgs struct { - Step StepInitializeVagrant - Expected []string - } - initTests := []testArgs{ - { - Step: StepInitializeVagrant{ - SourceBox: "my_source_box.box", - }, - Expected: []string{"my_source_box.box", "--template"}, - }, - { - Step: StepInitializeVagrant{ - SourceBox: "my_source_box", - BoxName: "My Box", - }, - Expected: []string{"My Box", "my_source_box", "--template"}, - }, - { - Step: StepInitializeVagrant{ - SourceBox: "my_source_box", - BoxName: "My Box", - BoxVersion: "42", - }, - Expected: []string{"My Box", "my_source_box", "--box-version", "42", "--template"}, - }, - { - Step: StepInitializeVagrant{ - SourceBox: "my_source_box", - BoxName: "My Box", - Minimal: true, - }, - Expected: []string{"My Box", "my_source_box", "-m", "--template"}, - }, - } - for _, initTest := range initTests { - initArgs, err := initTest.Step.prepInitArgs() - defer os.Remove(initArgs[len(initArgs)-1]) - if err != nil { - t.Fatalf(err.Error()) - } - for i, val := range initTest.Expected { - if strings.Compare(initArgs[i], val) != 0 { - t.Fatalf("expected %#v but received %#v", initTest.Expected, initArgs[:len(initArgs)-1]) - } - } - } -} diff --git a/website/source/docs/builders/vagrant.html.md b/website/source/docs/builders/vagrant.html.md index ae661629f..fc93d00c9 100644 --- a/website/source/docs/builders/vagrant.html.md +++ b/website/source/docs/builders/vagrant.html.md @@ -58,11 +58,11 @@ Optional: not recommended since OVA files can be very large and corruption does happen from time to time. -- `vagrantfile_template` (string) - a path to an ERB template to use for the - vagrantfile when calling `vagrant init`. See the blog post - [here](https://www.hashicorp.com/blog/hashicorp-vagrant-2-0-2#customized-vagrantfile-templates) - for some more details on how this works. Available variables are `box_name`, - `box_url`, and `box_version`. +- `vagrantfile_template` (string) - a path to a golang template for a + vagrantfile. Our default template can be found + [here](https://github.com/hashicorp/packer/tree/master/builder/vagrant/step_initialize_vagrant.go#L23-L30). So far the only template variables available to you are {{ .BoxName }} and + {{ .SyncedFolder }}, which correspond to the Packer options `box_name` and + `synced_folder` - `skip_add` (string) - Don't call "vagrant add" to add the box to your local environment; this is necesasry if you want to launch a box that is already @@ -73,10 +73,6 @@ Optional: - `box_version` (string) - What box version to use when initializing Vagrant. -- `init_minimal` (bool) - If true, will add the --minimal flag to the Vagrant - init command, creating a minimal vagrantfile instead of one filled with helpful - comments. - - `add_cacert` (string) - Equivalent to setting the [`--cacert`](https://www.vagrantup.com/docs/cli/box.html#cacert-certfile) option in `vagrant add`; defaults to unset. From 5057220ad2e12aa5d75790408ff884c8a38dfca3 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 7 Feb 2019 14:35:01 -0800 Subject: [PATCH 22/48] use VAGRANT_CWD rather than changing packer run directories --- builder/vagrant/builder.go | 7 ++++++- builder/vagrant/driver.go | 3 ++- builder/vagrant/driver_2_2.go | 4 ++++ builder/vagrant/step_create_vagrantfile.go | 2 -- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/builder/vagrant/builder.go b/builder/vagrant/builder.go index d14f6df8e..469270298 100644 --- a/builder/vagrant/builder.go +++ b/builder/vagrant/builder.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "path/filepath" "strings" "time" @@ -167,7 +168,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { // a VirtualBox appliance. func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { // Create the driver that we'll use to communicate with VirtualBox - driver, err := NewDriver() + VagrantCWD, err := filepath.Abs(b.config.OutputDir) + if err != nil { + return nil, err + } + driver, err := NewDriver(VagrantCWD) if err != nil { return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err) } diff --git a/builder/vagrant/driver.go b/builder/vagrant/driver.go index 2318a0382..56f7e8f68 100644 --- a/builder/vagrant/driver.go +++ b/builder/vagrant/driver.go @@ -42,7 +42,7 @@ type VagrantDriver interface { Version() (string, error) } -func NewDriver() (VagrantDriver, error) { +func NewDriver(outputDir string) (VagrantDriver, error) { // Hardcode path for now while I'm developing. Obviously this path needs // to be discovered based on OS. vagrantBinary := "vagrant" @@ -56,6 +56,7 @@ func NewDriver() (VagrantDriver, error) { driver := &Vagrant_2_2_Driver{ vagrantBinary: vagrantBinary, + VagrantCWD: outputDir, } if err := driver.Verify(); err != nil { diff --git a/builder/vagrant/driver_2_2.go b/builder/vagrant/driver_2_2.go index 9886939b1..01652f5c3 100644 --- a/builder/vagrant/driver_2_2.go +++ b/builder/vagrant/driver_2_2.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "regexp" "strings" @@ -16,6 +17,7 @@ const VAGRANT_MIN_VERSION = ">= 2.0.2" type Vagrant_2_2_Driver struct { vagrantBinary string + VagrantCWD string } // Calls "vagrant init" @@ -69,6 +71,7 @@ func (d *Vagrant_2_2_Driver) Destroy(id string) error { // Calls "vagrant package" func (d *Vagrant_2_2_Driver) Package(args []string) error { + args = append(args, "--output", filepath.Join(d.VagrantCWD, "package.box")) _, _, err := d.vagrantCmd(append([]string{"package"}, args...)...) return err } @@ -177,6 +180,7 @@ func (d *Vagrant_2_2_Driver) vagrantCmd(args ...string) (string, string, error) log.Printf("Calling Vagrant CLI: %#v", args) cmd := exec.Command(d.vagrantBinary, args...) + cmd.Env = append(os.Environ(), fmt.Sprintf("VAGRANT_CWD=%s", d.VagrantCWD)) cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() diff --git a/builder/vagrant/step_create_vagrantfile.go b/builder/vagrant/step_create_vagrantfile.go index 375b9b423..40ead21dd 100644 --- a/builder/vagrant/step_create_vagrantfile.go +++ b/builder/vagrant/step_create_vagrantfile.go @@ -89,8 +89,6 @@ func (s *StepCreateVagrantfile) Run(_ context.Context, state multistep.StateBag) } log.Printf("Created vagrantfile at %s", vagrantfilePath) - os.Chdir(s.OutputDir) - return multistep.ActionContinue } From 07c0c599e075fd2c7a2e43abee044cfde26aeff3 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 8 Feb 2019 09:15:15 -0800 Subject: [PATCH 23/48] attach guest additions via SATA when necessasry --- .../common/step_attach_guest_additions.go | 36 +++++++++++++---- .../virtualbox/common/step_remove_devices.go | 17 ++++++-- builder/virtualbox/iso/builder.go | 39 +++++++++++-------- 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/builder/virtualbox/common/step_attach_guest_additions.go b/builder/virtualbox/common/step_attach_guest_additions.go index d5698c372..dc3e2a0eb 100644 --- a/builder/virtualbox/common/step_attach_guest_additions.go +++ b/builder/virtualbox/common/step_attach_guest_additions.go @@ -21,8 +21,9 @@ import ( // // Produces: type StepAttachGuestAdditions struct { - attachedPath string - GuestAdditionsMode string + attachedPath string + GuestAdditionsMode string + GuestAdditionsInterface string } func (s *StepAttachGuestAdditions) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { @@ -40,12 +41,22 @@ func (s *StepAttachGuestAdditions) Run(_ context.Context, state multistep.StateB guestAdditionsPath := state.Get("guest_additions_path").(string) // Attach the guest additions to the computer + + controllerName := "IDE Controller" + port := "1" + device := "0" + if s.GuestAdditionsInterface == "sata" { + controllerName = "SATA Controller" + port = "2" + device = "0" + } + log.Println("Attaching guest additions ISO onto IDE controller...") command := []string{ "storageattach", vmName, - "--storagectl", "IDE Controller", - "--port", "1", - "--device", "0", + "--storagectl", controllerName, + "--port", port, + "--device", device, "--type", "dvddrive", "--medium", guestAdditionsPath, } @@ -71,11 +82,20 @@ func (s *StepAttachGuestAdditions) Cleanup(state multistep.StateBag) { driver := state.Get("driver").(Driver) vmName := state.Get("vmName").(string) + controllerName := "IDE Controller" + port := "1" + device := "0" + if s.GuestAdditionsInterface == "sata" { + controllerName = "SATA Controller" + port = "2" + device = "0" + } + command := []string{ "storageattach", vmName, - "--storagectl", "IDE Controller", - "--port", "1", - "--device", "0", + "--storagectl", controllerName, + "--port", port, + "--device", device, "--medium", "none", } diff --git a/builder/virtualbox/common/step_remove_devices.go b/builder/virtualbox/common/step_remove_devices.go index 461705763..d50afb398 100644 --- a/builder/virtualbox/common/step_remove_devices.go +++ b/builder/virtualbox/common/step_remove_devices.go @@ -20,7 +20,8 @@ import ( // // Produces: type StepRemoveDevices struct { - Bundling VBoxBundleConfig + Bundling VBoxBundleConfig + GuestAdditionsInterface string } func (s *StepRemoveDevices) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { @@ -100,11 +101,19 @@ func (s *StepRemoveDevices) Run(_ context.Context, state multistep.StateBag) mul if _, ok := state.GetOk("guest_additions_attached"); ok { ui.Message("Removing guest additions drive...") + controllerName := "IDE Controller" + port := "1" + device := "0" + if s.GuestAdditionsInterface == "sata" { + controllerName = "SATA Controller" + port = "2" + device = "0" + } command := []string{ "storageattach", vmName, - "--storagectl", "IDE Controller", - "--port", "1", - "--device", "0", + "--storagectl", controllerName, + "--port", port, + "--device", device, "--medium", "none", } if err := driver.VBoxManage(command...); err != nil { diff --git a/builder/virtualbox/iso/builder.go b/builder/virtualbox/iso/builder.go index 55aa602a9..dce65c4df 100644 --- a/builder/virtualbox/iso/builder.go +++ b/builder/virtualbox/iso/builder.go @@ -41,20 +41,21 @@ type Config struct { vboxcommon.VBoxVersionConfig `mapstructure:",squash"` vboxcommon.VBoxBundleConfig `mapstructure:",squash"` - DiskSize uint `mapstructure:"disk_size"` - GuestAdditionsMode string `mapstructure:"guest_additions_mode"` - GuestAdditionsPath string `mapstructure:"guest_additions_path"` - GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` - GuestAdditionsURL string `mapstructure:"guest_additions_url"` - GuestOSType string `mapstructure:"guest_os_type"` - HardDriveDiscard bool `mapstructure:"hard_drive_discard"` - HardDriveInterface string `mapstructure:"hard_drive_interface"` - SATAPortCount int `mapstructure:"sata_port_count"` - HardDriveNonrotational bool `mapstructure:"hard_drive_nonrotational"` - ISOInterface string `mapstructure:"iso_interface"` - KeepRegistered bool `mapstructure:"keep_registered"` - SkipExport bool `mapstructure:"skip_export"` - VMName string `mapstructure:"vm_name"` + DiskSize uint `mapstructure:"disk_size"` + GuestAdditionsMode string `mapstructure:"guest_additions_mode"` + GuestAdditionsPath string `mapstructure:"guest_additions_path"` + GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` + GuestAdditionsURL string `mapstructure:"guest_additions_url"` + GuestAdditionsInterface string `mapstructure:"guest_additions_interface"` + GuestOSType string `mapstructure:"guest_os_type"` + HardDriveDiscard bool `mapstructure:"hard_drive_discard"` + HardDriveInterface string `mapstructure:"hard_drive_interface"` + SATAPortCount int `mapstructure:"sata_port_count"` + HardDriveNonrotational bool `mapstructure:"hard_drive_nonrotational"` + ISOInterface string `mapstructure:"iso_interface"` + KeepRegistered bool `mapstructure:"keep_registered"` + SkipExport bool `mapstructure:"skip_export"` + VMName string `mapstructure:"vm_name"` ctx interpolate.Context } @@ -125,6 +126,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { b.config.ISOInterface = "ide" } + if b.config.GuestAdditionsInterface == "" { + b.config.GuestAdditionsInterface = b.config.ISOInterface + } + if b.config.VMName == "" { b.config.VMName = fmt.Sprintf( "packer-%s-%d", b.config.PackerBuildName, interpolate.InitTime.Unix()) @@ -227,7 +232,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepCreateDisk), new(stepAttachISO), &vboxcommon.StepAttachGuestAdditions{ - GuestAdditionsMode: b.config.GuestAdditionsMode, + GuestAdditionsMode: b.config.GuestAdditionsMode, + GuestAdditionsInterface: b.config.GuestAdditionsInterface, }, &vboxcommon.StepConfigureVRDP{ VRDPBindAddress: b.config.VRDPBindAddress, @@ -280,7 +286,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Delay: b.config.PostShutdownDelay, }, &vboxcommon.StepRemoveDevices{ - Bundling: b.config.VBoxBundleConfig, + Bundling: b.config.VBoxBundleConfig, + GuestAdditionsInterface: b.config.GuestAdditionsInterface, }, &vboxcommon.StepVBoxManage{ Commands: b.config.VBoxManagePost, From e2d8c0bfd78b9accb789e886bdac752b2d1a14aa Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 8 Feb 2019 09:28:10 -0800 Subject: [PATCH 24/48] docs --- builder/virtualbox/ovf/builder.go | 7 ++-- builder/virtualbox/ovf/config.go | 32 +++++++++++-------- .../docs/builders/virtualbox-iso.html.md.erb | 6 ++++ .../docs/builders/virtualbox-ovf.html.md.erb | 4 +++ 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/builder/virtualbox/ovf/builder.go b/builder/virtualbox/ovf/builder.go index 60142728f..3a8b3f901 100644 --- a/builder/virtualbox/ovf/builder.go +++ b/builder/virtualbox/ovf/builder.go @@ -84,7 +84,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ImportFlags: b.config.ImportFlags, }, &vboxcommon.StepAttachGuestAdditions{ - GuestAdditionsMode: b.config.GuestAdditionsMode, + GuestAdditionsMode: b.config.GuestAdditionsMode, + GuestAdditionsInterface: b.config.GuestAdditionsInterface, }, &vboxcommon.StepConfigureVRDP{ VRDPBindAddress: b.config.VRDPBindAddress, @@ -136,7 +137,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Timeout: b.config.ShutdownTimeout, Delay: b.config.PostShutdownDelay, }, - new(vboxcommon.StepRemoveDevices), + &vboxcommon.StepRemoveDevices{ + GuestAdditionsInterface: b.config.GuestAdditionsInterface, + }, &vboxcommon.StepVBoxManage{ Commands: b.config.VBoxManagePost, Ctx: b.config.ctx, diff --git a/builder/virtualbox/ovf/config.go b/builder/virtualbox/ovf/config.go index bd60a4c4c..03eccf029 100644 --- a/builder/virtualbox/ovf/config.go +++ b/builder/virtualbox/ovf/config.go @@ -1,4 +1,4 @@ -package ovf +Gpackage ovf import ( "fmt" @@ -28,19 +28,20 @@ type Config struct { vboxcommon.VBoxManagePostConfig `mapstructure:",squash"` vboxcommon.VBoxVersionConfig `mapstructure:",squash"` - Checksum string `mapstructure:"checksum"` - ChecksumType string `mapstructure:"checksum_type"` - GuestAdditionsMode string `mapstructure:"guest_additions_mode"` - GuestAdditionsPath string `mapstructure:"guest_additions_path"` - GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` - GuestAdditionsURL string `mapstructure:"guest_additions_url"` - ImportFlags []string `mapstructure:"import_flags"` - ImportOpts string `mapstructure:"import_opts"` - SourcePath string `mapstructure:"source_path"` - TargetPath string `mapstructure:"target_path"` - VMName string `mapstructure:"vm_name"` - KeepRegistered bool `mapstructure:"keep_registered"` - SkipExport bool `mapstructure:"skip_export"` + Checksum string `mapstructure:"checksum"` + ChecksumType string `mapstructure:"checksum_type"` + GuestAdditionsMode string `mapstructure:"guest_additions_mode"` + GuestAdditionsPath string `mapstructure:"guest_additions_path"` + GuestAdditionsInterface string `mapstructure:"guest_additions_interface"` + GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` + GuestAdditionsURL string `mapstructure:"guest_additions_url"` + ImportFlags []string `mapstructure:"import_flags"` + ImportOpts string `mapstructure:"import_opts"` + SourcePath string `mapstructure:"source_path"` + TargetPath string `mapstructure:"target_path"` + VMName string `mapstructure:"vm_name"` + KeepRegistered bool `mapstructure:"keep_registered"` + SkipExport bool `mapstructure:"skip_export"` ctx interpolate.Context } @@ -72,6 +73,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { if c.GuestAdditionsPath == "" { c.GuestAdditionsPath = "VBoxGuestAdditions.iso" } + if b.config.GuestAdditionsInterface == "" { + b.config.GuestAdditionsInterface = "ide" + } if c.VMName == "" { c.VMName = fmt.Sprintf( diff --git a/website/source/docs/builders/virtualbox-iso.html.md.erb b/website/source/docs/builders/virtualbox-iso.html.md.erb index 4337cb7c8..e7ec0b84d 100644 --- a/website/source/docs/builders/virtualbox-iso.html.md.erb +++ b/website/source/docs/builders/virtualbox-iso.html.md.erb @@ -164,6 +164,12 @@ builder. - `format` (string) - Either `ovf` or `ova`, this specifies the output format of the exported virtual machine. This defaults to `ovf`. +- `guest_additions_interface` (string) - The interface type to use to mount + guest additions when `guest_additions_mode` is set to `attach`. Will + default to the value set in `iso_interface`, if `iso_interface` is set. + Will default to "ide", if `iso_interface` is not set. Options are "ide" and + "sata". + - `guest_additions_mode` (string) - The method by which guest additions are made available to the guest for installation. Valid options are `upload`, `attach`, or `disable`. If the mode is `attach` the guest additions ISO will diff --git a/website/source/docs/builders/virtualbox-ovf.html.md.erb b/website/source/docs/builders/virtualbox-ovf.html.md.erb index d3ca4d7ae..379e5cb8a 100644 --- a/website/source/docs/builders/virtualbox-ovf.html.md.erb +++ b/website/source/docs/builders/virtualbox-ovf.html.md.erb @@ -152,6 +152,10 @@ builder. - `format` (string) - Either `ovf` or `ova`, this specifies the output format of the exported virtual machine. This defaults to `ovf`. +- `guest_additions_interface` (string) - The interface type to use to mount + guest additions when `guest_additions_mode` is set to `attach`. Will + default to "ide" if not set. Options are "ide" and "sata". + - `guest_additions_mode` (string) - The method by which guest additions are made available to the guest for installation. Valid options are `upload`, `attach`, or `disable`. If the mode is `attach` the guest additions ISO will From 3fd589fa7288934e871f6c183a7f6a186cbe2e77 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 8 Feb 2019 09:31:30 -0800 Subject: [PATCH 25/48] typos --- builder/virtualbox/ovf/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builder/virtualbox/ovf/config.go b/builder/virtualbox/ovf/config.go index 03eccf029..b49b5c948 100644 --- a/builder/virtualbox/ovf/config.go +++ b/builder/virtualbox/ovf/config.go @@ -1,4 +1,4 @@ -Gpackage ovf +package ovf import ( "fmt" @@ -73,8 +73,8 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { if c.GuestAdditionsPath == "" { c.GuestAdditionsPath = "VBoxGuestAdditions.iso" } - if b.config.GuestAdditionsInterface == "" { - b.config.GuestAdditionsInterface = "ide" + if c.GuestAdditionsInterface == "" { + c.GuestAdditionsInterface = "ide" } if c.VMName == "" { From 591533cee0607d9efc70b2a9b483c00cedb7ecd5 Mon Sep 17 00:00:00 2001 From: Yiorgos Adamopoulos Date: Mon, 11 Feb 2019 17:05:55 +0200 Subject: [PATCH 26/48] allow the basic example to pass packer validate tested with Packer v1.3.4. Without the builders section is complains for " Unknown root level key in template: " for every key in the JSON. --- website/source/docs/builders/hetzner-cloud.html.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/website/source/docs/builders/hetzner-cloud.html.md b/website/source/docs/builders/hetzner-cloud.html.md index 96e945521..9f35cac84 100644 --- a/website/source/docs/builders/hetzner-cloud.html.md +++ b/website/source/docs/builders/hetzner-cloud.html.md @@ -84,11 +84,13 @@ access tokens: ``` json { - "type": "hcloud", - "token": "YOUR API KEY", - "image": "ubuntu-18.04", - "location": "nbg1", - "server_type": "cx11", - "ssh_username": "root" + "builders": [{ + "type": "hcloud", + "token": "YOUR API KEY", + "image": "ubuntu-18.04", + "location": "nbg1", + "server_type": "cx11", + "ssh_username": "root" + }] } ``` From 37f474ceca8198b646da500ecc88997cf261ee6d Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 7 Feb 2019 15:34:48 -0800 Subject: [PATCH 27/48] add perms --- .../docs/post-processors/amazon-import.html.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/website/source/docs/post-processors/amazon-import.html.md b/website/source/docs/post-processors/amazon-import.html.md index 700542a40..2fd9c9b65 100644 --- a/website/source/docs/post-processors/amazon-import.html.md +++ b/website/source/docs/post-processors/amazon-import.html.md @@ -185,6 +185,22 @@ using ovftool. ] ``` +## Amazon Permissions + +You'll need at least the following permissions in the policy for your IAM user +in order to successfully upload an image via the amazon-import post-processor. +``` json + "ec2:CancelImportTask", + "ec2:CopyImage", + "ec2:CreateTags", + "ec2:DescribeImages", + "ec2:DescribeImportImageTasks", + "ec2:ImportImage", + "ec2:ModifyImageAttribute" + "ec2:DeregisterImage" +``` + + ## Troubleshooting Timeouts The amazon-import feature can take a long time to upload and convert your OVAs From 51b46b851a118b495d0184afa15e72bfe19f6840 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 11 Feb 2019 11:40:56 -0800 Subject: [PATCH 28/48] formatted amazon docs --- .../docs/builders/amazon-chroot.html.md | 71 ++++++++----------- .../source/docs/builders/amazon-ebs.html.md | 55 +++++++------- .../docs/builders/amazon-ebssurrogate.html.md | 53 ++++++-------- .../docs/builders/amazon-instance.html.md | 62 +++++++--------- 4 files changed, 107 insertions(+), 134 deletions(-) diff --git a/website/source/docs/builders/amazon-chroot.html.md b/website/source/docs/builders/amazon-chroot.html.md index 0bc4934ae..3b97c38eb 100644 --- a/website/source/docs/builders/amazon-chroot.html.md +++ b/website/source/docs/builders/amazon-chroot.html.md @@ -24,7 +24,7 @@ builder is able to build an EBS-backed AMI without launching a new EC2 instance. This can dramatically speed up AMI builds for organizations who need the extra fast build. -~> **This is an advanced builder** If you're just getting started with +\~> **This is an advanced builder** If you're just getting started with Packer, we recommend starting with the [amazon-ebs builder](/docs/builders/amazon-ebs.html), which is much easier to use. @@ -154,8 +154,8 @@ each category, the available configuration keys are alphabetized. associated with AMIs, which have been deregistered by `force_deregister`. Default `false`. -- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS verification of - the AWS EC2 endpoint. The default is `false`. +- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS + verification of the AWS EC2 endpoint. The default is `false`. - `kms_key_id` (string) - ID, alias or ARN of the KMS key to use for boot volume encryption. This only applies to the main `region`, other regions @@ -362,42 +362,33 @@ each category, the available configuration keys are alphabetized. [template engine](/docs/templates/engine.html), see [Build template data](#build-template-data) for more information. -- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws - secrets engine. You must already have created a role to use. For more - information about generating credentials via the Vault engine, see the - [Vault docs.] - (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) - If you set this - flag, you must also set the below options: - - `name` (string) - Required. Specifies the name of the role to generate - credentials against. This is part of the request URL. - - `engine_name` (string) - The name of the aws secrets engine. In the Vault - docs, this is normally referred to as "aws", and Packer will default to - "aws" if `engine_name` is not set. - - `role_arn` (string)- The ARN of the role to assume if credential_type on - the Vault role is assumed_role. Must match one of the allowed role ARNs - in the Vault role. Optional if the Vault role only allows a single AWS - role ARN; required otherwise. - - `ttl` (string) - Specifies the TTL for the use of the STS token. This is - specified as a string with a duration suffix. Valid only when - credential_type is assumed_role or federation_token. When not specified, - the default_sts_ttl set for the role will be used. If that is also not - set, then the default value of 3600s will be used. AWS places limits on - the maximum TTL allowed. See the AWS documentation on the DurationSeconds - parameter for AssumeRole (for assumed_role credential types) and - GetFederationToken (for federation_token credential types) for more - details. +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault + docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the + Vault docs, this is normally referred to as "aws", and Packer will + default to "aws" if `engine_name` is not set. + - `role_arn` (string)- The ARN of the role to assume if credential\_type + on the Vault role is assumed\_role. Must match one of the allowed role + ARNs in the Vault role. Optional if the Vault role only allows a single + AWS role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This + is specified as a string with a duration suffix. Valid only when + credential\_type is assumed\_role or federation\_token. When not + specified, the default\_sts\_ttl set for the role will be used. If that + is also not set, then the default value of 3600s will be used. AWS + places limits on the maximum TTL allowed. See the AWS documentation on + the DurationSeconds parameter for AssumeRole (for assumed\_role + credential types) and GetFederationToken (for federation\_token + credential types) for more details. - Example: - ``` json - { - "vault_aws_engine": { - "name": "myrole", - "role_arn": "myarn", - "ttl": "3600s" - } - } - ``` + Example: + `json { "vault_aws_engine": { "name": "myrole", "role_arn": "myarn", "ttl": "3600s" } }` ## Basic Example @@ -494,8 +485,8 @@ services: ### Ansible provisioner -Running ansible against `amazon-chroot` requires changing the Ansible connection -to chroot and running Ansible as root/sudo. +Running ansible against `amazon-chroot` requires changing the Ansible +connection to chroot and running Ansible as root/sudo. ### Using Instances with NVMe block devices. diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md index 58bcd9b9f..314bbb47c 100644 --- a/website/source/docs/builders/amazon-ebs.html.md +++ b/website/source/docs/builders/amazon-ebs.html.md @@ -47,7 +47,8 @@ builder. - `access_key` (string) - The access key used to communicate with AWS. [Learn how to set this](amazon.html#specifying-amazon-credentials). This is not - required if you are using `use_vault_aws_engine` for authentication instead. + required if you are using `use_vault_aws_engine` for authentication + instead. - `ami_name` (string) - The name of the resulting AMI that will appear when managing AMIs in the AWS console or via APIs. This must be unique. To help @@ -62,7 +63,8 @@ builder. - `secret_key` (string) - The secret key used to communicate with AWS. [Learn how to set this](amazon.html#specifying-amazon-credentials). This is not - required if you are using `use_vault_aws_engine` for authentication instead. + required if you are using `use_vault_aws_engine` for authentication + instead. - `source_ami` (string) - The initial AMI used as a base for the newly created machine. `source_ami_filter` may be used instead to populate this @@ -507,31 +509,30 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. -- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws - secrets engine. You must already have created a role to use. For more - information about generating credentials via the Vault engine, see the - [Vault docs.] - (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) - If you set this - flag, you must also set the below options: - - `name` (string) - Required. Specifies the name of the role to generate - credentials against. This is part of the request URL. - - `engine_name` (string) - The name of the aws secrets engine. In the Vault - docs, this is normally referred to as "aws", and Packer will default to - "aws" if `engine_name` is not set. - - `role_arn` (string)- The ARN of the role to assume if credential_type on - the Vault role is assumed_role. Must match one of the allowed role ARNs - in the Vault role. Optional if the Vault role only allows a single AWS - role ARN; required otherwise. - - `ttl` (string) - Specifies the TTL for the use of the STS token. This is - specified as a string with a duration suffix. Valid only when - credential_type is assumed_role or federation_token. When not specified, - the default_sts_ttl set for the role will be used. If that is also not - set, then the default value of 3600s will be used. AWS places limits on - the maximum TTL allowed. See the AWS documentation on the DurationSeconds - parameter for AssumeRole (for assumed_role credential types) and - GetFederationToken (for federation_token credential types) for more - details. +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault + docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the + Vault docs, this is normally referred to as "aws", and Packer will + default to "aws" if `engine_name` is not set. + - `role_arn` (string)- The ARN of the role to assume if credential\_type + on the Vault role is assumed\_role. Must match one of the allowed role + ARNs in the Vault role. Optional if the Vault role only allows a single + AWS role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This + is specified as a string with a duration suffix. Valid only when + credential\_type is assumed\_role or federation\_token. When not + specified, the default\_sts\_ttl set for the role will be used. If that + is also not set, then the default value of 3600s will be used. AWS + places limits on the maximum TTL allowed. See the AWS documentation on + the DurationSeconds parameter for AssumeRole (for assumed\_role + credential types) and GetFederationToken (for federation\_token + credential types) for more details. ``` json { diff --git a/website/source/docs/builders/amazon-ebssurrogate.html.md b/website/source/docs/builders/amazon-ebssurrogate.html.md index 126b770a5..04ddad84e 100644 --- a/website/source/docs/builders/amazon-ebssurrogate.html.md +++ b/website/source/docs/builders/amazon-ebssurrogate.html.md @@ -497,42 +497,33 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. - - `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws secrets engine. You must already have created a role to use. For more information about generating credentials via the Vault engine, see the - [Vault docs.] - (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + [Vault + docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) If you set this flag, you must also set the below options: - - `name` (string) - Required. Specifies the name of the role to generate - credentials against. This is part of the request URL. - - `engine_name` (string) - The name of the aws secrets engine. In the Vault - docs, this is normally referred to as "aws", and Packer will default to - "aws" if `engine_name` is not set. - - `role_arn` (string)- The ARN of the role to assume if credential_type on - the Vault role is assumed_role. Must match one of the allowed role ARNs - in the Vault role. Optional if the Vault role only allows a single AWS - role ARN; required otherwise. - - `ttl` (string) - Specifies the TTL for the use of the STS token. This is - specified as a string with a duration suffix. Valid only when - credential_type is assumed_role or federation_token. When not specified, - the default_sts_ttl set for the role will be used. If that is also not - set, then the default value of 3600s will be used. AWS places limits on - the maximum TTL allowed. See the AWS documentation on the DurationSeconds - parameter for AssumeRole (for assumed_role credential types) and - GetFederationToken (for federation_token credential types) for more - details. +- `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. +- `engine_name` (string) - The name of the aws secrets engine. In the Vault + docs, this is normally referred to as "aws", and Packer will default to + "aws" if `engine_name` is not set. +- `role_arn` (string)- The ARN of the role to assume if credential\_type on + the Vault role is assumed\_role. Must match one of the allowed role ARNs in + the Vault role. Optional if the Vault role only allows a single AWS role + ARN; required otherwise. +- `ttl` (string) - Specifies the TTL for the use of the STS token. This is + specified as a string with a duration suffix. Valid only when + credential\_type is assumed\_role or federation\_token. When not specified, + the default\_sts\_ttl set for the role will be used. If that is also not + set, then the default value of 3600s will be used. AWS places limits on the + maximum TTL allowed. See the AWS documentation on the DurationSeconds + parameter for AssumeRole (for assumed\_role credential types) and + GetFederationToken (for federation\_token credential types) for more + details. - Example: - ``` json - { - "vault_aws_engine": { - "name": "myrole", - "role_arn": "myarn", - "ttl": "3600s" - } - } - ``` + Example: + `json { "vault_aws_engine": { "name": "myrole", "role_arn": "myarn", "ttl": "3600s" } }` - `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID in order to create a temporary security group within the VPC. Requires `subnet_id` to be set. If this field is left blank, Packer will try to get diff --git a/website/source/docs/builders/amazon-instance.html.md b/website/source/docs/builders/amazon-instance.html.md index 0f74a6455..d4c81e3c9 100644 --- a/website/source/docs/builders/amazon-instance.html.md +++ b/website/source/docs/builders/amazon-instance.html.md @@ -489,43 +489,33 @@ builder. - `user_data_file` (string) - Path to a file that will be used for the user data when launching the instance. +- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws + secrets engine. You must already have created a role to use. For more + information about generating credentials via the Vault engine, see the + [Vault + docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) + If you set this flag, you must also set the below options: + - `name` (string) - Required. Specifies the name of the role to generate + credentials against. This is part of the request URL. + - `engine_name` (string) - The name of the aws secrets engine. In the + Vault docs, this is normally referred to as "aws", and Packer will + default to "aws" if `engine_name` is not set. + - `role_arn` (string)- The ARN of the role to assume if credential\_type + on the Vault role is assumed\_role. Must match one of the allowed role + ARNs in the Vault role. Optional if the Vault role only allows a single + AWS role ARN; required otherwise. + - `ttl` (string) - Specifies the TTL for the use of the STS token. This + is specified as a string with a duration suffix. Valid only when + credential\_type is assumed\_role or federation\_token. When not + specified, the default\_sts\_ttl set for the role will be used. If that + is also not set, then the default value of 3600s will be used. AWS + places limits on the maximum TTL allowed. See the AWS documentation on + the DurationSeconds parameter for AssumeRole (for assumed\_role + credential types) and GetFederationToken (for federation\_token + credential types) for more details. -- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws - secrets engine. You must already have created a role to use. For more - information about generating credentials via the Vault engine, see the - [Vault docs.] - (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials) - If you set this - flag, you must also set the below options: - - `name` (string) - Required. Specifies the name of the role to generate - credentials against. This is part of the request URL. - - `engine_name` (string) - The name of the aws secrets engine. In the Vault - docs, this is normally referred to as "aws", and Packer will default to - "aws" if `engine_name` is not set. - - `role_arn` (string)- The ARN of the role to assume if credential_type on - the Vault role is assumed_role. Must match one of the allowed role ARNs - in the Vault role. Optional if the Vault role only allows a single AWS - role ARN; required otherwise. - - `ttl` (string) - Specifies the TTL for the use of the STS token. This is - specified as a string with a duration suffix. Valid only when - credential_type is assumed_role or federation_token. When not specified, - the default_sts_ttl set for the role will be used. If that is also not - set, then the default value of 3600s will be used. AWS places limits on - the maximum TTL allowed. See the AWS documentation on the DurationSeconds - parameter for AssumeRole (for assumed_role credential types) and - GetFederationToken (for federation_token credential types) for more - details. - - Example: - ``` json - { - "vault_aws_engine": { - "name": "myrole", - "role_arn": "myarn", - "ttl": "3600s" - } - } - ``` + Example: + `json { "vault_aws_engine": { "name": "myrole", "role_arn": "myarn", "ttl": "3600s" } }` - `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID in order to create a temporary security group within the VPC. Requires From 3ae5a912d4f04c7f149701dbadc7197c952f0a4a Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Mon, 11 Feb 2019 11:41:58 -0800 Subject: [PATCH 29/48] Update builder/amazon/common/step_pre_validate.go Co-Authored-By: SwampDragons --- builder/amazon/common/step_pre_validate.go | 1 + 1 file changed, 1 insertion(+) diff --git a/builder/amazon/common/step_pre_validate.go b/builder/amazon/common/step_pre_validate.go index 018457b21..475d2065c 100644 --- a/builder/amazon/common/step_pre_validate.go +++ b/builder/amazon/common/step_pre_validate.go @@ -43,6 +43,7 @@ func (s *StepPreValidate) Run(_ context.Context, state multistep.StateBag) multi " to pass authentication... trying again.") return false, nil } + return true, err } else { return true, nil } From 2a613dd6e96c91a1a53283e5b0a5c74ce91ed1c0 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Mon, 11 Feb 2019 11:49:17 -0800 Subject: [PATCH 30/48] fix up the github suggestion --- builder/amazon/common/step_pre_validate.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/builder/amazon/common/step_pre_validate.go b/builder/amazon/common/step_pre_validate.go index 475d2065c..f420be990 100644 --- a/builder/amazon/common/step_pre_validate.go +++ b/builder/amazon/common/step_pre_validate.go @@ -44,11 +44,8 @@ func (s *StepPreValidate) Run(_ context.Context, state multistep.StateBag) multi return false, nil } return true, err - } else { - return true, nil } - - return true, err + return true, nil }) if err != nil { From 203b6ca8b4a973c5d65d61f1865b6ae87edfd1c6 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Mon, 11 Feb 2019 18:20:35 -0500 Subject: [PATCH 31/48] Prefix env vars w/ 'DIGITALOCEAN_' --- post-processor/digitalocean-import/post-processor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/post-processor/digitalocean-import/post-processor.go b/post-processor/digitalocean-import/post-processor.go index ac456a4c9..fdc8e3af5 100644 --- a/post-processor/digitalocean-import/post-processor.go +++ b/post-processor/digitalocean-import/post-processor.go @@ -81,11 +81,11 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } if p.config.SpacesKey == "" { - p.config.SpacesKey = os.Getenv("SPACES_ACCESS_KEY") + p.config.SpacesKey = os.Getenv("DIGITALOCEAN_SPACES_ACCESS_KEY") } if p.config.SpacesSecret == "" { - p.config.SpacesSecret = os.Getenv("SPACES_SECRET_KEY") + p.config.SpacesSecret = os.Getenv("DIGITALOCEAN_SPACES_SECRET_KEY") } if p.config.APIToken == "" { From 62865cf37bc6354e263630665228f5de340bc887 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Mon, 11 Feb 2019 18:24:16 -0500 Subject: [PATCH 32/48] Mention environmental variables in docs. --- .../docs/post-processors/digitalocean-import.html.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/website/source/docs/post-processors/digitalocean-import.html.md b/website/source/docs/post-processors/digitalocean-import.html.md index d4ef4c1c6..3120b2acb 100644 --- a/website/source/docs/post-processors/digitalocean-import.html.md +++ b/website/source/docs/post-processors/digitalocean-import.html.md @@ -31,11 +31,16 @@ There are some configuration options available for the post-processor. Required: - `api_token` (string) - A personal access token used to communicate with - the DigitalOcean v2 API. + the DigitalOcean v2 API. This may also be set using the + `DIGITALOCEAN_API_TOKEN` environmental variable. - `spaces_key` (string) - The access key used to communicate with Spaces. + This may also be set using the `DIGITALOCEAN_SPACES_ACCESS_KEY` + environmental variable. - `spaces_secret` (string) - The secret key used to communicate with Spaces. + This may also be set using the `DIGITALOCEAN_SPACES_SECRET_KEY` + environmental variable. - `spaces_region` (string) - The name of the region, such as `nyc3`, in which to upload the image to Spaces. @@ -56,7 +61,7 @@ Optional: - `image_description` (string) - The description to set for the resulting imported image. -- `image_distribution` (string) - The name of the distribution to set for +- `image_distribution` (string) - The name of the distribution to set for the resulting imported image. - `image_tags` (array of strings) - A list of tags to apply to the resulting From 8526244285f3fe1119f981fcd125592355752d71 Mon Sep 17 00:00:00 2001 From: xinau Date: Tue, 12 Feb 2019 06:32:19 +0000 Subject: [PATCH 33/48] changed local_port to uint representation --- provisioner/inspec/provisioner.go | 33 +++++++++---------- provisioner/inspec/provisioner_test.go | 4 +-- .../source/docs/provisioners/inspec.html.md | 8 ++--- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index b520c1a66..c5e2f747f 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -51,7 +51,7 @@ type Config struct { Backend string `mapstructure:"backend"` User string `mapstructure:"user"` Host string `mapstructure:"host"` - LocalPort string `mapstructure:"local_port"` + LocalPort uint `mapstructure:"local_port"` SSHHostKeyFile string `mapstructure:"ssh_host_key_file"` SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"` } @@ -109,24 +109,20 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } } - if _, ok := SupportedBackends[p.config.Backend]; !ok { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend)) - } - if p.config.Backend == "" { p.config.Backend = "ssh" } + if _, ok := SupportedBackends[p.config.Backend]; !ok { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend)) + } + if p.config.Host == "" { p.config.Host = "127.0.0.1" } - if len(p.config.LocalPort) > 0 { - if _, err := strconv.ParseUint(p.config.LocalPort, 10, 16); err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %s must be a valid port", p.config.LocalPort)) - } - } else { - p.config.LocalPort = "0" + if p.config.LocalPort > 65535 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %d must be a valid port", p.config.LocalPort)) } if len(p.config.AttributesDirectory) > 0 { @@ -245,11 +241,8 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { config.AddHostKey(hostSigner) localListener, err := func() (net.Listener, error) { - port, err := strconv.ParseUint(p.config.LocalPort, 10, 16) - if err != nil { - return nil, err - } + port := p.config.LocalPort tries := 1 if port != 0 { tries = 10 @@ -261,11 +254,17 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Say(err.Error()) continue } - _, p.config.LocalPort, err = net.SplitHostPort(l.Addr().String()) + _, portStr, err := net.SplitHostPort(l.Addr().String()) if err != nil { ui.Say(err.Error()) continue } + portUint64, err := strconv.ParseUint(portStr, 10, 0) + if err != nil { + ui.Say(err.Error()) + continue + } + p.config.LocalPort = uint(portUint64) return l, nil } return nil, errors.New("Error setting up SSH proxy connection") @@ -331,7 +330,7 @@ func (p *Provisioner) executeInspec(ui packer.Ui, comm packer.Communicator, priv args = append(args, "--key-files", privKeyFile) } args = append(args, "--user", p.config.User) - args = append(args, "--port", p.config.LocalPort) + args = append(args, "--port", strconv.FormatUint(uint64(p.config.LocalPort), 10)) } args = append(args, "--attrs") diff --git a/provisioner/inspec/provisioner_test.go b/provisioner/inspec/provisioner_test.go index d38d1f0c6..6857f7004 100644 --- a/provisioner/inspec/provisioner_test.go +++ b/provisioner/inspec/provisioner_test.go @@ -254,13 +254,13 @@ func TestProvisionerPrepare_LocalPort(t *testing.T) { config["ssh_authorized_key_file"] = publickey_file.Name() config["profile"] = profile_file.Name() - config["local_port"] = "65537" + config["local_port"] = uint(65537) err = p.Prepare(config) if err == nil { t.Fatal("should have error") } - config["local_port"] = "22222" + config["local_port"] = uint(22222) err = p.Prepare(config) if err != nil { t.Fatalf("err: %s", err) diff --git a/website/source/docs/provisioners/inspec.html.md b/website/source/docs/provisioners/inspec.html.md index 809b33b5e..56809050d 100644 --- a/website/source/docs/provisioners/inspec.html.md +++ b/website/source/docs/provisioners/inspec.html.md @@ -45,7 +45,7 @@ Replace the mock `api_token` value with your own. Required Parameters: -- `profile` - The profile to be executed by InSpec. +- `profile` (string) - The profile to be executed by InSpec. Optional Parameters: @@ -53,9 +53,7 @@ Optional Parameters: running InSpec. Usage example: ``` json - { "inspec_env_vars": [ "FOO=bar" ] - } ``` - `command` (string) - The command to invoke InSpec. Defaults to `inspec`. @@ -65,9 +63,7 @@ Optional Parameters: not be quoted. Usage example: ``` json - { "extra_arguments": [ "--sudo", "--reporter", "json" ] - } ``` - `attributes` (array of strings) - Attribute Files used by InSpec which will @@ -89,7 +85,7 @@ Optional Parameters: - `host` (string) - Host used for by InSpec for connection. Defaults to localhost. -- `local_port` (string) - The port on which to attempt to listen for SSH +- `local_port` (uint) - The port on which to attempt to listen for SSH connections. This value is a starting point. The provisioner will attempt to listen for SSH connections on the first available of ten ports, starting at `local_port`. A system-chosen port is used when `local_port` is missing or From 36c1e8d838d3d0cd94e738ee50fe40064e7c30ba Mon Sep 17 00:00:00 2001 From: xinau Date: Tue, 12 Feb 2019 07:10:57 +0000 Subject: [PATCH 34/48] moved adapter to common package --- .../ansible => common/adapter}/adapter.go | 22 +- .../inspec => common/adapter}/adapter_test.go | 4 +- .../ansible => common/adapter}/scp.go | 2 +- provisioner/ansible/adapter_test.go | 116 ------- provisioner/ansible/provisioner.go | 5 +- provisioner/inspec/adapter.go | 285 ------------------ provisioner/inspec/provisioner.go | 5 +- 7 files changed, 20 insertions(+), 419 deletions(-) rename {provisioner/ansible => common/adapter}/adapter.go (93%) rename {provisioner/inspec => common/adapter}/adapter_test.go (96%) rename {provisioner/ansible => common/adapter}/scp.go (99%) delete mode 100644 provisioner/ansible/adapter_test.go delete mode 100644 provisioner/inspec/adapter.go diff --git a/provisioner/ansible/adapter.go b/common/adapter/adapter.go similarity index 93% rename from provisioner/ansible/adapter.go rename to common/adapter/adapter.go index c3dfd3495..510ae40bb 100644 --- a/provisioner/ansible/adapter.go +++ b/common/adapter/adapter.go @@ -1,4 +1,4 @@ -package ansible +package adapter import ( "bytes" @@ -17,7 +17,7 @@ import ( // An adapter satisfies SSH requests (from an Ansible client) by delegating SSH // exec and subsystem commands to a packer.Communicator. -type adapter struct { +type Adapter struct { done <-chan struct{} l net.Listener config *ssh.ServerConfig @@ -26,8 +26,8 @@ type adapter struct { comm packer.Communicator } -func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *adapter { - return &adapter{ +func NewAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *Adapter { + return &Adapter{ done: done, l: l, config: config, @@ -37,7 +37,7 @@ func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, } } -func (c *adapter) Serve() { +func (c *Adapter) Serve() { log.Printf("SSH proxy: serving on %s", c.l.Addr()) for { @@ -62,7 +62,7 @@ func (c *adapter) Serve() { } } -func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error { +func (c *Adapter) Handle(conn net.Conn, ui packer.Ui) error { log.Print("SSH proxy: accepted connection") _, chans, reqs, err := ssh.NewServerConn(conn, c.config) if err != nil { @@ -89,7 +89,7 @@ func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error { return nil } -func (c *adapter) handleSession(newChannel ssh.NewChannel) error { +func (c *Adapter) handleSession(newChannel ssh.NewChannel) error { channel, requests, err := newChannel.Accept() if err != nil { return err @@ -182,11 +182,11 @@ func (c *adapter) handleSession(newChannel ssh.NewChannel) error { return nil } -func (c *adapter) Shutdown() { +func (c *Adapter) Shutdown() { c.l.Close() } -func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int { +func (c *Adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int { var exitStatus int switch { case strings.HasPrefix(command, "scp ") && serveSCP(command[4:]): @@ -206,7 +206,7 @@ func serveSCP(args string) bool { return bytes.IndexAny(opts, "tf") >= 0 } -func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error { +func (c *Adapter) scpExec(args string, in io.Reader, out io.Writer) error { opts, rest := scpOptions(args) // remove the quoting that ansible added to rest for shell safety. @@ -226,7 +226,7 @@ func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error { return errors.New("no scp mode specified") } -func (c *adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int { +func (c *Adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int { cmd := &packer.RemoteCmd{ Stdin: in, Stdout: out, diff --git a/provisioner/inspec/adapter_test.go b/common/adapter/adapter_test.go similarity index 96% rename from provisioner/inspec/adapter_test.go rename to common/adapter/adapter_test.go index 638cead58..a43b3bedc 100644 --- a/provisioner/inspec/adapter_test.go +++ b/common/adapter/adapter_test.go @@ -1,4 +1,4 @@ -package inspec +package adapter import ( "errors" @@ -26,7 +26,7 @@ func TestAdapter_Serve(t *testing.T) { ui := new(packer.NoopUi) - sut := newAdapter(done, &l, config, newUi(ui), communicator{}) + sut := NewAdapter(done, &l, config, "", ui, communicator{}) go func() { i := 0 for range acceptC { diff --git a/provisioner/ansible/scp.go b/common/adapter/scp.go similarity index 99% rename from provisioner/ansible/scp.go rename to common/adapter/scp.go index ca029605d..06043ff20 100644 --- a/provisioner/ansible/scp.go +++ b/common/adapter/scp.go @@ -1,4 +1,4 @@ -package ansible +package adapter import ( "bufio" diff --git a/provisioner/ansible/adapter_test.go b/provisioner/ansible/adapter_test.go deleted file mode 100644 index 29667cbe2..000000000 --- a/provisioner/ansible/adapter_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package ansible - -import ( - "errors" - "io" - "log" - "net" - "os" - "testing" - "time" - - "github.com/hashicorp/packer/packer" - - "golang.org/x/crypto/ssh" -) - -func TestAdapter_Serve(t *testing.T) { - - // done signals the adapter that the provisioner is done - done := make(chan struct{}) - - acceptC := make(chan struct{}) - l := listener{done: make(chan struct{}), acceptC: acceptC} - - config := &ssh.ServerConfig{} - - ui := new(packer.NoopUi) - - sut := newAdapter(done, &l, config, "", newUi(ui), communicator{}) - go func() { - i := 0 - for range acceptC { - i++ - if i == 4 { - close(done) - l.Close() - } - } - }() - - sut.Serve() -} - -type listener struct { - done chan struct{} - acceptC chan<- struct{} - i int -} - -func (l *listener) Accept() (net.Conn, error) { - log.Println("Accept() called") - l.acceptC <- struct{}{} - select { - case <-l.done: - log.Println("done, serving an error") - return nil, errors.New("listener is closed") - - case <-time.After(10 * time.Millisecond): - l.i++ - - if l.i%2 == 0 { - c1, c2 := net.Pipe() - - go func(c net.Conn) { - <-time.After(100 * time.Millisecond) - log.Println("closing c") - c.Close() - }(c1) - - return c2, nil - } - } - - return nil, errors.New("accept error") -} - -func (l *listener) Close() error { - close(l.done) - return nil -} - -func (l *listener) Addr() net.Addr { - return addr{} -} - -type addr struct{} - -func (a addr) Network() string { - return a.String() -} - -func (a addr) String() string { - return "test" -} - -type communicator struct{} - -func (c communicator) Start(*packer.RemoteCmd) error { - return errors.New("communicator not supported") -} - -func (c communicator) Upload(string, io.Reader, *os.FileInfo) error { - return errors.New("communicator not supported") -} - -func (c communicator) UploadDir(dst string, src string, exclude []string) error { - return errors.New("communicator not supported") -} - -func (c communicator) Download(string, io.Writer) error { - return errors.New("communicator not supported") -} - -func (c communicator) DownloadDir(src string, dst string, exclude []string) error { - return errors.New("communicator not supported") -} diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 9979fbd2a..7ea29f584 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -26,6 +26,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/adapter" commonhelper "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" @@ -63,7 +64,7 @@ type Config struct { type Provisioner struct { config Config - adapter *adapter + adapter *adapter.Adapter done chan struct{} ansibleVersion string ansibleMajVersion uint @@ -286,7 +287,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } ui = newUi(ui) - p.adapter = newAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) + p.adapter = adapter.NewAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) defer func() { log.Print("shutting down the SSH proxy") diff --git a/provisioner/inspec/adapter.go b/provisioner/inspec/adapter.go deleted file mode 100644 index ebd52d0a3..000000000 --- a/provisioner/inspec/adapter.go +++ /dev/null @@ -1,285 +0,0 @@ -package inspec - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "log" - "net" - - "github.com/hashicorp/packer/packer" - "golang.org/x/crypto/ssh" -) - -// An adapter satisfies SSH requests (from an Inspec client) by delegating SSH -// exec and subsystem commands to a packer.Communicator. -type adapter struct { - done <-chan struct{} - l net.Listener - config *ssh.ServerConfig - ui packer.Ui - comm packer.Communicator -} - -func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, ui packer.Ui, comm packer.Communicator) *adapter { - return &adapter{ - done: done, - l: l, - config: config, - ui: ui, - comm: comm, - } -} - -func (c *adapter) Serve() { - log.Printf("SSH proxy: serving on %s", c.l.Addr()) - - for { - // Accept will return if either the underlying connection is closed or if a connection is made. - // after returning, check to see if c.done can be received. If so, then Accept() returned because - // the connection has been closed. - conn, err := c.l.Accept() - select { - case <-c.done: - return - default: - if err != nil { - c.ui.Error(fmt.Sprintf("listen.Accept failed: %v", err)) - continue - } - go func(conn net.Conn) { - if err := c.Handle(conn, c.ui); err != nil { - c.ui.Error(err.Error()) - } - }(conn) - } - } -} - -func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error { - log.Print("SSH proxy: accepted connection") - _, chans, reqs, err := ssh.NewServerConn(conn, c.config) - if err != nil { - return errors.New("failed to handshake") - } - - // discard all global requests - go ssh.DiscardRequests(reqs) - - // Service the incoming NewChannels - for newChannel := range chans { - if newChannel.ChannelType() != "session" { - newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") - continue - } - - go func(ch ssh.NewChannel) { - if err := c.handleSession(ch); err != nil { - c.ui.Error(err.Error()) - } - }(newChannel) - } - - return nil -} - -func (c *adapter) handleSession(newChannel ssh.NewChannel) error { - channel, requests, err := newChannel.Accept() - if err != nil { - return err - } - defer channel.Close() - - done := make(chan struct{}) - - // Sessions have requests such as "pty-req", "shell", "env", and "exec". - // see RFC 4254, section 6 - go func(in <-chan *ssh.Request) { - env := make([]envRequestPayload, 4) - for req := range in { - switch req.Type { - case "pty-req": - log.Println("inspec provisioner pty-req request") - // accept pty-req requests, but don't actually do anything. Necessary for OpenSSH and sudo. - req.Reply(true, nil) - - case "env": - req, err := newEnvRequest(req) - if err != nil { - c.ui.Error(err.Error()) - req.Reply(false, nil) - continue - } - env = append(env, req.Payload) - log.Printf("new env request: %s", req.Payload) - req.Reply(true, nil) - case "exec": - req, err := newExecRequest(req) - if err != nil { - c.ui.Error(err.Error()) - req.Reply(false, nil) - close(done) - continue - } - - log.Printf("new exec request: %s", req.Payload) - - if len(req.Payload) == 0 { - req.Reply(false, nil) - close(done) - return - } - - go func(channel ssh.Channel) { - exit := c.exec(string(req.Payload), channel, channel, channel.Stderr()) - - exitStatus := make([]byte, 4) - binary.BigEndian.PutUint32(exitStatus, uint32(exit)) - channel.SendRequest("exit-status", false, exitStatus) - close(done) - }(channel) - req.Reply(true, nil) - case "subsystem": - req, err := newSubsystemRequest(req) - if err != nil { - c.ui.Error(err.Error()) - req.Reply(false, nil) - continue - } - - log.Printf("new subsystem request: %s", req.Payload) - - c.ui.Error(fmt.Sprintf("unsupported subsystem requested: %s", req.Payload)) - req.Reply(false, nil) - default: - log.Printf("rejecting %s request", req.Type) - req.Reply(false, nil) - } - } - }(requests) - - <-done - return nil -} - -func (c *adapter) Shutdown() { - c.l.Close() -} - -func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int { - var exitStatus int - exitStatus = c.remoteExec(command, in, out, err) - return exitStatus -} - -func (c *adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int { - cmd := &packer.RemoteCmd{ - Stdin: in, - Stdout: out, - Stderr: err, - Command: command, - } - - if err := c.comm.Start(cmd); err != nil { - c.ui.Error(err.Error()) - return cmd.ExitStatus - } - - cmd.Wait() - - return cmd.ExitStatus -} - -type envRequest struct { - *ssh.Request - Payload envRequestPayload -} - -type envRequestPayload struct { - Name string - Value string -} - -func (p envRequestPayload) String() string { - return fmt.Sprintf("%s=%s", p.Name, p.Value) -} - -func newEnvRequest(raw *ssh.Request) (*envRequest, error) { - r := new(envRequest) - r.Request = raw - - if err := ssh.Unmarshal(raw.Payload, &r.Payload); err != nil { - return nil, err - } - - return r, nil -} - -func sshString(buf io.Reader) (string, error) { - var size uint32 - err := binary.Read(buf, binary.BigEndian, &size) - if err != nil { - return "", err - } - - b := make([]byte, size) - err = binary.Read(buf, binary.BigEndian, b) - if err != nil { - return "", err - } - return string(b), nil -} - -type execRequest struct { - *ssh.Request - Payload execRequestPayload -} - -type execRequestPayload string - -func (p execRequestPayload) String() string { - return string(p) -} - -func newExecRequest(raw *ssh.Request) (*execRequest, error) { - r := new(execRequest) - r.Request = raw - buf := bytes.NewReader(r.Request.Payload) - - var err error - var payload string - if payload, err = sshString(buf); err != nil { - return nil, err - } - - r.Payload = execRequestPayload(payload) - return r, nil -} - -type subsystemRequest struct { - *ssh.Request - Payload subsystemRequestPayload -} - -type subsystemRequestPayload string - -func (p subsystemRequestPayload) String() string { - return string(p) -} - -func newSubsystemRequest(raw *ssh.Request) (*subsystemRequest, error) { - r := new(subsystemRequest) - r.Request = raw - buf := bytes.NewReader(r.Request.Payload) - - var err error - var payload string - if payload, err = sshString(buf); err != nil { - return nil, err - } - - r.Payload = subsystemRequestPayload(payload) - return r, nil -} diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index c5e2f747f..f2505351d 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -25,6 +25,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/adapter" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" @@ -58,7 +59,7 @@ type Config struct { type Provisioner struct { config Config - adapter *adapter + adapter *adapter.Adapter done chan struct{} inspecVersion string inspecMajVersion uint @@ -275,7 +276,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } ui = newUi(ui) - p.adapter = newAdapter(p.done, localListener, config, ui, comm) + p.adapter = adapter.NewAdapter(p.done, localListener, config, "", ui, comm) defer func() { log.Print("shutting down the SSH proxy") From bc69f3e1c31c011f0a9630f53eb5ce163d36d903 Mon Sep 17 00:00:00 2001 From: Christopher Boumenot Date: Tue, 12 Feb 2019 10:49:05 -0800 Subject: [PATCH 35/48] azure: new maintainer --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9bfeef5b4..ac0d593d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,7 +5,7 @@ /builder/alicloud/ @chhaj5236 /builder/amazon/ebssurrogate/ @jen20 /builder/amazon/ebsvolume/ @jen20 -/builder/azure/ @boumenot +/builder/azure/ @paulmey /builder/hyperv/ @taliesins /builder/lxc/ @ChrisLundquist /builder/lxd/ @ChrisLundquist From ae59b81f447e7aa2c4daa36c3500f060c30c5cd0 Mon Sep 17 00:00:00 2001 From: xinau Date: Tue, 12 Feb 2019 20:07:13 +0000 Subject: [PATCH 36/48] added check for docker backend --- provisioner/inspec/provisioner.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index f2505351d..7647b4286 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -118,6 +118,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend)) } + if p.config.Backend == "docker" && p.config.Host == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: host must be specified for docker backend")) + } + if p.config.Host == "" { p.config.Host = "127.0.0.1" } From ee430d9a40753e4e814bb9f915b07ca73b13c462 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Tue, 12 Feb 2019 20:20:39 -0500 Subject: [PATCH 37/48] Only filter on suffix if more than one file. --- .../digitalocean-import/post-processor.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/post-processor/digitalocean-import/post-processor.go b/post-processor/digitalocean-import/post-processor.go index fdc8e3af5..b3e7547d9 100644 --- a/post-processor/digitalocean-import/post-processor.go +++ b/post-processor/digitalocean-import/post-processor.go @@ -145,20 +145,26 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac } log.Printf("Rendered space_object_name as %s", p.config.ObjectName) - log.Println("Looking for image in artifact") source := "" - validSuffix := []string{"raw", "img", "qcow2", "vhdx", "vdi", "vmdk", "bz2", "tar.xz", "tar.gz"} - for _, path := range artifact.Files() { - for _, suffix := range validSuffix { - if strings.HasSuffix(path, suffix) { - source = path + artifacts := artifact.Files() + log.Println("Looking for image in artifact") + if len(artifacts) > 1 { + validSuffix := []string{"raw", "img", "qcow2", "vhdx", "vdi", "vmdk", "tar.bz2", "tar.xz", "tar.gz"} + for _, path := range artifact.Files() { + for _, suffix := range validSuffix { + if strings.HasSuffix(path, suffix) { + source = path + break + } + } + if source != "" { break } } - if source != "" { - break - } + } else { + source = artifact.Files()[0] } + if source == "" { return nil, false, fmt.Errorf("Image file not found") } From b449af84ee334d94e305b432db732259cfe2fb20 Mon Sep 17 00:00:00 2001 From: xinau Date: Wed, 13 Feb 2019 21:01:50 +0000 Subject: [PATCH 38/48] moved concurrency-safe ui code to packer/ui.go --- packer/ui.go | 47 ++++++++++++++++++++++++++- provisioner/ansible/provisioner.go | 51 +++--------------------------- provisioner/inspec/provisioner.go | 51 +++--------------------------- 3 files changed, 54 insertions(+), 95 deletions(-) diff --git a/packer/ui.go b/packer/ui.go index 2a0f5e275..e42e08139 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -343,7 +343,7 @@ func (u *MachineReadableUi) ProgressBar() ProgressBar { return new(NoopProgressBar) } -// TimestampedUi is a UI that wraps another UI implementation and prefixes +// TimestampedUi is a UI that wraps another UI implementation and // prefixes each message with an RFC3339 timestamp type TimestampedUi struct { Ui Ui @@ -376,3 +376,48 @@ func (u *TimestampedUi) ProgressBar() ProgressBar { return u.Ui.ProgressBar() } func (u *TimestampedUi) timestampLine(string string) string { return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string) } + +// Safe is a UI that wraps another UI implementation and +// provides concurrency-safe access +type SafeUi struct { + Sem chan int + Ui Ui +} + +var _ Ui = new(SafeUi) + +func (u *SafeUi) Ask(s string) (string, error) { + u.Sem <- 1 + ret, err := u.Ui.Ask(s) + <-u.Sem + + return ret, err +} + +func (u *SafeUi) Say(s string) { + u.Sem <- 1 + u.Ui.Say(s) + <-u.Sem +} + +func (u *SafeUi) Message(s string) { + u.Sem <- 1 + u.Ui.Message(s) + <-u.Sem +} + +func (u *SafeUi) Error(s string) { + u.Sem <- 1 + u.Ui.Error(s) + <-u.Sem +} + +func (u *SafeUi) Machine(t string, args ...string) { + u.Sem <- 1 + u.Ui.Machine(t, args...) + <-u.Sem +} + +func (u *SafeUi) ProgressBar() ProgressBar { + return new(NoopProgressBar) +} diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 7ea29f584..e836eb894 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -286,7 +286,10 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } - ui = newUi(ui) + ui = &packer.SafeUi{ + Sem: make(chan int, 1), + Ui: ui, + } p.adapter = adapter.NewAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm) defer func() { @@ -557,49 +560,3 @@ func getWinRMPassword(buildName string) string { packer.LogSecretFilter.Set(winRMPass) return winRMPass } - -// Ui provides concurrency-safe access to packer.Ui. -type Ui struct { - sem chan int - ui packer.Ui -} - -func newUi(ui packer.Ui) packer.Ui { - return &Ui{sem: make(chan int, 1), ui: ui} -} - -func (ui *Ui) Ask(s string) (string, error) { - ui.sem <- 1 - ret, err := ui.ui.Ask(s) - <-ui.sem - - return ret, err -} - -func (ui *Ui) Say(s string) { - ui.sem <- 1 - ui.ui.Say(s) - <-ui.sem -} - -func (ui *Ui) Message(s string) { - ui.sem <- 1 - ui.ui.Message(s) - <-ui.sem -} - -func (ui *Ui) Error(s string) { - ui.sem <- 1 - ui.ui.Error(s) - <-ui.sem -} - -func (ui *Ui) Machine(t string, args ...string) { - ui.sem <- 1 - ui.ui.Machine(t, args...) - <-ui.sem -} - -func (ui *Ui) ProgressBar() packer.ProgressBar { - return new(packer.NoopProgressBar) -} diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index 7647b4286..fab92ee49 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -279,7 +279,10 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } - ui = newUi(ui) + ui = &packer.SafeUi{ + Sem: make(chan int, 1), + Ui: ui, + } p.adapter = adapter.NewAdapter(p.done, localListener, config, "", ui, comm) defer func() { @@ -519,49 +522,3 @@ func newSigner(privKeyFile string) (*signer, error) { return signer, nil } - -// Ui provides concurrency-safe access to packer.Ui. -type Ui struct { - sem chan int - ui packer.Ui -} - -func newUi(ui packer.Ui) packer.Ui { - return &Ui{sem: make(chan int, 1), ui: ui} -} - -func (ui *Ui) Ask(s string) (string, error) { - ui.sem <- 1 - ret, err := ui.ui.Ask(s) - <-ui.sem - - return ret, err -} - -func (ui *Ui) Say(s string) { - ui.sem <- 1 - ui.ui.Say(s) - <-ui.sem -} - -func (ui *Ui) Message(s string) { - ui.sem <- 1 - ui.ui.Message(s) - <-ui.sem -} - -func (ui *Ui) Error(s string) { - ui.sem <- 1 - ui.ui.Error(s) - <-ui.sem -} - -func (ui *Ui) Machine(t string, args ...string) { - ui.sem <- 1 - ui.ui.Machine(t, args...) - <-ui.sem -} - -func (ui *Ui) ProgressBar() packer.ProgressBar { - return new(packer.NoopProgressBar) -} From bd3339f1f93a4392ff0ac65d93c126cfff8786ef Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Wed, 13 Feb 2019 15:41:37 -0800 Subject: [PATCH 39/48] also strip \r to work around windows file ending strangeness --- builder/vmware/common/driver_esx5.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/vmware/common/driver_esx5.go b/builder/vmware/common/driver_esx5.go index 602680ddc..4b083e5ff 100644 --- a/builder/vmware/common/driver_esx5.go +++ b/builder/vmware/common/driver_esx5.go @@ -47,7 +47,7 @@ type ESX5Driver struct { func (d *ESX5Driver) Clone(dst, src string, linked bool) error { - linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\n"), "\n") } + linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\r\n"), "\n") } d.SetOutputDir(path.Dir(filepath.ToSlash(dst))) srcVmx := d.datastorePath(src) From 37b330530174579add6facad7e1d70401a41e33a Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Wed, 13 Feb 2019 18:55:27 -0500 Subject: [PATCH 40/48] Better varibable naming. --- post-processor/digitalocean-import/post-processor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/post-processor/digitalocean-import/post-processor.go b/post-processor/digitalocean-import/post-processor.go index b3e7547d9..d7dcd867b 100644 --- a/post-processor/digitalocean-import/post-processor.go +++ b/post-processor/digitalocean-import/post-processor.go @@ -111,7 +111,7 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { errs, fmt.Errorf("Error parsing space_object_name template: %s", err)) } - templates := map[string]*string{ + requiredArgs := map[string]*string{ "api_token": &p.config.APIToken, "spaces_key": &p.config.SpacesKey, "spaces_secret": &p.config.SpacesSecret, @@ -120,7 +120,7 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { "image_name": &p.config.Name, "image_regions": &p.config.ImageRegions[0], } - for key, ptr := range templates { + for key, ptr := range requiredArgs { if *ptr == "" { errs = packer.MultiErrorAppend( errs, fmt.Errorf("%s must be set", key)) From 2a9f49a5c6b58a69373d9af86838498799444810 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Wed, 13 Feb 2019 16:21:55 -0800 Subject: [PATCH 41/48] make sure we don't try to load an iso into memory because of a user mistake --- common/iso_config.go | 5 +++++ common/iso_config_test.go | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/common/iso_config.go b/common/iso_config.go index 4f30580bc..75d285e11 100644 --- a/common/iso_config.go +++ b/common/iso_config.go @@ -58,6 +58,11 @@ func (c *ISOConfig) Prepare(ctx *interpolate.Context) (warnings []string, errs [ // If iso_checksum has no value use iso_checksum_url instead. if c.ISOChecksum == "" { + if strings.HasSuffix(strings.ToLower(c.ISOChecksumURL), ".iso") { + errs = append(errs, fmt.Errorf("Error parsing checksum:"+ + " .iso is not a valid checksum extension")) + return warnings, errs + } u, err := url.Parse(c.ISOChecksumURL) if err != nil { errs = append(errs, diff --git a/common/iso_config_test.go b/common/iso_config_test.go index 0a054f700..c38154f42 100644 --- a/common/iso_config_test.go +++ b/common/iso_config_test.go @@ -239,6 +239,16 @@ func TestISOConfigPrepare_ISOChecksumURL(t *testing.T) { if i.ISOChecksum != "bar0" { t.Fatalf("should've found \"bar0\" got: %s", i.ISOChecksum) } + + // Test that we won't try to read an iso into memory because of a user + // error + i = testISOConfig() + i.ISOChecksumURL = "file:///not_read.iso" + i.ISOChecksum = "" + warns, err = i.Prepare(nil) + if err == nil { + t.Fatalf("should have error because iso is bad filetype: %s", err) + } } func TestISOConfigPrepare_ISOChecksumType(t *testing.T) { From 7e7e4869011f958669bb8b0e61943c6c2d26d1e9 Mon Sep 17 00:00:00 2001 From: Kristi Date: Thu, 14 Feb 2019 11:53:53 -0800 Subject: [PATCH 42/48] add DescribeInstanceStatus to minimum permissions Packer needs DescribeInstanceStatus or else it ends up waiting forever for the instance to become ready. --- website/source/docs/builders/amazon.html.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/source/docs/builders/amazon.html.md b/website/source/docs/builders/amazon.html.md index 931ff86ae..3ea0b44a7 100644 --- a/website/source/docs/builders/amazon.html.md +++ b/website/source/docs/builders/amazon.html.md @@ -151,6 +151,7 @@ for Packer to work: "ec2:DescribeImageAttribute", "ec2:DescribeImages", "ec2:DescribeInstances", + "ec2:DescribeInstanceStatus", "ec2:DescribeRegions", "ec2:DescribeSecurityGroups", "ec2:DescribeSnapshots", From bbc52f9a7d24297630fcaf127fceedd861d8ecc5 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 14 Feb 2019 12:10:58 -0800 Subject: [PATCH 43/48] add noSSLVerify to ovftool args --- builder/vmware/common/driver_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/vmware/common/driver_config.go b/builder/vmware/common/driver_config.go index b4014768f..b600ae237 100644 --- a/builder/vmware/common/driver_config.go +++ b/builder/vmware/common/driver_config.go @@ -69,7 +69,7 @@ func (c *DriverConfig) Validate(SkipExport bool) error { // now, so that we don't fail for a simple mistake after a long // build ovftool := GetOVFTool() - ovfToolArgs := []string{"--verifyOnly", fmt.Sprintf("vi://%s:%s@%s", + ovfToolArgs := []string{"--noSSLVerify", "--verifyOnly", fmt.Sprintf("vi://%s:%s@%s", url.QueryEscape(c.RemoteUser), url.QueryEscape(c.RemotePassword), c.RemoteHost)} From 9f702af6d9e6a06cc55be282081437a042a3103a Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 14 Feb 2019 14:46:14 -0800 Subject: [PATCH 44/48] we lost the skip_add step somewhere --- builder/vagrant/builder.go | 1 + builder/vagrant/step_add_box.go | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/builder/vagrant/builder.go b/builder/vagrant/builder.go index 469270298..a0cd3202a 100644 --- a/builder/vagrant/builder.go +++ b/builder/vagrant/builder.go @@ -223,6 +223,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe SourceBox: b.config.SourceBox, BoxName: b.config.BoxName, GlobalID: b.config.GlobalID, + SkipAdd: b.config.SkipAdd, }, &StepUp{ TeardownMethod: b.config.TeardownMethod, diff --git a/builder/vagrant/step_add_box.go b/builder/vagrant/step_add_box.go index a1832969e..9c1e23eb6 100644 --- a/builder/vagrant/step_add_box.go +++ b/builder/vagrant/step_add_box.go @@ -21,10 +21,10 @@ type StepAddBox struct { SourceBox string BoxName string GlobalID string + SkipAdd bool } func (s *StepAddBox) generateAddArgs() []string { - addArgs := []string{} if strings.HasSuffix(s.SourceBox, ".box") { @@ -72,6 +72,11 @@ func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep. driver := state.Get("driver").(VagrantDriver) ui := state.Get("ui").(packer.Ui) + if s.SkipAdd { + ui.Say("skip_add was set so we assume the box is already in Vagrant...") + return multistep.ActionContinue + } + if s.GlobalID != "" { ui.Say("Using a global-id; skipping Vagrant add command...") return multistep.ActionContinue From 9b69790514668423d420620e181fc41410ca8ba8 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 15 Feb 2019 13:12:45 -0800 Subject: [PATCH 45/48] update changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28267c672..2cdf1b3f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ ## 1.3.5 (upcoming) +### IMPROVEMENTS: +* builder/amazon: AWS users can now use the Vault AWS engine to generate temporary credentials. [GH-7282] +* builder/virtualbox: New "guest_additions_interface" option to enable attaching via a SATA interface. [GH-7298] +* bulder/openstac: Deprecated compute/v2/images API [GH-7268] +* post-processor/manifest: Add "custom_data" key to packer manifest post-processor [GH-7248] +* builder/vmware: Add `cores` option for specifying the number of cores per socket. [GH-7191] +* post-processor/googlecompute-export: Extend auth for the GCE-post-processors to act like the GCE builder. [GH-7222] +* post-processor/googlecompute-import: Extend auth for the GCE-post-processors to act like the GCE builder. [GH-7222] + +### BUG FIXES: +* provisioner/salt: Force powershell to overwrite duplicate files [GH-7281] +* builder/vmware-esxi: Should properly strip whitespace from end of names of files stored on esxi. [GH-7310] +* builder/hyper-v: Fix regression where we improperly handled spaces in switch names [GH-7266] +* core: clean up Makefile [GH-7254][GH-7265] +* builder/cloudstack: Updated sdk version; can now use ostype name in template_os option. [GH-7264] +* builder/azure: Fixed Azure interactive authentication [GH-7276] +* builder/hyper-v: Fix integer overflows in 32-bit builds [GH-7251] +* builder/google: Change metadata url to use a FQDN fixing bug stemming from differing DNS/search domains. [GH-7260] +* core: Fixes mismatches in checksums for dependencies for Go 1.11.4+ [GH-7261] +* core: make sure 'only' option is completely ignored by post-processors [GH-7262] + +### Features: +**new provisioner**`inspec` Added inspec.io provisioner #[GH-7180] +**new post-processor** `digitalocean-import`Add digitalocean-import post-processor. [GH-7060] +**new builder** `vagrant` allows users to call vagrant to provision starting from vagrant boxes and save them as new vagrant boxes. [GH-7221] + ## 1.3.4 (January 30, 2019) ### IMPROVEMENTS: * builder/alicloud: delete copied image and snapshots if corresponding options From 31c8acc5bc1639beeea2d0bdc8f3bc000765ceec Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 15 Feb 2019 15:05:29 -0800 Subject: [PATCH 46/48] add a configurable pause before step_connect to work around bootstrap race conditions --- helper/communicator/config.go | 3 +++ helper/communicator/step_connect.go | 20 +++++++++++++++++++ .../docs/templates/communicator.html.md | 19 ++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/helper/communicator/config.go b/helper/communicator/config.go index 7d66254b5..fa02810d7 100644 --- a/helper/communicator/config.go +++ b/helper/communicator/config.go @@ -66,6 +66,9 @@ type Config struct { WinRMInsecure bool `mapstructure:"winrm_insecure"` WinRMUseNTLM bool `mapstructure:"winrm_use_ntlm"` WinRMTransportDecorator func() winrm.Transporter + + // Delay + PauseBeforeConnect time.Duration `mapstructure:"pause_before_connecting"` } // ReadSSHPrivateKeyFile returns the SSH private key bytes diff --git a/helper/communicator/step_connect.go b/helper/communicator/step_connect.go index 10adc5d2b..6cab6e004 100644 --- a/helper/communicator/step_connect.go +++ b/helper/communicator/step_connect.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "time" "github.com/hashicorp/packer/communicator/none" "github.com/hashicorp/packer/helper/multistep" @@ -44,9 +45,28 @@ type StepConnect struct { substep multistep.Step } +func (s *StepConnect) pause(pauseLen time.Duration, ctx context.Context) bool { + // Use a select to determine if we get cancelled during the wait + log.Printf("Pausing before connecting...") + select { + case <-ctx.Done(): + return true + case <-time.After(pauseLen): + } + log.Printf("Pause over; connecting...") + return false +} + func (s *StepConnect) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) + if s.Config.PauseBeforeConnect > 0 { + cancelled := s.pause(s.Config.PauseBeforeConnect, ctx) + if cancelled { + return multistep.ActionHalt + } + } + typeMap := map[string]multistep.Step{ "none": nil, "ssh": &StepConnectSSH{ diff --git a/website/source/docs/templates/communicator.html.md b/website/source/docs/templates/communicator.html.md index c8054f623..42a4c2e94 100644 --- a/website/source/docs/templates/communicator.html.md +++ b/website/source/docs/templates/communicator.html.md @@ -198,3 +198,22 @@ The WinRM communicator has the following options. - `winrm_use_ssl` (boolean) - If `true`, use HTTPS for WinRM. - `winrm_username` (string) - The username to use to connect to WinRM. + +## Pausing Before Connecting +We recommend that you enable SSH or WinRM as the very last step in your +guest's bootstrap script, but sometimes you may have a race condition where +you need Packer to wait before attempting to connect to your guest. + +If you end up in this situation, you can use the template option +`pause_before_connecting`. By default, there is no pause. For example: + +{ + "communicator": "ssh" + "ssh_username": "myuser", + "pause_before_connecting": "10m" +} + +In this example, Packer will wait 10 minutes before attempting to connect to +the guest. + + From f6be550f1a6d10e5c2dd0b8285727ba0aed488cc Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Tue, 19 Feb 2019 15:10:30 +0100 Subject: [PATCH 47/48] scripts/build.sh: allow to set build settings when building this will for example allow me to have the following alias: alias buildmain='export T=$(mktemp -d) && ALL_XC_OS="linux darwin windows" ALL_XC_ARCH="amd64" GOLDFLAGS="-s -w" ./scripts/build.sh && cd pkg/ && for dir in *; do zip -r "$dir.zip" $dir & ; done ; wait && mv *.zip $T/. && open $T' that build only on 'main' platforms, so that I can share binaries easily. --- scripts/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index cf44e0713..4ebda8185 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -2,8 +2,8 @@ # This script builds the application from source for multiple platforms. # Determine the arch/os combos we're building for -ALL_XC_ARCH="386 amd64 arm arm64 ppc64le" -ALL_XC_OS="linux darwin windows freebsd openbsd solaris" +ALL_XC_ARCH=${ALL_XC_ARCH:-"386 amd64 arm arm64 ppc64le"} +ALL_XC_OS=${ALL_XC_OS:-"linux darwin windows freebsd openbsd solaris"} # Exit immediately if a command fails set -e From 472b5ba710ad226b39645698967e133e8318d5c6 Mon Sep 17 00:00:00 2001 From: "Elijah Caine M. Voigt" Date: Thu, 21 Feb 2019 10:51:48 -0800 Subject: [PATCH 48/48] Update vsphere+vsphere-template array pairing docs The example and description of pairing `vsphere` and `vsphere-template` in a JSON array is accurate but ought to be more spelled out. This introduces a change which makes that part of the example and docs more verbose, hopefully mitigating potential confusion. --- .../source/docs/post-processors/vsphere-template.html.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/website/source/docs/post-processors/vsphere-template.html.md b/website/source/docs/post-processors/vsphere-template.html.md index 08fbf46c9..a3ef0e34e 100644 --- a/website/source/docs/post-processors/vsphere-template.html.md +++ b/website/source/docs/post-processors/vsphere-template.html.md @@ -91,7 +91,11 @@ for more information): "type": "vsphere-template", ... } - ] + ], + { + "type": "...", + ... + } ] } ``` @@ -100,4 +104,5 @@ In the example above, the result of each builder is passed through the defined sequence of post-processors starting with the `vsphere` post-processor which will upload the artifact to a vSphere endpoint. The resulting artifact is then passed on to the `vsphere-template` post-processor which handles marking a VM -as a template. +as a template. Note that the `vsphere` and `vsphere-template` post-processors +are paired together in their own JSON array.