From 08f1166656483115a97ac7fcd81c367e89fe760e Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 18 Mar 2026 10:42:33 +0100 Subject: [PATCH 01/13] feat(bootstrap): update labels on existing GCP projects --- internal/bootstrap/gcp/gcp.go | 36 +++++++++-- internal/bootstrap/gcp/gcp_client.go | 46 ++++++++++---- internal/bootstrap/gcp/gcp_test.go | 7 +-- internal/bootstrap/gcp/mocks.go | 94 +++++++++++++++++++++++----- 4 files changed, 146 insertions(+), 37 deletions(-) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 11a2ba5e..9d3593dd 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -470,21 +470,30 @@ func (b *GCPBootstrapper) EnsureProject() error { parent = fmt.Sprintf("folders/%s", b.Env.FolderID) } + deleteProjectAfter, err := calculateProjectExpiryLabel(b.Env.ProjectTTL) + + labels := map[string]string{ + OMSManagedLabel: "true", + DeleteAfterLabel: deleteProjectAfter, + } + existingProject, err := b.GCPClient.GetProjectByName(b.Env.FolderID, b.Env.ProjectName) if err == nil { b.Env.ProjectID = existingProject.ProjectId b.Env.ProjectName = existingProject.Name + + err := b.GCPClient.UpdateProject(existingProject.ProjectId, existingProject.DisplayName, labels) + if err != nil { + return fmt.Errorf("failed to update project: %w", err) + } + return nil } + if err.Error() == fmt.Sprintf("project not found: %s", b.Env.ProjectName) { projectId := b.GCPClient.CreateProjectID(b.Env.ProjectName) - projectTTL, err := time.ParseDuration(b.Env.ProjectTTL) - if err != nil { - return fmt.Errorf("invalid project TTL format: %w", err) - } - - _, err = b.GCPClient.CreateProject(parent, projectId, b.Env.ProjectName, projectTTL) + _, err = b.GCPClient.CreateProject(parent, projectId, b.Env.ProjectName, labels) if err != nil { return fmt.Errorf("failed to create project: %w", err) } @@ -496,6 +505,21 @@ func (b *GCPBootstrapper) EnsureProject() error { return fmt.Errorf("failed to get project: %w", err) } +func calculateProjectExpiryLabel(projectTTLStr string) (string, error) { + projectTTL, err := time.ParseDuration(projectTTLStr) + if err != nil { + return "", fmt.Errorf("invalid project TTL format: %w", err) + } + + // prepare label for gcp project deletion in custom UTC time format. + // GCP Labels are very limited. This is an easy way to add date and TZ info in one label. + gcpLabelLayout := "2006-01-02_15-04-05" + deleteProjectAfter := time.Now().UTC().Add(projectTTL).Format(gcpLabelLayout) + deleteProjectAfter = fmt.Sprintf("%s_utc", deleteProjectAfter) + + return deleteProjectAfter, nil +} + func (b *GCPBootstrapper) EnsureBilling() error { bi, err := b.GCPClient.GetBillingInfo(b.Env.ProjectID) if err != nil { diff --git a/internal/bootstrap/gcp/gcp_client.go b/internal/bootstrap/gcp/gcp_client.go index ccfde605..2aa8844b 100644 --- a/internal/bootstrap/gcp/gcp_client.go +++ b/internal/bootstrap/gcp/gcp_client.go @@ -8,7 +8,6 @@ import ( "fmt" "strings" "sync" - "time" "slices" @@ -30,13 +29,15 @@ import ( "google.golang.org/api/iterator" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) // Interface for high-level GCP operations type GCPClientManager interface { GetProjectByName(folderID string, displayName string) (*resourcemanagerpb.Project, error) CreateProjectID(projectName string) string - CreateProject(parent, projectName, displayName string, ttl time.Duration) (string, error) + CreateProject(parent, projectName, displayName string, labels map[string]string) (string, error) + UpdateProject(projectID, displayName string, labels map[string]string) error DeleteProject(projectID string) error IsOMSManagedProject(projectID string) (bool, error) GetBillingInfo(projectID string) (*cloudbilling.ProjectBillingInfo, error) @@ -116,25 +117,18 @@ func (c *GCPClient) CreateProjectID(projectName string) string { // CreateProject creates a new GCP project under the specified parent (folder or organization). // It returns the project ID of the newly created project. // The project is labeled with 'oms-managed=true' to identify it as created by OMS. -func (c *GCPClient) CreateProject(parent, projectID, displayName string, projectTTL time.Duration) (string, error) { +func (c *GCPClient) CreateProject(parent, projectID, displayName string, labels map[string]string) (string, error) { client, err := resourcemanager.NewProjectsClient(c.ctx) if err != nil { return "", err } defer util.IgnoreError(client.Close) - gcpLabelLayout := "2006-01-02_15-04-05" - deleteProjectAfter := time.Now().UTC().Add(projectTTL).Format(gcpLabelLayout) - deleteProjectAfter = fmt.Sprintf("%s_utc", deleteProjectAfter) // GCP Labels are very limited. This is the only way to add TZ info. - project := &resourcemanagerpb.Project{ ProjectId: projectID, DisplayName: displayName, Parent: parent, - Labels: map[string]string{ - OMSManagedLabel: "true", - DeleteAfterLabel: deleteProjectAfter, - }, + Labels: labels, } op, err := client.CreateProject(c.ctx, &resourcemanagerpb.CreateProjectRequest{Project: project}) @@ -150,6 +144,36 @@ func (c *GCPClient) CreateProject(parent, projectID, displayName string, project return resp.ProjectId, nil } +func (c *GCPClient) UpdateProject(projectID, displayName string, labels map[string]string) error { + client, err := resourcemanager.NewProjectsClient(c.ctx) + if err != nil { + return err + } + defer util.IgnoreError(client.Close) + + project := &resourcemanagerpb.Project{ + Name: getProjectResourceName(projectID), + DisplayName: displayName, + Labels: labels, + } + + op, err := client.UpdateProject(c.ctx, &resourcemanagerpb.UpdateProjectRequest{ + Project: project, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"labels"}, + }, + }) + if err != nil { + return fmt.Errorf("failed to update project %s with config %v: %w", projectID, project, err) + } + + if _, err = op.Wait(c.ctx); err != nil { + return fmt.Errorf("failed to wait for project update: %w", err) + } + + return nil +} + // DeleteProject deletes the specified GCP project. func (c *GCPClient) DeleteProject(projectID string) error { client, err := resourcemanager.NewProjectsClient(c.ctx) diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index fecdcffc..609c1f04 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "strings" - "time" "os" @@ -152,7 +151,7 @@ var _ = Describe("GCP Bootstrapper", func() { // 3. EnsureProject gc.EXPECT().GetProjectByName(mock.Anything, "test-project").Return(nil, fmt.Errorf("project not found: test-project")) gc.EXPECT().CreateProjectID("test-project").Return(projectId) - gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project", time.Hour).Return(mock.Anything, nil) + gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project", mock.Anything).Return(mock.Anything, nil) // 4. EnsureBilling gc.EXPECT().GetBillingInfo(projectId).Return(&cloudbilling.ProjectBillingInfo{BillingEnabled: false}, nil) @@ -506,7 +505,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("creates project when missing", func() { gc.EXPECT().GetProjectByName(csEnv.FolderID, csEnv.ProjectName).Return(nil, fmt.Errorf("project not found: %s", csEnv.ProjectName)) gc.EXPECT().CreateProjectID(csEnv.ProjectName).Return("new-proj-id") - gc.EXPECT().CreateProject(csEnv.FolderID, "new-proj-id", csEnv.ProjectName, time.Hour).Return("", nil) + gc.EXPECT().CreateProject(csEnv.FolderID, "new-proj-id", csEnv.ProjectName, mock.Anything).Return("", nil) err := bs.EnsureProject() Expect(err).NotTo(HaveOccurred()) @@ -526,7 +525,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("returns error when CreateProject fails", func() { gc.EXPECT().GetProjectByName("", csEnv.ProjectName).Return(nil, fmt.Errorf("project not found: %s", csEnv.ProjectName)) gc.EXPECT().CreateProjectID(csEnv.ProjectName).Return("fake-id") - gc.EXPECT().CreateProject("", "fake-id", csEnv.ProjectName, time.Hour).Return("", fmt.Errorf("create error")) + gc.EXPECT().CreateProject("", "fake-id", csEnv.ProjectName, mock.Anything).Return("", fmt.Errorf("create error")) err := bs.EnsureProject() Expect(err).To(HaveOccurred()) diff --git a/internal/bootstrap/gcp/mocks.go b/internal/bootstrap/gcp/mocks.go index 4fd00cb9..b9404aa7 100644 --- a/internal/bootstrap/gcp/mocks.go +++ b/internal/bootstrap/gcp/mocks.go @@ -11,7 +11,6 @@ import ( mock "github.com/stretchr/testify/mock" "google.golang.org/api/cloudbilling/v1" "google.golang.org/api/dns/v1" - "time" ) // NewMockGCPClientManager creates a new instance of MockGCPClientManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. @@ -377,8 +376,8 @@ func (_c *MockGCPClientManager_CreateInstance_Call) RunAndReturn(run func(projec } // CreateProject provides a mock function for the type MockGCPClientManager -func (_mock *MockGCPClientManager) CreateProject(parent string, projectName string, displayName string, ttl time.Duration) (string, error) { - ret := _mock.Called(parent, projectName, displayName, ttl) +func (_mock *MockGCPClientManager) CreateProject(parent string, projectName string, displayName string, labels map[string]string) (string, error) { + ret := _mock.Called(parent, projectName, displayName, labels) if len(ret) == 0 { panic("no return value specified for CreateProject") @@ -386,16 +385,16 @@ func (_mock *MockGCPClientManager) CreateProject(parent string, projectName stri var r0 string var r1 error - if returnFunc, ok := ret.Get(0).(func(string, string, string, time.Duration) (string, error)); ok { - return returnFunc(parent, projectName, displayName, ttl) + if returnFunc, ok := ret.Get(0).(func(string, string, string, map[string]string) (string, error)); ok { + return returnFunc(parent, projectName, displayName, labels) } - if returnFunc, ok := ret.Get(0).(func(string, string, string, time.Duration) string); ok { - r0 = returnFunc(parent, projectName, displayName, ttl) + if returnFunc, ok := ret.Get(0).(func(string, string, string, map[string]string) string); ok { + r0 = returnFunc(parent, projectName, displayName, labels) } else { r0 = ret.Get(0).(string) } - if returnFunc, ok := ret.Get(1).(func(string, string, string, time.Duration) error); ok { - r1 = returnFunc(parent, projectName, displayName, ttl) + if returnFunc, ok := ret.Get(1).(func(string, string, string, map[string]string) error); ok { + r1 = returnFunc(parent, projectName, displayName, labels) } else { r1 = ret.Error(1) } @@ -411,12 +410,12 @@ type MockGCPClientManager_CreateProject_Call struct { // - parent string // - projectName string // - displayName string -// - ttl time.Duration -func (_e *MockGCPClientManager_Expecter) CreateProject(parent interface{}, projectName interface{}, displayName interface{}, ttl interface{}) *MockGCPClientManager_CreateProject_Call { - return &MockGCPClientManager_CreateProject_Call{Call: _e.mock.On("CreateProject", parent, projectName, displayName, ttl)} +// - labels map[string]string +func (_e *MockGCPClientManager_Expecter) CreateProject(parent interface{}, projectName interface{}, displayName interface{}, labels interface{}) *MockGCPClientManager_CreateProject_Call { + return &MockGCPClientManager_CreateProject_Call{Call: _e.mock.On("CreateProject", parent, projectName, displayName, labels)} } -func (_c *MockGCPClientManager_CreateProject_Call) Run(run func(parent string, projectName string, displayName string, ttl time.Duration)) *MockGCPClientManager_CreateProject_Call { +func (_c *MockGCPClientManager_CreateProject_Call) Run(run func(parent string, projectName string, displayName string, labels map[string]string)) *MockGCPClientManager_CreateProject_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -430,9 +429,9 @@ func (_c *MockGCPClientManager_CreateProject_Call) Run(run func(parent string, p if args[2] != nil { arg2 = args[2].(string) } - var arg3 time.Duration + var arg3 map[string]string if args[3] != nil { - arg3 = args[3].(time.Duration) + arg3 = args[3].(map[string]string) } run( arg0, @@ -449,7 +448,7 @@ func (_c *MockGCPClientManager_CreateProject_Call) Return(s string, err error) * return _c } -func (_c *MockGCPClientManager_CreateProject_Call) RunAndReturn(run func(parent string, projectName string, displayName string, ttl time.Duration) (string, error)) *MockGCPClientManager_CreateProject_Call { +func (_c *MockGCPClientManager_CreateProject_Call) RunAndReturn(run func(parent string, projectName string, displayName string, labels map[string]string) (string, error)) *MockGCPClientManager_CreateProject_Call { _c.Call.Return(run) return _c } @@ -1570,3 +1569,66 @@ func (_c *MockGCPClientManager_RemoveIAMRoleBinding_Call) RunAndReturn(run func( _c.Call.Return(run) return _c } + +// UpdateProject provides a mock function for the type MockGCPClientManager +func (_mock *MockGCPClientManager) UpdateProject(projectID string, displayName string, labels map[string]string) error { + ret := _mock.Called(projectID, displayName, labels) + + if len(ret) == 0 { + panic("no return value specified for UpdateProject") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, string, map[string]string) error); ok { + r0 = returnFunc(projectID, displayName, labels) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockGCPClientManager_UpdateProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateProject' +type MockGCPClientManager_UpdateProject_Call struct { + *mock.Call +} + +// UpdateProject is a helper method to define mock.On call +// - projectID string +// - displayName string +// - labels map[string]string +func (_e *MockGCPClientManager_Expecter) UpdateProject(projectID interface{}, displayName interface{}, labels interface{}) *MockGCPClientManager_UpdateProject_Call { + return &MockGCPClientManager_UpdateProject_Call{Call: _e.mock.On("UpdateProject", projectID, displayName, labels)} +} + +func (_c *MockGCPClientManager_UpdateProject_Call) Run(run func(projectID string, displayName string, labels map[string]string)) *MockGCPClientManager_UpdateProject_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 map[string]string + if args[2] != nil { + arg2 = args[2].(map[string]string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockGCPClientManager_UpdateProject_Call) Return(err error) *MockGCPClientManager_UpdateProject_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockGCPClientManager_UpdateProject_Call) RunAndReturn(run func(projectID string, displayName string, labels map[string]string) error) *MockGCPClientManager_UpdateProject_Call { + _c.Call.Return(run) + return _c +} From a22223748e70dec651e488b749cc1e76ac5cbb41 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 18 Mar 2026 10:55:48 +0100 Subject: [PATCH 02/13] chore: cleanup --- internal/bootstrap/gcp/gcp.go | 2 ++ internal/bootstrap/gcp/gcp_client.go | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 9d3593dd..bdbc40e5 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -505,6 +505,8 @@ func (b *GCPBootstrapper) EnsureProject() error { return fmt.Errorf("failed to get project: %w", err) } +// calculateProjectExpiryLabel takes a TTL string (e.g. "24h") and +// returns a formatted UTC timestamp string that is usable as a GCP project label for automatic deletion. func calculateProjectExpiryLabel(projectTTLStr string) (string, error) { projectTTL, err := time.ParseDuration(projectTTLStr) if err != nil { diff --git a/internal/bootstrap/gcp/gcp_client.go b/internal/bootstrap/gcp/gcp_client.go index 2aa8844b..7d66376b 100644 --- a/internal/bootstrap/gcp/gcp_client.go +++ b/internal/bootstrap/gcp/gcp_client.go @@ -116,7 +116,6 @@ func (c *GCPClient) CreateProjectID(projectName string) string { // CreateProject creates a new GCP project under the specified parent (folder or organization). // It returns the project ID of the newly created project. -// The project is labeled with 'oms-managed=true' to identify it as created by OMS. func (c *GCPClient) CreateProject(parent, projectID, displayName string, labels map[string]string) (string, error) { client, err := resourcemanager.NewProjectsClient(c.ctx) if err != nil { @@ -144,6 +143,8 @@ func (c *GCPClient) CreateProject(parent, projectID, displayName string, labels return resp.ProjectId, nil } +// UpdateProject updates the display name and labels of an existing GCP project. +// Returns an error if the update operation fails or if the project does not exist. func (c *GCPClient) UpdateProject(projectID, displayName string, labels map[string]string) error { client, err := resourcemanager.NewProjectsClient(c.ctx) if err != nil { From 8d2ff8a011fdee84d75a0b4dd8bbd4b728f4ce36 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 18 Mar 2026 10:58:16 +0100 Subject: [PATCH 03/13] chore: cleanup --- internal/bootstrap/gcp/gcp.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index bdbc40e5..273ff682 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -471,6 +471,9 @@ func (b *GCPBootstrapper) EnsureProject() error { } deleteProjectAfter, err := calculateProjectExpiryLabel(b.Env.ProjectTTL) + if err != nil { + return fmt.Errorf("failed to calculate project expiry label: %w", err) + } labels := map[string]string{ OMSManagedLabel: "true", @@ -515,8 +518,8 @@ func calculateProjectExpiryLabel(projectTTLStr string) (string, error) { // prepare label for gcp project deletion in custom UTC time format. // GCP Labels are very limited. This is an easy way to add date and TZ info in one label. - gcpLabelLayout := "2006-01-02_15-04-05" - deleteProjectAfter := time.Now().UTC().Add(projectTTL).Format(gcpLabelLayout) + gcpExpiryLabelLayout := "2006-01-02_15-04-05" + deleteProjectAfter := time.Now().UTC().Add(projectTTL).Format(gcpExpiryLabelLayout) deleteProjectAfter = fmt.Sprintf("%s_utc", deleteProjectAfter) return deleteProjectAfter, nil From 730254616aebb4fe3c016e3bf7b21787db45aa77 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 18 Mar 2026 11:25:57 +0100 Subject: [PATCH 04/13] chore: debug workflow --- internal/bootstrap/gcp/gcp_test.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index 609c1f04..d574e9bb 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -41,6 +41,8 @@ var _ = Describe("GCP Bootstrapper", func() { ctx context.Context e env.Env + projectLabels map[string]string + icg *installer.MockInstallConfigManager gc *gcp.MockGCPClientManager fw *util.MockFileIO @@ -109,6 +111,8 @@ var _ = Describe("GCP Bootstrapper", func() { ControlPlaneNodes: []*node.Node{fakeNode("k0s-1", nodeClient), fakeNode("k0s-2", nodeClient), fakeNode("k0s-3", nodeClient)}, CephNodes: []*node.Node{fakeNode("ceph-1", nodeClient), fakeNode("ceph-2", nodeClient), fakeNode("ceph-3", nodeClient), fakeNode("ceph-4", nodeClient)}, } + + projectLabels = map[string]string{} }) Describe("NewGCPBootstrapper", func() { It("creates a valid GCPBootstrapper", func() { @@ -151,7 +155,7 @@ var _ = Describe("GCP Bootstrapper", func() { // 3. EnsureProject gc.EXPECT().GetProjectByName(mock.Anything, "test-project").Return(nil, fmt.Errorf("project not found: test-project")) gc.EXPECT().CreateProjectID("test-project").Return(projectId) - gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project", mock.Anything).Return(mock.Anything, nil) + gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project", projectLabels).Return(mock.Anything, nil) // 4. EnsureBilling gc.EXPECT().GetBillingInfo(projectId).Return(&cloudbilling.ProjectBillingInfo{BillingEnabled: false}, nil) @@ -505,7 +509,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("creates project when missing", func() { gc.EXPECT().GetProjectByName(csEnv.FolderID, csEnv.ProjectName).Return(nil, fmt.Errorf("project not found: %s", csEnv.ProjectName)) gc.EXPECT().CreateProjectID(csEnv.ProjectName).Return("new-proj-id") - gc.EXPECT().CreateProject(csEnv.FolderID, "new-proj-id", csEnv.ProjectName, mock.Anything).Return("", nil) + gc.EXPECT().CreateProject(csEnv.FolderID, "new-proj-id", csEnv.ProjectName, map[string]string{}).Return("", nil) err := bs.EnsureProject() Expect(err).NotTo(HaveOccurred()) @@ -525,7 +529,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("returns error when CreateProject fails", func() { gc.EXPECT().GetProjectByName("", csEnv.ProjectName).Return(nil, fmt.Errorf("project not found: %s", csEnv.ProjectName)) gc.EXPECT().CreateProjectID(csEnv.ProjectName).Return("fake-id") - gc.EXPECT().CreateProject("", "fake-id", csEnv.ProjectName, mock.Anything).Return("", fmt.Errorf("create error")) + gc.EXPECT().CreateProject("", "fake-id", csEnv.ProjectName, map[string]string{}).Return("", fmt.Errorf("create error")) err := bs.EnsureProject() Expect(err).To(HaveOccurred()) From fa6bb7cb9ee32440a1f4950d4f30ce6081fa2b4d Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 18 Mar 2026 14:50:14 +0100 Subject: [PATCH 05/13] test: update scenario --- internal/bootstrap/gcp/gcp_test.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index d574e9bb..fbe222fc 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -41,8 +41,6 @@ var _ = Describe("GCP Bootstrapper", func() { ctx context.Context e env.Env - projectLabels map[string]string - icg *installer.MockInstallConfigManager gc *gcp.MockGCPClientManager fw *util.MockFileIO @@ -111,8 +109,6 @@ var _ = Describe("GCP Bootstrapper", func() { ControlPlaneNodes: []*node.Node{fakeNode("k0s-1", nodeClient), fakeNode("k0s-2", nodeClient), fakeNode("k0s-3", nodeClient)}, CephNodes: []*node.Node{fakeNode("ceph-1", nodeClient), fakeNode("ceph-2", nodeClient), fakeNode("ceph-3", nodeClient), fakeNode("ceph-4", nodeClient)}, } - - projectLabels = map[string]string{} }) Describe("NewGCPBootstrapper", func() { It("creates a valid GCPBootstrapper", func() { @@ -155,7 +151,7 @@ var _ = Describe("GCP Bootstrapper", func() { // 3. EnsureProject gc.EXPECT().GetProjectByName(mock.Anything, "test-project").Return(nil, fmt.Errorf("project not found: test-project")) gc.EXPECT().CreateProjectID("test-project").Return(projectId) - gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project", projectLabels).Return(mock.Anything, nil) + gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project", mock.Anything).Return(mock.Anything, nil) // 4. EnsureBilling gc.EXPECT().GetBillingInfo(projectId).Return(&cloudbilling.ProjectBillingInfo{BillingEnabled: false}, nil) @@ -500,6 +496,7 @@ var _ = Describe("GCP Bootstrapper", func() { Describe("Valid EnsureProject", func() { It("uses existing project", func() { gc.EXPECT().GetProjectByName(csEnv.FolderID, csEnv.ProjectName).Return(&resourcemanagerpb.Project{ProjectId: "existing-id", Name: "existing-proj"}, nil) + gc.EXPECT().UpdateProject(mock.Anything, mock.Anything, mock.Anything).Return(nil) err := bs.EnsureProject() Expect(err).NotTo(HaveOccurred()) @@ -509,7 +506,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("creates project when missing", func() { gc.EXPECT().GetProjectByName(csEnv.FolderID, csEnv.ProjectName).Return(nil, fmt.Errorf("project not found: %s", csEnv.ProjectName)) gc.EXPECT().CreateProjectID(csEnv.ProjectName).Return("new-proj-id") - gc.EXPECT().CreateProject(csEnv.FolderID, "new-proj-id", csEnv.ProjectName, map[string]string{}).Return("", nil) + gc.EXPECT().CreateProject(csEnv.FolderID, "new-proj-id", csEnv.ProjectName, mock.Anything).Return("", nil) err := bs.EnsureProject() Expect(err).NotTo(HaveOccurred()) @@ -529,7 +526,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("returns error when CreateProject fails", func() { gc.EXPECT().GetProjectByName("", csEnv.ProjectName).Return(nil, fmt.Errorf("project not found: %s", csEnv.ProjectName)) gc.EXPECT().CreateProjectID(csEnv.ProjectName).Return("fake-id") - gc.EXPECT().CreateProject("", "fake-id", csEnv.ProjectName, map[string]string{}).Return("", fmt.Errorf("create error")) + gc.EXPECT().CreateProject("", "fake-id", csEnv.ProjectName, mock.Anything).Return("", fmt.Errorf("create error")) err := bs.EnsureProject() Expect(err).To(HaveOccurred()) From 9c5348cb47186300411e71847feb0ff1cb39e3d2 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Wed, 18 Mar 2026 15:04:43 +0100 Subject: [PATCH 06/13] fix: tests --- internal/bootstrap/gcp/gcp_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index fbe222fc..9570b867 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -134,11 +134,11 @@ var _ = Describe("GCP Bootstrapper", func() { // 1. EnsureInstallConfig fw.EXPECT().Exists("fake-config-file").Return(false) - icg.EXPECT().ApplyProfile("dev").Return(nil) + icg.EXPECT().ApplyProfile("minimal").Return(nil) // Returning a real install config to avoid nil pointer dereferences later icg.EXPECT().GetInstallConfig().RunAndReturn(func() *files.RootConfig { realIcm := installer.NewInstallConfigManager() - _ = realIcm.ApplyProfile("dev") + _ = realIcm.ApplyProfile("minimal") return realIcm.GetInstallConfig() }) @@ -415,7 +415,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("creates install config when missing", func() { fw.EXPECT().Exists(csEnv.InstallConfigPath).Return(false) - icg.EXPECT().ApplyProfile("dev").Return(nil) + icg.EXPECT().ApplyProfile("minimal").Return(nil) icg.EXPECT().GetInstallConfig().Return(&files.RootConfig{}) err := bs.EnsureInstallConfig() @@ -437,7 +437,7 @@ var _ = Describe("GCP Bootstrapper", func() { It("returns error when config file missing and applying profile fails", func() { fw.EXPECT().Exists(csEnv.InstallConfigPath).Return(false) - icg.EXPECT().ApplyProfile("dev").Return(fmt.Errorf("profile error")) + icg.EXPECT().ApplyProfile("minimal").Return(fmt.Errorf("profile error")) err := bs.EnsureInstallConfig() Expect(err).To(HaveOccurred()) From 410f2300a30f0e1ef3ae1bd0958af57c8af2cd70 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Thu, 19 Mar 2026 14:14:50 +0100 Subject: [PATCH 07/13] feat: remove displayName from UpdateProject --- internal/bootstrap/gcp/gcp.go | 2 +- internal/bootstrap/gcp/gcp_client.go | 9 ++++----- internal/bootstrap/gcp/gcp_test.go | 11 ++++++++++- internal/bootstrap/gcp/mocks.go | 26 ++++++++++---------------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 273ff682..9fcadd9b 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -485,7 +485,7 @@ func (b *GCPBootstrapper) EnsureProject() error { b.Env.ProjectID = existingProject.ProjectId b.Env.ProjectName = existingProject.Name - err := b.GCPClient.UpdateProject(existingProject.ProjectId, existingProject.DisplayName, labels) + err := b.GCPClient.UpdateProject(existingProject.ProjectId, labels) if err != nil { return fmt.Errorf("failed to update project: %w", err) } diff --git a/internal/bootstrap/gcp/gcp_client.go b/internal/bootstrap/gcp/gcp_client.go index 7d66376b..7e7338c6 100644 --- a/internal/bootstrap/gcp/gcp_client.go +++ b/internal/bootstrap/gcp/gcp_client.go @@ -37,7 +37,7 @@ type GCPClientManager interface { GetProjectByName(folderID string, displayName string) (*resourcemanagerpb.Project, error) CreateProjectID(projectName string) string CreateProject(parent, projectName, displayName string, labels map[string]string) (string, error) - UpdateProject(projectID, displayName string, labels map[string]string) error + UpdateProject(projectID string, labels map[string]string) error DeleteProject(projectID string) error IsOMSManagedProject(projectID string) (bool, error) GetBillingInfo(projectID string) (*cloudbilling.ProjectBillingInfo, error) @@ -145,7 +145,7 @@ func (c *GCPClient) CreateProject(parent, projectID, displayName string, labels // UpdateProject updates the display name and labels of an existing GCP project. // Returns an error if the update operation fails or if the project does not exist. -func (c *GCPClient) UpdateProject(projectID, displayName string, labels map[string]string) error { +func (c *GCPClient) UpdateProject(projectID string, labels map[string]string) error { client, err := resourcemanager.NewProjectsClient(c.ctx) if err != nil { return err @@ -153,9 +153,8 @@ func (c *GCPClient) UpdateProject(projectID, displayName string, labels map[stri defer util.IgnoreError(client.Close) project := &resourcemanagerpb.Project{ - Name: getProjectResourceName(projectID), - DisplayName: displayName, - Labels: labels, + Name: getProjectResourceName(projectID), + Labels: labels, } op, err := client.UpdateProject(c.ctx, &resourcemanagerpb.UpdateProjectRequest{ diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index 9570b867..e369b935 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -496,7 +496,7 @@ var _ = Describe("GCP Bootstrapper", func() { Describe("Valid EnsureProject", func() { It("uses existing project", func() { gc.EXPECT().GetProjectByName(csEnv.FolderID, csEnv.ProjectName).Return(&resourcemanagerpb.Project{ProjectId: "existing-id", Name: "existing-proj"}, nil) - gc.EXPECT().UpdateProject(mock.Anything, mock.Anything, mock.Anything).Return(nil) + gc.EXPECT().UpdateProject("existing-id", mock.Anything).Return(nil) err := bs.EnsureProject() Expect(err).NotTo(HaveOccurred()) @@ -533,6 +533,15 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err.Error()).To(ContainSubstring("failed to create project")) Expect(err.Error()).To(ContainSubstring("create error")) }) + + It("returns an error when UpdateProject fails", func() { + gc.EXPECT().GetProjectByName(csEnv.FolderID, csEnv.ProjectName).Return(&resourcemanagerpb.Project{ProjectId: "existing-id", Name: "existing-proj"}, nil) + gc.EXPECT().UpdateProject("existing-id", mock.Anything).Return(fmt.Errorf("failed to update project")) + + err := bs.EnsureProject() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to update project")) + }) }) }) diff --git a/internal/bootstrap/gcp/mocks.go b/internal/bootstrap/gcp/mocks.go index b9404aa7..52f01bbc 100644 --- a/internal/bootstrap/gcp/mocks.go +++ b/internal/bootstrap/gcp/mocks.go @@ -1571,16 +1571,16 @@ func (_c *MockGCPClientManager_RemoveIAMRoleBinding_Call) RunAndReturn(run func( } // UpdateProject provides a mock function for the type MockGCPClientManager -func (_mock *MockGCPClientManager) UpdateProject(projectID string, displayName string, labels map[string]string) error { - ret := _mock.Called(projectID, displayName, labels) +func (_mock *MockGCPClientManager) UpdateProject(projectID string, labels map[string]string) error { + ret := _mock.Called(projectID, labels) if len(ret) == 0 { panic("no return value specified for UpdateProject") } var r0 error - if returnFunc, ok := ret.Get(0).(func(string, string, map[string]string) error); ok { - r0 = returnFunc(projectID, displayName, labels) + if returnFunc, ok := ret.Get(0).(func(string, map[string]string) error); ok { + r0 = returnFunc(projectID, labels) } else { r0 = ret.Error(0) } @@ -1594,30 +1594,24 @@ type MockGCPClientManager_UpdateProject_Call struct { // UpdateProject is a helper method to define mock.On call // - projectID string -// - displayName string // - labels map[string]string -func (_e *MockGCPClientManager_Expecter) UpdateProject(projectID interface{}, displayName interface{}, labels interface{}) *MockGCPClientManager_UpdateProject_Call { - return &MockGCPClientManager_UpdateProject_Call{Call: _e.mock.On("UpdateProject", projectID, displayName, labels)} +func (_e *MockGCPClientManager_Expecter) UpdateProject(projectID interface{}, labels interface{}) *MockGCPClientManager_UpdateProject_Call { + return &MockGCPClientManager_UpdateProject_Call{Call: _e.mock.On("UpdateProject", projectID, labels)} } -func (_c *MockGCPClientManager_UpdateProject_Call) Run(run func(projectID string, displayName string, labels map[string]string)) *MockGCPClientManager_UpdateProject_Call { +func (_c *MockGCPClientManager_UpdateProject_Call) Run(run func(projectID string, labels map[string]string)) *MockGCPClientManager_UpdateProject_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } - var arg1 string + var arg1 map[string]string if args[1] != nil { - arg1 = args[1].(string) - } - var arg2 map[string]string - if args[2] != nil { - arg2 = args[2].(map[string]string) + arg1 = args[1].(map[string]string) } run( arg0, arg1, - arg2, ) }) return _c @@ -1628,7 +1622,7 @@ func (_c *MockGCPClientManager_UpdateProject_Call) Return(err error) *MockGCPCli return _c } -func (_c *MockGCPClientManager_UpdateProject_Call) RunAndReturn(run func(projectID string, displayName string, labels map[string]string) error) *MockGCPClientManager_UpdateProject_Call { +func (_c *MockGCPClientManager_UpdateProject_Call) RunAndReturn(run func(projectID string, labels map[string]string) error) *MockGCPClientManager_UpdateProject_Call { _c.Call.Return(run) return _c } From 4981cf441a5a9cd5cce1292e70136c396bcd7834 Mon Sep 17 00:00:00 2001 From: joka134 <27293650+joka134@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:15:50 +0000 Subject: [PATCH 08/13] chore(docs): Auto-update docs and licenses Signed-off-by: joka134 <27293650+joka134@users.noreply.github.com> --- NOTICE | 60 ++++++++++++++++++++++---------------------- internal/tmpl/NOTICE | 60 ++++++++++++++++++++++---------------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/NOTICE b/NOTICE index 6d4dba00..e1b54f8c 100644 --- a/NOTICE +++ b/NOTICE @@ -23,9 +23,9 @@ License URL: https://github.com/googleapis/google-cloud-go/blob/auth/oauth2adapt ---------- Module: cloud.google.com/go/compute -Version: v1.56.0 +Version: v1.57.0 License: Apache-2.0 -License URL: https://github.com/googleapis/google-cloud-go/blob/compute/v1.56.0/compute/LICENSE +License URL: https://github.com/googleapis/google-cloud-go/blob/compute/v1.57.0/compute/LICENSE ---------- Module: cloud.google.com/go/compute/metadata @@ -155,9 +155,9 @@ License URL: https://github.com/cloudflare/circl/blob/v1.6.3/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go -Version: v0.19.4 +Version: v0.21.1 License: Apache-2.0 -License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.19.4/LICENSE +License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.21.1/LICENSE ---------- Module: github.com/codesphere-cloud/oms/internal/tmpl @@ -419,9 +419,9 @@ License URL: https://github.com/googleapis/enterprise-certificate-proxy/blob/v0. ---------- Module: github.com/googleapis/gax-go/v2 -Version: v2.17.0 +Version: v2.18.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/gax-go/blob/v2.17.0/v2/LICENSE +License URL: https://github.com/googleapis/gax-go/blob/v2.18.0/v2/LICENSE ---------- Module: github.com/gosuri/uitable @@ -815,9 +815,9 @@ License URL: https://cs.opensource.google/go/x/crypto/+/v0.49.0:LICENSE ---------- Module: golang.org/x/net -Version: v0.51.0 +Version: v0.52.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/net/+/v0.51.0:LICENSE +License URL: https://cs.opensource.google/go/x/net/+/v0.52.0:LICENSE ---------- Module: golang.org/x/oauth2 @@ -857,39 +857,39 @@ License URL: https://cs.opensource.google/go/x/time/+/v0.15.0:LICENSE ---------- Module: google.golang.org/api -Version: v0.271.0 +Version: v0.272.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.271.0/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.272.0/LICENSE ---------- Module: google.golang.org/api/internal/third_party/uritemplates -Version: v0.271.0 +Version: v0.272.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.271.0/internal/third_party/uritemplates/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.272.0/internal/third_party/uritemplates/LICENSE ---------- Module: google.golang.org/genproto/googleapis -Version: v0.0.0-20260128011058-8636f8732409 +Version: v0.0.0-20260217215200-42d3e9bedb6d License: Apache-2.0 -License URL: https://github.com/googleapis/go-genproto/blob/8636f8732409/LICENSE +License URL: https://github.com/googleapis/go-genproto/blob/42d3e9bedb6d/LICENSE ---------- Module: google.golang.org/genproto/googleapis/api -Version: v0.0.0-20260203192932-546029d2fa20 +Version: v0.0.0-20260226221140-a57be14db171 License: Apache-2.0 -License URL: https://github.com/googleapis/go-genproto/blob/546029d2fa20/googleapis/api/LICENSE +License URL: https://github.com/googleapis/go-genproto/blob/a57be14db171/googleapis/api/LICENSE ---------- Module: google.golang.org/genproto/googleapis/rpc -Version: v0.0.0-20260226221140-a57be14db171 +Version: v0.0.0-20260311181403-84a4fc48630c License: Apache-2.0 -License URL: https://github.com/googleapis/go-genproto/blob/a57be14db171/googleapis/rpc/LICENSE +License URL: https://github.com/googleapis/go-genproto/blob/84a4fc48630c/googleapis/rpc/LICENSE ---------- Module: google.golang.org/grpc -Version: v1.79.2 +Version: v1.79.3 License: Apache-2.0 -License URL: https://github.com/grpc/grpc-go/blob/v1.79.2/LICENSE +License URL: https://github.com/grpc/grpc-go/blob/v1.79.3/LICENSE ---------- Module: google.golang.org/protobuf @@ -929,9 +929,9 @@ License URL: https://github.com/helm/helm/blob/v4.1.3/LICENSE ---------- Module: k8s.io/api -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/api/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/api/blob/v0.35.3/LICENSE ---------- Module: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions @@ -941,15 +941,15 @@ License URL: https://github.com/kubernetes/apiextensions-apiserver/blob/v0.35.1/ ---------- Module: k8s.io/apimachinery/pkg -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.3/LICENSE ---------- Module: k8s.io/apimachinery/third_party/forked/golang -Version: v0.35.2 +Version: v0.35.3 License: BSD-3-Clause -License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/third_party/forked/golang/LICENSE +License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.3/third_party/forked/golang/LICENSE ---------- Module: k8s.io/apiserver/pkg/endpoints/deprecation @@ -965,15 +965,15 @@ License URL: https://github.com/kubernetes/cli-runtime/blob/v0.35.2/LICENSE ---------- Module: k8s.io/client-go -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/client-go/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/client-go/blob/v0.35.3/LICENSE ---------- Module: k8s.io/client-go/third_party/forked/golang/template -Version: v0.35.2 +Version: v0.35.3 License: BSD-3-Clause -License URL: https://github.com/kubernetes/client-go/blob/v0.35.2/third_party/forked/golang/LICENSE +License URL: https://github.com/kubernetes/client-go/blob/v0.35.3/third_party/forked/golang/LICENSE ---------- Module: k8s.io/component-base/version diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index 6d4dba00..e1b54f8c 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -23,9 +23,9 @@ License URL: https://github.com/googleapis/google-cloud-go/blob/auth/oauth2adapt ---------- Module: cloud.google.com/go/compute -Version: v1.56.0 +Version: v1.57.0 License: Apache-2.0 -License URL: https://github.com/googleapis/google-cloud-go/blob/compute/v1.56.0/compute/LICENSE +License URL: https://github.com/googleapis/google-cloud-go/blob/compute/v1.57.0/compute/LICENSE ---------- Module: cloud.google.com/go/compute/metadata @@ -155,9 +155,9 @@ License URL: https://github.com/cloudflare/circl/blob/v1.6.3/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go -Version: v0.19.4 +Version: v0.21.1 License: Apache-2.0 -License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.19.4/LICENSE +License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.21.1/LICENSE ---------- Module: github.com/codesphere-cloud/oms/internal/tmpl @@ -419,9 +419,9 @@ License URL: https://github.com/googleapis/enterprise-certificate-proxy/blob/v0. ---------- Module: github.com/googleapis/gax-go/v2 -Version: v2.17.0 +Version: v2.18.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/gax-go/blob/v2.17.0/v2/LICENSE +License URL: https://github.com/googleapis/gax-go/blob/v2.18.0/v2/LICENSE ---------- Module: github.com/gosuri/uitable @@ -815,9 +815,9 @@ License URL: https://cs.opensource.google/go/x/crypto/+/v0.49.0:LICENSE ---------- Module: golang.org/x/net -Version: v0.51.0 +Version: v0.52.0 License: BSD-3-Clause -License URL: https://cs.opensource.google/go/x/net/+/v0.51.0:LICENSE +License URL: https://cs.opensource.google/go/x/net/+/v0.52.0:LICENSE ---------- Module: golang.org/x/oauth2 @@ -857,39 +857,39 @@ License URL: https://cs.opensource.google/go/x/time/+/v0.15.0:LICENSE ---------- Module: google.golang.org/api -Version: v0.271.0 +Version: v0.272.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.271.0/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.272.0/LICENSE ---------- Module: google.golang.org/api/internal/third_party/uritemplates -Version: v0.271.0 +Version: v0.272.0 License: BSD-3-Clause -License URL: https://github.com/googleapis/google-api-go-client/blob/v0.271.0/internal/third_party/uritemplates/LICENSE +License URL: https://github.com/googleapis/google-api-go-client/blob/v0.272.0/internal/third_party/uritemplates/LICENSE ---------- Module: google.golang.org/genproto/googleapis -Version: v0.0.0-20260128011058-8636f8732409 +Version: v0.0.0-20260217215200-42d3e9bedb6d License: Apache-2.0 -License URL: https://github.com/googleapis/go-genproto/blob/8636f8732409/LICENSE +License URL: https://github.com/googleapis/go-genproto/blob/42d3e9bedb6d/LICENSE ---------- Module: google.golang.org/genproto/googleapis/api -Version: v0.0.0-20260203192932-546029d2fa20 +Version: v0.0.0-20260226221140-a57be14db171 License: Apache-2.0 -License URL: https://github.com/googleapis/go-genproto/blob/546029d2fa20/googleapis/api/LICENSE +License URL: https://github.com/googleapis/go-genproto/blob/a57be14db171/googleapis/api/LICENSE ---------- Module: google.golang.org/genproto/googleapis/rpc -Version: v0.0.0-20260226221140-a57be14db171 +Version: v0.0.0-20260311181403-84a4fc48630c License: Apache-2.0 -License URL: https://github.com/googleapis/go-genproto/blob/a57be14db171/googleapis/rpc/LICENSE +License URL: https://github.com/googleapis/go-genproto/blob/84a4fc48630c/googleapis/rpc/LICENSE ---------- Module: google.golang.org/grpc -Version: v1.79.2 +Version: v1.79.3 License: Apache-2.0 -License URL: https://github.com/grpc/grpc-go/blob/v1.79.2/LICENSE +License URL: https://github.com/grpc/grpc-go/blob/v1.79.3/LICENSE ---------- Module: google.golang.org/protobuf @@ -929,9 +929,9 @@ License URL: https://github.com/helm/helm/blob/v4.1.3/LICENSE ---------- Module: k8s.io/api -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/api/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/api/blob/v0.35.3/LICENSE ---------- Module: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions @@ -941,15 +941,15 @@ License URL: https://github.com/kubernetes/apiextensions-apiserver/blob/v0.35.1/ ---------- Module: k8s.io/apimachinery/pkg -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.3/LICENSE ---------- Module: k8s.io/apimachinery/third_party/forked/golang -Version: v0.35.2 +Version: v0.35.3 License: BSD-3-Clause -License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.2/third_party/forked/golang/LICENSE +License URL: https://github.com/kubernetes/apimachinery/blob/v0.35.3/third_party/forked/golang/LICENSE ---------- Module: k8s.io/apiserver/pkg/endpoints/deprecation @@ -965,15 +965,15 @@ License URL: https://github.com/kubernetes/cli-runtime/blob/v0.35.2/LICENSE ---------- Module: k8s.io/client-go -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/client-go/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/client-go/blob/v0.35.3/LICENSE ---------- Module: k8s.io/client-go/third_party/forked/golang/template -Version: v0.35.2 +Version: v0.35.3 License: BSD-3-Clause -License URL: https://github.com/kubernetes/client-go/blob/v0.35.2/third_party/forked/golang/LICENSE +License URL: https://github.com/kubernetes/client-go/blob/v0.35.3/third_party/forked/golang/LICENSE ---------- Module: k8s.io/component-base/version From 2004cf9855b1cf7110b20ec16bd15b2222ab084b Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Thu, 19 Mar 2026 15:43:50 +0100 Subject: [PATCH 09/13] chore: move calculateProjectExpiryLabel to new helper package to allow testing --- internal/bootstrap/gcp/gcp.go | 17 -------- internal/bootstrap/gcp/gcp_test.go | 5 +++ internal/bootstrap/gcp/helper.go | 23 +++++++++++ internal/bootstrap/gcp/helper_test.go | 56 +++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 internal/bootstrap/gcp/helper.go create mode 100644 internal/bootstrap/gcp/helper_test.go diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 9fcadd9b..16853934 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -508,23 +508,6 @@ func (b *GCPBootstrapper) EnsureProject() error { return fmt.Errorf("failed to get project: %w", err) } -// calculateProjectExpiryLabel takes a TTL string (e.g. "24h") and -// returns a formatted UTC timestamp string that is usable as a GCP project label for automatic deletion. -func calculateProjectExpiryLabel(projectTTLStr string) (string, error) { - projectTTL, err := time.ParseDuration(projectTTLStr) - if err != nil { - return "", fmt.Errorf("invalid project TTL format: %w", err) - } - - // prepare label for gcp project deletion in custom UTC time format. - // GCP Labels are very limited. This is an easy way to add date and TZ info in one label. - gcpExpiryLabelLayout := "2006-01-02_15-04-05" - deleteProjectAfter := time.Now().UTC().Add(projectTTL).Format(gcpExpiryLabelLayout) - deleteProjectAfter = fmt.Sprintf("%s_utc", deleteProjectAfter) - - return deleteProjectAfter, nil -} - func (b *GCPBootstrapper) EnsureBilling() error { bi, err := b.GCPClient.GetBillingInfo(b.Env.ProjectID) if err != nil { diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index e369b935..f93bb69e 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -542,6 +542,11 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to update project")) }) + + It("return an error when calculateProjectExpiryLabel fails", func() { + // var ExportInternalCalculateLabel = calculateProjectExpiryLabel + }) + // gcp.calculateProjectExpiryLabel() }) }) diff --git a/internal/bootstrap/gcp/helper.go b/internal/bootstrap/gcp/helper.go new file mode 100644 index 00000000..d4d57458 --- /dev/null +++ b/internal/bootstrap/gcp/helper.go @@ -0,0 +1,23 @@ +package gcp + +import ( + "fmt" + "time" +) + +// calculateProjectExpiryLabel takes a TTL string (e.g. "24h") and +// returns a formatted UTC timestamp string that is usable as a GCP project label for automatic deletion. +func calculateProjectExpiryLabel(projectTTLStr string) (string, error) { + projectTTL, err := time.ParseDuration(projectTTLStr) + if err != nil { + return "", fmt.Errorf("invalid project TTL format: %w", err) + } + + // prepare label for gcp project deletion in custom UTC time format. + // GCP Labels are very limited. This is an easy way to add date and TZ info in one label. + gcpExpiryLabelLayout := "2006-01-02_15-04-05" + deleteProjectAfter := time.Now().UTC().Add(projectTTL).Format(gcpExpiryLabelLayout) + deleteProjectAfter = fmt.Sprintf("%s_utc", deleteProjectAfter) + + return deleteProjectAfter, nil +} diff --git a/internal/bootstrap/gcp/helper_test.go b/internal/bootstrap/gcp/helper_test.go new file mode 100644 index 00000000..2e7c0ce1 --- /dev/null +++ b/internal/bootstrap/gcp/helper_test.go @@ -0,0 +1,56 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package gcp + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("calculateProjectExpiryLabel", func() { + const customDateFormat string = "2006-01-02_15-04-05_utc" + const customDateFormatRegex string = `^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}_utc$` + + type validTestCase struct { + inputTTL string + expectedDuration time.Duration + } + + DescribeTable("calculating the expiry label from string durations", + func(tc validTestCase) { + label, err := calculateProjectExpiryLabel(tc.inputTTL) + Expect(err).NotTo(HaveOccurred()) + Expect(label).To(MatchRegexp(customDateFormatRegex)) + + parsedTime, err := time.Parse(customDateFormat, label) + Expect(err).NotTo(HaveOccurred()) + + expectedTime := time.Now().UTC().Add(tc.expectedDuration) + + Expect(parsedTime).To(BeTemporally("~", expectedTime, 10*time.Second)) + }, + + // Define the table rows using the temp struct + Entry("1 Minute", validTestCase{ + inputTTL: "1m", + expectedDuration: 1 * time.Minute, + }), + Entry("1 hour", validTestCase{ + inputTTL: "1h", + expectedDuration: 1 * time.Hour, + }), + Entry("1 Day", validTestCase{ + inputTTL: "24h", + expectedDuration: 24 * time.Hour, + }), + ) + + It("returns an error for invalid duration strings", func() { + _, err := calculateProjectExpiryLabel("1d") // 'd' is not a valid time unit in Go's duration parsing + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid project TTL format")) + }) +}) From 4c16fe98e724f72f906cd6e1a5fa2f51f4654c66 Mon Sep 17 00:00:00 2001 From: joka134 <27293650+joka134@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:45:10 +0000 Subject: [PATCH 10/13] chore(docs): Auto-update docs and licenses Signed-off-by: joka134 <27293650+joka134@users.noreply.github.com> --- internal/bootstrap/gcp/helper.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/bootstrap/gcp/helper.go b/internal/bootstrap/gcp/helper.go index d4d57458..179e0a7f 100644 --- a/internal/bootstrap/gcp/helper.go +++ b/internal/bootstrap/gcp/helper.go @@ -1,3 +1,6 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + package gcp import ( From 24d2f32714b20ff34303b6cb9f63dc82920bde43 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Thu, 19 Mar 2026 15:56:17 +0100 Subject: [PATCH 11/13] chore: cleanup --- internal/bootstrap/gcp/helper_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/bootstrap/gcp/helper_test.go b/internal/bootstrap/gcp/helper_test.go index 2e7c0ce1..9df25f96 100644 --- a/internal/bootstrap/gcp/helper_test.go +++ b/internal/bootstrap/gcp/helper_test.go @@ -33,7 +33,7 @@ var _ = Describe("calculateProjectExpiryLabel", func() { Expect(parsedTime).To(BeTemporally("~", expectedTime, 10*time.Second)) }, - // Define the table rows using the temp struct + // Define test scenarios Entry("1 Minute", validTestCase{ inputTTL: "1m", expectedDuration: 1 * time.Minute, @@ -49,8 +49,9 @@ var _ = Describe("calculateProjectExpiryLabel", func() { ) It("returns an error for invalid duration strings", func() { - _, err := calculateProjectExpiryLabel("1d") // 'd' is not a valid time unit in Go's duration parsing + label, err := calculateProjectExpiryLabel("1d") // 'd' is not a valid time unit in Go's duration parsing Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("invalid project TTL format")) + Expect(label).To(BeEmpty()) }) }) From c3b7bbadcec596e66cb500edffacdac198a622f6 Mon Sep 17 00:00:00 2001 From: Jonas Kauke Date: Fri, 20 Mar 2026 14:37:38 +0100 Subject: [PATCH 12/13] chore: cleanup --- internal/bootstrap/gcp/gcp_client.go | 2 +- internal/bootstrap/gcp/gcp_test.go | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/bootstrap/gcp/gcp_client.go b/internal/bootstrap/gcp/gcp_client.go index 7e7338c6..2a1068e4 100644 --- a/internal/bootstrap/gcp/gcp_client.go +++ b/internal/bootstrap/gcp/gcp_client.go @@ -143,7 +143,7 @@ func (c *GCPClient) CreateProject(parent, projectID, displayName string, labels return resp.ProjectId, nil } -// UpdateProject updates the display name and labels of an existing GCP project. +// UpdateProject updates the project's labels of an existing GCP project. // Returns an error if the update operation fails or if the project does not exist. func (c *GCPClient) UpdateProject(projectID string, labels map[string]string) error { client, err := resourcemanager.NewProjectsClient(c.ctx) diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index f93bb69e..e369b935 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -542,11 +542,6 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to update project")) }) - - It("return an error when calculateProjectExpiryLabel fails", func() { - // var ExportInternalCalculateLabel = calculateProjectExpiryLabel - }) - // gcp.calculateProjectExpiryLabel() }) }) From 06ab0d319d0d5e2c4afd966fec7aa347973a491d Mon Sep 17 00:00:00 2001 From: joka134 <27293650+joka134@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:40:01 +0000 Subject: [PATCH 13/13] chore(docs): Auto-update docs and licenses Signed-off-by: joka134 <27293650+joka134@users.noreply.github.com> --- NOTICE | 12 ++++++------ internal/tmpl/NOTICE | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/NOTICE b/NOTICE index e1b54f8c..644932ff 100644 --- a/NOTICE +++ b/NOTICE @@ -155,9 +155,9 @@ License URL: https://github.com/cloudflare/circl/blob/v1.6.3/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go -Version: v0.21.1 +Version: v0.22.0 License: Apache-2.0 -License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.21.1/LICENSE +License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.22.0/LICENSE ---------- Module: github.com/codesphere-cloud/oms/internal/tmpl @@ -797,9 +797,9 @@ License URL: https://github.com/open-telemetry/opentelemetry-proto-go/blob/otlp/ ---------- Module: go.yaml.in/yaml/v2 -Version: v2.4.3 +Version: v2.4.4 License: Apache-2.0 -License URL: https://github.com/yaml/go-yaml/blob/v2.4.3/LICENSE +License URL: https://github.com/yaml/go-yaml/blob/v2.4.4/LICENSE ---------- Module: go.yaml.in/yaml/v3 @@ -959,9 +959,9 @@ License URL: https://github.com/kubernetes/apiserver/blob/v0.35.1/LICENSE ---------- Module: k8s.io/cli-runtime/pkg -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/cli-runtime/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/cli-runtime/blob/v0.35.3/LICENSE ---------- Module: k8s.io/client-go diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index e1b54f8c..644932ff 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -155,9 +155,9 @@ License URL: https://github.com/cloudflare/circl/blob/v1.6.3/LICENSE ---------- Module: github.com/codesphere-cloud/cs-go -Version: v0.21.1 +Version: v0.22.0 License: Apache-2.0 -License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.21.1/LICENSE +License URL: https://github.com/codesphere-cloud/cs-go/blob/v0.22.0/LICENSE ---------- Module: github.com/codesphere-cloud/oms/internal/tmpl @@ -797,9 +797,9 @@ License URL: https://github.com/open-telemetry/opentelemetry-proto-go/blob/otlp/ ---------- Module: go.yaml.in/yaml/v2 -Version: v2.4.3 +Version: v2.4.4 License: Apache-2.0 -License URL: https://github.com/yaml/go-yaml/blob/v2.4.3/LICENSE +License URL: https://github.com/yaml/go-yaml/blob/v2.4.4/LICENSE ---------- Module: go.yaml.in/yaml/v3 @@ -959,9 +959,9 @@ License URL: https://github.com/kubernetes/apiserver/blob/v0.35.1/LICENSE ---------- Module: k8s.io/cli-runtime/pkg -Version: v0.35.2 +Version: v0.35.3 License: Apache-2.0 -License URL: https://github.com/kubernetes/cli-runtime/blob/v0.35.2/LICENSE +License URL: https://github.com/kubernetes/cli-runtime/blob/v0.35.3/LICENSE ---------- Module: k8s.io/client-go