Skip to content

Commit 8c8fa7f

Browse files
committed
feat: Add OIDC client resource.
1 parent 6007b0b commit 8c8fa7f

3 files changed

Lines changed: 330 additions & 0 deletions

File tree

rancher2/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func Provider() terraform.ResourceProvider {
140140
"rancher2_namespace": resourceRancher2Namespace(),
141141
"rancher2_node_driver": resourceRancher2NodeDriver(),
142142
"rancher2_node_pool": resourceRancher2NodePool(),
143+
"rancher2_oidc_client": resourceRancher2OIDCClient(),
143144
"rancher2_pod_security_admission_configuration_template": resourceRancher2PodSecurityAdmissionConfigurationTemplate(),
144145
"rancher2_project": resourceRancher2Project(),
145146
"rancher2_project_role_template_binding": resourceRancher2ProjectRoleTemplateBinding(),
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package rancher2
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"log"
7+
"maps"
8+
"time"
9+
10+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
11+
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
12+
managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3"
13+
)
14+
15+
const (
16+
oidcClientAPIType = "management.cattle.io.oidcclient"
17+
)
18+
19+
func resourceRancher2OIDCClient() *schema.Resource {
20+
return &schema.Resource{
21+
Create: resourceRancher2OIDCClientCreate,
22+
Read: resourceRancher2OIDCClientRead,
23+
Update: resourceRancher2OIDCClientUpdate,
24+
Delete: resourceRancher2OIDCClientDelete,
25+
26+
Schema: oidcClientFields(),
27+
Timeouts: &schema.ResourceTimeout{
28+
Create: schema.DefaultTimeout(5 * time.Minute),
29+
Update: schema.DefaultTimeout(5 * time.Minute),
30+
Delete: schema.DefaultTimeout(5 * time.Minute),
31+
},
32+
}
33+
}
34+
35+
func oidcClientFields() map[string]*schema.Schema {
36+
s := map[string]*schema.Schema{
37+
"token_expiration_seconds": {
38+
Type: schema.TypeInt,
39+
Optional: true,
40+
Computed: true,
41+
Description: "The duration (in seconds) before an access token and ID token expires. Reducing this will invalidate existing tokens.",
42+
ValidateFunc: validation.IntAtLeast(1),
43+
},
44+
"refresh_token_expiration_seconds": {
45+
Type: schema.TypeInt,
46+
Optional: true,
47+
Computed: true,
48+
Description: "The duration (in seconds) a refresh token remains valid before expiration. Reducing this will invalidate existing tokens.",
49+
ValidateFunc: validation.IntAtLeast(1),
50+
},
51+
"redirect_uris": {
52+
Description: "List of allowed redirect_uris for this client.",
53+
Required: true,
54+
Type: schema.TypeList,
55+
Elem: &schema.Schema{
56+
Type: schema.TypeString,
57+
},
58+
},
59+
"description": {
60+
Type: schema.TypeString,
61+
Optional: true,
62+
Description: "OIDCClient description",
63+
},
64+
}
65+
66+
maps.Copy(s, commonAnnotationLabelFields())
67+
68+
return s
69+
}
70+
71+
func resourceRancher2OIDCClientCreate(d *schema.ResourceData, meta any) error {
72+
log.Printf("[INFO] Creating OIDCClient")
73+
oidcClient, err := expandOIDCClient(d)
74+
if err != nil {
75+
return err
76+
}
77+
78+
client, err := meta.(*Config).ManagementClient()
79+
if err != nil {
80+
log.Printf("[ERROR] getting a client when updating OIDC Client: %s", err)
81+
return err
82+
}
83+
84+
updatedClient, err := client.OIDCClient.Create(oidcClient)
85+
if err != nil {
86+
log.Printf("[ERROR] creating OIDCClient: %s", err)
87+
return err
88+
}
89+
90+
d.SetId(updatedClient.ID)
91+
92+
return resourceRancher2OIDCClientRead(d, meta)
93+
}
94+
95+
func resourceRancher2OIDCClientRead(d *schema.ResourceData, meta any) error {
96+
log.Printf("[INFO] Refreshing OIDCClient ID %s", d.Id())
97+
98+
client, err := meta.(*Config).ManagementClient()
99+
if err != nil {
100+
log.Printf("[ERROR] getting a client when reading OIDC Client: %s", err)
101+
return err
102+
}
103+
oidcClient, err := client.OIDCClient.ByID(d.Id())
104+
if err != nil {
105+
log.Printf("[ERROR] reading OIDC Client %s: %s", d.Id(), err)
106+
return err
107+
}
108+
109+
return flattenOIDCClient(d, oidcClient)
110+
}
111+
112+
func resourceRancher2OIDCClientUpdate(d *schema.ResourceData, meta any) error {
113+
log.Printf("[INFO] Updating OIDCClient ID %s", d.Id())
114+
115+
client, err := meta.(*Config).ManagementClient()
116+
if err != nil {
117+
log.Printf("[ERROR] getting a client when updating OIDC Client: %s", err)
118+
return err
119+
}
120+
121+
oidcClient, err := client.OIDCClient.ByID(d.Id())
122+
if err != nil {
123+
log.Printf("[ERROR] getting OIDC Client %s for update: %s", d.Id(), err)
124+
return err
125+
}
126+
127+
update := map[string]any{
128+
"description": d.Get("description"),
129+
"redirectURIs": d.Get("redirect_uris"),
130+
"token_expiration_seconds": d.Get("token_expiration_seconds"),
131+
"refresh_token_expiration_seconds": d.Get("refresh_token_expiration_seconds"),
132+
}
133+
134+
_, err = client.OIDCClient.Update(oidcClient, update)
135+
if err != nil {
136+
log.Printf("[ERROR] updating OIDC Client %s: %s", d.Id(), err)
137+
}
138+
139+
return resourceRancher2OIDCClientRead(d, meta)
140+
}
141+
142+
func resourceRancher2OIDCClientDelete(d *schema.ResourceData, meta any) error {
143+
log.Printf("[INFO] Deleting OIDCClient ID %s", d.Id())
144+
145+
client, err := meta.(*Config).ManagementClient()
146+
if err != nil {
147+
log.Printf("[ERROR] getting a client when deleting OIDC Client: %s", err)
148+
return err
149+
}
150+
151+
oidcClient, err := client.OIDCClient.ByID(d.Id())
152+
if err != nil {
153+
log.Printf("[ERROR] getting OIDC Client %s for deletion: %s", d.Id(), err)
154+
return err
155+
}
156+
157+
if err := client.OIDCClient.Delete(oidcClient); err != nil {
158+
log.Printf("[ERROR] deleting OIDC Client %s: %s", d.Id(), err)
159+
return err
160+
}
161+
162+
return nil
163+
}
164+
165+
func expandOIDCClient(in *schema.ResourceData) (*managementClient.OIDCClient, error) {
166+
obj := &managementClient.OIDCClient{}
167+
if in == nil {
168+
return nil, fmt.Errorf("[ERROR] expanding cluster: Input ResourceData is nil")
169+
}
170+
171+
if v := in.Id(); len(v) > 0 {
172+
obj.ID = v
173+
}
174+
175+
if v, ok := in.GetOk("description"); ok {
176+
obj.Description = v.(string)
177+
}
178+
179+
v := in.Get("redirect_uris")
180+
obj.RedirectURIs = toArrayStringSorted(v.([]any))
181+
182+
v, ok := in.GetOk("token_expiration_seconds")
183+
if ok {
184+
// This should be safe because the field declared as an integer.
185+
tokenExpirationSeconds, _ := v.(int)
186+
obj.TokenExpirationSeconds = int64(tokenExpirationSeconds)
187+
} else {
188+
obj.TokenExpirationSeconds = 0
189+
}
190+
191+
v, ok = in.GetOk("refresh_token_expiration_seconds")
192+
if ok {
193+
// This should be safe because the field declared as an integer.
194+
refreshTokenExpirationSeconds, _ := v.(int)
195+
obj.RefreshTokenExpirationSeconds = int64(refreshTokenExpirationSeconds)
196+
} else {
197+
obj.RefreshTokenExpirationSeconds = 0
198+
}
199+
200+
return obj, nil
201+
}
202+
203+
func flattenOIDCClient(d *schema.ResourceData, in *managementClient.OIDCClient) error {
204+
if in == nil {
205+
return fmt.Errorf("[ERROR] flattening OIDCClient: Input config is nil")
206+
}
207+
208+
if in.ID != "" {
209+
d.SetId(in.ID)
210+
}
211+
212+
return errors.Join(
213+
d.Set("description", in.Description),
214+
d.Set("redirect_uris", in.RedirectURIs),
215+
d.Set("token_expiration_seconds", in.TokenExpirationSeconds),
216+
d.Set("refresh_token_expiration_seconds", in.RefreshTokenExpirationSeconds),
217+
)
218+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package rancher2
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
7+
managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
const (
13+
testOIDCClientConfig = `
14+
resource "rancher2_oidc_client" "test_client" {
15+
description = "Test OIDC Client"
16+
token_expiration_seconds = 600 # expiration of the id_token and access_token
17+
refresh_token_expiration_seconds = 3600 # expiration of the refresh_token
18+
redirect_uris = [
19+
"https://rancher.example.com/auth/rancher/callback",
20+
]
21+
}`
22+
)
23+
24+
func TestFlattenOIDCClient(t *testing.T) {
25+
oidcClient := &managementClient.OIDCClient{
26+
Name: "testing-client",
27+
RedirectURIs: []string{
28+
"http://127.0.0.1:5556/auth/rancher/callback",
29+
"http://127.0.0.1:33418/",
30+
"https://vscode.dev/redirect",
31+
},
32+
Description: "Access for Rancher AI Agent",
33+
TokenExpirationSeconds: 6000,
34+
RefreshTokenExpirationSeconds: 12000,
35+
}
36+
37+
flattened := schema.TestResourceDataRaw(t, oidcClientFields(), nil)
38+
err := flattenOIDCClient(flattened, oidcClient)
39+
require.NoError(t, err)
40+
41+
want := map[string]any{
42+
"token_expiration_seconds": 6000,
43+
"refresh_token_expiration_seconds": 12000,
44+
"redirect_uris": []any{
45+
"http://127.0.0.1:5556/auth/rancher/callback",
46+
"http://127.0.0.1:33418/",
47+
"https://vscode.dev/redirect",
48+
},
49+
}
50+
for key, want := range want {
51+
assert.Equal(t, want, flattened.Get(key), "unexpected output from flattenOIDCClient")
52+
}
53+
}
54+
55+
func TestExpandOIDCClient(t *testing.T) {
56+
expandTests := map[string]struct {
57+
data map[string]any
58+
want *managementClient.OIDCClient
59+
}{
60+
"all fields populated": {
61+
data: map[string]any{
62+
"token_expiration_seconds": 6000,
63+
"refresh_token_expiration_seconds": 12000,
64+
"redirect_uris": []any{
65+
"http://127.0.0.1:5556/auth/rancher/callback",
66+
"http://127.0.0.1:33418/",
67+
"https://vscode.dev/redirect",
68+
},
69+
"description": "Testing OIDC Client",
70+
},
71+
want: &managementClient.OIDCClient{
72+
TokenExpirationSeconds: 6000,
73+
RefreshTokenExpirationSeconds: 12000,
74+
RedirectURIs: []string{
75+
"http://127.0.0.1:33418/",
76+
"http://127.0.0.1:5556/auth/rancher/callback",
77+
"https://vscode.dev/redirect",
78+
},
79+
Description: "Testing OIDC Client",
80+
},
81+
},
82+
"only required fields populated": {
83+
data: map[string]any{
84+
"redirect_uris": []any{
85+
"http://127.0.0.1:5556/auth/rancher/callback",
86+
"http://127.0.0.1:33418/",
87+
"https://vscode.dev/redirect",
88+
},
89+
},
90+
want: &managementClient.OIDCClient{
91+
TokenExpirationSeconds: 0,
92+
RedirectURIs: []string{
93+
"http://127.0.0.1:33418/",
94+
"http://127.0.0.1:5556/auth/rancher/callback",
95+
"https://vscode.dev/redirect",
96+
},
97+
},
98+
},
99+
}
100+
101+
for name, tt := range expandTests {
102+
t.Run(name, func(t *testing.T) {
103+
inputResourceData := schema.TestResourceDataRaw(t, oidcClientFields(), tt.data)
104+
105+
expanded, err := expandOIDCClient(inputResourceData)
106+
assert.NoError(t, err, "Error in expandOIDCClient")
107+
108+
assert.Equal(t, tt.want, expanded, "Unexpected output from expandOIDCClient")
109+
})
110+
}
111+
}

0 commit comments

Comments
 (0)