mirror of
https://github.com/hashicorp/packer.git
synced 2026-02-03 20:39:29 -05:00
189 lines
5.7 KiB
Go
189 lines
5.7 KiB
Go
// Copyright IBM Corp. 2013, 2025
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
//go:generate packer-sdc struct-markdown
|
|
//go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config
|
|
package http
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"github.com/hashicorp/packer-plugin-sdk/common"
|
|
"github.com/hashicorp/packer-plugin-sdk/hcl2helper"
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
type Config struct {
|
|
common.PackerConfig `mapstructure:",squash"`
|
|
// The URL to request data from. This URL must respond with a `2xx` range response code and a `text/*` or `application/json` Content-Type.
|
|
Url string `mapstructure:"url" required:"true"`
|
|
// HTTP method used for the request. Supported methods are `HEAD`, `GET`, `POST`, `PUT`, `DELETE`, `OPTIONS`, `PATCH`. Default is `GET`.
|
|
Method string `mapstructure:"method" required:"false"`
|
|
// A map of strings representing additional HTTP headers to include in the request.
|
|
RequestHeaders map[string]string `mapstructure:"request_headers" required:"false"`
|
|
// HTTP request payload send with the request. Default is empty.
|
|
RequestBody string `mapstructure:"request_body" required:"false"`
|
|
}
|
|
|
|
type Datasource struct {
|
|
config Config
|
|
}
|
|
|
|
type DatasourceOutput struct {
|
|
// The URL the data was requested from.
|
|
Url string `mapstructure:"url"`
|
|
// The raw body of the HTTP response.
|
|
ResponseBody string `mapstructure:"body"`
|
|
// A map of strings representing the response HTTP headers.
|
|
// Duplicate headers are concatenated with, according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2).
|
|
ResponseHeaders map[string]string `mapstructure:"request_headers"`
|
|
}
|
|
|
|
func (d *Datasource) ConfigSpec() hcldec.ObjectSpec {
|
|
return d.config.FlatMapstructure().HCL2Spec()
|
|
}
|
|
|
|
func (d *Datasource) Configure(raws ...interface{}) error {
|
|
err := config.Decode(&d.config, nil, raws...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var errs *packersdk.MultiError
|
|
|
|
if d.config.Url == "" {
|
|
errs = packersdk.MultiErrorAppend(
|
|
errs,
|
|
fmt.Errorf("the `url` must be specified"))
|
|
}
|
|
|
|
// Default to GET if no method is specified
|
|
if d.config.Method == "" {
|
|
d.config.Method = "GET"
|
|
}
|
|
|
|
// Check if the input is in the list of allowed methods
|
|
validMethod := false
|
|
allowedMethods := []string{"HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}
|
|
for _, method := range allowedMethods {
|
|
if method == d.config.Method {
|
|
validMethod = true
|
|
break
|
|
}
|
|
}
|
|
if !validMethod {
|
|
errs = packersdk.MultiErrorAppend(
|
|
errs,
|
|
fmt.Errorf("the `method` must be one of %v", allowedMethods))
|
|
}
|
|
|
|
if errs != nil && len(errs.Errors) > 0 {
|
|
return errs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
|
|
return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec()
|
|
}
|
|
|
|
// This is to prevent potential issues w/ binary files
|
|
// and generally unprintable characters
|
|
// See https://github.com/hashicorp/terraform/pull/3858#issuecomment-156856738
|
|
func isContentTypeText(contentType string) bool {
|
|
|
|
parsedType, params, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
allowedContentTypes := []*regexp.Regexp{
|
|
regexp.MustCompile("^text/.+"),
|
|
regexp.MustCompile("^application/json$"),
|
|
regexp.MustCompile(`^application/samlmetadata\+xml`),
|
|
}
|
|
|
|
for _, r := range allowedContentTypes {
|
|
if r.MatchString(parsedType) {
|
|
charset := strings.ToLower(params["charset"])
|
|
return charset == "" || charset == "utf-8" || charset == "us-ascii"
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Most of this code comes from http terraform provider data source
|
|
// https://github.com/hashicorp/terraform-provider-http/blob/main/internal/provider/data_source.go
|
|
func (d *Datasource) Execute() (cty.Value, error) {
|
|
ctx := context.TODO()
|
|
url, method, headers := d.config.Url, d.config.Method, d.config.RequestHeaders
|
|
client := &http.Client{}
|
|
|
|
// Create request body if it is provided
|
|
var requestBody io.Reader
|
|
if d.config.RequestBody != "" {
|
|
requestBody = strings.NewReader(d.config.RequestBody)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, requestBody)
|
|
// TODO: How to make a test case for this?
|
|
if err != nil {
|
|
fmt.Println("Error creating http request")
|
|
return cty.NullVal(cty.EmptyObject), err
|
|
}
|
|
|
|
for name, value := range headers {
|
|
req.Header.Set(name, value)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
// TODO: How to make test case for this
|
|
if err != nil {
|
|
fmt.Println("Error making performing http request")
|
|
return cty.NullVal(cty.EmptyObject), err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return cty.NullVal(cty.EmptyObject), fmt.Errorf("HTTP request error. Response code: %d", resp.StatusCode)
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if contentType == "" || isContentTypeText(contentType) == false {
|
|
fmt.Printf("Content-Type is not recognized as a text type, got %q\n",
|
|
contentType)
|
|
fmt.Println("If the content is binary data, Packer may not properly handle the contents of the response.")
|
|
}
|
|
|
|
bytes, err := io.ReadAll(resp.Body)
|
|
// TODO: How to make test case for this?
|
|
if err != nil {
|
|
fmt.Println("Error processing response body of call")
|
|
return cty.NullVal(cty.EmptyObject), err
|
|
}
|
|
|
|
responseHeaders := make(map[string]string)
|
|
for k, v := range resp.Header {
|
|
// Concatenate according to RFC2616
|
|
// cf. https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
|
|
responseHeaders[k] = strings.Join(v, ", ")
|
|
}
|
|
|
|
output := DatasourceOutput{
|
|
Url: d.config.Url,
|
|
ResponseHeaders: responseHeaders,
|
|
ResponseBody: string(bytes),
|
|
}
|
|
return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil
|
|
}
|