Skip to content

Commit 066b31b

Browse files
authored
Merge pull request #7 from SiM22/feature/keepalive-endpoint
Implement Kasm Keepalive Resource and Tests
2 parents 8adb7ca + feb7334 commit 066b31b

File tree

12 files changed

+435
-5
lines changed

12 files changed

+435
-5
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,7 @@ jobs:
262262
uses: actions/upload-artifact@v4
263263
with:
264264
name: acceptance-test-logs
265-
path: |
266-
terraform.log
267-
testdata/ai_debug_output.md
265+
path: terraform.log
268266

269267
- name: Cleanup Kasm
270268
if: always()

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@ package-lock.json
7575
package.json
7676
branch-diff.go
7777
docs/reference docs/
78+
.github/workflows/tests.yml
7879

7980
.trunk/

docs/API_IMPLEMENTATION_STATUS.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ These APIs are officially documented in the Kasm API documentation.
3232
| POST /api/public/destroy_kasm | Implemented | kasm_session | internal/resources/session || internal/resources/kasm/session/tests/session_test.go |
3333
| POST /api/public/join_kasm | Implemented | kasm_join | internal/resources/join || internal/resources/kasm/session/tests/session_test.go |
3434
| POST /api/public/set_session_permissions | Implemented | kasm_session_permission | internal/resources/session_permission || internal/resources/session_permission/tests/session_permission_basic_test.go |
35-
| POST /api/public/keepalive | Not Implemented (Client Implementation Exists) | - | - | | - |
35+
| POST /api/public/keepalive | Implemented | kasm_keepalive | internal/resources/keepalive | | Unit: internal/resources/keepalive/resource_test.go, Acceptance: internal/resources/keepalive/tests/keepalive_test.go |
3636
| POST /api/public/frame_stats | Not Implemented (Client Implementation Exists) | - | - || - |
3737
| POST /api/public/screenshot | Not Implemented (Client Implementation Exists) | - | - || - |
3838
| POST /api/public/exec_command | Not Implemented (Client Implementation Exists) | - | - || - |
@@ -201,7 +201,6 @@ These APIs are not officially documented in the Kasm API documentation but are a
201201

202202
### Missing Resources (Documented APIs)
203203
1. Session Features:
204-
- POST /api/public/keepalive (for kasm_keepalive) - Client implementation exists
205204
- POST /api/public/frame_stats (for kasm_stats) - Client implementation exists
206205
- POST /api/public/screenshot (for kasm_screenshot) - Client implementation exists
207206
- POST /api/public/exec_command (for kasm_exec) - Client implementation exists

internal/client/client_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,69 @@ func TestUpdateUserAttributes(t *testing.T) {
225225
})
226226
}
227227
}
228+
229+
func TestClient_Keepalive(t *testing.T) {
230+
tests := []struct {
231+
name string
232+
kasmID string
233+
mockResponse string
234+
statusCode int
235+
expectError bool
236+
}{
237+
{
238+
name: "Successful keepalive",
239+
kasmID: "test-kasm-id",
240+
mockResponse: `{"usage_reached": false}`,
241+
statusCode: http.StatusOK,
242+
expectError: false,
243+
},
244+
{
245+
name: "Keepalive with usage reached",
246+
kasmID: "test-kasm-id",
247+
mockResponse: `{"usage_reached": true}`,
248+
statusCode: http.StatusOK,
249+
expectError: false,
250+
},
251+
{
252+
name: "Keepalive with invalid response",
253+
kasmID: "test-kasm-id",
254+
mockResponse: `invalid`,
255+
statusCode: http.StatusOK,
256+
expectError: true,
257+
},
258+
{
259+
name: "Keepalive with server error",
260+
kasmID: "test-kasm-id",
261+
mockResponse: `{}`,
262+
statusCode: http.StatusInternalServerError,
263+
expectError: true,
264+
},
265+
}
266+
267+
for _, tt := range tests {
268+
t.Run(tt.name, func(t *testing.T) {
269+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
270+
w.WriteHeader(tt.statusCode)
271+
w.Write([]byte(tt.mockResponse))
272+
}))
273+
defer server.Close()
274+
275+
client := &Client{
276+
HTTPClient: server.Client(),
277+
BaseURL: server.URL,
278+
APIKey: "test-api-key",
279+
APISecret: "test-api-secret",
280+
}
281+
282+
response, err := client.Keepalive(tt.kasmID)
283+
if (err != nil) != tt.expectError {
284+
t.Errorf("Client.Keepalive() error = %v, expectError %v", err, tt.expectError)
285+
return
286+
}
287+
288+
if !tt.expectError && response == nil {
289+
t.Error("Client.Keepalive() returned nil response")
290+
}
291+
})
292+
}
293+
}

