-
Notifications
You must be signed in to change notification settings - Fork 48
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
HTTPConfig
: Add http_content to be served
#43
Changes from 19 commits
c11f1dd
db40699
63c43d5
987cca7
44894bc
e82d5ba
4020574
3d20a49
4952a68
13abb7d
83074fd
fe6a971
845c14f
6af63a9
2197883
e5e327b
a1fa039
ef19d83
975e728
a7673bc
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,24 @@ | ||
package didyoumean | ||
|
||
import ( | ||
"github.com/agext/levenshtein" | ||
) | ||
|
||
// NameSuggestion tries to find a name from the given slice of suggested names | ||
// that is close to the given name and returns it if found. If no suggestion is | ||
// close enough, returns the empty string. | ||
// | ||
// The suggestions are tried in order, so earlier suggestions take precedence if | ||
// the given string is similar to two or more suggestions. | ||
// | ||
// This function is intended to be used with a relatively-small number of | ||
// suggestions. It's not optimized for hundreds or thousands of them. | ||
func NameSuggestion(given string, suggestions []string) string { | ||
for _, suggestion := range suggestions { | ||
dist := levenshtein.Distance(given, suggestion, nil) | ||
if dist < 3 { // threshold determined experimentally | ||
return suggestion | ||
} | ||
} | ||
return "" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,15 @@ type HTTPConfig struct { | |
// started. The address and port of the HTTP server will be available as | ||
// variables in `boot_command`. This is covered in more detail below. | ||
HTTPDir string `mapstructure:"http_directory"` | ||
// Key/Values to serve using an HTTP server. http_content works like and | ||
// conflicts with http_directory The keys represent the paths and the values | ||
// contents. This is useful for hosting kickstart files and so on. By | ||
// default this is empty, which means no HTTP server will be started. The | ||
// address and port of the HTTP server will be available as variables in | ||
// `boot_command`. This is covered in more detail below. Example: Setting | ||
// `"foo/bar"="baz"`, will allow you to http get on | ||
// `http://{http_ip}:{http_port}/foo/bar`. | ||
HTTPContent map[string]string `mapstructure:"http_content"` | ||
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. This is not a backward incompatibility but it is something that will require all builder components using http_config to update their hcl2spec files in order to be compatible with this feature.
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. Good questions!
The earlier we merge this one, the less manual updates we will have to do in plugins in order to support this. So my guess would be to merge this one now-ish.
🤔 I wasn't planning on doing that ! But you are right ! 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! We just need to remember to run generate on Packer when this one is merged. So that 1.7.1 will be compatible with these changes. |
||
// These are the minimum and maximum port to use for the HTTP server | ||
// started to serve the `http_directory`. Because Packer often runs in | ||
// parallel, Packer will choose a randomly available port in this range to | ||
|
@@ -66,5 +75,10 @@ func (c *HTTPConfig) Prepare(ctx *interpolate.Context) []error { | |
errors.New("http_port_min must be less than http_port_max")) | ||
} | ||
|
||
if len(c.HTTPContent) > 0 && len(c.HTTPDir) > 0 { | ||
errs = append(errs, | ||
errors.New("http_content cannot be used in conjunction with http_dir. Consider using the file function to load file in memory and serve them with http_content: https://www.packer.io/docs/templates/hcl_templates/functions/file/file")) | ||
} | ||
|
||
return errs | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,14 +3,28 @@ package commonsteps | |
import ( | ||
"context" | ||
"fmt" | ||
|
||
"log" | ||
"net/http" | ||
"os" | ||
"path" | ||
"sort" | ||
|
||
"github.com/hashicorp/packer-plugin-sdk/didyoumean" | ||
"github.com/hashicorp/packer-plugin-sdk/multistep" | ||
"github.com/hashicorp/packer-plugin-sdk/net" | ||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer" | ||
) | ||
|
||
func HTTPServerFromHTTPConfig(cfg *HTTPConfig) *StepHTTPServer { | ||
return &StepHTTPServer{ | ||
HTTPDir: cfg.HTTPDir, | ||
HTTPContent: cfg.HTTPContent, | ||
HTTPPortMin: cfg.HTTPPortMin, | ||
HTTPPortMax: cfg.HTTPPortMax, | ||
HTTPAddress: cfg.HTTPAddress, | ||
} | ||
} | ||
|
||
// This step creates and runs the HTTP server that is serving files from the | ||
// directory specified by the 'http_directory` configuration parameter in the | ||
// template. | ||
|
@@ -22,23 +36,66 @@ import ( | |
// http_port int - The port the HTTP server started on. | ||
type StepHTTPServer struct { | ||
HTTPDir string | ||
HTTPContent map[string]string | ||
HTTPPortMin int | ||
HTTPPortMax int | ||
HTTPAddress string | ||
|
||
l *net.Listener | ||
} | ||
|
||
func (s *StepHTTPServer) Handler() http.Handler { | ||
if s.HTTPDir != "" { | ||
return http.FileServer(http.Dir(s.HTTPDir)) | ||
} | ||
|
||
return MapServer(s.HTTPContent) | ||
} | ||
|
||
type MapServer map[string]string | ||
|
||
func (s MapServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
path := path.Clean(r.URL.Path) | ||
content, found := s[path] | ||
if !found { | ||
paths := make([]string, 0, len(s)) | ||
for k := range s { | ||
paths = append(paths, k) | ||
} | ||
sort.Strings(paths) | ||
err := fmt.Sprintf("%s not found.", path) | ||
if sug := didyoumean.NameSuggestion(path, paths); sug != "" { | ||
err += fmt.Sprintf("Did you mean %q?", sug) | ||
} | ||
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. This makes lives easier <3 ! 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. ( even saved me a few times in tests mode already ) 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. this is super cool :D |
||
|
||
http.Error(w, err, http.StatusNotFound) | ||
return | ||
} | ||
|
||
if _, err := w.Write([]byte(content)); err != nil { | ||
// log err in case the file couldn't be 100% transferred for example. | ||
log.Printf("http_content serve error: %v", err) | ||
} | ||
} | ||
|
||
func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { | ||
ui := state.Get("ui").(packersdk.Ui) | ||
|
||
if s.HTTPDir == "" { | ||
if s.HTTPDir == "" && len(s.HTTPContent) == 0 { | ||
state.Put("http_port", 0) | ||
return multistep.ActionContinue | ||
} | ||
|
||
if s.HTTPDir != "" { | ||
if _, err := os.Stat(s.HTTPDir); err != nil { | ||
err := fmt.Errorf("Error finding %q: %s", s.HTTPDir, err) | ||
state.Put("error", err) | ||
ui.Error(err.Error()) | ||
return multistep.ActionHalt | ||
} | ||
} | ||
|
||
// Find an available TCP port for our HTTP server | ||
var httpAddr string | ||
var err error | ||
s.l, err = net.ListenRangeConfig{ | ||
Min: s.HTTPPortMin, | ||
|
@@ -57,8 +114,7 @@ func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) mult | |
ui.Say(fmt.Sprintf("Starting HTTP server on port %d", s.l.Port)) | ||
|
||
// Start the HTTP server and run it in the background | ||
fileServer := http.FileServer(http.Dir(s.HTTPDir)) | ||
server := &http.Server{Addr: httpAddr, Handler: fileServer} | ||
server := &http.Server{Addr: "", Handler: s.Handler()} | ||
go server.Serve(s.l) | ||
|
||
// Save the address into the state so it can be accessed in the future | ||
|
@@ -67,9 +123,20 @@ func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) mult | |
return multistep.ActionContinue | ||
} | ||
|
||
func (s *StepHTTPServer) Cleanup(multistep.StateBag) { | ||
func (s *StepHTTPServer) Cleanup(state multistep.StateBag) { | ||
if s.l != nil { | ||
ui := state.Get("ui").(packersdk.Ui) | ||
|
||
// Close the listener so that the HTTP server stops | ||
s.l.Close() | ||
if err := s.l.Close(); err != nil { | ||
err = fmt.Errorf("Failed closing http server on port %d: %w", s.l.Port, err) | ||
ui.Error(err.Error()) | ||
// Here this error should be shown to the UI but it won't | ||
// specifically stop Packer from terminating successfully. It could | ||
// cause a "Listen leak" if it happenned a lot. Though Listen will | ||
// try other ports if one is already used. In the case we want to | ||
// Listen on only one port, the next Listen call could fail or be | ||
// longer than expected. | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package commonsteps | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/hashicorp/packer-plugin-sdk/multistep" | ||
) | ||
|
||
func TestStepHTTPServer_Run(t *testing.T) { | ||
|
||
tests := []struct { | ||
cfg *HTTPConfig | ||
want multistep.StepAction | ||
wantPort interface{} | ||
wantContent map[string]string | ||
}{ | ||
{ | ||
&HTTPConfig{}, | ||
multistep.ActionContinue, | ||
0, | ||
nil, | ||
}, | ||
{ | ||
&HTTPConfig{HTTPDir: "unknown_folder"}, | ||
multistep.ActionHalt, | ||
nil, | ||
nil, | ||
}, | ||
{ | ||
&HTTPConfig{HTTPDir: "test-fixtures", HTTPPortMin: 9000}, | ||
multistep.ActionContinue, | ||
9000, | ||
map[string]string{ | ||
"SomeDir/myfile.txt": "", | ||
}, | ||
}, | ||
{ | ||
&HTTPConfig{HTTPContent: map[string]string{"/foo.txt": "biz", "/foo/bar.txt": "baz"}, HTTPPortMin: 9001}, | ||
multistep.ActionContinue, | ||
9001, | ||
map[string]string{ | ||
"foo.txt": "biz", | ||
"/foo.txt": "biz", | ||
"foo/bar.txt": "baz", | ||
"/foo/bar.txt": "baz", | ||
}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(fmt.Sprintf("%#v", tt.cfg), func(t *testing.T) { | ||
s := HTTPServerFromHTTPConfig(tt.cfg) | ||
state := testState(t) | ||
got := s.Run(context.Background(), state) | ||
defer s.Cleanup(state) | ||
if !reflect.DeepEqual(got, tt.want) { | ||
t.Errorf("StepHTTPServer.Run() = %s, want %s", got, tt.want) | ||
} | ||
gotPort := state.Get("http_port") | ||
if !reflect.DeepEqual(gotPort, tt.wantPort) { | ||
t.Errorf("StepHTTPServer.Run() unexpected port = %v, want %v", gotPort, tt.wantPort) | ||
} | ||
for k, wantResponse := range tt.wantContent { | ||
resp, err := http.Get(fmt.Sprintf("http://:%d/%s", gotPort, k)) | ||
if err != nil { | ||
t.Fatalf("http.Get: %v", err) | ||
} | ||
b, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
t.Fatalf("readall: %v", err) | ||
} | ||
gotResponse := string(b) | ||
if diff := cmp.Diff(wantResponse, gotResponse); diff != "" { | ||
t.Fatalf("Unexpected %q content: %s", k, diff) | ||
} | ||
} | ||
}) | ||
} | ||
} |
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.
I like the package name!