Skip to content

Commit 03c7536

Browse files
committed
Add new provider
0 parents  commit 03c7536

14 files changed

+1381
-0
lines changed

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
main.tf
2+
.terraform
3+
terraform-provider-restapi
4+
test.sh
5+
test.log
6+
terraform.tfstate
7+
terraform.tfstate.backup
8+
crash.log
9+
**/github_api_token
10+
**/release_info.md
11+
scripts/terraform-provider-restapi*

LICENSE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2018 Mastercard
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Terraform provider for generic REST APIs
2+
3+
This terraform provider allows you to interact with APIs that may not yet have a first-class provider available.
4+
5+
There are a few requirements about how the API must work for this provider to be able to do its thing:
6+
* The API is expected to support the following HTTP methods:
7+
* POST: create an object
8+
* GET: read an object
9+
* PUT: update an object
10+
* DELETE: remove an object
11+
* An "object" in the API has a unique identifier the API will return
12+
* Objects live under a distinct path such that for the path `/api/v1/things`...
13+
* POST on `/api/v1/things` creates a new object
14+
* GET, PUT and DELETE on `/api/v1/things/{id}` manages an existing object
15+
16+
 
17+
18+
## Provider configuration
19+
- `uri` (string, required): URI of the REST API endpoint. This serves as the base of all requests. Example: `https://myapi.env.local/api/v1`.
20+
- `insecure` (boolean, optional): When using https, this disables TLS verification of the host.
21+
- `username` (string, optional): When set, will use this username for BASIC auth to the API.
22+
- `password` (string, optional): When set, will use this password for BASIC auth to the API.
23+
- `authorization_header` (string, optional): If the API does not support BASIC authentication, you can set the Authorization header contents to be sent in all requests. This is useful if you want to use a script via the `external` provider or provide a pre-approved token. This takes precedence over BASIC auth credentials.
24+
- `timeout` (integer, optional): When set, will cause requests taking longer than this time (in seconds) to be aborted. Default is `0` which means no timeout is set.
25+
- `id_attribute` (string, optional): When set, this key will be used to operate on REST objects. For example, if the ID is set to 'name', changes to the API object will be to `http://foo.com/bar/VALUE_OF_NAME`.
26+
- `copy_keys` (array of strings, optional): When set, any `PUT` to the API for an object will copy these keys from the data the provider has gathered about the object. This is useful if internal API information must also be provided with updates, such as the revision of the object.
27+
- `write_returns_object` (boolean, optional): Set this when the API returns the object created on all write operations (`POST`, `PUT`). This is used by the provider to refresh internal data structures.
28+
- `create_returns_object` (boolean, optional): Set this when the API returns the object created only on creation operations (`POST`). This is used by the provider to refresh internal data structures.
29+
- `debug` (boolean, optional): Enabling this will cause lots of debug information to be printed to STDOUT by the API client. This can be gathered by setting `TF_LOG=1` environment variable.
30+
31+
 
32+
33+
## `restapi` resource configuration
34+
- `path` (string, required): The API path on top of the base URL set in the provider that represents objects of this type on the API server.
35+
- `data` (string, required): Valid JSON data that this provider will manage with the API server. This should represent the whole API object that you want to create. The provider's information.
36+
- `debug` (boolean, optional): Whether to emit verbose debug output while working with the API object on the server. This can be gathered by setting `TF_LOG=1` environment variable.
37+
38+
This provider also exports the following parameters:
39+
- `id`: The ID of the object that is being managed.
40+
- `api_data`: After data from the API server is read, this map will include k/v pairs usable in other terraform resources as readable objects. Currently the value is the golang fmt package's representation of the value (simple primitives are set as expected, but complex types like arrays and maps contain golang formatting).

main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package main
2+
3+
import (
4+
"github.com/hashicorp/terraform/plugin"
5+
"github.com/hashicorp/terraform/terraform"
6+
"github.com/Mastercard/terraform-provider-restapi/restapi"
7+
)
8+
9+
func main() {
10+
plugin.Serve(&plugin.ServeOpts{
11+
ProviderFunc: func() terraform.ResourceProvider {
12+
return restapi.Provider()
13+
},
14+
})
15+
}