internal/client/kasm_ops.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,38 @@ func (c *Client) CreateKasm(userID string, imageID string, sessionToken string,
306306

307307
return nil, fmt.Errorf("failed after 3 retries: %v", lastErr)
308308
}
309+
310+
// Keepalive sends a keepalive request to reset the expiration time of a Kasm session.
311+
func (c *Client) Keepalive(kasmID string) (*KeepaliveResponse, error) {
312+
requestBody := KeepaliveRequest{
313+
APIKey: c.APIKey,
314+
APISecret: c.APISecret,
315+
KasmID: kasmID,
316+
}
317+
318+
body, err := json.Marshal(requestBody)
319+
if err != nil {
320+
return nil, fmt.Errorf("error marshaling request body: %v", err)
321+
}
322+
323+
log.Printf("[DEBUG] Keepalive request URL: %s", c.BaseURL+"/api/public/keepalive")
324+
log.Printf("[DEBUG] Keepalive request body: %s", string(body))
325+
326+
resp, err := c.HTTPClient.Post(c.BaseURL+"/api/public/keepalive", "application/json", bytes.NewBuffer(body))
327+
if err != nil {
328+
return nil, fmt.Errorf("error making request: %v", err)
329+
}
330+
defer resp.Body.Close()
331+
332+
if resp.StatusCode != http.StatusOK {
333+
log.Printf("[WARN] Unexpected status code in keepalive: %d", resp.StatusCode)
334+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
335+
}
336+
337+
var keepaliveResponse KeepaliveResponse
338+
if err := json.NewDecoder(resp.Body).Decode(&keepaliveResponse); err != nil {
339+
return nil, fmt.Errorf("error decoding response: %v", err)
340+
}
341+
342+
return &keepaliveResponse, nil
343+
}

internal/client/kasm_types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,17 @@ type KasmServer struct {
148148
ZoneName string `json:"zone_name"`
149149
Provider string `json:"provider"`
150150
}
151+
152+
// KeepaliveRequest represents the request body for the keepalive endpoint
153+
type KeepaliveRequest struct {
154+
APIKey string `json:"api_key"`
155+
APISecret string `json:"api_key_secret"`
156+
KasmID string `json:"kasm_id"`
157+
}
158+
159+
// KeepaliveResponse represents the response from the keepalive endpoint
160+
type KeepaliveResponse struct {
161+
UsageReached bool `json:"usage_reached"`
162+
Success bool `json:"success"`
163+
Message string `json:"message"`
164+
}

internal/client/license_ops_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
//go:build unit
2+
// +build unit
3+
14
package client
25

36
import (

internal/provider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
imageres "terraform-provider-kasm/internal/resources/image"
2828
"terraform-provider-kasm/internal/resources/join"
2929
"terraform-provider-kasm/internal/resources/kasm"
30+
"terraform-provider-kasm/internal/resources/keepalive"
3031
"terraform-provider-kasm/internal/resources/license"
3132
"terraform-provider-kasm/internal/resources/login"
3233
"terraform-provider-kasm/internal/resources/registry"
@@ -209,6 +210,7 @@ func (p *kasmProvider) Resources(_ context.Context) []func() resource.Resource {
209210
group_image.New,
210211
group_membership.New,
211212
join.New,
213+
keepalive.NewKeepaliveResource,
212214
}
213215
}
214216

internal/provider/provider_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package provider
22

33
import (
44
"context"
5+
"os"
56
"testing"
67

78
"github.com/hashicorp/terraform-plugin-framework/diag"
@@ -17,6 +18,18 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe
1718
"kasm": providerserver.NewProtocol6WithError(New()),
1819
}
1920

