Skip to content

Commit

Permalink
chore(feo11y): Infer API URL from stack data
Browse files Browse the repository at this point in the history
  • Loading branch information
kpelelis committed Feb 11, 2025
1 parent 3645845 commit f25a604
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 358 deletions.
8 changes: 7 additions & 1 deletion docs/data-sources/frontend_o11y_app.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ description: |-
## Example Usage

```terraform
data "grafana_cloud_stack" "teststack" {
provider = grafana.cloud
name = "gcloudstacktest"
}
data "grafana_frontend_o11y_app" "test-app" {
stack_id = 1
provider = grafana.cloud
stack_id = data.grafana_cloud_stack.teststack.id
name = "test-app"
}
```
Expand Down
30 changes: 3 additions & 27 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,39 +248,16 @@ resource "grafana_oncall_escalation" "example_notify_step" {

Before using the Terraform Provider to manage Grafana Frontend Observability resources, such as your apps, you need to create an access policy token on the Grafana Cloud Portal. This token is used to authenticate the provider to the Grafana Frontend Observability API.
[These docs](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/authorize-services/#create-an-access-policy-for-a-stack) will guide you on how to create
an access policy. The required permissions, or scopes, are `frontendo11y:write`, `frontendo11y:read` and `stacks:read`.
an access policy. The required permissions, or scopes, are `frontend-observability:read`, `frontend-observability:write`, `frontend-observability:delete` and `stacks:read`.

Also, by default the Access Policies UI will not show those scopes, instead, search for it using the `Add Scope` textbox, as shown in the following image:

#### Obtaining Frontend Observability API URL

Having created the token, we can find the correct Frontend Observability API hostname by running the following script, that requires `curl` and [`jq`](https://jqlang.github.io/jq/) installed:

```bash
curl -sH "Authorization: Bearer <Access Token from previous step>" "https://grafana.com/api/instances" | \
jq '[.items[]|{stackName: .slug, clusterName:.clusterSlug, frontendAPIUrl: "https://faro-api-\(.clusterSlug).grafana.net"}]'
```

This script will return a list of all the Grafana Cloud stacks you own, with the Frontend Observability API hostname for each one. Choose the correct hostname for the stack you want to manage.
For example, in the following response, the correct hostname for the `examplestackname` stack is `https://connections-api-prod-eu-west-0.grafana.net`.

```json
[
{
"stackName": "examplestackname",
"clusterName": "prod-eu-west-0",
"frontendAPIUrl": "https://faro-api-prod-eu-west-0.grafana.net"
}
]
```
You can also use the `cloud_access_policy_token` provided it has the aforementioned scopes included.

#### Configuring the Provider to use the Frontend Observability API

Once you have the token and Frontend Observability API hostname, you can configure the provider as follows:
Once you have the token you can configure the provider as follows:

```hcl
provider "grafana" {
frontend_o11y_api_url = "<Frontend Observability API URL from previous step>"
frontend_o11y_api_access_token = "<Access Token from previous step>"
}
```
Expand All @@ -299,7 +276,6 @@ provider "grafana" {
- `connections_api_access_token` (String, Sensitive) A Grafana Connections API access token. May alternatively be set via the `GRAFANA_CONNECTIONS_API_ACCESS_TOKEN` environment variable.
- `connections_api_url` (String) A Grafana Connections API address. May alternatively be set via the `GRAFANA_CONNECTIONS_API_URL` environment variable.
- `frontend_o11y_api_access_token` (String, Sensitive) A Grafana Frontend Observability API access token. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_ACCESS_TOKEN` environment variable.
- `frontend_o11y_api_url` (String) A Grafana Frontend Observability API address. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_URL` environment variable.
- `http_headers` (Map of String, Sensitive) Optional. HTTP headers mapping keys to values used for accessing the Grafana and Grafana Cloud APIs. May alternatively be set via the `GRAFANA_HTTP_HEADERS` environment variable in JSON format.
- `insecure_skip_verify` (Boolean) Skip TLS certificate verification. May alternatively be set via the `GRAFANA_INSECURE_SKIP_VERIFY` environment variable.
- `oncall_access_token` (String, Sensitive) A Grafana OnCall access token. May alternatively be set via the `GRAFANA_ONCALL_ACCESS_TOKEN` environment variable.
Expand Down
10 changes: 8 additions & 2 deletions docs/resources/frontend_o11y_app.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "grafana_frontend_o11y_app Resource - terraform-provider-grafana"
subcategory: "Connections"
subcategory: "Frontend Observability"
description: |-
---
Expand All @@ -13,8 +13,14 @@ description: |-
## Example Usage

```terraform
data "grafana_cloud_stack" "teststack" {
provider = grafana.cloud
name = "gcloudstacktest"
}
resource "grafana_frontend_o11y_app" "test-app" {
stack_id = 1
provider = grafana.cloud
stack_id = data.grafana_cloud_stack.teststack.id
name = "test-app"
allowed_origins = ["https://grafana.com"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
data "grafana_cloud_stack" "teststack" {
provider = grafana.cloud
name = "gcloudstacktest"
}

data "grafana_frontend_o11y_app" "test-app" {
stack_id = 1
provider = grafana.cloud
stack_id = data.grafana_cloud_stack.teststack.id
name = "test-app"
}
8 changes: 7 additions & 1 deletion examples/resources/grafana_frontend_o11y_app/resource.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
data "grafana_cloud_stack" "teststack" {
provider = grafana.cloud
name = "gcloudstacktest"
}

resource "grafana_frontend_o11y_app" "test-app" {
stack_id = 1
provider = grafana.cloud
stack_id = data.grafana_cloud_stack.teststack.id
name = "test-app"
allowed_origins = ["https://grafana.com"]

Expand Down
45 changes: 24 additions & 21 deletions internal/common/frontendo11yapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ var _ json.Unmarshaler = &App{}

type Client struct {
authToken string
apiURL url.URL
client *http.Client
cloudAPIHost string
userAgent string
defaultHeaders map[string]string
}
Expand All @@ -30,13 +30,7 @@ const (
pathPrefix = "/api/v1"
)

func NewClient(authToken string, rawURL string, client *http.Client, userAgent string, defaultHeaders map[string]string) (*Client, error) {
parsedURL, err := url.Parse(rawURL)

if err != nil {
return nil, fmt.Errorf("failed to parse frontend o11y API url: %w", err)
}

func NewClient(cloudAPIHost string, authToken string, client *http.Client, userAgent string, defaultHeaders map[string]string) (*Client, error) {
if client == nil {
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = defaultRetries
Expand All @@ -46,8 +40,8 @@ func NewClient(authToken string, rawURL string, client *http.Client, userAgent s

return &Client{
authToken: authToken,
apiURL: *parsedURL,
client: client,
cloudAPIHost: cloudAPIHost,
userAgent: userAgent,
defaultHeaders: defaultHeaders,
}, nil
Expand Down Expand Up @@ -86,49 +80,53 @@ type App struct {
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}

func (c *Client) CreateApp(ctx context.Context, stackID int64, appData App) (App, error) {
func (c *Client) urlForStack(stackCluster string) string {
return fmt.Sprintf("https://faro-api-%s.%s/faro", stackCluster, c.cloudAPIHost)
}

func (c *Client) CreateApp(ctx context.Context, stackCluster string, stackID int64, appData App) (App, error) {
path := fmt.Sprintf("%s/app", pathPrefix)
var app App
err := c.doAPIRequest(ctx, stackID, http.MethodPost, path, &appData, &app)
err := c.doAPIRequest(ctx, c.urlForStack(stackCluster), stackID, http.MethodPost, path, &appData, &app)
if err != nil {
return App{}, fmt.Errorf("failed to create faro app %q: %w", appData.Name, err)
}
return app, nil
}

func (c *Client) GetApps(ctx context.Context, stackID int64) ([]App, error) {
func (c *Client) GetApps(ctx context.Context, stackCluster string, stackID int64) ([]App, error) {
path := fmt.Sprintf("%s/app", pathPrefix)
var apps []App
err := c.doAPIRequest(ctx, stackID, http.MethodGet, path, nil, &apps)
err := c.doAPIRequest(ctx, c.urlForStack(stackCluster), stackID, http.MethodGet, path, nil, &apps)
if err != nil {
return []App{}, fmt.Errorf("failed to get faro apps: %w", err)
}
return apps, nil
}

func (c *Client) GetApp(ctx context.Context, stackID int64, appID int64) (App, error) {
func (c *Client) GetApp(ctx context.Context, stackCluster string, stackID int64, appID int64) (App, error) {
path := fmt.Sprintf("%s/app/%d", pathPrefix, appID)
var app App
err := c.doAPIRequest(ctx, stackID, http.MethodGet, path, nil, &app)
err := c.doAPIRequest(ctx, c.urlForStack(stackCluster), stackID, http.MethodGet, path, nil, &app)
if err != nil {
return App{}, fmt.Errorf("failed to get faro apps: %w", err)
}
return app, nil
}

func (c *Client) UpdateApp(ctx context.Context, stackID int64, appID int64, appData App) (App, error) {
func (c *Client) UpdateApp(ctx context.Context, stackCluster string, stackID int64, appID int64, appData App) (App, error) {
path := fmt.Sprintf("%s/app/%d", pathPrefix, appID)
var app App
err := c.doAPIRequest(ctx, stackID, http.MethodPut, path, &appData, &app)
err := c.doAPIRequest(ctx, c.urlForStack(stackCluster), stackID, http.MethodPut, path, &appData, &app)
if err != nil {
return App{}, fmt.Errorf("failed to update faro app %q: %w", appData.Name, err)
}
return app, nil
}

func (c *Client) DeleteApp(ctx context.Context, stackID int64, appID int64) error {
func (c *Client) DeleteApp(ctx context.Context, stackCluster string, stackID int64, appID int64) error {
path := fmt.Sprintf("%s/app/%d", pathPrefix, appID)
err := c.doAPIRequest(ctx, stackID, http.MethodDelete, path, nil, nil)
err := c.doAPIRequest(ctx, c.urlForStack(stackCluster), stackID, http.MethodDelete, path, nil, nil)
if err != nil {
return fmt.Errorf("failed to delete faro app id=%d: %w", appID, err)
}
Expand All @@ -140,7 +138,12 @@ var (
ErrUnauthorized = fmt.Errorf("request not authorized for stack")
)

func (c *Client) doAPIRequest(ctx context.Context, stackID int64, method string, path string, body any, responseData any) error {
func (c *Client) doAPIRequest(ctx context.Context, rawURL string, stackID int64, method string, path string, body any, responseData any) error {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("failed to parse frontend o11y API url: %w", err)
}

var reqBodyBytes io.Reader
if body != nil {
bs, err := json.Marshal(body)
Expand All @@ -150,7 +153,7 @@ func (c *Client) doAPIRequest(ctx context.Context, stackID int64, method string,
reqBodyBytes = bytes.NewReader(bs)
}

req, err := http.NewRequestWithContext(ctx, method, c.apiURL.String()+path, reqBodyBytes)
req, err := http.NewRequestWithContext(ctx, method, parsedURL.String()+path, reqBodyBytes)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
Expand Down
Loading

0 comments on commit f25a604

Please sign in to comment.