restapi/api_client.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package restapi
2+
3+
import (
4+
"log"
5+
"net/http"
6+
"crypto/tls"
7+
"errors"
8+
"fmt"
9+
"io/ioutil"
10+
"strings"
11+
"bytes"
12+
"time"
13+
)
14+
15+
type api_client struct {
16+
http_client *http.Client
17+
uri string
18+
insecure bool
19+
username string
20+
password string
21+
auth_header string
22+
redirects int
23+
timeout int
24+
id_attribute string
25+
copy_keys []string
26+
write_returns_object bool
27+
create_returns_object bool
28+
debug bool
29+
}
30+
31+
32+
// Make a new api client for RESTful calls
33+
func NewAPIClient (i_uri string, i_insecure bool, i_username string, i_password string, i_auth_header string, i_timeout int, i_id_attribute string, i_copy_keys []string, i_wro bool, i_cro bool, i_debug bool) *api_client {
34+
if i_debug {
35+
log.Printf("api_client.go: Constructing debug api_client\n")
36+
}
37+
38+
/* Sane default */
39+
if i_id_attribute == "" {
40+
i_id_attribute = "id"
41+
}
42+
43+
/* Remove any trailing slashes since we will append
44+
to this URL with our own root-prefixed location */
45+
if strings.HasSuffix(i_uri, "/") {
46+
i_uri = i_uri[:len(i_uri)-1]
47+
}
48+
49+
/* Disable TLS verification if requested */
50+
tr := &http.Transport{
51+
TLSClientConfig: &tls.Config{InsecureSkipVerify: i_insecure},
52+
}
53+
54+
client := api_client{
55+
http_client: &http.Client{
56+
Timeout: time.Second * time.Duration(i_timeout),
57+
Transport: tr,
58+
},
59+
uri: i_uri,
60+
insecure: i_insecure,
61+
username: i_username,
62+
password: i_password,
63+
auth_header: i_auth_header,
64+
id_attribute: i_id_attribute,
65+
copy_keys: i_copy_keys,
66+
write_returns_object: i_wro,
67+
create_returns_object: i_cro,
68+
redirects: 5,
69+
debug: i_debug,
70+
}
71+
return &client
72+
}
73+
74+
/* Helper function that handles sending/receiving and handling
75+
of HTTP data in and out.
76+
TODO: Handle redirects */
77+
func (client *api_client) send_request (method string, path string, data string) (string, error) {
78+
full_uri := client.uri + path
79+
var req *http.Request
80+
var err error
81+
82+
if client.debug {
83+
log.Printf("api_client.go: method='%s', path='%s', full uri (derived)='%s', data='%s'\n", method, path, full_uri, data)
84+
}
85+
86+
buffer := bytes.NewBuffer([]byte(data))
87+
88+
if data == "" {
89+
req, err = http.NewRequest(method, full_uri, nil)
90+
} else {
91+
req, err = http.NewRequest(method, full_uri, buffer)
92+
93+
if err == nil {
94+
req.Header.Set("Content-Type", "application/json")
95+
}
96+
}
97+
98+
if err != nil {
99+
log.Fatal(err)
100+
return "", err
101+
}
102+
103+
if client.debug {
104+
log.Printf("api_client.go: Sending HTTP request to %s...\n", req.URL)
105+
}
106+
107+
/* Allow for tokens or other pre-created secrets */
108+
if client.auth_header != "" {
109+
req.Header.Set("Authorization", client.auth_header)
110+
} else if client.username != "" && client.password != "" {
111+
/* ... and fall back to basic auth if configured */
112+
req.SetBasicAuth(client.username, client.password)
113+
}
114+
115+
if client.debug {
116+
log.Printf("api_client.go: Request headers:\n")
117+
for name, headers := range req.Header {
118+
for _, h := range headers {
119+
log.Printf("api_client.go: %v: %v", name, h)
120+
}
121+
}
122+
123+
log.Printf("api_client.go: BODY:\n")
124+
body := "<none>"
125+
if req.Body != nil {
126+
body = string(data)
127+
}
128+
log.Printf("%s\n", body)
129+
}
130+
131+
for num_redirects := client.redirects; num_redirects >= 0; num_redirects-- {
132+
resp, err := client.http_client.Do(req)
133+
134+
if err != nil {
135+
//log.Printf("api_client.go: Error detected: %s\n", err)
136+
return "", err
137+
}
138+
139+
if client.debug {
140+
log.Printf("api_client.go: Response code: %d\n", resp.StatusCode)
141+
log.Printf("api_client.go: Response headers:\n")
142+
for name, headers := range resp.Header {
143+
for _, h := range headers {
144+
log.Printf("api_client.go: %v: %v", name, h)
145+
}
146+
}
147+
}
148+
149+
bodyBytes, err2 := ioutil.ReadAll(resp.Body)
150+
resp.Body.Close()
151+
152+
if err2 != nil { return "", err2 }
153+
body := string(bodyBytes)
154+
155+
if resp.StatusCode == 301 || resp.StatusCode == 302 {
156+
//Redirecting... decrement num_redirects and proceed to the next loop
157+
//uri = URI.parse(rsp['Location'])
158+
} else if resp.StatusCode == 404 || resp.StatusCode < 200 || resp.StatusCode >= 303 {
159+
return "", errors.New(fmt.Sprintf("Unexpected response code '%d': %s", resp.StatusCode, body))
160+
} else {
161+
if client.debug { log.Printf("api_client.go: BODY:\n%s\n", body) }
162+
return body, nil
163+
}
164+
165+
} //End loop through redirect attempts
166+
167+
return "", errors.New("Error - too many redirects!")
168+
}

