Skip to content

Commit

Permalink
Support dynamic AAP API endpoints (#30)
Browse files Browse the repository at this point in the history
* Support dynamic AAP API endpoints

* Code review updates

* Fix lint issue

* Update AAP endpoint discovery

* Update internal/provider/client_test.go

* format code

* use httptest for client code testing
  • Loading branch information
abikouo authored Jul 2, 2024
1 parent 8f0a2d3 commit 48f2caf
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 48 deletions.
3 changes: 3 additions & 0 deletions changelogs/fragments/20240619-aap-versioning.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
minor_changes:
- Support dynamic value for AAP endpoints since the value depends on the AAP version (https://github.com/ansible/terraform-provider-aap/pull/30).
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.1.0
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.19.1
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.6.0
github.com/stretchr/testify v1.8.4
)
Expand Down Expand Up @@ -45,7 +46,6 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
Expand Down
16 changes: 0 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -86,28 +86,18 @@ github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly8
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hc-install v0.6.4 h1:QLqlM56/+SIIGvGcfFiwMY3z5WGXT066suo/v9Km8e0=
github.com/hashicorp/hc-install v0.6.4/go.mod h1:05LWLy8TD842OtgcfBbOT0WMoInBMUSHjmDx10zuBIA=
github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC1659aBk=
github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA=
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo=
github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw=
github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=
github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg=
github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U=
github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec=
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-plugin-docs v0.19.2 h1:YjdKa1vuqt9EnPYkkrv9HnGZz175HhSJ7Vsn8yZeWus=
github.com/hashicorp/terraform-plugin-docs v0.19.2/go.mod h1:gad2aP6uObFKhgNE8DR9nsEuEQnibp7il0jZYYOunWY=
github.com/hashicorp/terraform-plugin-docs v0.19.4 h1:G3Bgo7J22OMtegIgn8Cd/CaSeyEljqjH3G39w28JK4c=
github.com/hashicorp/terraform-plugin-docs v0.19.4/go.mod h1:4pLASsatTmRynVzsjEhbXZ6s7xBlUw/2Kt0zfrq8HxA=
github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI=
Expand Down Expand Up @@ -228,8 +218,6 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U=
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand All @@ -238,8 +226,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -270,8 +256,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
75 changes: 69 additions & 6 deletions internal/provider/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package provider
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
Expand All @@ -18,18 +20,64 @@ type ProviderHTTPClient interface {
Get(path string) ([]byte, diag.Diagnostics)
Update(path string, data io.Reader) ([]byte, diag.Diagnostics)
Delete(path string) ([]byte, diag.Diagnostics)
setApiEndpoint() diag.Diagnostics
getApiEndpoint() string
}

type AAPApiEndpointResponse struct {
Apis struct {
Controller string `json:"controller"`
} `json:"apis"`
CurrentVersion string `json:"current_version"`
}

func readApiEndpoint(client ProviderHTTPClient) (string, diag.Diagnostics) {
body, diags := client.Get("/api/")
if diags.HasError() {
return "", diags
}
var response AAPApiEndpointResponse
err := json.Unmarshal(body, &response)
if err != nil {
diags.AddError(
fmt.Sprintf("Unable to parse AAP API endpoint response: %s", string(body)),
fmt.Sprintf("Unexpected error: %s", err.Error()),
)
return "", diags
}
if len(response.Apis.Controller) > 0 {
body, diags = client.Get(response.Apis.Controller)
if diags.HasError() {
return "", diags
}
// Parse response
err = json.Unmarshal(body, &response)
if err != nil {
diags.AddError(
fmt.Sprintf("Unable to parse AAP API endpoint response: %s", string(body)),
fmt.Sprintf("Unexpected error: %s", err.Error()),
)
return "", diags
}
}
if len(response.CurrentVersion) == 0 {
diags.AddError("Unable to determine API Endpoint", "The controller endpoint is missing from response")
return "", diags
}
return response.CurrentVersion, diags
}

// Client -
type AAPClient struct {
HostURL string
Username *string
Password *string
httpClient *http.Client
HostURL string
Username *string
Password *string
httpClient *http.Client
ApiEndpoint string
}

// NewClient - create new AAPClient instance
func NewClient(host string, username *string, password *string, insecureSkipVerify bool, timeout int64) (*AAPClient, error) {
func NewClient(host string, username *string, password *string, insecureSkipVerify bool, timeout int64) (*AAPClient, diag.Diagnostics) {
hostURL, _ := url.JoinPath(host, "/")
client := AAPClient{
HostURL: hostURL,
Expand All @@ -42,7 +90,22 @@ func NewClient(host string, username *string, password *string, insecureSkipVeri
}
client.httpClient = &http.Client{Transport: tr, Timeout: time.Duration(timeout) * time.Second}

return &client, nil
// Set AAP API endpoint
diags := client.setApiEndpoint()
return &client, diags
}

func (c *AAPClient) setApiEndpoint() diag.Diagnostics {
endpoint, diags := readApiEndpoint(c)
if diags.HasError() {
return diags
}
c.ApiEndpoint = endpoint
return diags
}

func (c *AAPClient) getApiEndpoint() string {
return c.ApiEndpoint
}

func (c *AAPClient) computeURLPath(path string) string {
Expand Down
54 changes: 50 additions & 4 deletions internal/provider/client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package provider

import (
"net/http"
"net/http/httptest"
"testing"

"fmt"
Expand All @@ -22,12 +24,56 @@ func TestComputeURLPath(t *testing.T) {
var expected = "https://localhost:8043/api/v2/state/"
for _, tc := range testTable {
t.Run(tc.name, func(t *testing.T) {
client, err := NewClient(tc.url, nil, nil, true, 0)
if err != nil {
t.Fatalf(`Failed to create provider client %v`, err)
client := AAPClient{
HostURL: tc.url,
Username: nil,
Password: nil,
httpClient: nil,
ApiEndpoint: "",
}
result := client.computeURLPath(tc.path)
assert.Equal(t, result, expected, fmt.Sprintf("expected (%s), got (%s)", expected, result))
assert.Equal(t, expected, result, fmt.Sprintf("expected (%s), got (%s)", expected, result))
})
}
}

func TestReadApiEndpoint(t *testing.T) {
server_24 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/" {
t.Errorf("Expected to request '/api/', got: %s", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"current_version": "/api/v2/"}`)) //nolint:errcheck
}))
defer server_24.Close()

server_25 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"apis":{"gateway": "/api/gateway/", "controller": "/api/controller/"}}`)) //nolint:errcheck
case "/api/controller/":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"current_version": "/api/controller/v2/"}`)) //nolint:errcheck
default:
t.Errorf("Expected to request one of '/api/', '/api/controller/', got: %s", r.URL.Path)
}
}))
defer server_25.Close()

testTable := []struct {
Name string
URL string
expected string
}{
{Name: "AAP 2.4", URL: server_24.URL, expected: "/api/v2/"},
{Name: "AAP 2.5+", URL: server_25.URL, expected: "/api/controller/v2/"},
}
for _, tc := range testTable {
t.Run(tc.Name, func(t *testing.T) {
client, diags := NewClient(tc.URL, nil, nil, true, 0) // readApiEndpoint() is called when creating client
assert.Equal(t, false, diags.HasError(), fmt.Sprintf("readApiEndpoint() returns errors (%v)", diags))
assert.Equal(t, tc.expected, client.getApiEndpoint())
})
}
}
4 changes: 3 additions & 1 deletion internal/provider/group_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"path"

"github.com/ansible/terraform-provider-aap/internal/provider/customtypes"
"github.com/hashicorp/terraform-plugin-framework/diag"
Expand Down Expand Up @@ -137,7 +138,8 @@ func (r *GroupResource) Create(ctx context.Context, req resource.CreateRequest,
requestData := bytes.NewReader(createRequestBody)

// Create new group in AAP
createResponseBody, diags := r.client.Create("/api/v2/groups/", requestData)
groupsURL := path.Join(r.client.getApiEndpoint(), "groups")
createResponseBody, diags := r.client.Create(groupsURL, requestData)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
Expand Down
4 changes: 3 additions & 1 deletion internal/provider/host_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"path"
"slices"
"sync"

Expand Down Expand Up @@ -162,7 +163,8 @@ func (r *HostResource) Create(ctx context.Context, req resource.CreateRequest, r
requestData := bytes.NewReader(createRequestBody)

// Create new host in AAP
createResponseBody, diags := r.client.Create("/api/v2/hosts/", requestData)
hostsURL := path.Join(r.client.getApiEndpoint(), "hosts")
createResponseBody, diags := r.client.Create(hostsURL, requestData)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
Expand Down
4 changes: 3 additions & 1 deletion internal/provider/inventory_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"path"

"github.com/ansible/terraform-provider-aap/internal/provider/customtypes"
"github.com/hashicorp/terraform-plugin-framework/datasource"
Expand Down Expand Up @@ -78,7 +79,8 @@ func (d *InventoryDataSource) Read(ctx context.Context, req datasource.ReadReque
return
}

readResponseBody, diags := d.client.Get("api/v2/inventories/" + state.Id.String())
resourceURL := path.Join(d.client.getApiEndpoint(), "inventories", state.Id.String())
readResponseBody, diags := d.client.Get(resourceURL)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
Expand Down
4 changes: 3 additions & 1 deletion internal/provider/inventory_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"path"

"github.com/ansible/terraform-provider-aap/internal/provider/customtypes"
"github.com/hashicorp/terraform-plugin-framework/diag"
Expand Down Expand Up @@ -122,7 +123,8 @@ func (r *InventoryResource) Create(ctx context.Context, req resource.CreateReque
requestData := bytes.NewReader(createRequestBody)

// Create new inventory in AAP
createResponseBody, diags := r.client.Create("/api/v2/inventories/", requestData)
inventoriesURL := path.Join(r.client.getApiEndpoint(), "inventories")
createResponseBody, diags := r.client.Create(inventoriesURL, requestData)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
Expand Down
3 changes: 2 additions & 1 deletion internal/provider/job_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"path"

"github.com/ansible/terraform-provider-aap/internal/provider/customtypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
Expand Down Expand Up @@ -302,7 +303,7 @@ func (r *JobResource) LaunchJob(data *JobResourceModel) diag.Diagnostics {
}

requestData := bytes.NewReader(requestBody)
var postURL = "/api/v2/job_templates/" + data.GetTemplateID() + "/launch/"
var postURL = path.Join(r.client.getApiEndpoint(), "job_templates", data.GetTemplateID(), "launch")
resp, body, err := r.client.doRequest(http.MethodPost, postURL, requestData)
diags.Append(ValidateResponse(resp, body, err, []int{http.StatusCreated})...)
if diags.HasError() {
Expand Down
12 changes: 2 additions & 10 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,8 @@ func (p *aapProvider) Configure(ctx context.Context, req provider.ConfigureReque
}

// Create a new http client using the configuration values
client, err := NewClient(host, &username, &password, insecureSkipVerify, timeout)
if err != nil {
resp.Diagnostics.AddError(
"Unable to Create AAP API Client",
"An unexpected error occurred when creating the AAP API client. "+
"If the error is not clear, please contact the provider developers.\n\n"+
"http Client Error: "+err.Error(),
)
return
}
client, diags := NewClient(host, &username, &password, insecureSkipVerify, timeout)
resp.Diagnostics.Append(diags...)

// Make the http client available during DataSource and Resource
// type Configure methods.
Expand Down
11 changes: 5 additions & 6 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,17 @@ func testGetResource(urlPath string) ([]byte, error) {
username := os.Getenv("AAP_USERNAME")
password := os.Getenv("AAP_PASSWORD")

client, err := NewClient(host, &username, &password, true, 0)
if err != nil {
return nil, err
client, diags := NewClient(host, &username, &password, true, 0)
if diags.HasError() {
return nil, fmt.Errorf("%v", diags.Errors())
}

body, diags := client.Get(urlPath)
if diags.HasError() {
err = fmt.Errorf("%v", diags.Errors())
return nil, err
return nil, fmt.Errorf("%v", diags.Errors())
}

return body, err
return body, nil
}

func TestReadValues(t *testing.T) {
Expand Down

0 comments on commit 48f2caf

Please sign in to comment.