-
Notifications
You must be signed in to change notification settings - Fork 3.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add http datasource #11658
feat: add http datasource #11658
Changes from all commits
1f5f16e
13c407a
9dfa1c0
7c926b7
db79495
095f387
8405a29
74ca041
7462a54
3774f57
b2b6a78
8caa5ac
6f201d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
//go:generate packer-sdc struct-markdown | ||
//go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config | ||
package http | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io/ioutil" | ||
"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 `200 OK` response and a `text/*` or `application/json` Content-Type | ||
Url string `mapstructure:"url" required:"true"` | ||
// A map of strings representing additional HTTP headers to include in the request. | ||
Request_headers map[string]string `mapstructure:"request_headers" 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. | ||
Response_body string `mapstructure:"body"` | ||
// A map of strings representing the response HTTP headers. | ||
// Duplicate headers are contatenated with , according to [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) | ||
Response_headers 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")) | ||
} | ||
|
||
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, headers := d.config.Url, d.config.Request_headers | ||
client := &http.Client{} | ||
|
||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) | ||
// TODO: How to make a test case for this? | ||
if err != nil { | ||
fmt.Println("Error creating http request") | ||
return cty.NullVal(cty.EmptyObject), err | ||
} | ||
Comment on lines
+105
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I shouldn't be here, don't tell anyone, but the Also, 🥷 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can make a PR with this update. Thanks! |
||
|
||
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 { | ||
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.Println(fmt.Sprintf( | ||
"Content-Type is not recognized as a text type, got %q", | ||
contentType)) | ||
fmt.Println("If the content is binary data, Packer may not properly handle the contents of the response.") | ||
} | ||
|
||
bytes, err := ioutil.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, | ||
Response_headers: responseHeaders, | ||
Response_body: string(bytes), | ||
} | ||
return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package http | ||
|
||
import ( | ||
_ "embed" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"os/exec" | ||
"regexp" | ||
"testing" | ||
|
||
"github.com/hashicorp/packer-plugin-sdk/acctest" | ||
) | ||
|
||
//go:embed test-fixtures/basic.pkr.hcl | ||
var testDatasourceBasic string | ||
|
||
//go:embed test-fixtures/empty_url.pkr.hcl | ||
var testDatasourceEmptyUrl string | ||
|
||
//go:embed test-fixtures/404_url.pkr.hcl | ||
var testDatasource404Url string | ||
|
||
func TestHttpDataSource(t *testing.T) { | ||
tests := []struct { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks 😄 |
||
Name string | ||
Path string | ||
Error bool | ||
Outputs map[string]string | ||
}{ | ||
{ | ||
Name: "basic_test", | ||
Path: testDatasourceBasic, | ||
Error: false, | ||
Outputs: map[string]string{ | ||
"url": "url is https://www.packer.io/", | ||
// Check that body is not empty | ||
"body": "body is true", | ||
}, | ||
}, | ||
{ | ||
Name: "url_is_empty", | ||
Path: testDatasourceEmptyUrl, | ||
Error: true, | ||
Outputs: map[string]string{ | ||
"error": "the `url` must be specified", | ||
}, | ||
}, | ||
{ | ||
Name: "404_url", | ||
Path: testDatasource404Url, | ||
Error: true, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.Name, func(t *testing.T) { | ||
testCase := &acctest.PluginTestCase{ | ||
Name: tt.Name, | ||
Setup: func() error { | ||
return nil | ||
}, | ||
Teardown: func() error { | ||
return nil | ||
}, | ||
Template: tt.Path, | ||
Type: "http", | ||
Check: func(buildCommand *exec.Cmd, logfile string) error { | ||
if buildCommand.ProcessState != nil { | ||
if buildCommand.ProcessState.ExitCode() != 0 && !tt.Error { | ||
return fmt.Errorf("Bad exit code. Logfile: %s", logfile) | ||
} | ||
if tt.Error && buildCommand.ProcessState.ExitCode() == 0 { | ||
return fmt.Errorf("Expected Bad exit code.") | ||
} | ||
} | ||
|
||
if tt.Outputs != nil { | ||
logs, err := os.Open(logfile) | ||
if err != nil { | ||
return fmt.Errorf("Unable find %s", logfile) | ||
} | ||
defer logs.Close() | ||
|
||
logsBytes, err := ioutil.ReadAll(logs) | ||
if err != nil { | ||
return fmt.Errorf("Unable to read %s", logfile) | ||
} | ||
logsString := string(logsBytes) | ||
|
||
for key, val := range tt.Outputs { | ||
if matched, _ := regexp.MatchString(val+".*", logsString); !matched { | ||
t.Fatalf( | ||
"logs doesn't contain expected log %v with value %v in %q", | ||
key, | ||
val, | ||
logsString) | ||
} | ||
} | ||
|
||
} | ||
|
||
return nil | ||
}, | ||
} | ||
acctest.TestPlugin(t, testCase) | ||
}) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
|
||
source "null" "example" { | ||
communicator = "none" | ||
} | ||
|
||
data "http" "basic" { | ||
url = "https://www.packer.io/thisWillFail" | ||
} | ||
|
||
locals { | ||
url = "${data.http.basic.url}" | ||
} | ||
|
||
build { | ||
name = "mybuild" | ||
sources = [ | ||
"source.null.example" | ||
] | ||
provisioner "shell-local" { | ||
inline = [ | ||
"echo data is ${local.url}", | ||
] | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The returned error message can be improved a little bit to help understand the step it failed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some print statements before throwing errors to make them more traceable. But I assume we still want to throw current error up the chain.