restapi/api_client_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package restapi
2+
3+
import (
4+
"log"
5+
"testing"
6+
"net/http"
7+
"time"
8+
)
9+
10+
var api_client_server *http.Server
11+
12+
func TestAPIClient(t *testing.T) {
13+
debug := false
14+
15+
if debug { log.Println("client_test.go: Starting HTTP server") }
16+
setup_api_client_server()
17+
18+
/* Notice the intentional trailing / */
19+
client := NewAPIClient ("http://127.0.0.1:8080/", false, "", "", "", 2, "id", make([]string, 0), false, false, debug)
20+
21+
var res string
22+
var err error
23+
24+
log.Printf("api_client_test.go: Testing standard OK request\n")
25+
res, err = client.send_request("GET", "/ok", "")
26+
if err != nil { t.Fatalf("client_test.go: %s", err) }
27+
if res != "It works!" {
28+
t.Fatalf("client_test.go: Got back '%s' but expected 'It works!'\n", res)
29+
}
30+
31+
/* Verify timeout works */
32+
log.Printf("api_client_test.go: Testing timeout aborts requests\n")
33+
res, err = client.send_request("GET", "/slow", "")
34+
if err == nil { t.Fatalf("client_test.go: Timeout did not trigger on slow request") }
35+
36+
if debug { log.Println("client_test.go: Stopping HTTP server") }
37+
shutdown_api_client_server()
38+
if debug { log.Println("client_test.go: Done") }
39+
}
40+
41+
func setup_api_client_server () {
42+
serverMux := http.NewServeMux()
43+
serverMux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
44+
w.Write([]byte("It works!"))
45+
})
46+
serverMux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
47+
time.Sleep(9999 * time.Second)
48+
w.Write([]byte("This will never return!!!!!"))
49+
})
50+
51+
52+
api_client_server = &http.Server{
53+
Addr: "127.0.0.1:8080",
54+
Handler: serverMux,
55+
}
56+
go api_client_server.ListenAndServe()
57+
/* let the server start */
58+
time.Sleep(1 * time.Second)
59+
}
60+
61+
func shutdown_api_client_server () {
62+
api_client_server.Close()
63+
}

0 commit comments

Comments
 (0)