21+
func testAccPreCheck(t *testing.T) {
22+
if v := os.Getenv("KASM_BASE_URL"); v == "" {
23+
t.Fatal("KASM_BASE_URL must be set for acceptance tests")
24+
}
25+
if v := os.Getenv("KASM_API_KEY"); v == "" {
26+
t.Fatal("KASM_API_KEY must be set for acceptance tests")
27+
}
28+
if v := os.Getenv("KASM_API_SECRET"); v == "" {
29+
t.Fatal("KASM_API_SECRET must be set for acceptance tests")
30+
}
31+
}
32+
2033
func TestProvider(t *testing.T) {
2134
t.Parallel()
2235

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package keepalive
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/resource"
8+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
9+
"github.com/hashicorp/terraform-plugin-framework/types"
10+
11+
"terraform-provider-kasm/internal/client"
12+
)
13+
14+
// Ensure the implementation satisfies the expected interfaces.
15+
var (
16+
_ resource.Resource = &keepaliveResource{}
17+
)
18+
19+
// NewKeepaliveResource is a helper function to simplify the provider implementation.
20+
func NewKeepaliveResource() resource.Resource {
21+
return &keepaliveResource{}
22+
}
23+
24+
// keepaliveResource is the resource implementation.
25+
type keepaliveResource struct {
26+
client *client.Client
27+
}
28+
29+
// Metadata returns the resource type name.
30+
func (r *keepaliveResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
31+
resp.TypeName = req.ProviderTypeName + "_keepalive"
32+
}
33+
34+
// Schema defines the schema for the resource.
35+
func (r *keepaliveResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
36+
resp.Schema = schema.Schema{
37+
Attributes: map[string]schema.Attribute{
38+
"id": schema.StringAttribute{
39+
Computed: true,
40+
},
41+
"kasm_id": schema.StringAttribute{
42+
Required: true,
43+
},
44+
},
45+
}
46+
}
47+
48+
// Configure adds the provider configured client to the resource.
49+
func (r *keepaliveResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
50+
if req.ProviderData == nil {
51+
return
52+
}
53+
54+
client, ok := req.ProviderData.(*client.Client)
55+
if !ok {
56+
resp.Diagnostics.AddError(
57+
"Unexpected Resource Configure Type",
58+
fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
59+
)
60+
return
61+
}
62+
63+
r.client = client
64+
}
65+
66+
// Create creates the resource and sets the initial Terraform state.
67+
func (r *keepaliveResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
68+
var plan KeepaliveResourceModel
69+
diags := req.Plan.Get(ctx, &plan)
70+
resp.Diagnostics.Append(diags...)
71+
if resp.Diagnostics.HasError() {
72+
return
73+
}
74+
75+
// Set a unique ID before making the API call
76+
plan.ID = types.StringValue(plan.KasmID.ValueString())
77+
78+
// Make the keepalive API call
79+
_, err := r.client.Keepalive(plan.KasmID.ValueString())
80+
if err != nil {
81+
resp.Diagnostics.AddError(
82+
"Error sending keepalive",
83+
fmt.Sprintf("Could not send keepalive: %v", err),
84+
)
85+
return
86+
}
87+
88+
diags = resp.State.Set(ctx, plan)
89+
resp.Diagnostics.Append(diags...)
90+
if resp.Diagnostics.HasError() {
91+
return
92+
}
93+
}
94+
95+
// Read refreshes the Terraform state with the latest data.
96+
func (r *keepaliveResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
97+
var state KeepaliveResourceModel
98+
diags := req.State.Get(ctx, &state)
99+
resp.Diagnostics.Append(diags...)
100+
if resp.Diagnostics.HasError() {
101+
return
102+
}
103+
104+
diags = resp.State.Set(ctx, state)
105+
resp.Diagnostics.Append(diags...)
106+
if resp.Diagnostics.HasError() {
107+
return
108+
}
109+
}
110+
111+
// Update updates the resource and sets the updated Terraform state on success.
112+
func (r *keepaliveResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
113+
var plan KeepaliveResourceModel
114+
diags := req.Plan.Get(ctx, &plan)
115+
resp.Diagnostics.Append(diags...)
116+
if resp.Diagnostics.HasError() {
117+
return
118+
}
119+
120+
// Make the keepalive API call
121+
_, err := r.client.Keepalive(plan.KasmID.ValueString())
122+
if err != nil {
123+
resp.Diagnostics.AddError(
124+
"Error sending keepalive",
125+
fmt.Sprintf("Could not send keepalive: %v", err),
126+
)
127+
return
128+
}
129+
130+
diags = resp.State.Set(ctx, plan)
131+
resp.Diagnostics.Append(diags...)
132+
if resp.Diagnostics.HasError() {
133+
return
134+
}
135+
}
136+
137+
// Delete deletes the resource and removes the Terraform state on success.
138+
func (r *keepaliveResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
139+
var state KeepaliveResourceModel
140+
diags := req.State.Get(ctx, &state)
141+
resp.Diagnostics.Append(diags...)
142+
if resp.Diagnostics.HasError() {
143+
return
144+
}
145+
146+
// No API call needed for deletion
147+
}
148+
149+
// KeepaliveResourceModel maps the resource schema data.
150+
type KeepaliveResourceModel struct {
151+
ID types.String `tfsdk:"id"`
152+
KasmID types.String `tfsdk:"kasm_id"`
153+
}

0 commit comments

Comments
 (0)