Skip to content

Commit a45c5c2

Browse files
Backport of "credentials: add new function to read credentials" to rel/v0.20 (#786)
Backport of #782 to `rel/v0.20`. --- Introduce a new function to read Oxide API credentials from a local credentials file, which can be useful when reading credentials based on profile names. Close #781 Co-authored-by: Luiz Aoqui <luiz@oxidecomputer.com>
1 parent 5ae0631 commit a45c5c2

9 files changed

Lines changed: 271 additions & 2 deletions

File tree

.changelog/0.20.1.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[[features]]
2+
title = "New function `provider::oxide::credentials`"
3+
description = "Added new function to read Oxide API credentials from a credentials file. [#782](https://github.com/oxidecomputer/terraform-provider-oxide/pull/782)"

docs/functions/credentials.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "credentials function - terraform-provider-oxide"
4+
subcategory: ""
5+
description: |-
6+
Reads an Oxide credentials file.
7+
---
8+
9+
# function: credentials
10+
11+
Reads Oxide API credentials from a local credentials file and returns a map of
12+
credentials grouped by profile name. Refer to the [Oxide CLI
13+
documentation](https://docs.oxide.computer/cli/guides/introduction) for more
14+
information.
15+
16+
## Example Usage
17+
18+
```terraform
19+
locals {
20+
credentials = provider::oxide::credentials(pathexpand("~/.config/oxide/credentials.toml"))
21+
22+
# Alternatively, pass null or an empty string to use the default
23+
# configuration file path.
24+
# credentials = provider::oxide::credentials(null)
25+
26+
prod_host = local.credentials["prod"].host
27+
prod_api_token = local.credentials["prod"].token
28+
}
29+
```
30+
31+
## Signature
32+
33+
<!-- signature generated by tfplugindocs -->
34+
```text
35+
credentials(path string) map of object
36+
```
37+
38+
## Arguments
39+
40+
<!-- arguments generated by tfplugindocs -->
41+
1. `path` (String, Nullable) Credentials file path. Defaults to `$HOME/.config/oxide/credentials.toml` if empty or `null`.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
locals {
2+
credentials = provider::oxide::credentials(pathexpand("~/.config/oxide/credentials.toml"))
3+
4+
# Alternatively, pass null or an empty string to use the default
5+
# configuration file path.
6+
# credentials = provider::oxide::credentials(null)
7+
8+
prod_host = local.credentials["prod"].host
9+
prod_api_token = local.credentials["prod"].token
10+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/oxidecomputer/terraform-provider-oxide
33
go 1.25.8
44

55
require (
6+
github.com/BurntSushi/toml v1.6.0
67
github.com/google/uuid v1.6.0
78
github.com/hashicorp/terraform-plugin-framework v1.19.0
89
github.com/hashicorp/terraform-plugin-framework-nettypes v0.3.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
22
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
3+
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
4+
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
35
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
46
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
57
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package credentials
2+
3+
import (
4+
"context"
5+
"maps"
6+
"os"
7+
"path"
8+
9+
"github.com/BurntSushi/toml"
10+
"github.com/hashicorp/terraform-plugin-framework/attr"
11+
"github.com/hashicorp/terraform-plugin-framework/function"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
)
14+
15+
var _ function.Function = &Function{}
16+
17+
const DefaultPath = ".config/oxide/credentials.toml"
18+
19+
type credential struct {
20+
Host string `toml:"host" tfsdk:"host"`
21+
Token string `toml:"token" tfsdk:"token"`
22+
TokenID string `toml:"token_id" tfsdk:"token_id"`
23+
User string `toml:"user" tfsdk:"user"`
24+
}
25+
26+
type credentials struct {
27+
Profiles map[string]credential `toml:"profile"`
28+
}
29+
30+
func NewFunction() function.Function {
31+
return &Function{}
32+
}
33+
34+
type Function struct{}
35+
36+
func (f *Function) Metadata(
37+
ctx context.Context,
38+
req function.MetadataRequest,
39+
resp *function.MetadataResponse,
40+
) {
41+
resp.Name = "credentials"
42+
}
43+
44+
func (f *Function) Definition(
45+
ctx context.Context,
46+
req function.DefinitionRequest,
47+
resp *function.DefinitionResponse,
48+
) {
49+
resp.Definition = function.Definition{
50+
Summary: "Reads an Oxide credentials file.",
51+
MarkdownDescription: `
52+
Reads Oxide API credentials from a local credentials file and returns a map of
53+
credentials grouped by profile name. Refer to the [Oxide CLI
54+
documentation](https://docs.oxide.computer/cli/guides/introduction) for more
55+
information.
56+
`,
57+
Parameters: []function.Parameter{
58+
function.StringParameter{
59+
Name: "path",
60+
Description: "Credentials file path. Defaults to `$HOME/.config/oxide/credentials.toml` if empty or `null`.",
61+
AllowNullValue: true,
62+
},
63+
},
64+
Return: function.MapReturn{
65+
ElementType: types.ObjectType{
66+
AttrTypes: map[string]attr.Type{
67+
"host": types.StringType,
68+
"token": types.StringType,
69+
"token_id": types.StringType,
70+
"user": types.StringType,
71+
},
72+
},
73+
},
74+
}
75+
}
76+
77+
func (f *Function) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
78+
var pathInput types.String
79+
resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &pathInput))
80+
81+
pathStr := pathInput.ValueString()
82+
if pathStr == "" {
83+
homeDir, err := os.UserHomeDir()
84+
if err != nil {
85+
resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(err.Error()))
86+
return
87+
}
88+
89+
pathStr = path.Join(homeDir, DefaultPath)
90+
}
91+
92+
var creds credentials
93+
if _, err := toml.DecodeFile(pathStr, &creds); err != nil {
94+
resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(err.Error()))
95+
return
96+
}
97+
98+
output := make(map[string]credential)
99+
maps.Copy(output, creds.Profiles)
100+
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, output))
101+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package credentials_test
2+
3+
import (
4+
"os"
5+
"path"
6+
"regexp"
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
11+
"github.com/hashicorp/terraform-plugin-testing/statecheck"
12+
13+
"github.com/oxidecomputer/terraform-provider-oxide/internal/provider/credentials"
14+
"github.com/oxidecomputer/terraform-provider-oxide/internal/provider/sharedtest"
15+
)
16+
17+
var credentialsToml = `
18+
[profile.alpha]
19+
host = "https://alpha.example.com"
20+
token = "oxide-token-alpha"
21+
token_id = "f431a587-bb8b-41c7-8ccb-6a9e59240bc7"
22+
user = "14e23a0b-5fbd-411c-9385-3a7d2462867e"
23+
24+
[profile.beta]
25+
host = "https://beta.example.com"
26+
token = "oxide-token-beta"
27+
token_id = "3e70d706-5e18-47bb-8205-cd48552fd6da"
28+
user = "85044b9a-7a89-411d-b634-fddba4713d42"
29+
`
30+
31+
var functionTpl = `
32+
locals {
33+
creds = provider::oxide::credentials({{.Path}})
34+
}
35+
36+
output "alpha_token" {
37+
value = local.creds["alpha"].token
38+
}
39+
40+
output "beta_host" {
41+
value = local.creds["beta"].host
42+
}
43+
`
44+
45+
type functionTplConfig struct {
46+
Path string
47+
}
48+
49+
func TestFunction(t *testing.T) {
50+
// Temporarily override $HOME to test reading credentials from the default
51+
// file path.
52+
tmpHome := t.TempDir()
53+
t.Setenv("HOME", tmpHome)
54+
55+
// Create temporary credential file.
56+
credsPath := path.Join(tmpHome, credentials.DefaultPath)
57+
if err := os.MkdirAll(path.Dir(credsPath), 0777); err != nil {
58+
t.Fatalf("error creating test credentials file: %v", err)
59+
}
60+
if err := os.WriteFile(credsPath, []byte(credentialsToml), 0666); err != nil {
61+
t.Fatalf("error creating test credentials file: %v", err)
62+
}
63+
64+
config := func(path string) string {
65+
return sharedtest.ParsedAccConfig(t,
66+
functionTplConfig{Path: path},
67+
functionTpl,
68+
)
69+
}
70+
71+
stateChecks := []statecheck.StateCheck{
72+
statecheck.ExpectKnownOutputValue(
73+
"alpha_token",
74+
knownvalue.StringExact("oxide-token-alpha"),
75+
),
76+
statecheck.ExpectKnownOutputValue(
77+
"beta_host",
78+
knownvalue.StringExact("https://beta.example.com"),
79+
),
80+
}
81+
82+
resource.Test(t, resource.TestCase{
83+
PreCheck: func() { sharedtest.PreCheck(t) },
84+
ProtoV6ProviderFactories: sharedtest.ProviderFactories(),
85+
Steps: []resource.TestStep{
86+
{
87+
// Use default credential file if empty.
88+
Config: config(`""`),
89+
ConfigStateChecks: stateChecks,
90+
},
91+
{
92+
// Use default credential file if null.
93+
Config: config("null"),
94+
ConfigStateChecks: stateChecks,
95+
},
96+
{
97+
// Use specified file.
98+
Config: config(`"` + credsPath + `"`),
99+
ConfigStateChecks: stateChecks,
100+
},
101+
{
102+
// Invalid file.
103+
Config: config(`"doesnt-exist"`),
104+
ExpectError: regexp.MustCompile(`doesnt-exist`),
105+
},
106+
},
107+
})
108+
}

