Skip to content

Commit dcf6dcc

Browse files
committed
refactor file structure
1 parent e8f1e59 commit dcf6dcc

18 files changed

Lines changed: 1199 additions & 1166 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: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package kubetest2
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/stackitcloud/cloud-provider-stackit/pkg/stackit/stackiterrors"
8+
resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api"
9+
serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api"
10+
"k8s.io/klog/v2"
11+
)
12+
13+
type managedProject struct {
14+
ContainerID string
15+
ProjectID string
16+
Name string
17+
Labels map[string]string
18+
}
19+
20+
// ensureProject idempotently resolves (or creates) the managed STACKIT project
21+
// and enables the SKE service for it. Sets d.projectID on success.
22+
func (d *Deployer) ensureProject(ctx context.Context) error {
23+
project, err := d.resolveManagedProject(ctx)
24+
if err != nil {
25+
return err
26+
}
27+
d.projectID = project.ProjectID
28+
29+
return d.ensureSKEServiceEnabled(ctx, project.ProjectID)
30+
}
31+
32+
func (d *Deployer) findManagedProject(ctx context.Context) (*managedProject, error) {
33+
projects, err := d.projectClient.ListProjects(ctx, d.parentContainerID)
34+
if err != nil {
35+
return nil, fmt.Errorf("list STACKIT projects under parent container %q: %w", d.parentContainerID, err)
36+
}
37+
38+
matches := make([]managedProject, 0, 1)
39+
for i := range projects {
40+
project := &projects[i]
41+
if !d.matchesManagedProject(project) {
42+
continue
43+
}
44+
matches = append(matches, managedProject{
45+
ContainerID: project.GetContainerId(),
46+
ProjectID: project.GetProjectId(),
47+
Name: project.GetName(),
48+
Labels: project.GetLabels(),
49+
})
50+
}
51+
52+
switch len(matches) {
53+
case 0:
54+
return nil, nil
55+
case 1:
56+
return &matches[0], nil
57+
default:
58+
return nil, fmt.Errorf(
59+
"found %d managed STACKIT projects for run token %q under parent container %q",
60+
len(matches),
61+
d.runToken(),
62+
d.parentContainerID,
63+
)
64+
}
65+
}
66+
67+
func (d *Deployer) resolveManagedProject(ctx context.Context) (*managedProject, error) {
68+
project, err := d.findManagedProject(ctx)
69+
if err != nil {
70+
return nil, err
71+
}
72+
if project != nil {
73+
klog.Infof("Reusing managed project=%q project_id=%q", project.Name, project.ProjectID)
74+
return project, nil
75+
}
76+
77+
klog.Infof("Creating managed project=%q under parent_container_id=%q", d.projectName(), d.parentContainerID)
78+
createdProject, err := d.projectClient.CreateProject(
79+
ctx,
80+
d.parentContainerID,
81+
d.projectName(),
82+
d.projectMemberEmail,
83+
d.managedProjectLabels(),
84+
)
85+
if err != nil {
86+
return nil, fmt.Errorf("create STACKIT project %q: %w", d.projectName(), err)
87+
}
88+
89+
activeProject, err := d.projectClient.WaitForProjectActive(ctx, createdProject.GetContainerId())
90+
if err != nil {
91+
return nil, fmt.Errorf("wait for STACKIT project %q to become active: %w", createdProject.GetProjectId(), err)
92+
}
93+
94+
return &managedProject{
95+
ContainerID: activeProject.GetContainerId(),
96+
ProjectID: activeProject.GetProjectId(),
97+
Name: activeProject.GetName(),
98+
Labels: activeProject.GetLabels(),
99+
}, nil
100+
}
101+
102+
func (d *Deployer) managedProjectLabels() map[string]string {
103+
return map[string]string{
104+
projectLabelScopeKey: projectLabelScopeValue,
105+
projectLabelManagedKey: projectLabelManagedValue,
106+
projectLabelRunIDKey: d.runToken(),
107+
}
108+
}
109+
110+
func (d *Deployer) matchesManagedProject(project *resourcemanager.Project) bool {
111+
if project.GetName() != d.projectName() {
112+
return false
113+
}
114+
labels := project.GetLabels()
115+
if labels == nil {
116+
return false
117+
}
118+
return labels[projectLabelScopeKey] == projectLabelScopeValue &&
119+
labels[projectLabelManagedKey] == projectLabelManagedValue &&
120+
labels[projectLabelRunIDKey] == d.runToken()
121+
}
122+
123+
// ensureSKEServiceEnabled idempotently enables the SKE (Kubernetes Engine)
124+
// service for the managed project and waits until it is enabled.
125+
func (d *Deployer) ensureSKEServiceEnabled(ctx context.Context, projectID string) error {
126+
status, err := d.serviceEnablementClient.GetServiceStatus(ctx, d.region, projectID, skeServiceID)
127+
if err != nil {
128+
if !stackiterrors.IsNotFound(err) {
129+
return fmt.Errorf("get SKE service status for project %q: %w", projectID, err)
130+
}
131+
klog.Infof("SKE service not yet enabled for project_id=%q", projectID)
132+
} else if status.GetState() == serviceenablement.SERVICESTATUSSTATE_ENABLED {
133+
klog.Infof("SKE service already enabled for project_id=%q", projectID)
134+
return nil
135+
} else {
136+
klog.Infof("SKE service in state %q for project_id=%q, enabling", status.GetState(), projectID)
137+
}
138+
139+
if err := d.serviceEnablementClient.EnableService(ctx, d.region, projectID, skeServiceID); err != nil {
140+
return fmt.Errorf("enable SKE service for project %q: %w", projectID, err)
141+
}
142+
if err := d.serviceEnablementClient.WaitForServiceEnabled(ctx, d.region, projectID, skeServiceID); err != nil {
143+
return fmt.Errorf("wait for SKE service enablement for project %q: %w", projectID, err)
144+
}
145+
return nil
146+
}
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)