Skip to content

Commit 5a7ab7f

Browse files
authored
HTTPConfig : Add http_content to be served (#43)
* test * docs
1 parent 4b7031f commit 5a7ab7f

6 files changed

Lines changed: 206 additions & 7 deletions

File tree

didyoumean/name_suggestion.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package didyoumean
2+
3+
import (
4+
"github.com/agext/levenshtein"
5+
)
6+
7+
// NameSuggestion tries to find a name from the given slice of suggested names
8+
// that is close to the given name and returns it if found. If no suggestion is
9+
// close enough, returns the empty string.
10+
//
11+
// The suggestions are tried in order, so earlier suggestions take precedence if
12+
// the given string is similar to two or more suggestions.
13+
//
14+
// This function is intended to be used with a relatively-small number of
15+
// suggestions. It's not optimized for hundreds or thousands of them.
16+
func NameSuggestion(given string, suggestions []string) string {
17+
for _, suggestion := range suggestions {
18+
dist := levenshtein.Distance(given, suggestion, nil)
19+
if dist < 3 { // threshold determined experimentally
20+
return suggestion
21+
}
22+
}
23+
return ""
24+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module github.com/hashicorp/packer-plugin-sdk
22

33
require (
4+
github.com/agext/levenshtein v1.2.1
45
github.com/aws/aws-sdk-go v1.36.5
56
github.com/dylanmei/winrmtest v0.0.0-20170819153634-c2fbb09e6c08
67
github.com/fatih/camelcase v1.0.0

multistep/commonsteps/http_config.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ type HTTPConfig struct {
2323
// started. The address and port of the HTTP server will be available as
2424
// variables in `boot_command`. This is covered in more detail below.
2525
HTTPDir string `mapstructure:"http_directory"`
26+
// Key/Values to serve using an HTTP server. http_content works like and
27+
// conflicts with http_directory The keys represent the paths and the values
28+
// contents. This is useful for hosting kickstart files and so on. By
29+
// default this is empty, which means no HTTP server will be started. The
30+
// address and port of the HTTP server will be available as variables in
31+
// `boot_command`. This is covered in more detail below. Example: Setting
32+
// `"foo/bar"="baz"`, will allow you to http get on
33+
// `http://{http_ip}:{http_port}/foo/bar`.
34+
HTTPContent map[string]string `mapstructure:"http_content"`
2635
// These are the minimum and maximum port to use for the HTTP server
2736
// started to serve the `http_directory`. Because Packer often runs in
2837
// parallel, Packer will choose a randomly available port in this range to
@@ -66,5 +75,10 @@ func (c *HTTPConfig) Prepare(ctx *interpolate.Context) []error {
6675
errors.New("http_port_min must be less than http_port_max"))
6776
}
6877

78+
if len(c.HTTPContent) > 0 && len(c.HTTPDir) > 0 {
79+
errs = append(errs,
80+
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"))
81+
}
82+
6983
return errs
7084
}

multistep/commonsteps/step_http_server.go

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,28 @@ package commonsteps
33
import (
44
"context"
55
"fmt"
6-
6+
"log"
77
"net/http"
8+
"os"
9+
"path"
10+
"sort"
811

12+
"github.com/hashicorp/packer-plugin-sdk/didyoumean"
913
"github.com/hashicorp/packer-plugin-sdk/multistep"
1014
"github.com/hashicorp/packer-plugin-sdk/net"
1115
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
1216
)
1317

18+
func HTTPServerFromHTTPConfig(cfg *HTTPConfig) *StepHTTPServer {
19+
return &StepHTTPServer{
20+
HTTPDir: cfg.HTTPDir,
21+
HTTPContent: cfg.HTTPContent,
22+
HTTPPortMin: cfg.HTTPPortMin,
23+
HTTPPortMax: cfg.HTTPPortMax,
24+
HTTPAddress: cfg.HTTPAddress,
25+
}
26+
}
27+
1428
// This step creates and runs the HTTP server that is serving files from the
1529
// directory specified by the 'http_directory` configuration parameter in the
1630
// template.
@@ -22,23 +36,66 @@ import (
2236
// http_port int - The port the HTTP server started on.
2337
type StepHTTPServer struct {
2438
HTTPDir string
39+
HTTPContent map[string]string
2540
HTTPPortMin int
2641
HTTPPortMax int
2742
HTTPAddress string
2843

2944
l *net.Listener
3045
}
3146

47+
func (s *StepHTTPServer) Handler() http.Handler {
48+
if s.HTTPDir != "" {
49+
return http.FileServer(http.Dir(s.HTTPDir))
50+
}
51+
52+
return MapServer(s.HTTPContent)
53+
}
54+
55+
type MapServer map[string]string
56+
57+
func (s MapServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
58+
path := path.Clean(r.URL.Path)
59+
content, found := s[path]
60+
if !found {
61+
paths := make([]string, 0, len(s))
62+
for k := range s {
63+
paths = append(paths, k)
64+
}
65+
sort.Strings(paths)
66+
err := fmt.Sprintf("%s not found.", path)
67+
if sug := didyoumean.NameSuggestion(path, paths); sug != "" {
68+
err += fmt.Sprintf(" Did you mean %q?", sug)
69+
}
70+
71+
http.Error(w, err, http.StatusNotFound)
72+
return
73+
}
74+
75+
if _, err := w.Write([]byte(content)); err != nil {
76+
// log err in case the file couldn't be 100% transferred for example.
77+
log.Printf("http_content serve error: %v", err)
78+
}
79+
}
80+
3281
func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
3382
ui := state.Get("ui").(packersdk.Ui)
3483

35-
if s.HTTPDir == "" {
84+
if s.HTTPDir == "" && len(s.HTTPContent) == 0 {
3685
state.Put("http_port", 0)
3786
return multistep.ActionContinue
3887
}
3988

89+
if s.HTTPDir != "" {
90+
if _, err := os.Stat(s.HTTPDir); err != nil {
91+
err := fmt.Errorf("Error finding %q: %s", s.HTTPDir, err)
92+
state.Put("error", err)
93+
ui.Error(err.Error())
94+
return multistep.ActionHalt
95+
}
96+
}
97+
4098
// Find an available TCP port for our HTTP server
41-
var httpAddr string
4299
var err error
43100
s.l, err = net.ListenRangeConfig{
44101
Min: s.HTTPPortMin,
@@ -57,8 +114,7 @@ func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) mult
57114
ui.Say(fmt.Sprintf("Starting HTTP server on port %d", s.l.Port))
58115

59116
// Start the HTTP server and run it in the background
60-
fileServer := http.FileServer(http.Dir(s.HTTPDir))
61-
server := &http.Server{Addr: httpAddr, Handler: fileServer}
117+
server := &http.Server{Addr: "", Handler: s.Handler()}
62118
go server.Serve(s.l)
63119

64120
// 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
67123
return multistep.ActionContinue
68124
}
69125

70-
func (s *StepHTTPServer) Cleanup(multistep.StateBag) {
126+
func (s *StepHTTPServer) Cleanup(state multistep.StateBag) {
71127
if s.l != nil {
128+
ui := state.Get("ui").(packersdk.Ui)
129+
72130
// Close the listener so that the HTTP server stops
73-
s.l.Close()
131+
if err := s.l.Close(); err != nil {
132+
err = fmt.Errorf("Failed closing http server on port %d: %w", s.l.Port, err)
133+
ui.Error(err.Error())
134+
// Here this error should be shown to the UI but it won't
135+
// specifically stop Packer from terminating successfully. It could
136+
// cause a "Listen leak" if it happenned a lot. Though Listen will
137+
// try other ports if one is already used. In the case we want to
138+
// Listen on only one port, the next Listen call could fail or be
139+
// longer than expected.
140+
}
74141
}
75142
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package commonsteps
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io/ioutil"
7+
"net/http"
8+
"reflect"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/hashicorp/packer-plugin-sdk/multistep"
13+
)
14+
15+
func TestStepHTTPServer_Run(t *testing.T) {
16+
17+
tests := []struct {
18+
cfg *HTTPConfig
19+
want multistep.StepAction
20+
wantPort interface{}
21+
wantContent map[string]string
22+
}{
23+
{
24+
&HTTPConfig{},
25+
multistep.ActionContinue,
26+
0,
27+
nil,
28+
},
29+
{
30+
&HTTPConfig{HTTPDir: "unknown_folder"},
31+
multistep.ActionHalt,
32+
nil,
33+
nil,
34+
},
35+
{
36+
&HTTPConfig{HTTPDir: "test-fixtures", HTTPPortMin: 9000},
37+
multistep.ActionContinue,
38+
9000,
39+
map[string]string{
40+
"SomeDir/myfile.txt": "",
41+
},
42+
},
43+
{
44+
&HTTPConfig{HTTPContent: map[string]string{"/foo.txt": "biz", "/foo/bar.txt": "baz"}, HTTPPortMin: 9001},
45+
multistep.ActionContinue,
46+
9001,
47+
map[string]string{
48+
"foo.txt": "biz",
49+
"/foo.txt": "biz",
50+
"foo/bar.txt": "baz",
51+
"/foo/bar.txt": "baz",
52+
},
53+
},
54+
}
55+
for _, tt := range tests {
56+
t.Run(fmt.Sprintf("%#v", tt.cfg), func(t *testing.T) {
57+
s := HTTPServerFromHTTPConfig(tt.cfg)
58+
state := testState(t)
59+
got := s.Run(context.Background(), state)
60+
defer s.Cleanup(state)
61+
if !reflect.DeepEqual(got, tt.want) {
62+
t.Errorf("StepHTTPServer.Run() = %s, want %s", got, tt.want)
63+
}
64+
gotPort := state.Get("http_port")
65+
if !reflect.DeepEqual(gotPort, tt.wantPort) {
66+
t.Errorf("StepHTTPServer.Run() unexpected port = %v, want %v", gotPort, tt.wantPort)
67+
}
68+
for k, wantResponse := range tt.wantContent {
69+
resp, err := http.Get(fmt.Sprintf("http://:%d/%s", gotPort, k))
70+
if err != nil {
71+
t.Fatalf("http.Get: %v", err)
72+
}
73+
b, err := ioutil.ReadAll(resp.Body)
74+
if err != nil {
75+
t.Fatalf("readall: %v", err)
76+
}
77+
gotResponse := string(b)
78+
if diff := cmp.Diff(wantResponse, gotResponse); diff != "" {
79+
t.Fatalf("Unexpected %q content: %s", k, diff)
80+
}
81+
}
82+
})
83+
}
84+
}

website/content/partials/multistep/commonsteps/HTTPConfig-not-required.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
started. The address and port of the HTTP server will be available as
88
variables in `boot_command`. This is covered in more detail below.
99

10+
- `http_content` (map[string]string) - Key/Values to serve using an HTTP server. http_content works like and
11+
conflicts with http_directory The keys represent the paths and the values
12+
contents. This is useful for hosting kickstart files and so on. By
13+
default this is empty, which means no HTTP server will be started. The
14+
address and port of the HTTP server will be available as variables in
15+
`boot_command`. This is covered in more detail below. Example: Setting
16+
`"foo/bar"="baz"`, will allow you to http get on
17+
`http://{http_ip}:{http_port}/foo/bar`.
18+
1019
- `http_port_min` (int) - These are the minimum and maximum port to use for the HTTP server
1120
started to serve the `http_directory`. Because Packer often runs in
1221
parallel, Packer will choose a randomly available port in this range to

0 commit comments

Comments
 (0)