-
Notifications
You must be signed in to change notification settings - Fork 222
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 03c7536
Showing
14 changed files
with
1,381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
main.tf | ||
.terraform | ||
terraform-provider-restapi | ||
test.sh | ||
test.log | ||
terraform.tfstate | ||
terraform.tfstate.backup | ||
crash.log | ||
**/github_api_token | ||
**/release_info.md | ||
scripts/terraform-provider-restapi* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
Copyright 2018 Mastercard | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Terraform provider for generic REST APIs | ||
|
||
This terraform provider allows you to interact with APIs that may not yet have a first-class provider available. | ||
|
||
There are a few requirements about how the API must work for this provider to be able to do its thing: | ||
* The API is expected to support the following HTTP methods: | ||
* POST: create an object | ||
* GET: read an object | ||
* PUT: update an object | ||
* DELETE: remove an object | ||
* An "object" in the API has a unique identifier the API will return | ||
* Objects live under a distinct path such that for the path `/api/v1/things`... | ||
* POST on `/api/v1/things` creates a new object | ||
* GET, PUT and DELETE on `/api/v1/things/{id}` manages an existing object | ||
|
||
| ||
|
||
## Provider configuration | ||
- `uri` (string, required): URI of the REST API endpoint. This serves as the base of all requests. Example: `https://myapi.env.local/api/v1`. | ||
- `insecure` (boolean, optional): When using https, this disables TLS verification of the host. | ||
- `username` (string, optional): When set, will use this username for BASIC auth to the API. | ||
- `password` (string, optional): When set, will use this password for BASIC auth to the API. | ||
- `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. | ||
- `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. | ||
- `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`. | ||
- `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. | ||
- `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. | ||
- `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. | ||
- `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. | ||
|
||
| ||
|
||
## `restapi` resource configuration | ||
- `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. | ||
- `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. | ||
- `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. | ||
|
||
This provider also exports the following parameters: | ||
- `id`: The ID of the object that is being managed. | ||
- `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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package main | ||
|
||
import ( | ||
"github.com/hashicorp/terraform/plugin" | ||
"github.com/hashicorp/terraform/terraform" | ||
"github.com/Mastercard/terraform-provider-restapi/restapi" | ||
) | ||
|
||
func main() { | ||
plugin.Serve(&plugin.ServeOpts{ | ||
ProviderFunc: func() terraform.ResourceProvider { | ||
return restapi.Provider() | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package restapi | ||
|
||
import ( | ||
"log" | ||
"net/http" | ||
"crypto/tls" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"strings" | ||
"bytes" | ||
"time" | ||
) | ||
|
||
type api_client struct { | ||
http_client *http.Client | ||
uri string | ||
insecure bool | ||
username string | ||
password string | ||
auth_header string | ||
redirects int | ||
timeout int | ||
id_attribute string | ||
copy_keys []string | ||
write_returns_object bool | ||
create_returns_object bool | ||
debug bool | ||
} | ||
|
||
|
||
// Make a new api client for RESTful calls | ||
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 { | ||
if i_debug { | ||
log.Printf("api_client.go: Constructing debug api_client\n") | ||
} | ||
|
||
/* Sane default */ | ||
if i_id_attribute == "" { | ||
i_id_attribute = "id" | ||
} | ||
|
||
/* Remove any trailing slashes since we will append | ||
to this URL with our own root-prefixed location */ | ||
if strings.HasSuffix(i_uri, "/") { | ||
i_uri = i_uri[:len(i_uri)-1] | ||
} | ||
|
||
/* Disable TLS verification if requested */ | ||
tr := &http.Transport{ | ||
TLSClientConfig: &tls.Config{InsecureSkipVerify: i_insecure}, | ||
} | ||
|
||
client := api_client{ | ||
http_client: &http.Client{ | ||
Timeout: time.Second * time.Duration(i_timeout), | ||
Transport: tr, | ||
}, | ||
uri: i_uri, | ||
insecure: i_insecure, | ||
username: i_username, | ||
password: i_password, | ||
auth_header: i_auth_header, | ||
id_attribute: i_id_attribute, | ||
copy_keys: i_copy_keys, | ||
write_returns_object: i_wro, | ||
create_returns_object: i_cro, | ||
redirects: 5, | ||
debug: i_debug, | ||
} | ||
return &client | ||
} | ||
|
||
/* Helper function that handles sending/receiving and handling | ||
of HTTP data in and out. | ||
TODO: Handle redirects */ | ||
func (client *api_client) send_request (method string, path string, data string) (string, error) { | ||
full_uri := client.uri + path | ||
var req *http.Request | ||
var err error | ||
|
||
if client.debug { | ||
log.Printf("api_client.go: method='%s', path='%s', full uri (derived)='%s', data='%s'\n", method, path, full_uri, data) | ||
} | ||
|
||
buffer := bytes.NewBuffer([]byte(data)) | ||
|
||
if data == "" { | ||
req, err = http.NewRequest(method, full_uri, nil) | ||
} else { | ||
req, err = http.NewRequest(method, full_uri, buffer) | ||
|
||
if err == nil { | ||
req.Header.Set("Content-Type", "application/json") | ||
} | ||
} | ||
|
||
if err != nil { | ||
log.Fatal(err) | ||
return "", err | ||
} | ||
|
||
if client.debug { | ||
log.Printf("api_client.go: Sending HTTP request to %s...\n", req.URL) | ||
} | ||
|
||
/* Allow for tokens or other pre-created secrets */ | ||
if client.auth_header != "" { | ||
req.Header.Set("Authorization", client.auth_header) | ||
} else if client.username != "" && client.password != "" { | ||
/* ... and fall back to basic auth if configured */ | ||
req.SetBasicAuth(client.username, client.password) | ||
} | ||
|
||
if client.debug { | ||
log.Printf("api_client.go: Request headers:\n") | ||
for name, headers := range req.Header { | ||
for _, h := range headers { | ||
log.Printf("api_client.go: %v: %v", name, h) | ||
} | ||
} | ||
|
||
log.Printf("api_client.go: BODY:\n") | ||
body := "<none>" | ||
if req.Body != nil { | ||
body = string(data) | ||
} | ||
log.Printf("%s\n", body) | ||
} | ||
|
||
for num_redirects := client.redirects; num_redirects >= 0; num_redirects-- { | ||
resp, err := client.http_client.Do(req) | ||
|
||
if err != nil { | ||
//log.Printf("api_client.go: Error detected: %s\n", err) | ||
return "", err | ||
} | ||
|
||
if client.debug { | ||
log.Printf("api_client.go: Response code: %d\n", resp.StatusCode) | ||
log.Printf("api_client.go: Response headers:\n") | ||
for name, headers := range resp.Header { | ||
for _, h := range headers { | ||
log.Printf("api_client.go: %v: %v", name, h) | ||
} | ||
} | ||
} | ||
|
||
bodyBytes, err2 := ioutil.ReadAll(resp.Body) | ||
resp.Body.Close() | ||
|
||
if err2 != nil { return "", err2 } | ||
body := string(bodyBytes) | ||
|
||
if resp.StatusCode == 301 || resp.StatusCode == 302 { | ||
//Redirecting... decrement num_redirects and proceed to the next loop | ||
//uri = URI.parse(rsp['Location']) | ||
} else if resp.StatusCode == 404 || resp.StatusCode < 200 || resp.StatusCode >= 303 { | ||
return "", errors.New(fmt.Sprintf("Unexpected response code '%d': %s", resp.StatusCode, body)) | ||
} else { | ||
if client.debug { log.Printf("api_client.go: BODY:\n%s\n", body) } | ||
return body, nil | ||
} | ||
|
||
} //End loop through redirect attempts | ||
|
||
return "", errors.New("Error - too many redirects!") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package restapi | ||
|
||
import ( | ||
"log" | ||
"testing" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
var api_client_server *http.Server | ||
|
||
func TestAPIClient(t *testing.T) { | ||
debug := false | ||
|
||
if debug { log.Println("client_test.go: Starting HTTP server") } | ||
setup_api_client_server() | ||
|
||
/* Notice the intentional trailing / */ | ||
client := NewAPIClient ("http://127.0.0.1:8080/", false, "", "", "", 2, "id", make([]string, 0), false, false, debug) | ||
|
||
var res string | ||
var err error | ||
|
||
log.Printf("api_client_test.go: Testing standard OK request\n") | ||
res, err = client.send_request("GET", "/ok", "") | ||
if err != nil { t.Fatalf("client_test.go: %s", err) } | ||
if res != "It works!" { | ||
t.Fatalf("client_test.go: Got back '%s' but expected 'It works!'\n", res) | ||
} | ||
|
||
/* Verify timeout works */ | ||
log.Printf("api_client_test.go: Testing timeout aborts requests\n") | ||
res, err = client.send_request("GET", "/slow", "") | ||
if err == nil { t.Fatalf("client_test.go: Timeout did not trigger on slow request") } | ||
|
||
if debug { log.Println("client_test.go: Stopping HTTP server") } | ||
shutdown_api_client_server() | ||
if debug { log.Println("client_test.go: Done") } | ||
} | ||
|
||
func setup_api_client_server () { | ||
serverMux := http.NewServeMux() | ||
serverMux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) { | ||
w.Write([]byte("It works!")) | ||
}) | ||
serverMux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) { | ||
time.Sleep(9999 * time.Second) | ||
w.Write([]byte("This will never return!!!!!")) | ||
}) | ||
|
||
|
||
api_client_server = &http.Server{ | ||
Addr: "127.0.0.1:8080", | ||
Handler: serverMux, | ||
} | ||
go api_client_server.ListenAndServe() | ||
/* let the server start */ | ||
time.Sleep(1 * time.Second) | ||
} | ||
|
||
func shutdown_api_client_server () { | ||
api_client_server.Close() | ||
} |
Oops, something went wrong.