Skip to content

Commit 8b18bfb

Browse files
authored
Merge pull request #255 from jumppad-labs/f-http-resource
HTTP resource
2 parents 5a1f61d + 88f921b commit 8b18bfb

File tree

5 files changed

+306
-0
lines changed

5 files changed

+306
-0
lines changed

examples/http/main.hcl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
resource "container" "httpbin" {
2+
image {
3+
name = "kong/httpbin:0.1.0"
4+
}
5+
6+
port {
7+
local = 80
8+
host = 80
9+
}
10+
11+
health_check {
12+
timeout = "30s"
13+
14+
http {
15+
address = "http://localhost/get"
16+
success_codes = [200]
17+
}
18+
}
19+
}
20+
21+
resource "http" "get" {
22+
method = "GET"
23+
24+
url = "http://${resource.container.httpbin.container_name}/get"
25+
26+
headers = {
27+
Accept = "application/json"
28+
}
29+
}
30+
31+
resource "http" "post" {
32+
method = "POST"
33+
34+
url = "http://${resource.container.httpbin.container_name}/post"
35+
36+
payload = jsonencode({
37+
foo = "bar"
38+
})
39+
40+
headers = {
41+
Accept = "application/json"
42+
}
43+
}
44+
45+
output "get_body" {
46+
value = jsondecode(resource.http.get.body)
47+
}
48+
49+
output "get_status" {
50+
value = resource.http.get.status
51+
}
52+
53+
output "post_body" {
54+
value = jsondecode(resource.http.post.body)
55+
}
56+
57+
output "post_status" {
58+
value = resource.http.post.status
59+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package http
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"time"
10+
11+
"github.com/jumppad-labs/hclconfig/types"
12+
"github.com/jumppad-labs/jumppad/pkg/clients/logger"
13+
sdk "github.com/jumppad-labs/plugin-sdk"
14+
)
15+
16+
type Provider struct {
17+
config *HTTP
18+
log logger.Logger
19+
client http.Client
20+
}
21+
22+
func (p *Provider) Init(cfg types.Resource, l sdk.Logger) error {
23+
c, ok := cfg.(*HTTP)
24+
if !ok {
25+
return fmt.Errorf("unable to initialize provider, resource is not of type HTTP")
26+
}
27+
28+
client := http.Client{
29+
Timeout: 10 * time.Second,
30+
}
31+
32+
p.client = client
33+
p.config = c
34+
p.log = l
35+
36+
return nil
37+
}
38+
39+
func (p *Provider) Create(ctx context.Context) error {
40+
p.log.Info(fmt.Sprintf("Creating %s", p.config.Metadata().Type), "ref", p.config.Metadata().ID)
41+
42+
// If a timeout was specified, set it
43+
if p.config.Timeout != "" {
44+
timeout, err := time.ParseDuration(p.config.Timeout)
45+
if err != nil {
46+
return err
47+
}
48+
49+
p.client.Timeout = timeout
50+
}
51+
52+
var payload io.Reader
53+
if p.config.Method == "POST" {
54+
payload = bytes.NewBuffer([]byte(p.config.Payload))
55+
}
56+
57+
// create a http request
58+
request, err := http.NewRequest(p.config.Method, p.config.URL, payload)
59+
if err != nil {
60+
return err
61+
}
62+
63+
// add headers
64+
for k, v := range p.config.Headers {
65+
request.Header.Add(k, v)
66+
}
67+
68+
// make the request
69+
response, err := p.client.Do(request)
70+
if err != nil {
71+
return err
72+
}
73+
74+
// read the response body
75+
body, err := io.ReadAll(response.Body)
76+
if err != nil {
77+
return err
78+
}
79+
80+
// set the outputs
81+
p.config.Status = response.StatusCode
82+
p.config.Body = string(body)
83+
84+
return nil
85+
}
86+
87+
func (p *Provider) Destroy(ctx context.Context, force bool) error {
88+
return nil
89+
}
90+
91+
func (p *Provider) Lookup() ([]string, error) {
92+
return nil, nil
93+
}
94+
95+
func (p *Provider) Refresh(ctx context.Context) error {
96+
return nil
97+
}
98+
99+
func (p *Provider) Changed() (bool, error) {
100+
return false, nil
101+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package http
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
12+
"github.com/jumppad-labs/hclconfig/types"
13+
"github.com/jumppad-labs/jumppad/pkg/clients/logger"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func setupHttp(t *testing.T) (*HTTP, *Provider) {
18+
h := &HTTP{ResourceBase: types.ResourceBase{Meta: types.Meta{Name: "test"}}}
19+
20+
p := &Provider{h, logger.NewTestLogger(t), *http.DefaultClient}
21+
22+
return h, p
23+
}
24+
25+
func TestHttpResourceGet(t *testing.T) {
26+
expected := "your response"
27+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
fmt.Fprint(w, expected)
29+
}))
30+
31+
defer ts.Close()
32+
33+
h, p := setupHttp(t)
34+
h.Method = "GET"
35+
h.URL = ts.URL
36+
37+
err := p.Create(context.Background())
38+
require.NoError(t, err)
39+
40+
require.Equal(t, 200, h.Status)
41+
require.Equal(t, expected, h.Body)
42+
}
43+
44+
func TestHttpResourcePost(t *testing.T) {
45+
var body string
46+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47+
data, err := io.ReadAll(r.Body)
48+
require.NoError(t, err)
49+
50+
body = string(data)
51+
}))
52+
53+
defer ts.Close()
54+
55+
h, p := setupHttp(t)
56+
h.Method = "POST"
57+
h.URL = ts.URL
58+
h.Payload = "your request"
59+
60+
err := p.Create(context.Background())
61+
require.NoError(t, err)
62+
63+
require.Equal(t, 200, h.Status)
64+
require.Equal(t, h.Payload, body)
65+
}
66+
67+
func TestHttpResourceHeaders(t *testing.T) {
68+
headers := map[string]string{}
69+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70+
headers["X-First"] = r.Header.Get("X-First")
71+
headers["X-Second"] = r.Header.Get("X-Second")
72+
}))
73+
74+
defer ts.Close()
75+
76+
h, p := setupHttp(t)
77+
h.Method = "POST"
78+
h.URL = ts.URL
79+
h.Headers = map[string]string{
80+
"X-First": "first",
81+
"X-Second": "second",
82+
}
83+
84+
err := p.Create(context.Background())
85+
require.NoError(t, err)
86+
87+
require.Equal(t, 200, h.Status)
88+
require.Equal(t, h.Headers, headers)
89+
}
90+
91+
func TestHttpResourceTimeout(t *testing.T) {
92+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
93+
// do nothing
94+
time.Sleep(10 * time.Second)
95+
}))
96+
97+
defer ts.Close()
98+
99+
h, p := setupHttp(t)
100+
h.Method = "GET"
101+
h.URL = ts.URL
102+
h.Timeout = "5s"
103+
104+
err := p.Create(context.Background())
105+
require.Error(t, err)
106+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package http
2+
3+
import (
4+
"github.com/jumppad-labs/hclconfig/types"
5+
"github.com/jumppad-labs/jumppad/pkg/config"
6+
)
7+
8+
const TypeHTTP string = "http"
9+
10+
type HTTP struct {
11+
types.ResourceBase `hcl:",remain"`
12+
13+
Method string `hcl:"method" json:"method"`
14+
URL string `hcl:"url" json:"url"`
15+
16+
Headers map[string]string `hcl:"headers,optional" json:"headers,omitempty"`
17+
Payload string `hcl:"payload,optional" json:"payload,omitempty"`
18+
Timeout string `hcl:"timeout,optional" json:"timeout,omitempty"`
19+
20+
// Output parameters
21+
Status int `hcl:"status,optional" json:"status"`
22+
Body string `hcl:"body,optional" json:"body"`
23+
}
24+
25+
func (t *HTTP) Process() error {
26+
cfg, err := config.LoadState()
27+
if err == nil {
28+
// try and find the resource in the state
29+
r, _ := cfg.FindResource(t.Meta.ID)
30+
if r != nil {
31+
state := r.(*HTTP)
32+
t.Status = state.Status
33+
t.Body = state.Body
34+
}
35+
}
36+
37+
return nil
38+
}

