Skip to content

feat: add client for the IaC API [ENG-9440] - #265

Merged
albertodonato merged 1 commit into
mainfrom
ack/iac-client
Oct 23, 2026
Merged

albertodonato merged 1 commit into
mainfrom
ack/iac-client

Conversation

@albertodonato

@albertodonato albertodonato commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

ENG-9440

what

Add internal/api/iac, a JSON REST client for the IaC (Sinistral) API, built
on the shared transports from internal/api. Non-successful responses are
converted to api.NotFound (404) or api.APIError, reporting the FastAPI
detail message (including validation errors) or the API gateway message.

Add the iac_endpoint provider attribute (also STACKLET_IAC_ENDPOINT
environment variable) for the IaC API endpoint. If not set, it's derived from
the Stacklet API endpoint by replacing the api. hostname prefix with
api.sinistral.. The IaC client uses the same API key as the platform one.

If the IaC endpoint can't be determined, the provider configuration fails.

Provider data now includes an IaCAPI client, which is exposed to resources
and data sources as iacAPI.

why

Allow adding stacklet_iac_* resources and data sources.

testing

Unit tests against a local HTTP server for all request methods, path escaping
and error responses, and for endpoint resolution precedence and derivation.
just lint and just test pass.

docs

Regenerated provider docs, updated dev docs.

@albertodonato
albertodonato requested a review from a team as a code owner October 23, 2026 09:24
@albertodonato albertodonato changed the title feat: add client for the IaC API feat: add client for the IaC API [ENG-9440] Oct 23, 2026
@albertodonato
albertodonato marked this pull request as draft October 23, 2026 09:24

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 13 files

Confidence score: 2/5

  • iac.New returns an *iac.API with no exported request operations, so resources and data sources cannot make IaC requests. Expose the client operations on API.
  • client.go accepts http:// endpoints and sends the bearer API key without TLS. Reject non-HTTPS endpoint schemes.
  • client.go can send requests to the wrong endpoint: path normalization removes dot-segment identifiers, and query handling drops parameters from the configured endpoint. Preserve path segments and merge query parameters.
  • client_test.go uses malformed JSON in its raw-string fixtures, so the tests fail before exercising the client. Remove the unnecessary backslashes from the fixtures.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/api/iac/client_test.go">

<violation number="1" location="internal/api/iac/client_test.go:50">
P2: These raw-string JSON fixtures escape quotes unnecessarily, so the tests send malformed JSON and fail instead of exercising the client. Remove the backslashes from all JSON raw strings, including the expected request body.</violation>
</file>

<file name="internal/api/iac/client.go">

<violation number="1" location="internal/api/iac/client.go:68">
P1: Custom agent: **Flag Security Vulnerabilities**

This accepts `http://` IaC endpoints, so the shared transport sends the bearer API key without TLS. Reject non-HTTPS endpoint schemes before making the request.</violation>

<violation number="2" location="internal/api/iac/client.go:72">
P2: `JoinPath` cleans `.` and `..` after `resourcePath` escapes its segments, so identifiers equal to a dot segment are removed and the request can target the wrong API path. Build the URL without path normalization or encode dot segments in a way that survives joining.</violation>

<violation number="3" location="internal/api/iac/client.go:73">
P2: This assignment drops query parameters supplied as part of the configured IaC endpoint. Preserve the endpoint query and merge request parameters instead, otherwise valid endpoint configurations with tenant, routing, or gateway parameters send requests to the wrong API context.</violation>
</file>

<file name="internal/api/iac/api.go">

