Skip to content

Commit eae29d0

Browse files
committed
[3.1.5] fixed serverless-standalone does not permit start/stop
1 parent 4922d9a commit eae29d0

File tree

3 files changed

+252
-0
lines changed

3 files changed

+252
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [3.1.5] - 2025-10-23
4+
### Fixed
5+
- prevent start/stop operations on serverless-standalone services.
6+
37
## [3.1.4] - 2025-07-17
48
### Features
59
- service `tags` support added.

internal/provider/service_resource.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,23 @@ func (r *ServiceResource) ModifyPlan(ctx context.Context, req resource.ModifyPla
13971397
}
13981398
}
13991399

1400+
// Block start/stop operations for serverless-standalone services
1401+
if plan.Topology.ValueString() == "serverless-standalone" {
1402+
if state == nil && !plan.IsActive.IsUnknown() {
1403+
// Prevent setting is_active during creation
1404+
resp.Diagnostics.AddAttributeError(path.Root("is_active"),
1405+
"Attempt to set read-only attribute",
1406+
"Start/stop operations are not supported for serverless services")
1407+
}
1408+
1409+
if state != nil && !plan.IsActive.Equal(state.IsActive) {
1410+
// Prevent changing is_active during update
1411+
resp.Diagnostics.AddAttributeError(path.Root("is_active"),
1412+
"Attempt to modify read-only attribute",
1413+
"Start/stop operations are not supported for serverless services")
1414+
}
1415+
}
1416+
14001417
if state != nil && plan.Architecture.ValueString() != state.Architecture.ValueString() {
14011418
resp.Diagnostics.AddError("Cannot change service architecture",
14021419
"To prevent accidental deletion of data, changing architecture isn't allowed. "+
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package provider
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"github.com/google/uuid"
7+
"github.com/hashicorp/terraform-plugin-framework/providerserver"
8+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
"github.com/skysqlinc/terraform-provider-skysql/internal/skysql/provisioning"
11+
"github.com/stretchr/testify/require"
12+
"net/http"
13+
"os"
14+
"regexp"
15+
"testing"
16+
"time"
17+
)
18+
19+
func TestServiceResourceServerlessStandalone_IsActiveReadOnly(t *testing.T) {
20+
const serviceID = "dbdgf42002419"
21+
22+
testUrl, expectRequest, closeAPI := mockSkySQLAPI(t)
23+
defer closeAPI()
24+
os.Setenv("TF_SKYSQL_API_KEY", "[api-key]")
25+
os.Setenv("TF_SKYSQL_API_BASE_URL", testUrl)
26+
27+
r := require.New(t)
28+
29+
configureOnce.Reset()
30+
// Check API connectivity
31+
expectRequest(func(w http.ResponseWriter, req *http.Request) {
32+
r.Equal(http.MethodGet, req.Method)
33+
r.Equal("/provisioning/v1/versions", req.URL.Path)
34+
r.Equal("page_size=1", req.URL.RawQuery)
35+
w.Header().Set("Content-Type", "application/json")
36+
w.WriteHeader(http.StatusOK)
37+
json.NewEncoder(w).Encode([]provisioning.Version{})
38+
})
39+
var service *provisioning.Service
40+
// Create service
41+
expectRequest(func(w http.ResponseWriter, req *http.Request) {
42+
r.Equal(http.MethodPost, req.Method)
43+
r.Equal("/provisioning/v1/services", req.URL.Path)
44+
w.Header().Set("Content-Type", "application/json")
45+
payload := provisioning.CreateServiceRequest{}
46+
err := json.NewDecoder(req.Body).Decode(&payload)
47+
r.NoError(err)
48+
service = &provisioning.Service{
49+
ID: serviceID,
50+
Name: payload.Name,
51+
Region: payload.Region,
52+
Provider: payload.Provider,
53+
Tier: "foundation",
54+
Topology: payload.Topology,
55+
Version: "11.4.2",
56+
Architecture: "amd64",
57+
Size: "sky-2x8",
58+
Nodes: 1,
59+
Status: "pending_create",
60+
CreatedOn: int(time.Now().Unix()),
61+
UpdatedOn: int(time.Now().Unix()),
62+
CreatedBy: uuid.New().String(),
63+
UpdatedBy: uuid.New().String(),
64+
Endpoints: []provisioning.Endpoint{
65+
{
66+
Name: "primary",
67+
Ports: []provisioning.Port{
68+
{
69+
Name: "readwrite",
70+
Port: 3306,
71+
Purpose: "readwrite",
72+
},
73+
},
74+
Visibility: "public",
75+
Mechanism: "nlb",
76+
},
77+
},
78+
StorageVolume: struct {
79+
Size int `json:"size"`
80+
VolumeType string `json:"volume_type"`
81+
IOPS int `json:"iops"`
82+
Throughput int `json:"throughput"`
83+
}{
84+
Size: int(payload.Storage),
85+
VolumeType: payload.VolumeType,
86+
IOPS: int(payload.VolumeIOPS),
87+
},
88+
IsActive: true,
89+
ServiceType: payload.ServiceType,
90+
SSLEnabled: payload.SSLEnabled,
91+
}
92+
w.WriteHeader(http.StatusOK)
93+
json.NewEncoder(w).Encode(service)
94+
})
95+
// Multiple GET requests for service status checks during and after creation
96+
for i := 0; i < 4; i++ {
97+
expectRequest(func(w http.ResponseWriter, req *http.Request) {
98+
r.Equal(http.MethodGet, req.Method)
99+
r.Equal(fmt.Sprintf("/provisioning/v1/services/%s", serviceID), req.URL.Path)
100+
w.Header().Set("Content-Type", "application/json")
101+
service.Status = "ready"
102+
service.IsActive = true
103+
w.WriteHeader(http.StatusOK)
104+
json.NewEncoder(w).Encode(service)
105+
})
106+
}
107+
108+
// Delete service
109+
expectRequest(func(w http.ResponseWriter, req *http.Request) {
110+
r.Equal(http.MethodDelete, req.Method)
111+
r.Equal(fmt.Sprintf("/provisioning/v1/services/%s", serviceID), req.URL.Path)
112+
w.Header().Set("Content-Type", "application/json")
113+
w.WriteHeader(http.StatusOK)
114+
json.NewEncoder(w).Encode(service)
115+
})
116+
117+
// Verify deletion (404)
118+
expectRequest(func(w http.ResponseWriter, req *http.Request) {
119+
r.Equal(http.MethodGet, req.Method)
120+
r.Equal(fmt.Sprintf("/provisioning/v1/services/%s", serviceID), req.URL.Path)
121+
w.Header().Set("Content-Type", "application/json")
122+
w.WriteHeader(http.StatusNotFound)
123+
json.NewEncoder(w).Encode(map[string]interface{}{
124+
"code": http.StatusNotFound,
125+
})
126+
})
127+
128+
resource.UnitTest(t, resource.TestCase{
129+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
130+
"skysql": providerserver.NewProtocol6WithError(New("test")()),
131+
},
132+
Steps: []resource.TestStep{
133+
// Test 1: Attempt to set is_active during creation - should fail
134+
{
135+
Config: `
136+
resource "skysql_service" default {
137+
service_type = "transactional"
138+
topology = "serverless-standalone"
139+
cloud_provider = "aws"
140+
region = "us-east-1"
141+
name = "sls-standalone-test"
142+
is_active = true
143+
wait_for_creation = true
144+
wait_for_deletion = true
145+
wait_for_update = true
146+
deletion_protection = false
147+
ssl_enabled = true
148+
size = "sky-2x8"
149+
storage = 100
150+
volume_type = "io1"
151+
volume_iops = 3000
152+
}
153+
`,
154+
ExpectError: regexp.MustCompile(`Start/stop operations are not supported for serverless services`),
155+
Destroy: false,
156+
},
157+
{
158+
Config: `
159+
resource "skysql_service" default {
160+
service_type = "transactional"
161+
topology = "serverless-standalone"
162+
cloud_provider = "aws"
163+
region = "us-east-1"
164+
name = "sls-standalone-test"
165+
is_active = false
166+
wait_for_creation = true
167+
wait_for_deletion = true
168+
wait_for_update = true
169+
deletion_protection = false
170+
ssl_enabled = true
171+
size = "sky-2x8"
172+
storage = 100
173+
volume_type = "io1"
174+
volume_iops = 3000
175+
}
176+
`,
177+
ExpectError: regexp.MustCompile(`Start/stop operations are not supported for serverless services`),
178+
Destroy: false,
179+
},
180+
// Test 2: Create without is_active - should succeed
181+
{
182+
Config: `
183+
resource "skysql_service" default {
184+
service_type = "transactional"
185+
topology = "serverless-standalone"
186+
cloud_provider = "aws"
187+
region = "us-east-1"
188+
name = "sls-standalone-test"
189+
wait_for_creation = true
190+
wait_for_deletion = true
191+
wait_for_update = true
192+
deletion_protection = false
193+
ssl_enabled = true
194+
size = "sky-2x8"
195+
storage = 100
196+
volume_type = "io1"
197+
volume_iops = 3000
198+
}
199+
`,
200+
Check: resource.ComposeAggregateTestCheckFunc([]resource.TestCheckFunc{
201+
resource.TestCheckResourceAttr("skysql_service.default", "id", serviceID),
202+
resource.TestCheckResourceAttr("skysql_service.default", "is_active", "true"),
203+
}...),
204+
},
205+
// Test 3: Attempt to change is_active during update - should fail
206+
{
207+
Config: `
208+
resource "skysql_service" default {
209+
service_type = "transactional"
210+
topology = "serverless-standalone"
211+
cloud_provider = "aws"
212+
region = "us-east-1"
213+
name = "sls-standalone-test"
214+
is_active = false
215+
wait_for_creation = true
216+
wait_for_deletion = true
217+
wait_for_update = true
218+
deletion_protection = false
219+
ssl_enabled = true
220+
size = "sky-2x8"
221+
storage = 100
222+
volume_type = "io1"
223+
volume_iops = 3000
224+
}
225+
`,
226+
ExpectError: regexp.MustCompile(`Start/stop operations are not supported for serverless services`),
227+
Destroy: false,
228+
},
229+
},
230+
})
231+
}

0 commit comments

Comments
 (0)