Skip to content

Commit 02cc1eb

Browse files
committed
refactor file structure
1 parent e8f1e59 commit 02cc1eb

17 files changed

Lines changed: 1188 additions & 1107 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.15.1
2121
github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.2
2222
github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.20.1
23+
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0
2324
github.com/stackitcloud/stackit-sdk-go/services/ske v1.6.0
2425
go.uber.org/mock v0.6.0
2526
golang.org/x/sync v0.22.0
@@ -96,7 +97,6 @@ require (
9697
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
9798
github.com/prometheus/common v0.70.1 // indirect
9899
github.com/prometheus/procfs v0.21.1 // indirect
99-
github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0 // indirect
100100
github.com/stoewer/go-strcase v1.3.1 // indirect
101101
github.com/stretchr/objx v0.5.3 // indirect
102102
github.com/x448/float16 v0.8.4 // indirect

test/kubetest2/bootstrap_csi.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package kubetest2
2+
3+
import (
4+
"context"
5+
6+
"k8s.io/klog/v2"
7+
)
8+
9+
// ensureCSI deploys the STACKIT CSI driver into the freshly provisioned
10+
// cluster as the last bootstrap step. Not yet implemented; will install the
11+
// CSI Helm chart here.
12+
func (d *Deployer) ensureCSI(_ context.Context) error {
13+
klog.Infof("CSI deployment not yet implemented, skipping")
14+
return nil
15+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package kubetest2
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
9+
oapierror "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
10+
resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api"
11+
serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api"
12+
"k8s.io/klog/v2"
13+
)
14+
15+
type managedProject struct {
16+
ContainerID string
17+
ProjectID string
18+
Name string
19+
Labels map[string]string
20+
}
21+
22+
// ensureProject idempotently resolves (or creates) the managed STACKIT project
23+
// and enables the SKE service for it. Sets d.projectID on success.
24+
func (d *Deployer) ensureProject(ctx context.Context) error {
25+
project, err := d.resolveManagedProject(ctx)
26+
if err != nil {
27+
return err
28+
}
29+
d.projectID = project.ProjectID
30+
31+
return d.ensureSKEServiceEnabled(ctx, project.ProjectID)
32+
}
33+
34+
func (d *Deployer) findManagedProject(ctx context.Context) (*managedProject, error) {
35+
projects, err := d.projectClient.ListProjects(ctx, d.parentContainerID)
36+
if err != nil {
37+
return nil, fmt.Errorf("list STACKIT projects under parent container %q: %w", d.parentContainerID, err)
38+
}
39+
40+
matches := make([]managedProject, 0, 1)
41+
for i := range projects {
42+
project := &projects[i]
43+
if !d.matchesManagedProject(project) {
44+
continue
45+
}
46+
matches = append(matches, managedProject{
47+
ContainerID: project.GetContainerId(),
48+
ProjectID: project.GetProjectId(),
49+
Name: project.GetName(),
50+
Labels: project.GetLabels(),
51+
})
52+
}
53+
54+
switch len(matches) {
55+
case 0:
56+
return nil, nil
57+
case 1:
58+
return &matches[0], nil
59+
default:
60+
return nil, fmt.Errorf(
61+
"found %d managed STACKIT projects for run token %q under parent container %q",
62+
len(matches),
63+
d.runToken(),
64+
d.parentContainerID,
65+
)
66+
}
67+
}
68+
69+
func (d *Deployer) resolveManagedProject(ctx context.Context) (*managedProject, error) {
70+
project, err := d.findManagedProject(ctx)
71+
if err != nil {
72+
return nil, err
73+
}
74+
if project != nil {
75+
klog.Infof("Reusing managed project=%q project_id=%q", project.Name, project.ProjectID)
76+
return project, nil
77+
}
78+
79+
klog.Infof("Creating managed project=%q under parent_container_id=%q", d.projectName(), d.parentContainerID)
80+
createdProject, err := d.projectClient.CreateProject(
81+
ctx,
82+
d.parentContainerID,
83+
d.projectName(),
84+
d.projectMemberEmail,
85+
d.managedProjectLabels(),
86+
)
87+
if err != nil {
88+
return nil, fmt.Errorf("create STACKIT project %q: %w", d.projectName(), err)
89+
}
90+
91+
activeProject, err := d.projectClient.WaitForProjectActive(ctx, createdProject.GetContainerId())
92+
if err != nil {
93+
return nil, fmt.Errorf("wait for STACKIT project %q to become active: %w", createdProject.GetProjectId(), err)
94+
}
95+
96+
return &managedProject{
97+
ContainerID: activeProject.GetContainerId(),
98+
ProjectID: activeProject.GetProjectId(),
99+
Name: activeProject.GetName(),
100+
Labels: activeProject.GetLabels(),
101+
}, nil
102+
}
103+
104+
func (d *Deployer) managedProjectLabels() map[string]string {
105+
return map[string]string{
106+
projectLabelScopeKey: projectLabelScopeValue,
107+
projectLabelManagedKey: projectLabelManagedValue,
108+
projectLabelRunIDKey: d.runToken(),
109+
}
110+
}
111+
112+
func (d *Deployer) matchesManagedProject(project *resourcemanager.Project) bool {
113+
if project.GetName() != d.projectName() {
114+
return false
115+
}
116+
labels := project.GetLabels()
117+
if labels == nil {
118+
return false
119+
}
120+
return labels[projectLabelScopeKey] == projectLabelScopeValue &&
121+
labels[projectLabelManagedKey] == projectLabelManagedValue &&
122+
labels[projectLabelRunIDKey] == d.runToken()
123+
}
124+
125+
// ensureSKEServiceEnabled idempotently enables the SKE (Kubernetes Engine)
126+
// service for the managed project and waits until it is enabled.
127+
func (d *Deployer) ensureSKEServiceEnabled(ctx context.Context, projectID string) error {
128+
status, err := d.serviceEnablementClient.GetServiceStatus(ctx, d.region, projectID, skeServiceID)
129+
if err != nil {
130+
if !isNotFound(err) {
131+
return fmt.Errorf("get SKE service status for project %q: %w", projectID, err)
132+
}
133+
klog.Infof("SKE service not yet enabled for project_id=%q", projectID)
134+
} else if status.GetState() == serviceenablement.SERVICESTATUSSTATE_ENABLED {
135+
klog.Infof("SKE service already enabled for project_id=%q", projectID)
136+
return nil
137+
} else {
138+
klog.Infof("SKE service in state %q for project_id=%q, enabling", status.GetState(), projectID)
139+
}
140+
141+
if err := d.serviceEnablementClient.EnableService(ctx, d.region, projectID, skeServiceID); err != nil {
142+
return fmt.Errorf("enable SKE service for project %q: %w", projectID, err)
143+
}
144+
if err := d.serviceEnablementClient.WaitForServiceEnabled(ctx, d.region, projectID, skeServiceID); err != nil {
145+
return fmt.Errorf("wait for SKE service enablement for project %q: %w", projectID, err)
146+
}
147+
return nil
148+
}
149+
150+
func isNotFound(err error) bool {
151+
var oapiErr *oapierror.GenericOpenAPIError
152+
return errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound
153+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package kubetest2
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
oapierror "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
10+
resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api"
11+
serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api"
12+
)
13+
14+
var _ = Describe("matchesManagedProject", func() {
15+
It("matches a project with the expected name and labels", func() {
16+
d := newTestDeployer()
17+
project := projectFixture(d.projectName(), "project-1", "container-1", d.managedProjectLabels())
18+
Expect(d.matchesManagedProject(project)).To(BeTrue())
19+
})
20+
21+
It("rejects a project with a different name", func() {
22+
d := newTestDeployer()
23+
project := projectFixture("other-name", "project-1", "container-1", d.managedProjectLabels())
24+
Expect(d.matchesManagedProject(project)).To(BeFalse())
25+
})
26+
27+
It("rejects a project without labels", func() {
28+
d := newTestDeployer()
29+
project := projectFixture(d.projectName(), "project-1", "container-1", nil)
30+
Expect(d.matchesManagedProject(project)).To(BeFalse())
31+
})
32+
33+
DescribeTable("rejects a project with mismatched labels",
34+
func(labels map[string]string) {
35+
d := newTestDeployer()
36+
project := projectFixture(d.projectName(), "project-1", "container-1", labels)
37+
Expect(d.matchesManagedProject(project)).To(BeFalse())
38+
},
39+
Entry("wrong scope", map[string]string{
40+
projectLabelScopeKey: "PRIVATE",
41+
projectLabelManagedKey: projectLabelManagedValue,
42+
projectLabelRunIDKey: runTokenForRun("run-123"),
43+
}),
44+
Entry("missing managed label", map[string]string{
45+
projectLabelScopeKey: projectLabelScopeValue,
46+
projectLabelRunIDKey: runTokenForRun("run-123"),
47+
}),
48+
Entry("wrong run id", map[string]string{
49+
projectLabelScopeKey: projectLabelScopeValue,
50+
projectLabelManagedKey: projectLabelManagedValue,
51+
projectLabelRunIDKey: "deadbeef",
52+
}),
53+
)
54+
})
55+
56+
var _ = Describe("resolveManagedProject", func() {
57+
It("creates a project when missing", func() {
58+
d := newTestDeployer()
59+
projectClient := &fakeProjectClient{
60+
createProjectResult: projectFixture(d.projectName(), "project-123", "container-123", d.managedProjectLabels()),
61+
waitActiveResult: projectResponseFixture(d.projectName(), "project-123", "container-123", d.managedProjectLabels()),
62+
}
63+
d.projectClient = projectClient
64+
65+
project, err := d.resolveManagedProject(context.Background())
66+
Expect(err).NotTo(HaveOccurred())
67+
Expect(project.ProjectID).To(Equal("project-123"))
68+
Expect(projectClient.createCalls).To(Equal(1))
69+
Expect(projectClient.waitActiveCalls).To(Equal(1))
70+
Expect(projectClient.lastCreateParentContainerID).To(Equal(d.parentContainerID))
71+
Expect(projectClient.lastCreateName).To(Equal(d.projectName()))
72+
Expect(projectClient.lastCreateOwnerEmail).To(Equal(d.projectMemberEmail))
73+
Expect(projectClient.lastCreateLabels).To(Equal(d.managedProjectLabels()))
74+
})
75+
76+
It("errors on multiple matches", func() {
77+
d := newTestDeployer()
78+
d.projectClient = &fakeProjectClient{
79+
listProjectsResult: []resourcemanager.Project{
80+
*projectFixture(d.projectName(), "project-1", "container-1", d.managedProjectLabels()),
81+
*projectFixture(d.projectName(), "project-2", "container-2", d.managedProjectLabels()),
82+
},
83+
}
84+
85+
_, err := d.findManagedProject(context.Background())
86+
Expect(err).To(MatchError(ContainSubstring("found 2 managed STACKIT projects")))
87+
})
88+
})
89+
90+
var _ = Describe("ensureProject", func() {
91+
It("resolves the managed project and enables the SKE service", func() {
92+
d := newTestDeployer()
93+
d.projectClient = &fakeProjectClient{
94+
listProjectsResult: []resourcemanager.Project{
95+
*projectFixture(d.projectName(), "project-123", "container-123", d.managedProjectLabels()),
96+
},
97+
}
98+
serviceEnablementClient := &fakeServiceEnablementClient{getStatusResult: serviceenablement.NewServiceStatus()}
99+
d.serviceEnablementClient = serviceEnablementClient
100+
101+
Expect(d.ensureProject(context.Background())).To(Succeed())
102+
Expect(d.projectID).To(Equal("project-123"))
103+
Expect(serviceEnablementClient.lastGetStatusProjectID).To(Equal("project-123"))
104+
Expect(serviceEnablementClient.enableCalls).To(Equal(0))
105+
})
106+
})
107+
108+
var _ = Describe("ensureSKEServiceEnabled", func() {
109+
It("skips when already enabled", func() {
110+
d := newTestDeployer()
111+
client := &fakeServiceEnablementClient{getStatusResult: serviceenablement.NewServiceStatus()}
112+
d.serviceEnablementClient = client
113+
114+
Expect(d.ensureSKEServiceEnabled(context.Background(), "project-123")).To(Succeed())
115+
Expect(client.enableCalls).To(Equal(0))
116+
Expect(client.waitCalls).To(Equal(0))
117+
Expect(client.lastGetStatusServiceID).To(Equal(skeServiceID))
118+
})
119+
120+
It("enables when not found", func() {
121+
d := newTestDeployer()
122+
client := &fakeServiceEnablementClient{
123+
getStatusErr: &oapierror.GenericOpenAPIError{StatusCode: http.StatusNotFound},
124+
}
125+
d.serviceEnablementClient = client
126+
127+
Expect(d.ensureSKEServiceEnabled(context.Background(), "project-123")).To(Succeed())
128+
Expect(client.enableCalls).To(Equal(1))
129+
Expect(client.waitCalls).To(Equal(1))
130+
Expect(client.lastEnableProjectID).To(Equal("project-123"))
131+
Expect(client.lastEnableServiceID).To(Equal(skeServiceID))
132+
})
133+
134+
It("enables when disabled", func() {
135+
d := newTestDeployer()
136+
client := &fakeServiceEnablementClient{
137+
getStatusResult: disabledServiceStatusFixture(),
138+
}
139+
d.serviceEnablementClient = client
140+
141+
Expect(d.ensureSKEServiceEnabled(context.Background(), "project-123")).To(Succeed())
142+
Expect(client.enableCalls).To(Equal(1))
143+
Expect(client.waitCalls).To(Equal(1))
144+
})
145+
146+
It("fails on get status error", func() {
147+
d := newTestDeployer()
148+
client := &fakeServiceEnablementClient{
149+
getStatusErr: &oapierror.GenericOpenAPIError{StatusCode: http.StatusForbidden},
150+
}
151+
d.serviceEnablementClient = client
152+
153+
err := d.ensureSKEServiceEnabled(context.Background(), "project-123")
154+
Expect(err).To(HaveOccurred())
155+
Expect(client.enableCalls).To(Equal(0))
156+
})
157+
})

0 commit comments

Comments
 (0)