mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add Bedrock Role ARN field in UI (#26578)
Adds a Role ARN field for the Bedrock provider in the UI. When set, the gateway assumes that IAM role (using the base identity) before calling Bedrock. The field is optional and non-secret, so it round-trips back into the form on edit and clears when left blank. <hr/> Related PR: https://github.com/coder/coder/pull/26527 Related issue: https://linear.app/codercom/issue/AIGOV-371/support-dynamic-bedrock-assumerole-across-aws-accounts-for-ai-gateway
This commit is contained in:
@@ -25,6 +25,7 @@ export type ProviderFormValues = {
|
||||
smallFastModel: string;
|
||||
accessKey: string;
|
||||
accessKeySecret: string;
|
||||
roleArn: string;
|
||||
apiKey: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
@@ -66,6 +67,7 @@ const defaultInitialValues: ProviderFormValues = {
|
||||
smallFastModel: "",
|
||||
accessKey: "",
|
||||
accessKeySecret: "",
|
||||
roleArn: "",
|
||||
apiKey: "",
|
||||
enabled: true,
|
||||
};
|
||||
@@ -526,6 +528,16 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
View docs
|
||||
</DocsLink>
|
||||
</p>
|
||||
<FormField
|
||||
field={getFieldHelpers("roleArn")}
|
||||
label="Role ARN"
|
||||
className="w-full"
|
||||
placeholder="arn:aws:iam::123456789012:role/BedrockRole"
|
||||
/>
|
||||
<p className="text-xs text-content-secondary m-0">
|
||||
Optional. When a role ARN is set, the gateway assumes that role
|
||||
(using the base identity) before calling Bedrock.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ const baseOpenAIFormValues: ProviderFormValues = {
|
||||
smallFastModel: "",
|
||||
accessKey: "",
|
||||
accessKeySecret: "",
|
||||
roleArn: "",
|
||||
apiKey: "sk-test",
|
||||
enabled: true,
|
||||
};
|
||||
@@ -42,6 +43,7 @@ const baseBedrockFormValues: ProviderFormValues = {
|
||||
smallFastModel: "anthropic.claude-haiku-4-5",
|
||||
accessKey: "AKIA-test",
|
||||
accessKeySecret: "secret",
|
||||
roleArn: "",
|
||||
apiKey: "",
|
||||
enabled: true,
|
||||
};
|
||||
@@ -55,6 +57,7 @@ const baseCopilotFormValues: ProviderFormValues = {
|
||||
smallFastModel: "",
|
||||
accessKey: "",
|
||||
accessKeySecret: "",
|
||||
roleArn: "",
|
||||
apiKey: "",
|
||||
enabled: true,
|
||||
};
|
||||
@@ -407,6 +410,30 @@ describe("providerFormValuesToCreate", () => {
|
||||
});
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes role_arn when a role ARN is provided", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseBedrockFormValues,
|
||||
roleArn: "arn:aws:iam::123456789012:role/BedrockRole",
|
||||
});
|
||||
const s = req.settings as unknown as Record<string, unknown>;
|
||||
expect(s.role_arn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
|
||||
});
|
||||
|
||||
it("omits role_arn when the form value is blank", () => {
|
||||
const req = providerFormValuesToCreate(baseBedrockFormValues);
|
||||
const s = req.settings as unknown as Record<string, unknown>;
|
||||
expect(s.role_arn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("trims whitespace around the role ARN", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseBedrockFormValues,
|
||||
roleArn: " arn:aws:iam::123456789012:role/BedrockRole ",
|
||||
});
|
||||
const s = req.settings as unknown as Record<string, unknown>;
|
||||
expect(s.role_arn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copilot", () => {
|
||||
@@ -559,6 +586,32 @@ describe("providerFormValuesToUpdate", () => {
|
||||
expect(s.access_key).toBeUndefined();
|
||||
expect(s.access_key_secret).toBeUndefined();
|
||||
});
|
||||
|
||||
it("sends role_arn even when the access keys are kept", () => {
|
||||
const req = providerFormValuesToUpdate(
|
||||
{
|
||||
...baseBedrockFormValues,
|
||||
accessKey: SAVED_CREDENTIAL_MASK,
|
||||
accessKeySecret: SAVED_CREDENTIAL_MASK,
|
||||
roleArn: "arn:aws:iam::123456789012:role/BedrockRole",
|
||||
},
|
||||
MockAIProviderBedrock,
|
||||
);
|
||||
const s = req.settings as unknown as Record<string, unknown>;
|
||||
expect(s.role_arn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
|
||||
});
|
||||
|
||||
it("omits role_arn when the field is cleared", () => {
|
||||
const req = providerFormValuesToUpdate(
|
||||
{
|
||||
...baseBedrockFormValues,
|
||||
roleArn: "",
|
||||
},
|
||||
MockAIProviderBedrock,
|
||||
);
|
||||
const s = req.settings as unknown as Record<string, unknown>;
|
||||
expect(s.role_arn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copilot", () => {
|
||||
@@ -635,6 +688,23 @@ describe("aiProviderToFormValues", () => {
|
||||
expect(values.accessKeySecret).toBe("");
|
||||
});
|
||||
|
||||
it("round-trips role_arn back into the form", () => {
|
||||
const provider: AIProvider = {
|
||||
...MockAIProviderBedrock,
|
||||
settings: settings({
|
||||
_type: "bedrock",
|
||||
role_arn: "arn:aws:iam::123456789012:role/BedrockRole",
|
||||
}),
|
||||
};
|
||||
const values = aiProviderToFormValues(provider);
|
||||
expect(values.roleArn).toBe("arn:aws:iam::123456789012:role/BedrockRole");
|
||||
});
|
||||
|
||||
it("seeds an empty role ARN when the provider has none", () => {
|
||||
const values = aiProviderToFormValues(MockAIProviderBedrock);
|
||||
expect(values.roleArn).toBe("");
|
||||
});
|
||||
|
||||
it("seeds Copilot form values without a credential field", () => {
|
||||
const values = aiProviderToFormValues(MockAIProviderCopilot);
|
||||
expect(values.type).toBe("copilot");
|
||||
|
||||
@@ -109,6 +109,7 @@ const buildBedrockSettings = (
|
||||
smallFastModel: string,
|
||||
accessKey: string,
|
||||
accessKeySecret: string,
|
||||
roleArn: string,
|
||||
): BedrockSettingsWire => ({
|
||||
_type: BEDROCK_SETTINGS_TYPE,
|
||||
_version: BEDROCK_SETTINGS_VERSION,
|
||||
@@ -117,6 +118,7 @@ const buildBedrockSettings = (
|
||||
small_fast_model: smallFastModel,
|
||||
...(accessKey ? { access_key: accessKey } : {}),
|
||||
...(accessKeySecret ? { access_key_secret: accessKeySecret } : {}),
|
||||
...(roleArn ? { role_arn: roleArn } : {}),
|
||||
});
|
||||
|
||||
// Bedrock credentials live in `settings`; openai/anthropic keys go in
|
||||
@@ -141,6 +143,7 @@ export const providerFormValuesToCreate = (
|
||||
values.smallFastModel.trim(),
|
||||
sanitizeCredential(values.accessKey),
|
||||
sanitizeCredential(values.accessKeySecret),
|
||||
values.roleArn.trim(),
|
||||
);
|
||||
return {
|
||||
type: "anthropic",
|
||||
@@ -215,6 +218,7 @@ export const providerFormValuesToUpdate = (
|
||||
values.smallFastModel.trim(),
|
||||
credentialsChanged ? newAccessKey : "",
|
||||
credentialsChanged ? newAccessKeySecret : "",
|
||||
values.roleArn.trim(),
|
||||
);
|
||||
|
||||
return { ...base, settings: settings as AIProviderSettings };
|
||||
@@ -238,6 +242,7 @@ export const aiProviderToFormValues = (
|
||||
smallFastModel: s.small_fast_model ?? "",
|
||||
accessKey: "",
|
||||
accessKeySecret: "",
|
||||
roleArn: s.role_arn ?? "",
|
||||
enabled: provider.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user