Skip to content

Commit a69c40e

Browse files
committed
feat: add license resource
1 parent bf558f5 commit a69c40e

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed

internal/provider/license_resource.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/coder/coder/v2/codersdk"
9+
"github.com/hashicorp/terraform-plugin-framework/resource"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
)
16+
17+
// Ensure provider defined types fully satisfy framework interfaces.
18+
var _ resource.Resource = &LicenseResource{}
19+
20+
func NewLicenseResource() resource.Resource {
21+
return &LicenseResource{}
22+
}
23+
24+
// LicenseResource defines the resource implementation.
25+
type LicenseResource struct {
26+
data *CoderdProviderData
27+
}
28+
29+
// LicenseResourceModel describes the resource data model.
30+
type LicenseResourceModel struct {
31+
ID types.Int32 `tfsdk:"id"`
32+
ExpiresAt types.Int64 `tfsdk:"expires_at"`
33+
License types.String `tfsdk:"license"`
34+
}
35+
36+
func (r *LicenseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
37+
resp.TypeName = req.ProviderTypeName + "_license"
38+
}
39+
40+
func (r *LicenseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
41+
resp.Schema = schema.Schema{
42+
MarkdownDescription: "A license for a Coder deployment.\n\nTerraform does not guarantee this resource" +
43+
"will be created before other resources or attributes that require a licensed deployment. " +
44+
"It is instead recommended to use the `depends_on` meta-argument.",
45+
46+
Attributes: map[string]schema.Attribute{
47+
"id": schema.Int32Attribute{
48+
MarkdownDescription: "Integer ID of the license.",
49+
Computed: true,
50+
PlanModifiers: []planmodifier.Int32{
51+
int32planmodifier.UseStateForUnknown(),
52+
},
53+
},
54+
"expires_at": schema.Int64Attribute{
55+
MarkdownDescription: "Unix timestamp of when the license expires.",
56+
Computed: true,
57+
},
58+
"license": schema.StringAttribute{
59+
MarkdownDescription: "A license key for Coder.",
60+
Required: true,
61+
Sensitive: true,
62+
PlanModifiers: []planmodifier.String{
63+
stringplanmodifier.RequiresReplace(),
64+
},
65+
},
66+
},
67+
}
68+
}
69+
70+
func (r *LicenseResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
71+
// Prevent panic if the provider has not been configured.
72+
if req.ProviderData == nil {
73+
return
74+
}
75+
76+
data, ok := req.ProviderData.(*CoderdProviderData)
77+
78+
if !ok {
79+
resp.Diagnostics.AddError(
80+
"Unexpected Resource Configure Type",
81+
fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
82+
)
83+
84+
return
85+
}
86+
87+
r.data = data
88+
}
89+
90+
func (r *LicenseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
91+
var data LicenseResourceModel
92+
93+
// Read Terraform plan data into the model
94+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
95+
96+
if resp.Diagnostics.HasError() {
97+
return
98+
}
99+
100+
client := r.data.Client
101+
102+
license, err := client.AddLicense(ctx, codersdk.AddLicenseRequest{
103+
License: data.License.ValueString(),
104+
})
105+
if err != nil {
106+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add license, got error: %s", err))
107+
return
108+
}
109+
data.ID = types.Int32Value(license.ID)
110+
expiresAt, err := license.ExpiresAt()
111+
if err != nil {
112+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse license expiration, got error: %s", err))
113+
return
114+
}
115+
data.ExpiresAt = types.Int64Value(expiresAt.Unix())
116+
117+
// Save data into Terraform state
118+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
119+
}
120+
121+
func (r *LicenseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
122+
var data LicenseResourceModel
123+
124+
// Read Terraform prior state data into the model
125+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
126+
127+
if resp.Diagnostics.HasError() {
128+
return
129+
}
130+
131+
licenses, err := r.data.Client.Licenses(ctx)
132+
if err != nil {
133+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to list licenses, got error: %s", err))
134+
return
135+
}
136+
137+
found := false
138+
for _, license := range licenses {
139+
if license.ID == data.ID.ValueInt32() {
140+
found = true
141+
expiresAt, err := license.ExpiresAt()
142+
if err != nil {
143+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse license expiration, got error: %s", err))
144+
return
145+
}
146+
if expiresAt.Before(time.Now()) {
147+
resp.Diagnostics.AddError("Client Error", "License has expired.")
148+
return
149+
}
150+
data.ExpiresAt = types.Int64Value(expiresAt.Unix())
151+
}
152+
}
153+
if !found {
154+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("License with ID %d not found", data.ID.ValueInt32()))
155+
}
156+
157+
// Save updated data into Terraform state
158+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
159+
}
160+
161+
func (r *LicenseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
162+
var data LicenseResourceModel
163+
164+
// Read Terraform plan data into the model
165+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
166+
167+
if resp.Diagnostics.HasError() {
168+
return
169+
}
170+
171+
// Update is handled by replacement
172+
173+
// Save updated data into Terraform state
174+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
175+
}
176+
177+
func (r *LicenseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
178+
var data LicenseResourceModel
179+
180+
// Read Terraform prior state data into the model
181+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
182+
183+
if resp.Diagnostics.HasError() {
184+
return
185+
}
186+
187+
client := r.data.Client
188+
189+
err := client.DeleteLicense(ctx, data.ID.ValueInt32())
190+
if err != nil {
191+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete license, got error: %s", err))
192+
return
193+
}
194+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"os"
6+
"strings"
7+
"testing"
8+
"text/template"
9+
10+
"github.com/coder/terraform-provider-coderd/integration"
11+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestAccLicenseResource(t *testing.T) {
16+
if os.Getenv("TF_ACC") == "" {
17+
t.Skip("Acceptance tests are disabled.")
18+
}
19+
ctx := context.Background()
20+
client := integration.StartCoder(ctx, t, "license_acc", false)
21+
22+
license := os.Getenv("CODER_ENTERPRISE_LICENSE")
23+
if license == "" {
24+
t.Skip("No license found for license resource tests, skipping")
25+
}
26+
27+
cfg1 := testAccLicenseResourceconfig{
28+
URL: client.URL.String(),
29+
Token: client.SessionToken(),
30+
License: license,
31+
}
32+
33+
resource.Test(t, resource.TestCase{
34+
IsUnitTest: true,
35+
PreCheck: func() { testAccPreCheck(t) },
36+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
37+
Steps: []resource.TestStep{
38+
{
39+
Config: cfg1.String(t),
40+
},
41+
},
42+
})
43+
}
44+
45+
type testAccLicenseResourceconfig struct {
46+
URL string
47+
Token string
48+
License string
49+
}
50+
51+
func (c testAccLicenseResourceconfig) String(t *testing.T) string {
52+
t.Helper()
53+
tpl := `
54+
provider coderd {
55+
url = "{{.URL}}"
56+
token = "{{.Token}}"
57+
}
58+
59+
resource "coderd_license" "test" {
60+
license = "{{.License}}"
61+
}
62+
`
63+
funcMap := template.FuncMap{
64+
"orNull": PrintOrNull,
65+
}
66+
67+
buf := strings.Builder{}
68+
tmpl, err := template.New("licenseResource").Funcs(funcMap).Parse(tpl)
69+
require.NoError(t, err)
70+
71+
err = tmpl.Execute(&buf, c)
72+
require.NoError(t, err)
73+
return buf.String()
74+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour
138138
NewGroupResource,
139139
NewTemplateResource,
140140
NewWorkspaceProxyResource,
141+
NewLicenseResource,
141142
}
142143
}
143144

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy