Skip to content

Commit 97a2589

Browse files
committed
add ReadyForNetworkAreaDeletionWaitHandler
1 parent c619d21 commit 97a2589

File tree

4 files changed

+254
-16
lines changed

4 files changed

+254
-16
lines changed

services/iaas/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.21
55
require (
66
github.com/google/go-cmp v0.7.0
77
github.com/stackitcloud/stackit-sdk-go/core v0.17.3
8+
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1
89
)
910

1011
require (

services/iaas/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
66
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
77
github.com/stackitcloud/stackit-sdk-go/core v0.17.3 h1:GsZGmRRc/3GJLmCUnsZswirr5wfLRrwavbnL/renOqg=
88
github.com/stackitcloud/stackit-sdk-go/core v0.17.3/go.mod h1:HBCXJGPgdRulplDzhrmwC+Dak9B/x0nzNtmOpu+1Ahg=
9+
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1 h1:r7oaINTwLmIG31AaqKTuQHHFF8YNuYGzi+46DOuSjw4=
10+
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1/go.mod h1:ipcrPRbwfQXHH18dJVfY7K5ujHF5dTT6isoXgmA7YwQ=

services/iaas/wait/wait.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ package wait
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"net/http"
8+
"strings"
79
"time"
810

9-
"errors"
10-
1111
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
1212
"github.com/stackitcloud/stackit-sdk-go/core/wait"
1313
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
14+
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
1415
)
1516

1617
const (
@@ -48,6 +49,7 @@ const (
4849
// Interfaces needed for tests
4950
type APIClientInterface interface {
5051
GetNetworkAreaExecute(ctx context.Context, organizationId, areaId string) (*iaas.NetworkArea, error)
52+
ListNetworkAreaProjectsExecute(ctx context.Context, organizationId, areaId string) (*iaas.ProjectListResponse, error)
5153
GetProjectRequestExecute(ctx context.Context, projectId string, requestId string) (*iaas.Request, error)
5254
GetNetworkExecute(ctx context.Context, projectId, networkId string) (*iaas.Network, error)
5355
GetVolumeExecute(ctx context.Context, projectId string, volumeId string) (*iaas.Volume, error)
@@ -58,6 +60,10 @@ type APIClientInterface interface {
5860
GetSnapshotExecute(ctx context.Context, projectId string, snapshotId string) (*iaas.Snapshot, error)
5961
}
6062

63+
type ResourceManagerAPIClientInterface interface {
64+
GetProjectExecute(ctx context.Context, id string) (*resourcemanager.GetProjectResponse, error)
65+
}
66+
6167
// CreateNetworkAreaWaitHandler will wait for network area creation
6268
func CreateNetworkAreaWaitHandler(ctx context.Context, a APIClientInterface, organizationId, areaId string) *wait.AsyncActionHandler[iaas.NetworkArea] {
6369
handler := wait.New(func() (waitFinished bool, response *iaas.NetworkArea, err error) {
@@ -98,6 +104,53 @@ func UpdateNetworkAreaWaitHandler(ctx context.Context, a APIClientInterface, org
98104
return handler
99105
}
100106

107+
// ReadyForNetworkAreaDeletionWaitHandler will wait until a deletion of network area is possible
108+
// Workaround for https://github.com/stackitcloud/terraform-provider-stackit/issues/907.
109+
// When the deletion for a project is triggered, the backend starts a workflow in the background which cleans up all resources
110+
// within a project and deletes the project in each service. When the project is attached to an SNA, the SNA can't be
111+
// deleted until the workflow inform the IaaS-API that the project is deleted.
112+
func ReadyForNetworkAreaDeletionWaitHandler(ctx context.Context, a APIClientInterface, r ResourceManagerAPIClientInterface, organizationId, areaId string) *wait.AsyncActionHandler[iaas.ProjectListResponse] {
113+
handler := wait.New(func() (waitFinished bool, response *iaas.ProjectListResponse, err error) {
114+
projectList, err := a.ListNetworkAreaProjectsExecute(ctx, organizationId, areaId)
115+
if err != nil {
116+
return false, projectList, err
117+
}
118+
if projectList.Items == nil {
119+
return false, nil, fmt.Errorf("read failed for projects in network area with id %s, the response is not valid: the items are missing", areaId)
120+
}
121+
if len(*projectList.Items) == 0 {
122+
return true, projectList, nil
123+
}
124+
var activeProjects []string
125+
var forbiddenProjects []string
126+
for _, projectId := range *projectList.Items {
127+
_, err := r.GetProjectExecute(ctx, projectId)
128+
if err == nil {
129+
activeProjects = append(activeProjects, projectId)
130+
continue
131+
}
132+
var oapiErr *oapierror.GenericOpenAPIError
133+
ok := errors.As(err, &oapiErr)
134+
if !ok {
135+
return false, nil, fmt.Errorf("could not convert error to oapierror.GenericOpenAPIError")
136+
}
137+
// The resource manager api responds with StatusForbidden(=403) when a project is deleted or if the project does not exist
138+
if oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusForbidden {
139+
forbiddenProjects = append(forbiddenProjects, projectId)
140+
}
141+
}
142+
if len(activeProjects) > 0 {
143+
return false, nil, fmt.Errorf("network area with id %s has still active projects: %s", areaId, strings.Join(activeProjects, ","))
144+
}
145+
if len(forbiddenProjects) > 0 {
146+
return false, nil, nil
147+
}
148+
return true, projectList, nil
149+
})
150+
handler.SetTimeout(1 * time.Minute)
151+
return handler
152+
}
153+
101154
// DeleteNetworkAreaWaitHandler will wait for network area deletion
102155
func DeleteNetworkAreaWaitHandler(ctx context.Context, a APIClientInterface, organizationId, areaId string) *wait.AsyncActionHandler[iaas.NetworkArea] {
103156
handler := wait.New(func() (waitFinished bool, response *iaas.NetworkArea, err error) {

services/iaas/wait/wait_test.go

Lines changed: 196 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,63 @@ package wait
22

33
import (
44
"context"
5+
"net/http"
56
"testing"
67
"time"
78

89
"github.com/google/go-cmp/cmp"
910
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
1011
"github.com/stackitcloud/stackit-sdk-go/core/utils"
1112
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
13+
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
1214
)
1315

1416
type apiClientMocked struct {
15-
getNetworkAreaFails bool
16-
getNetworkFails bool
17-
getProjectRequestFails bool
18-
isDeleted bool
19-
resourceState string
20-
getVolumeFails bool
21-
getServerFails bool
22-
getAttachedVolumeFails bool
23-
getImageFails bool
24-
getBackupFails bool
25-
isAttached bool
26-
requestAction string
27-
returnResizing bool
28-
getSnapshotFails bool
17+
getNetworkAreaFails bool
18+
getNetworkFails bool
19+
getProjectRequestFails bool
20+
isDeleted bool
21+
resourceState string
22+
getVolumeFails bool
23+
getServerFails bool
24+
getAttachedVolumeFails bool
25+
getImageFails bool
26+
getBackupFails bool
27+
isAttached bool
28+
requestAction string
29+
returnResizing bool
30+
getSnapshotFails bool
31+
listProjectsResponses []listProjectsResponses
32+
listProjectsResponsesIdx int
33+
}
34+
35+
type listProjectsResponses struct {
36+
resp *iaas.ProjectListResponse
37+
err error
38+
}
39+
40+
type resourceManagerClientMocked struct {
41+
getProjectResponses []getProjectResponse
42+
getProjectResponsesIdx int
43+
}
44+
45+
type getProjectResponse struct {
46+
resp *resourcemanager.GetProjectResponse
47+
err error
48+
}
49+
50+
func (r *resourceManagerClientMocked) GetProjectExecute(_ context.Context, _ string) (*resourcemanager.GetProjectResponse, error) {
51+
resp := r.getProjectResponses[r.getProjectResponsesIdx].resp
52+
err := r.getProjectResponses[r.getProjectResponsesIdx].err
53+
r.getProjectResponsesIdx = (r.getProjectResponsesIdx + 1) % len(r.getProjectResponses)
54+
return resp, err
55+
}
56+
57+
func (a *apiClientMocked) ListNetworkAreaProjectsExecute(_ context.Context, _, _ string) (*iaas.ProjectListResponse, error) {
58+
resp := a.listProjectsResponses[a.listProjectsResponsesIdx].resp
59+
err := a.listProjectsResponses[a.listProjectsResponsesIdx].err
60+
a.listProjectsResponsesIdx = (a.listProjectsResponsesIdx + 1) % len(a.listProjectsResponses)
61+
return resp, err
2962
}
3063

3164
func (a *apiClientMocked) GetNetworkAreaExecute(_ context.Context, _, _ string) (*iaas.NetworkArea, error) {
@@ -1856,3 +1889,152 @@ func TestDeleteSnapshotWaitHandler(t *testing.T) {
18561889
})
18571890
}
18581891
}
1892+
1893+
func TestReadyForNetworkAreaDeletionWaitHandler(t *testing.T) {
1894+
tests := []struct {
1895+
desc string
1896+
listProjectsResponses []listProjectsResponses
1897+
getProjectResponses []getProjectResponse
1898+
wantErr bool
1899+
wantResp bool
1900+
}{
1901+
{
1902+
desc: "projects still active",
1903+
listProjectsResponses: []listProjectsResponses{
1904+
{
1905+
resp: &iaas.ProjectListResponse{
1906+
Items: utils.Ptr([]string{"project1", "project2"}),
1907+
},
1908+
err: nil,
1909+
},
1910+
},
1911+
getProjectResponses: []getProjectResponse{
1912+
{&resourcemanager.GetProjectResponse{}, nil},
1913+
{&resourcemanager.GetProjectResponse{}, nil},
1914+
},
1915+
wantErr: true,
1916+
wantResp: false,
1917+
},
1918+
{
1919+
desc: "no projects - ready for deletion",
1920+
listProjectsResponses: []listProjectsResponses{
1921+
{
1922+
resp: &iaas.ProjectListResponse{
1923+
Items: utils.Ptr([]string{}),
1924+
},
1925+
err: nil,
1926+
},
1927+
},
1928+
getProjectResponses: []getProjectResponse{},
1929+
wantErr: false,
1930+
wantResp: true,
1931+
},
1932+
{
1933+
desc: "projects in deletion state",
1934+
listProjectsResponses: []listProjectsResponses{
1935+
{
1936+
resp: &iaas.ProjectListResponse{
1937+
Items: utils.Ptr([]string{"project1", "project2"}),
1938+
},
1939+
err: nil,
1940+
},
1941+
{
1942+
resp: &iaas.ProjectListResponse{
1943+
Items: utils.Ptr([]string{}),
1944+
},
1945+
err: nil,
1946+
},
1947+
},
1948+
getProjectResponses: []getProjectResponse{
1949+
{nil, oapierror.NewError(http.StatusForbidden, "")},
1950+
{nil, oapierror.NewError(http.StatusForbidden, "")},
1951+
},
1952+
wantErr: false,
1953+
wantResp: true,
1954+
},
1955+
{
1956+
desc: "network area includes one active project",
1957+
listProjectsResponses: []listProjectsResponses{
1958+
{
1959+
resp: &iaas.ProjectListResponse{
1960+
Items: utils.Ptr([]string{"project1", "project2", "project3"}),
1961+
},
1962+
err: nil,
1963+
},
1964+
},
1965+
getProjectResponses: []getProjectResponse{
1966+
{nil, oapierror.NewError(http.StatusForbidden, "")},
1967+
{nil, oapierror.NewError(http.StatusForbidden, "")},
1968+
{&resourcemanager.GetProjectResponse{}, nil},
1969+
},
1970+
wantErr: true,
1971+
wantResp: false,
1972+
},
1973+
{
1974+
desc: "network area not found",
1975+
listProjectsResponses: []listProjectsResponses{
1976+
{
1977+
resp: nil,
1978+
err: oapierror.NewError(http.StatusNotFound, "not found"),
1979+
},
1980+
},
1981+
getProjectResponses: []getProjectResponse{},
1982+
wantErr: true,
1983+
wantResp: false,
1984+
},
1985+
{
1986+
desc: "network area items is nil",
1987+
listProjectsResponses: []listProjectsResponses{
1988+
{
1989+
resp: &iaas.ProjectListResponse{
1990+
Items: nil,
1991+
},
1992+
},
1993+
},
1994+
wantErr: true,
1995+
wantResp: false,
1996+
},
1997+
{
1998+
desc: "timeout",
1999+
listProjectsResponses: []listProjectsResponses{
2000+
{
2001+
resp: &iaas.ProjectListResponse{
2002+
Items: utils.Ptr([]string{"project1"}),
2003+
},
2004+
err: nil,
2005+
},
2006+
},
2007+
getProjectResponses: []getProjectResponse{
2008+
{nil, oapierror.NewError(http.StatusForbidden, "")},
2009+
},
2010+
wantErr: true,
2011+
wantResp: false,
2012+
},
2013+
}
2014+
for _, tt := range tests {
2015+
t.Run(tt.desc, func(t *testing.T) {
2016+
apiClient := &apiClientMocked{
2017+
listProjectsResponses: tt.listProjectsResponses,
2018+
}
2019+
2020+
rmApiClient := &resourceManagerClientMocked{
2021+
getProjectResponses: tt.getProjectResponses,
2022+
}
2023+
2024+
var wantRes *iaas.ProjectListResponse
2025+
if tt.wantResp {
2026+
wantRes = tt.listProjectsResponses[len(tt.listProjectsResponses)-1].resp
2027+
}
2028+
2029+
handler := ReadyForNetworkAreaDeletionWaitHandler(context.Background(), apiClient, rmApiClient, "oid", "aid")
2030+
gotRes, err := handler.SetTimeout(200 * time.Millisecond).SetThrottle(5 * time.Millisecond).WaitWithContext(context.Background())
2031+
2032+
if (err != nil) != tt.wantErr {
2033+
t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr)
2034+
}
2035+
if !cmp.Equal(gotRes, wantRes) {
2036+
t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes)
2037+
}
2038+
})
2039+
}
2040+
}

0 commit comments

Comments
 (0)