pkg/jumppad/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/jumppad-labs/jumppad/pkg/config/resources/docs"
1414
"github.com/jumppad-labs/jumppad/pkg/config/resources/exec"
1515
"github.com/jumppad-labs/jumppad/pkg/config/resources/helm"
16+
"github.com/jumppad-labs/jumppad/pkg/config/resources/http"
1617
"github.com/jumppad-labs/jumppad/pkg/config/resources/ingress"
1718
"github.com/jumppad-labs/jumppad/pkg/config/resources/k8s"
1819
"github.com/jumppad-labs/jumppad/pkg/config/resources/network"
@@ -39,6 +40,7 @@ func init() {
3940
config.RegisterResource(docs.TypeBook, &docs.Book{}, &null.Provider{})
4041
config.RegisterResource(exec.TypeExec, &exec.Exec{}, &exec.Provider{})
4142
config.RegisterResource(helm.TypeHelm, &helm.Helm{}, &helm.Provider{})
43+
config.RegisterResource(http.TypeHTTP, &http.HTTP{}, &http.Provider{})
4244
config.RegisterResource(ingress.TypeIngress, &ingress.Ingress{}, &ingress.Provider{})
4345
config.RegisterResource(k8s.TypeK8sCluster, &k8s.Cluster{}, &k8s.ClusterProvider{})
4446
config.RegisterResource(k8s.TypeK8sConfig, &k8s.Config{}, &k8s.ConfigProvider{})

0 commit comments

Comments
 (0)