internal/provider/provider.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
addresslot "github.com/oxidecomputer/terraform-provider-oxide/internal/provider/address_lot"
2424
antiaffinitygroup "github.com/oxidecomputer/terraform-provider-oxide/internal/provider/anti_affinity_group"
25+
"github.com/oxidecomputer/terraform-provider-oxide/internal/provider/credentials"
2526
"github.com/oxidecomputer/terraform-provider-oxide/internal/provider/disk"
2627
externalsubnet "github.com/oxidecomputer/terraform-provider-oxide/internal/provider/external_subnet"
2728
externalsubnetattachment "github.com/oxidecomputer/terraform-provider-oxide/internal/provider/external_subnet_attachment"
@@ -246,5 +247,7 @@ func (p *oxideProvider) Resources(_ context.Context) []func() resource.Resource
246247

247248
// Functions defines the functions implemented in the provider.
248249
func (p *oxideProvider) Functions(_ context.Context) []func() function.Function {
249-
return []func() function.Function{}
250+
return []func() function.Function{
251+
credentials.NewFunction,
252+
}
250253
}

internal/provider/sharedtest/sharedtest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import (
88
"bytes"
99
"fmt"
1010
"hash/fnv"
11-
"html/template"
1211
"os"
1312
"sync/atomic"
1413
"testing"
14+
"text/template"
1515

1616
"github.com/google/uuid"
1717
"github.com/hashicorp/terraform-plugin-framework/providerserver"

0 commit comments

Comments
 (0)