<violation number="1" location="internal/api/iac/api.go:6">
P1: `iac.New` returns an API wrapper with no exported operations, so resources and data sources receiving `*iac.API` cannot make any IaC request. Expose the request methods on `API` (or embed an exported client) before wiring this client into provider data.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant TF as Terraform Core
    participant Prov as Stacklet Provider
    participant Cred as getCredentials()
    participant Platform as Platform API Client
    participant IaCClient as IaC REST Client
    participant Transport as Shared Transport
    participant IaCAPI as IaC (Sinistral) API

    Note over TF,IaCAPI: Provider Configuration Flow

    TF->>Prov: Configure(provider block)
    Prov->>Cred: Resolve credentials

    alt iac_endpoint set in provider block
        Cred->>Cred: Use iac_endpoint from config
    else STACKLET_IAC_ENDPOINT env var set
        Cred->>Cred: Use env var value
    else Derive from api endpoint
        Cred->>Cred: Replace "api." prefix with "api.sinistral."
    else Cannot derive
        Cred->>Prov: Configuration error
    end

    Cred-->>Prov: Endpoint + shared API key
    Prov->>Platform: Create platform client
    Prov->>IaCClient: Create IaC client (same API key)
    Prov-->>TF: Provider ready

    Note over TF,IaCAPI: IaC Resource/Data Source Request Flow

    TF->>Prov: Read/Create/Update/Delete resource
    Prov->>IaCClient: Get()/Post()/Put()/Patch()/Delete()
    IaCClient->>IaCClient: resourcePath() escape components
    IaCClient->>Transport: HTTP request + Bearer token
    Transport->>IaCAPI: JSON request
    IaCAPI-->>Transport: JSON response

    alt 2xx success
        IaCClient->>IaCClient: Decode response into model
        IaCClient-->>Prov: Success (or no content)
        Prov-->>TF: Resource state
    else 404 Not Found
        IaCAPI-->>IaCClient: {"detail": "..."}
        IaCClient->>IaCClient: return api.NotFound
        IaCClient-->>Prov: NotFound error
        Prov-->>TF: Error diagnostic
    else other error status
        IaCAPI-->>IaCClient: {"detail": [...]} or {"message": "..."}
        IaCClient->>IaCClient: return api.APIError
        IaCClient-->>Prov: APIError
        Prov-->>TF: Error diagnostic
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

