Skip to content

Commit

Permalink
Add new provider
Browse files Browse the repository at this point in the history
  • Loading branch information
DRuggeri committed Apr 27, 2018
0 parents commit 03c7536
Show file tree
Hide file tree
Showing 14 changed files with 1,381 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
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*
13 changes: 13 additions & 0 deletions LICENSE
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.
40 changes: 40 additions & 0 deletions README.md
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).
15 changes: 15 additions & 0 deletions main.go
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()
},
})
}
168 changes: 168 additions & 0 deletions restapi/api_client.go
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!")
}
63 changes: 63 additions & 0 deletions restapi/api_client_test.go
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()
}
Loading

0 comments on commit 03c7536

Please sign in to comment.