func (c *client) do(ctx context.Context, method string, path string, query url.Values, payload any, response any) error {
reqURL, err := url.Parse(c.endpoint)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

This accepts http:// IaC endpoints, so the shared transport sends the bearer API key without TLS. Reject non-HTTPS endpoint schemes before making the request.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/iac/client.go, line 68:

<comment>This accepts `http://` IaC endpoints, so the shared transport sends the bearer API key without TLS. Reject non-HTTPS endpoint schemes before making the request.</comment>

<file context>
@@ -0,0 +1,123 @@
+}
+
+func (c *client) do(ctx context.Context, method string, path string, query url.Values, payload any, response any) error {
+	reqURL, err := url.Parse(c.endpoint)
+	if err != nil {
+		return api.NewAPIError(fmt.Errorf("invalid IaC API endpoint: %w", err))
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not relevant at the client level, actual deployments always use https

Comment thread internal/api/iac/api.go
package iac

// API provides access to the IaC REST API.
type API struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: iac.New returns an API wrapper with no exported operations, so resources and data sources receiving *iac.API cannot make any IaC request. Expose the request methods on API (or embed an exported client) before wiring this client into provider data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/iac/api.go, line 6:

<comment>`iac.New` returns an API wrapper with no exported operations, so resources and data sources receiving `*iac.API` cannot make any IaC request. Expose the request methods on `API` (or embed an exported client) before wiring this client into provider data.</comment>

<file context>
@@ -0,0 +1,15 @@
+package iac
+
+// API provides access to the IaC REST API.
+type API struct {
+	c *client
+}
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch just adds the barebone client, followup will start exposing resources through the API object (similar to the graphql one)

}

func TestClientGet(t *testing.T) {
c, req := newTestServer(t, http.StatusOK, `{"name": "foo"}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These raw-string JSON fixtures escape quotes unnecessarily, so the tests send malformed JSON and fail instead of exercising the client. Remove the backslashes from all JSON raw strings, including the expected request body.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/iac/client_test.go, line 50:

<comment>These raw-string JSON fixtures escape quotes unnecessarily, so the tests send malformed JSON and fail instead of exercising the client. Remove the backslashes from all JSON raw strings, including the expected request body.</comment>

<file context>
@@ -0,0 +1,176 @@
+}
+
+func TestClientGet(t *testing.T) {
+	c, req := newTestServer(t, http.StatusOK, `{"name": "foo"}`)
+
+	var response item
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not true, the content is passed as-is

return api.NewAPIError(fmt.Errorf("invalid IaC API endpoint: %w", err))
}
reqURL = reqURL.JoinPath(path)
reqURL.RawQuery = query.Encode()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This assignment drops query parameters supplied as part of the configured IaC endpoint. Preserve the endpoint query and merge request parameters instead, otherwise valid endpoint configurations with tenant, routing, or gateway parameters send requests to the wrong API context.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/iac/client.go, line 73:

<comment>This assignment drops query parameters supplied as part of the configured IaC endpoint. Preserve the endpoint query and merge request parameters instead, otherwise valid endpoint configurations with tenant, routing, or gateway parameters send requests to the wrong API context.</comment>

<file context>
@@ -0,0 +1,123 @@
+		return api.NewAPIError(fmt.Errorf("invalid IaC API endpoint: %w", err))
+	}
+	reqURL = reqURL.JoinPath(path)
+	reqURL.RawQuery = query.Encode()
+
+	var body io.Reader
</file context>
Suggested change
reqURL.RawQuery = query.Encode()
endpointQuery := reqURL.Query()
for key, values := range query {
for _, value := range values {
endpointQuery.Add(key, value)
}
}
reqURL.RawQuery = endpointQuery.Encode()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request URL is meant to be a plain URL, not containing parameters

if err != nil {
return api.NewAPIError(fmt.Errorf("invalid IaC API endpoint: %w", err))
}
reqURL = reqURL.JoinPath(path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: JoinPath cleans . and .. after resourcePath escapes its segments, so identifiers equal to a dot segment are removed and the request can target the wrong API path. Build the URL without path normalization or encode dot segments in a way that survives joining.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/api/iac/client.go, line 72:

<comment>`JoinPath` cleans `.` and `..` after `resourcePath` escapes its segments, so identifiers equal to a dot segment are removed and the request can target the wrong API path. Build the URL without path normalization or encode dot segments in a way that survives joining.</comment>

<file context>
@@ -0,0 +1,123 @@
+	if err != nil {
+		return api.NewAPIError(fmt.Errorf("invalid IaC API endpoint: %w", err))
+	}
+	reqURL = reqURL.JoinPath(path)
+	reqURL.RawQuery = query.Encode()
+
</file context>

Comment thread docs/index.md Outdated
@albertodonato
albertodonato force-pushed the ack/iac-client branch 2 times, most recently from 0f9ac0f to 98cd14e Compare October 23, 2026 13:59
### what

Add `internal/api/iac`, a JSON REST client for the IaC (Sinistral) API, built
on the shared transports from `internal/api`. Non-successful responses are
converted to `api.NotFound` (404) or `api.APIError`, reporting the FastAPI
`detail` message (including validation errors) or the API gateway `message`.

Add the `iac_endpoint` provider attribute (also `STACKLET_IAC_ENDPOINT`
environment variable) for the IaC API endpoint. If not set, it's derived from
the Stacklet API endpoint by replacing the `api.` hostname prefix with
`api.sinistral.`. The IaC client uses the same API key as the platform one.

If the IaC endpoint can't be determined, the provider configuration fails.

Provider data now includes an `IaCAPI` client, which is exposed to resources
and data sources as `iacAPI`.

### why

Allow adding `stacklet_iac_*` resources and data sources.

### testing

Unit tests against a local HTTP server for all request methods, path escaping
and error responses, and for endpoint resolution precedence and derivation.
`just lint` and `just test` pass.

### docs

Regenerated provider docs, updated dev docs.
@albertodonato
albertodonato marked this pull request as ready for review October 23, 2026 14:05
@albertodonato
albertodonato added this pull request to stack #267 October 23, 2026 14:57

@fwereade fwereade left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks

@albertodonato
albertodonato merged commit 6181a0b into main Oct 23, 2026
4 checks passed
@albertodonato
albertodonato deleted the ack/iac-client branch October 23, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants