diff --git a/api/handlers/fake/cfsecurity_group_repository.go b/api/handlers/fake/cfsecurity_group_repository.go index bc8ef5cb3..c2453d92b 100644 --- a/api/handlers/fake/cfsecurity_group_repository.go +++ b/api/handlers/fake/cfsecurity_group_repository.go @@ -11,6 +11,21 @@ import ( ) type CFSecurityGroupRepository struct { + BindSecurityGroupStub func(context.Context, authorization.Info, repositories.BindSecurityGroupMessage) (repositories.SecurityGroupRecord, error) + bindSecurityGroupMutex sync.RWMutex + bindSecurityGroupArgsForCall []struct { + arg1 context.Context + arg2 authorization.Info + arg3 repositories.BindSecurityGroupMessage + } + bindSecurityGroupReturns struct { + result1 repositories.SecurityGroupRecord + result2 error + } + bindSecurityGroupReturnsOnCall map[int]struct { + result1 repositories.SecurityGroupRecord + result2 error + } CreateSecurityGroupStub func(context.Context, authorization.Info, repositories.CreateSecurityGroupMessage) (repositories.SecurityGroupRecord, error) createSecurityGroupMutex sync.RWMutex createSecurityGroupArgsForCall []struct { @@ -26,10 +41,106 @@ type CFSecurityGroupRepository struct { result1 repositories.SecurityGroupRecord result2 error } + GetSecurityGroupStub func(context.Context, authorization.Info, string) (repositories.SecurityGroupRecord, error) + getSecurityGroupMutex sync.RWMutex + getSecurityGroupArgsForCall []struct { + arg1 context.Context + arg2 authorization.Info + arg3 string + } + getSecurityGroupReturns struct { + result1 repositories.SecurityGroupRecord + result2 error + } + getSecurityGroupReturnsOnCall map[int]struct { + result1 repositories.SecurityGroupRecord + result2 error + } + ListSecurityGroupsStub func(context.Context, authorization.Info, repositories.ListSecurityGroupMessage) (repositories.ListResult[repositories.SecurityGroupRecord], error) + listSecurityGroupsMutex sync.RWMutex + listSecurityGroupsArgsForCall []struct { + arg1 context.Context + arg2 authorization.Info + arg3 repositories.ListSecurityGroupMessage + } + listSecurityGroupsReturns struct { + result1 repositories.ListResult[repositories.SecurityGroupRecord] + result2 error + } + listSecurityGroupsReturnsOnCall map[int]struct { + result1 repositories.ListResult[repositories.SecurityGroupRecord] + result2 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } +func (fake *CFSecurityGroupRepository) BindSecurityGroup(arg1 context.Context, arg2 authorization.Info, arg3 repositories.BindSecurityGroupMessage) (repositories.SecurityGroupRecord, error) { + fake.bindSecurityGroupMutex.Lock() + ret, specificReturn := fake.bindSecurityGroupReturnsOnCall[len(fake.bindSecurityGroupArgsForCall)] + fake.bindSecurityGroupArgsForCall = append(fake.bindSecurityGroupArgsForCall, struct { + arg1 context.Context + arg2 authorization.Info + arg3 repositories.BindSecurityGroupMessage + }{arg1, arg2, arg3}) + stub := fake.BindSecurityGroupStub + fakeReturns := fake.bindSecurityGroupReturns + fake.recordInvocation("BindSecurityGroup", []interface{}{arg1, arg2, arg3}) + fake.bindSecurityGroupMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *CFSecurityGroupRepository) BindSecurityGroupCallCount() int { + fake.bindSecurityGroupMutex.RLock() + defer fake.bindSecurityGroupMutex.RUnlock() + return len(fake.bindSecurityGroupArgsForCall) +} + +func (fake *CFSecurityGroupRepository) BindSecurityGroupCalls(stub func(context.Context, authorization.Info, repositories.BindSecurityGroupMessage) (repositories.SecurityGroupRecord, error)) { + fake.bindSecurityGroupMutex.Lock() + defer fake.bindSecurityGroupMutex.Unlock() + fake.BindSecurityGroupStub = stub +} + +func (fake *CFSecurityGroupRepository) BindSecurityGroupArgsForCall(i int) (context.Context, authorization.Info, repositories.BindSecurityGroupMessage) { + fake.bindSecurityGroupMutex.RLock() + defer fake.bindSecurityGroupMutex.RUnlock() + argsForCall := fake.bindSecurityGroupArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *CFSecurityGroupRepository) BindSecurityGroupReturns(result1 repositories.SecurityGroupRecord, result2 error) { + fake.bindSecurityGroupMutex.Lock() + defer fake.bindSecurityGroupMutex.Unlock() + fake.BindSecurityGroupStub = nil + fake.bindSecurityGroupReturns = struct { + result1 repositories.SecurityGroupRecord + result2 error + }{result1, result2} +} + +func (fake *CFSecurityGroupRepository) BindSecurityGroupReturnsOnCall(i int, result1 repositories.SecurityGroupRecord, result2 error) { + fake.bindSecurityGroupMutex.Lock() + defer fake.bindSecurityGroupMutex.Unlock() + fake.BindSecurityGroupStub = nil + if fake.bindSecurityGroupReturnsOnCall == nil { + fake.bindSecurityGroupReturnsOnCall = make(map[int]struct { + result1 repositories.SecurityGroupRecord + result2 error + }) + } + fake.bindSecurityGroupReturnsOnCall[i] = struct { + result1 repositories.SecurityGroupRecord + result2 error + }{result1, result2} +} + func (fake *CFSecurityGroupRepository) CreateSecurityGroup(arg1 context.Context, arg2 authorization.Info, arg3 repositories.CreateSecurityGroupMessage) (repositories.SecurityGroupRecord, error) { fake.createSecurityGroupMutex.Lock() ret, specificReturn := fake.createSecurityGroupReturnsOnCall[len(fake.createSecurityGroupArgsForCall)] @@ -96,6 +207,138 @@ func (fake *CFSecurityGroupRepository) CreateSecurityGroupReturnsOnCall(i int, r }{result1, result2} } +func (fake *CFSecurityGroupRepository) GetSecurityGroup(arg1 context.Context, arg2 authorization.Info, arg3 string) (repositories.SecurityGroupRecord, error) { + fake.getSecurityGroupMutex.Lock() + ret, specificReturn := fake.getSecurityGroupReturnsOnCall[len(fake.getSecurityGroupArgsForCall)] + fake.getSecurityGroupArgsForCall = append(fake.getSecurityGroupArgsForCall, struct { + arg1 context.Context + arg2 authorization.Info + arg3 string + }{arg1, arg2, arg3}) + stub := fake.GetSecurityGroupStub + fakeReturns := fake.getSecurityGroupReturns + fake.recordInvocation("GetSecurityGroup", []interface{}{arg1, arg2, arg3}) + fake.getSecurityGroupMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *CFSecurityGroupRepository) GetSecurityGroupCallCount() int { + fake.getSecurityGroupMutex.RLock() + defer fake.getSecurityGroupMutex.RUnlock() + return len(fake.getSecurityGroupArgsForCall) +} + +func (fake *CFSecurityGroupRepository) GetSecurityGroupCalls(stub func(context.Context, authorization.Info, string) (repositories.SecurityGroupRecord, error)) { + fake.getSecurityGroupMutex.Lock() + defer fake.getSecurityGroupMutex.Unlock() + fake.GetSecurityGroupStub = stub +} + +func (fake *CFSecurityGroupRepository) GetSecurityGroupArgsForCall(i int) (context.Context, authorization.Info, string) { + fake.getSecurityGroupMutex.RLock() + defer fake.getSecurityGroupMutex.RUnlock() + argsForCall := fake.getSecurityGroupArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *CFSecurityGroupRepository) GetSecurityGroupReturns(result1 repositories.SecurityGroupRecord, result2 error) { + fake.getSecurityGroupMutex.Lock() + defer fake.getSecurityGroupMutex.Unlock() + fake.GetSecurityGroupStub = nil + fake.getSecurityGroupReturns = struct { + result1 repositories.SecurityGroupRecord + result2 error + }{result1, result2} +} + +func (fake *CFSecurityGroupRepository) GetSecurityGroupReturnsOnCall(i int, result1 repositories.SecurityGroupRecord, result2 error) { + fake.getSecurityGroupMutex.Lock() + defer fake.getSecurityGroupMutex.Unlock() + fake.GetSecurityGroupStub = nil + if fake.getSecurityGroupReturnsOnCall == nil { + fake.getSecurityGroupReturnsOnCall = make(map[int]struct { + result1 repositories.SecurityGroupRecord + result2 error + }) + } + fake.getSecurityGroupReturnsOnCall[i] = struct { + result1 repositories.SecurityGroupRecord + result2 error + }{result1, result2} +} + +func (fake *CFSecurityGroupRepository) ListSecurityGroups(arg1 context.Context, arg2 authorization.Info, arg3 repositories.ListSecurityGroupMessage) (repositories.ListResult[repositories.SecurityGroupRecord], error) { + fake.listSecurityGroupsMutex.Lock() + ret, specificReturn := fake.listSecurityGroupsReturnsOnCall[len(fake.listSecurityGroupsArgsForCall)] + fake.listSecurityGroupsArgsForCall = append(fake.listSecurityGroupsArgsForCall, struct { + arg1 context.Context + arg2 authorization.Info + arg3 repositories.ListSecurityGroupMessage + }{arg1, arg2, arg3}) + stub := fake.ListSecurityGroupsStub + fakeReturns := fake.listSecurityGroupsReturns + fake.recordInvocation("ListSecurityGroups", []interface{}{arg1, arg2, arg3}) + fake.listSecurityGroupsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *CFSecurityGroupRepository) ListSecurityGroupsCallCount() int { + fake.listSecurityGroupsMutex.RLock() + defer fake.listSecurityGroupsMutex.RUnlock() + return len(fake.listSecurityGroupsArgsForCall) +} + +func (fake *CFSecurityGroupRepository) ListSecurityGroupsCalls(stub func(context.Context, authorization.Info, repositories.ListSecurityGroupMessage) (repositories.ListResult[repositories.SecurityGroupRecord], error)) { + fake.listSecurityGroupsMutex.Lock() + defer fake.listSecurityGroupsMutex.Unlock() + fake.ListSecurityGroupsStub = stub +} + +func (fake *CFSecurityGroupRepository) ListSecurityGroupsArgsForCall(i int) (context.Context, authorization.Info, repositories.ListSecurityGroupMessage) { + fake.listSecurityGroupsMutex.RLock() + defer fake.listSecurityGroupsMutex.RUnlock() + argsForCall := fake.listSecurityGroupsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *CFSecurityGroupRepository) ListSecurityGroupsReturns(result1 repositories.ListResult[repositories.SecurityGroupRecord], result2 error) { + fake.listSecurityGroupsMutex.Lock() + defer fake.listSecurityGroupsMutex.Unlock() + fake.ListSecurityGroupsStub = nil + fake.listSecurityGroupsReturns = struct { + result1 repositories.ListResult[repositories.SecurityGroupRecord] + result2 error + }{result1, result2} +} + +func (fake *CFSecurityGroupRepository) ListSecurityGroupsReturnsOnCall(i int, result1 repositories.ListResult[repositories.SecurityGroupRecord], result2 error) { + fake.listSecurityGroupsMutex.Lock() + defer fake.listSecurityGroupsMutex.Unlock() + fake.ListSecurityGroupsStub = nil + if fake.listSecurityGroupsReturnsOnCall == nil { + fake.listSecurityGroupsReturnsOnCall = make(map[int]struct { + result1 repositories.ListResult[repositories.SecurityGroupRecord] + result2 error + }) + } + fake.listSecurityGroupsReturnsOnCall[i] = struct { + result1 repositories.ListResult[repositories.SecurityGroupRecord] + result2 error + }{result1, result2} +} + func (fake *CFSecurityGroupRepository) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() diff --git a/api/handlers/security_group.go b/api/handlers/security_group.go index f6ffd56c5..672b84034 100644 --- a/api/handlers/security_group.go +++ b/api/handlers/security_group.go @@ -18,8 +18,11 @@ import ( ) const ( - SecurityGroupsPath = "/v3/security_groups" - spaceNotFoundErr = "Space does not exist, or you do not have access." + SecurityGroupsPath = "/v3/security_groups" + SecurityGroupPath = "/v3/security_groups/{guid}" + SecurityGroupRunningSpacesPath = "/v3/security_groups/{guid}/relationships/running_spaces" + SecurityGroupStagingSpacesPath = "/v3/security_groups/{guid}/relationships/staging_spaces" + spaceNotFoundErr = "Space does not exist, or you do not have access." ) type SecurityGroup struct { @@ -31,7 +34,10 @@ type SecurityGroup struct { //counterfeiter:generate -o fake -fake-name CFSecurityGroupRepository . CFSecurityGroupRepository type CFSecurityGroupRepository interface { + GetSecurityGroup(context.Context, authorization.Info, string) (repositories.SecurityGroupRecord, error) + ListSecurityGroups(context.Context, authorization.Info, repositories.ListSecurityGroupMessage) (repositories.ListResult[repositories.SecurityGroupRecord], error) CreateSecurityGroup(context.Context, authorization.Info, repositories.CreateSecurityGroupMessage) (repositories.SecurityGroupRecord, error) + BindSecurityGroup(context.Context, authorization.Info, repositories.BindSecurityGroupMessage) (repositories.SecurityGroupRecord, error) } func NewSecurityGroup( @@ -48,6 +54,39 @@ func NewSecurityGroup( } } +func (h *SecurityGroup) get(r *http.Request) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(r.Context()) + logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.security-group.get") + + guid := routing.URLParam(r, "guid") + securityGroup, err := h.securityGroupRepo.GetSecurityGroup(r.Context(), authInfo, guid) + if err != nil { + return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "failed to fetch security group", "securityGroupGUID", guid) + } + + return routing.NewResponse(http.StatusOK).WithBody(presenter.ForSecurityGroup(securityGroup, h.serverURL)), nil +} + +func (h *SecurityGroup) list(r *http.Request) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(r.Context()) + logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.security-group.list") + + payload := new(payloads.SecurityGroupList) + err := h.requestValidator.DecodeAndValidateURLValues(r, payload) + if err != nil { + return nil, apierrors.LogAndReturn(logger, err, "Unable to decode request query parameters") + } + + logger.Info("Listing security groups", "payload", payload) + + securityGroups, err := h.securityGroupRepo.ListSecurityGroups(r.Context(), authInfo, payload.ToMessage()) + if err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to list security groups") + } + + return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForSecurityGroup, securityGroups, h.serverURL, *r.URL)), nil +} + func (h *SecurityGroup) create(r *http.Request) (*routing.Response, error) { authInfo, _ := authorization.InfoFromContext(r.Context()) logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.security-group.create") @@ -83,12 +122,80 @@ func (h *SecurityGroup) create(r *http.Request) (*routing.Response, error) { return routing.NewResponse(http.StatusCreated).WithBody(presenter.ForSecurityGroup(securityGroup, h.serverURL)), nil } +func (h *SecurityGroup) bindRunning(r *http.Request) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(r.Context()) + logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.security-group.bind-running-spaces") + + payload := new(payloads.SecurityGroupBind) + if err := h.requestValidator.DecodeAndValidateJSONPayload(r, payload); err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to decode payload") + } + + guid := routing.URLParam(r, "guid") + securityGroup, err := h.bind(logger, authInfo, r.Context(), payload, guid, repositories.SecurityGroupRunningSpaceType) + if err != nil { + return nil, err + } + + return routing.NewResponse(http.StatusOK).WithBody(presenter.ForSecurityGroupRunningSpaces(securityGroup, h.serverURL)), nil +} + +func (h *SecurityGroup) bindStaging(r *http.Request) (*routing.Response, error) { + authInfo, _ := authorization.InfoFromContext(r.Context()) + logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.security-group.bind-staging-spaces") + + payload := new(payloads.SecurityGroupBind) + if err := h.requestValidator.DecodeAndValidateJSONPayload(r, payload); err != nil { + return nil, apierrors.LogAndReturn(logger, err, "failed to decode payload") + } + + guid := routing.URLParam(r, "guid") + securityGroup, err := h.bind(logger, authInfo, r.Context(), payload, guid, repositories.SecurityGroupStagingSpaceType) + if err != nil { + return nil, err + } + + return routing.NewResponse(http.StatusOK).WithBody(presenter.ForSecurityGroupStagingSpaces(securityGroup, h.serverURL)), nil +} + +func (h *SecurityGroup) bind(logger logr.Logger, authInfo authorization.Info, ctx context.Context, payload *payloads.SecurityGroupBind, guid, spaceType string) (repositories.SecurityGroupRecord, error) { + _, err := h.securityGroupRepo.GetSecurityGroup(ctx, authInfo, guid) + if err != nil { + return repositories.SecurityGroupRecord{}, apierrors.LogAndReturn(logger, err, "failed to bind security group, it does not exist") + } + + spaceGUIDs := slices.Collect(it.Map(slices.Values(payload.Data), func(d payloads.RelationshipData) string { return d.GUID })) + spaces, err := h.spaceRepo.ListSpaces(ctx, authInfo, repositories.ListSpacesMessage{GUIDs: spaceGUIDs}) + if err != nil { + return repositories.SecurityGroupRecord{}, apierrors.LogAndReturn(logger, err, "failed to list spaces for binding to security group", "securityGroupGUID", guid) + } + + if len(spaces.Records) == 0 { + return repositories.SecurityGroupRecord{}, apierrors.LogAndReturn( + logger, + apierrors.NewUnprocessableEntityError(fmt.Errorf("failed bind %s space to security group", spaceType), spaceNotFoundErr), + spaceNotFoundErr, + ) + } + + securityGroup, err := h.securityGroupRepo.BindSecurityGroup(ctx, authInfo, payload.ToMessage(spaceType, guid)) + if err != nil { + return repositories.SecurityGroupRecord{}, apierrors.LogAndReturn(logger, err, "failed to bind security group to %s space", spaceType) + } + + return securityGroup, nil +} + func (h *SecurityGroup) UnauthenticatedRoutes() []routing.Route { return nil } func (h *SecurityGroup) AuthenticatedRoutes() []routing.Route { return []routing.Route{ + {Method: "GET", Pattern: SecurityGroupPath, Handler: h.get}, + {Method: "GET", Pattern: SecurityGroupsPath, Handler: h.list}, {Method: "POST", Pattern: SecurityGroupsPath, Handler: h.create}, + {Method: "POST", Pattern: SecurityGroupRunningSpacesPath, Handler: h.bindRunning}, + {Method: "POST", Pattern: SecurityGroupStagingSpacesPath, Handler: h.bindStaging}, } } diff --git a/api/handlers/security_group_test.go b/api/handlers/security_group_test.go index 7bfe31960..97dcabee7 100644 --- a/api/handlers/security_group_test.go +++ b/api/handlers/security_group_test.go @@ -49,6 +49,48 @@ var _ = Describe("SecurityGroup", func() { routerBuilder.Build().ServeHTTP(rr, req) }) + Describe("GET /v3/security_groups/{guid}", func() { + BeforeEach(func() { + requestMethod = http.MethodGet + requestPath = "/v3/security_groups/test-guid" + requestBody = "" + + securityGroupRepo.GetSecurityGroupReturns(repositories.SecurityGroupRecord{ + GUID: "test-guid", + Name: "test-security-group", + }, nil) + }) + + It("returns the security group", func() { + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + Expect(rr).To(HaveHTTPBody(SatisfyAll( + MatchJSONPath("$.guid", "test-guid"), + MatchJSONPath("$.name", "test-security-group"), + ))) + }) + + When("the security group is not found", func() { + BeforeEach(func() { + securityGroupRepo.GetSecurityGroupReturns(repositories.SecurityGroupRecord{}, apierrors.NewNotFoundError(nil, "SecurityGroup")) + }) + + It("returns a not found error", func() { + expectNotFoundError("SecurityGroup") + }) + }) + + When("the repository returns an error", func() { + BeforeEach(func() { + securityGroupRepo.GetSecurityGroupReturns(repositories.SecurityGroupRecord{}, errors.New("boom")) + }) + + It("returns an unknown error", func() { + expectUnknownError() + }) + }) + }) + Describe("POST /v3/security_groups", func() { var payload payloads.SecurityGroupCreate @@ -185,4 +227,250 @@ var _ = Describe("SecurityGroup", func() { }) }) }) + + Describe("POST /v3/security_groups/{guid}/relationships/running_spaces", func() { + var payload payloads.SecurityGroupBind + + BeforeEach(func() { + requestMethod = http.MethodPost + requestPath = "/v3/security_groups/test-guid/relationships/running_spaces" + requestBody = "the-json-body" + + payload = payloads.SecurityGroupBind{ + Data: []payloads.RelationshipData{{GUID: "space1"}}, + } + requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) + + securityGroupRepo.GetSecurityGroupReturns(repositories.SecurityGroupRecord{ + GUID: "test-guid", + Name: "test-security-group", + Rules: []repositories.SecurityGroupRule{ + { + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }, + }, + }, nil) + + spaceRepo.ListSpacesReturns(repositories.ListResult[repositories.SpaceRecord]{ + Records: []repositories.SpaceRecord{ + {GUID: "space1"}, + }, + PageInfo: descriptors.PageInfo{TotalResults: 1, TotalPages: 1}, + }, nil) + + securityGroupRepo.BindSecurityGroupReturns(repositories.SecurityGroupRecord{ + GUID: "test-guid", + Name: "test-security-group", + Rules: []repositories.SecurityGroupRule{ + { + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }, + }, + RunningSpaces: []string{"space1"}, + }, nil) + }) + + It("validates the request", func() { + Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1)) + actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0) + Expect(bodyString(actualReq)).To(Equal("the-json-body")) + }) + + It("binds running spaces to the security group", func() { + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + + Expect(securityGroupRepo.GetSecurityGroupCallCount()).To(Equal(1)) + _, actualAuthInfo, guid := securityGroupRepo.GetSecurityGroupArgsForCall(0) + + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(guid).To(Equal("test-guid")) + + Expect(spaceRepo.ListSpacesCallCount()).To(Equal(1)) + _, _, listMessage := spaceRepo.ListSpacesArgsForCall(0) + Expect(listMessage.GUIDs).To(ConsistOf("space1")) + + Expect(securityGroupRepo.BindSecurityGroupCallCount()).To(Equal(1)) + _, _, bindMessage := securityGroupRepo.BindSecurityGroupArgsForCall(0) + Expect(bindMessage.GUID).To(Equal("test-guid")) + Expect(bindMessage.Spaces).To(ConsistOf("space1")) + }) + + When("the payload is invalid", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("validation-error")) + }) + + It("returns a validation error", func() { + expectUnknownError() + }) + }) + + When("the security group does not exist", func() { + BeforeEach(func() { + securityGroupRepo.GetSecurityGroupReturns(repositories.SecurityGroupRecord{}, apierrors.NewNotFoundError(nil, "SecurityGroup")) + }) + + It("returns a 404 not found error", func() { + expectNotFoundError("SecurityGroup") + }) + }) + + When("the spaces does not exist", func() { + BeforeEach(func() { + spaceRepo.ListSpacesReturns(repositories.ListResult[repositories.SpaceRecord]{}, nil) + }) + + It("returns an error", func() { + expectUnprocessableEntityError("Space does not exist, or you do not have access.") + }) + }) + + When("fetching the spaces returns an error", func() { + BeforeEach(func() { + spaceRepo.ListSpacesReturns(repositories.ListResult[repositories.SpaceRecord]{}, errors.New("boom!")) + }) + + It("returns an error", func() { + expectUnknownError() + }) + }) + + When("the repository returns an error", func() { + BeforeEach(func() { + securityGroupRepo.BindSecurityGroupReturns(repositories.SecurityGroupRecord{}, errors.New("boom")) + }) + + It("returns an unknown error", func() { + expectUnknownError() + }) + }) + }) + + Describe("POST /v3/security_groups/{guid}/relationships/staging_spaces", func() { + var payload payloads.SecurityGroupBind + + BeforeEach(func() { + requestMethod = http.MethodPost + requestPath = "/v3/security_groups/test-guid/relationships/staging_spaces" + requestBody = "the-json-body" + + payload = payloads.SecurityGroupBind{ + Data: []payloads.RelationshipData{{GUID: "space1"}}, + } + requestValidator.DecodeAndValidateJSONPayloadStub = decodeAndValidatePayloadStub(&payload) + + securityGroupRepo.GetSecurityGroupReturns(repositories.SecurityGroupRecord{ + GUID: "test-guid", + Name: "test-security-group", + Rules: []repositories.SecurityGroupRule{ + { + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }, + }, + }, nil) + + spaceRepo.ListSpacesReturns(repositories.ListResult[repositories.SpaceRecord]{ + Records: []repositories.SpaceRecord{ + {GUID: "space1"}, + }, + PageInfo: descriptors.PageInfo{TotalResults: 1, TotalPages: 1}, + }, nil) + + securityGroupRepo.BindSecurityGroupReturns(repositories.SecurityGroupRecord{ + GUID: "test-guid", + Name: "test-security-group", + Rules: []repositories.SecurityGroupRule{ + { + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }, + }, + StagingSpaces: []string{"space1"}, + }, nil) + }) + + It("validates the request", func() { + Expect(requestValidator.DecodeAndValidateJSONPayloadCallCount()).To(Equal(1)) + actualReq, _ := requestValidator.DecodeAndValidateJSONPayloadArgsForCall(0) + Expect(bodyString(actualReq)).To(Equal("the-json-body")) + }) + + It("binds staging spaces to the security group", func() { + Expect(rr).To(HaveHTTPStatus(http.StatusOK)) + Expect(rr).To(HaveHTTPHeaderWithValue("Content-Type", "application/json")) + + Expect(securityGroupRepo.GetSecurityGroupCallCount()).To(Equal(1)) + _, actualAuthInfo, guid := securityGroupRepo.GetSecurityGroupArgsForCall(0) + + Expect(actualAuthInfo).To(Equal(authInfo)) + Expect(guid).To(Equal("test-guid")) + + Expect(spaceRepo.ListSpacesCallCount()).To(Equal(1)) + _, _, listMessage := spaceRepo.ListSpacesArgsForCall(0) + Expect(listMessage.GUIDs).To(ConsistOf("space1")) + + Expect(securityGroupRepo.BindSecurityGroupCallCount()).To(Equal(1)) + _, _, bindMessage := securityGroupRepo.BindSecurityGroupArgsForCall(0) + Expect(bindMessage.GUID).To(Equal("test-guid")) + Expect(bindMessage.Spaces).To(ConsistOf("space1")) + }) + + When("the payload is invalid", func() { + BeforeEach(func() { + requestValidator.DecodeAndValidateJSONPayloadReturns(errors.New("validation-error")) + }) + + It("returns a validation error", func() { + expectUnknownError() + }) + }) + + When("the security group does not exist", func() { + BeforeEach(func() { + securityGroupRepo.GetSecurityGroupReturns(repositories.SecurityGroupRecord{}, apierrors.NewNotFoundError(nil, "SecurityGroup")) + }) + + It("returns a 404 not found error", func() { + expectNotFoundError("SecurityGroup") + }) + }) + + When("the spaces does not exist", func() { + BeforeEach(func() { + spaceRepo.ListSpacesReturns(repositories.ListResult[repositories.SpaceRecord]{}, nil) + }) + + It("returns an error", func() { + expectUnprocessableEntityError("Space does not exist, or you do not have access.") + }) + }) + + When("fetching the spaces returns an error", func() { + BeforeEach(func() { + spaceRepo.ListSpacesReturns(repositories.ListResult[repositories.SpaceRecord]{}, errors.New("boom!")) + }) + + It("returns an error", func() { + expectUnknownError() + }) + }) + + When("the repository returns an error", func() { + BeforeEach(func() { + securityGroupRepo.BindSecurityGroupReturns(repositories.SecurityGroupRecord{}, errors.New("boom")) + }) + + It("returns an unknown error", func() { + expectUnknownError() + }) + }) + }) }) diff --git a/api/payloads/security_group.go b/api/payloads/security_group.go index 2882dae1f..c87f2b891 100644 --- a/api/payloads/security_group.go +++ b/api/payloads/security_group.go @@ -1,8 +1,11 @@ package payloads import ( + "net/url" "slices" + "code.cloudfoundry.org/korifi/api/payloads/parse" + "code.cloudfoundry.org/korifi/api/payloads/validation" "code.cloudfoundry.org/korifi/api/repositories" "github.com/BooleanCat/go-functional/v2/it" jellidation "github.com/jellydator/validation" @@ -81,3 +84,66 @@ func (c SecurityGroupCreate) ToMessage() repositories.CreateSecurityGroupMessage Spaces: spaces, } } + +type SecurityGroupList struct { + Names string + GUIDs string + RunningSpaceGUIDs string + StagingSpaceGUIDs string + GloballyEnabledRunning bool + GloballyEnabledStaging bool + OrderBy string +} + +func (a SecurityGroupList) Validate() error { + return jellidation.ValidateStruct(&a, + jellidation.Field(&a.OrderBy, validation.OneOfOrderBy("created_at", "updated_at")), + ) +} + +func (a *SecurityGroupList) ToMessage() repositories.ListSecurityGroupMessage { + return repositories.ListSecurityGroupMessage{ + Names: parse.ArrayParam(a.Names), + GUIDs: parse.ArrayParam(a.GUIDs), + RunningSpaceGUIDs: parse.ArrayParam(a.RunningSpaceGUIDs), + StagingSpaceGUIDs: parse.ArrayParam(a.StagingSpaceGUIDs), + GloballyEnabledRunning: a.GloballyEnabledRunning, + GloballyEnabledStaging: a.GloballyEnabledStaging, + OrderBy: a.OrderBy, + } +} + +func (a *SecurityGroupList) SupportedKeys() []string { + return []string{"names", "guids", "globally_enabled_running", "globally_enabled_staging", "running_space_guids", "staging_space_guids", "order_by", "per_page", "page"} +} + +func (a *SecurityGroupList) DecodeFromURLValues(values url.Values) error { + a.Names = values.Get("names") + a.GUIDs = values.Get("guids") + a.OrderBy = values.Get("order_by") + a.RunningSpaceGUIDs = values.Get("running_space_guids") + a.StagingSpaceGUIDs = values.Get("staging_space_guids") + a.GloballyEnabledRunning = values.Get("globally_enabled_running") == "true" + a.GloballyEnabledStaging = values.Get("globally_enabled_staging") == "true" + return nil +} + +type SecurityGroupBind struct { + Data []RelationshipData `json:"data"` +} + +func (b SecurityGroupBind) Validate() error { + return jellidation.ValidateStruct(&b, + jellidation.Field(&b.Data, jellidation.Required), + ) +} + +func (b SecurityGroupBind) ToMessage(workload, guid string) repositories.BindSecurityGroupMessage { + return repositories.BindSecurityGroupMessage{ + GUID: guid, + Spaces: slices.Collect(it.Map(slices.Values(b.Data), func(v RelationshipData) string { + return v.GUID + })), + Workload: workload, + } +} diff --git a/api/payloads/security_group_test.go b/api/payloads/security_group_test.go index ed4a141c2..f3b0355ce 100644 --- a/api/payloads/security_group_test.go +++ b/api/payloads/security_group_test.go @@ -92,3 +92,63 @@ var _ = Describe("SecurityGroupCreate", func() { }) }) }) + +var _ = Describe("SecurityGroupBind", func() { + var ( + bindPayload payloads.SecurityGroupBind + securityGroupBind *payloads.SecurityGroupBind + validatorErr error + ) + + BeforeEach(func() { + securityGroupBind = new(payloads.SecurityGroupBind) + }) + + Describe("Validation", func() { + BeforeEach(func() { + bindPayload = payloads.SecurityGroupBind{ + Data: []payloads.RelationshipData{{GUID: "space1"}, {GUID: "space2"}}, + } + }) + + JustBeforeEach(func() { + validatorErr = validator.DecodeAndValidateJSONPayload(createJSONRequest(bindPayload), securityGroupBind) + }) + + It("succeeds with valid payload", func() { + Expect(validatorErr).NotTo(HaveOccurred()) + Expect(securityGroupBind).To(PointTo(Equal(bindPayload))) + }) + + When("Data is empty", func() { + BeforeEach(func() { + bindPayload.Data = []payloads.RelationshipData{} + }) + It("returns an error", func() { + expectUnprocessableEntityError(validatorErr, "data cannot be blank") + }) + }) + + When("Converting to repo message", func() { + var message repositories.BindSecurityGroupMessage + + BeforeEach(func() { + bindPayload = payloads.SecurityGroupBind{ + Data: []payloads.RelationshipData{{GUID: "space1"}, {GUID: "space2"}}, + } + }) + + JustBeforeEach(func() { + message = bindPayload.ToMessage(repositories.SecurityGroupRunningSpaceType, "sg-guid") + }) + + It("converts the message correctly", func() { + Expect(message).To(Equal(repositories.BindSecurityGroupMessage{ + GUID: "sg-guid", + Spaces: []string{"space1", "space2"}, + Workload: repositories.SecurityGroupRunningSpaceType, + })) + }) + }) + }) +}) diff --git a/api/presenter/security_group.go b/api/presenter/security_group.go index 2c1349df5..b6d6a92e8 100644 --- a/api/presenter/security_group.go +++ b/api/presenter/security_group.go @@ -24,6 +24,11 @@ type SecurityGroupResponse struct { Links SecurityGroupLinks `json:"links"` } +type SecurityGroupSpacesResponse struct { + Data []payloads.RelationshipData `json:"data"` + Links SecurityGroupLinks `json:"links"` +} + type SecurityGroupLinks struct { Self Link `json:"self"` } @@ -52,6 +57,32 @@ func ForSecurityGroup(securityGroupRecord repositories.SecurityGroupRecord, base } } +func ForSecurityGroupRunningSpaces(securityGroupRecord repositories.SecurityGroupRecord, baseURL url.URL) SecurityGroupSpacesResponse { + return SecurityGroupSpacesResponse{ + Data: slices.Collect(it.Map(slices.Values(securityGroupRecord.RunningSpaces), func(v string) payloads.RelationshipData { + return payloads.RelationshipData{GUID: v} + })), + Links: SecurityGroupLinks{ + Self: Link{ + HRef: buildURL(baseURL).appendPath(securityGroupBase, securityGroupRecord.GUID, "relationships", "running_spaces").build(), + }, + }, + } +} + +func ForSecurityGroupStagingSpaces(securityGroupRecord repositories.SecurityGroupRecord, baseURL url.URL) SecurityGroupSpacesResponse { + return SecurityGroupSpacesResponse{ + Data: slices.Collect(it.Map(slices.Values(securityGroupRecord.StagingSpaces), func(v string) payloads.RelationshipData { + return payloads.RelationshipData{GUID: v} + })), + Links: SecurityGroupLinks{ + Self: Link{ + HRef: buildURL(baseURL).appendPath(securityGroupBase, securityGroupRecord.GUID, "relationships", "staging_spaces").build(), + }, + }, + } +} + func toManyRelationshipData(guids []string) []payloads.RelationshipData { if len(guids) == 0 { return []payloads.RelationshipData{} diff --git a/api/presenter/security_group_test.go b/api/presenter/security_group_test.go index e0409a806..cce92e617 100644 --- a/api/presenter/security_group_test.go +++ b/api/presenter/security_group_test.go @@ -45,61 +45,114 @@ var _ = Describe("SecurityGroup", func() { } }) - JustBeforeEach(func() { - response := presenter.ForSecurityGroup(record, *baseURL) - var err error - output, err = json.Marshal(response) - Expect(err).NotTo(HaveOccurred()) + Describe("ForSecurityGroup", func() { + JustBeforeEach(func() { + response := presenter.ForSecurityGroup(record, *baseURL) + var err error + output, err = json.Marshal(response) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the expected JSON", func() { + Expect(output).To(MatchJSON(`{ + "guid": "security-group-guid", + "created_at": "1970-01-01T00:00:01Z", + "updated_at": "1970-01-01T00:00:02Z", + "name": "my-security-group", + "globally_enabled": { + "running": true, + "staging": false + }, + "rules": [ + { + "protocol": "tcp", + "ports": "80", + "destination": "192.168.1.1" + } + ], + "relationships": { + "running_spaces": { + "data": [ + { "guid": "space-1" }, + { "guid": "space-2" } + ] + }, + "staging_spaces": { + "data": [ + { "guid": "space-3" } + ] + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/security-group-guid" + } + } + }`)) + }) + + When("there are no rules and no spaces", func() { + BeforeEach(func() { + record.Rules = []repositories.SecurityGroupRule{} + record.RunningSpaces = []string{} + record.StagingSpaces = []string{} + }) + + It("returns empty arrays for rules and relationships", func() { + Expect(output).To(MatchJSONPath("$.relationships.running_spaces.data", BeEmpty())) + Expect(output).To(MatchJSONPath("$.relationships.staging_spaces.data", BeEmpty())) + }) + }) }) - It("returns the expected JSON", func() { - Expect(output).To(MatchJSON(`{ - "guid": "security-group-guid", - "created_at": "1970-01-01T00:00:01Z", - "updated_at": "1970-01-01T00:00:02Z", - "name": "my-security-group", - "globally_enabled": { - "running": true, - "staging": false - }, - "rules": [ - { - "protocol": "tcp", - "ports": "80", - "destination": "192.168.1.1" - } - ], - "relationships": { - "running_spaces": { - "data": [ - { "guid": "space-1" }, - { "guid": "space-2" } - ] - }, - "staging_spaces": { - "data": [ - { "guid": "space-3" } - ] - } - }, - "links": { - "self": { - "href": "https://api.example.org/v3/security_groups/security-group-guid" - } - } - }`)) + Describe("ForSecurityGroupStagingSpaces", func() { + JustBeforeEach(func() { + response := presenter.ForSecurityGroupRunningSpaces(record, *baseURL) + var err error + output, err = json.Marshal(response) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns the expected JSON", func() { + Expect(output).To(MatchJSON(`{ + "data": [ + { + "guid": "space-1" + }, + { + "guid": "space-2" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/security-group-guid/relationships/running_spaces" + } + } + }`)) + }) }) - When("there are no rules and no spaces", func() { - BeforeEach(func() { - record.Rules = []repositories.SecurityGroupRule{} - record.RunningSpaces = []string{} - record.StagingSpaces = []string{} + Describe("ForSecurityGroupStagingSpaces", func() { + JustBeforeEach(func() { + response := presenter.ForSecurityGroupStagingSpaces(record, *baseURL) + var err error + output, err = json.Marshal(response) + Expect(err).NotTo(HaveOccurred()) }) - It("returns empty arrays for rules and relationships", func() { - Expect(output).To(MatchJSONPath("$.relationships.running_spaces.data", BeEmpty())) - Expect(output).To(MatchJSONPath("$.relationships.staging_spaces.data", BeEmpty())) + It("returns the expected JSON", func() { + Expect(output).To(MatchJSON(`{ + "data": [ + { + "guid": "space-3" + } + ], + "links": { + "self": { + "href": "https://api.example.org/v3/security_groups/security-group-guid/relationships/staging_spaces" + } + } + }`)) }) }) }) diff --git a/api/repositories/security_group_repository.go b/api/repositories/security_group_repository.go index dfb598b7b..482d6ec26 100644 --- a/api/repositories/security_group_repository.go +++ b/api/repositories/security_group_repository.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "fmt" "slices" "time" @@ -10,11 +11,16 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "github.com/BooleanCat/go-functional/v2/it" + "github.com/BooleanCat/go-functional/v2/it/itx" "github.com/google/uuid" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const SecurityGroupResourceType = "Security Group" +const ( + SecurityGroupResourceType = "Security Group" + SecurityGroupRunningSpaceType = "running" + SecurityGroupStagingSpaceType = "staging" +) type SecurityGroupRule struct { Protocol string `json:"protocol"` @@ -53,6 +59,58 @@ type CreateSecurityGroupMessage struct { GloballyEnabled SecurityGroupWorkloads } +type ListSecurityGroupMessage struct { + GUIDs []string + Names []string + GloballyEnabledRunning bool + GloballyEnabledStaging bool + RunningSpaceGUIDs []string + StagingSpaceGUIDs []string + OrderBy string + Pagination Pagination + Namespace string +} + +func (m *ListSecurityGroupMessage) toListOptions() []ListOption { + return []ListOption{ + WithLabelIn(korifiv1alpha1.GUIDLabelKey, m.GUIDs), + WithLabelIn(korifiv1alpha1.CFSecurityGroupDisplayNameLabel, m.Names), + WithLabelIn(korifiv1alpha1.CFSecurityGroupRunningSpaceGUIDsLabel, m.RunningSpaceGUIDs), + WithLabelIn(korifiv1alpha1.CFSecurityGroupStagingSpaceGUIDsLabel, m.StagingSpaceGUIDs), + WithLabel(korifiv1alpha1.CFSecurityGroupGloballyEnabledRunningLabel, fmt.Sprintf("%t", m.GloballyEnabledRunning)), + WithLabel(korifiv1alpha1.CFSecurityGroupGloballyEnabledStagingLabel, fmt.Sprintf("%t", m.GloballyEnabledStaging)), + InNamespace(m.Namespace), + WithOrdering(m.OrderBy, + "name", "Display Name", + ), + WithPaging(m.Pagination), + } +} + +type BindSecurityGroupMessage struct { + GUID string + Spaces []string + Workload string +} + +func (m BindSecurityGroupMessage) apply(cfSecurityGroup *korifiv1alpha1.CFSecurityGroup) { + if cfSecurityGroup.Spec.Spaces == nil { + cfSecurityGroup.Spec.Spaces = make(map[string]korifiv1alpha1.SecurityGroupWorkloads) + } + + for _, space := range m.Spaces { + workloads := cfSecurityGroup.Spec.Spaces[space] + + if m.Workload == SecurityGroupRunningSpaceType { + workloads.Running = true + } else { + workloads.Staging = true + } + + cfSecurityGroup.Spec.Spaces[space] = workloads + } +} + type SecurityGroupRecord struct { GUID string CreatedAt time.Time @@ -65,11 +123,58 @@ type SecurityGroupRecord struct { StagingSpaces []string } +func (r *SecurityGroupRepo) GetSecurityGroup(ctx context.Context, authInfo authorization.Info, GUID string) (SecurityGroupRecord, error) { + cfSecurityGroup := &korifiv1alpha1.CFSecurityGroup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.rootNamespace, + Name: GUID, + }, + } + + if err := r.klient.Get(ctx, cfSecurityGroup); err != nil { + return SecurityGroupRecord{}, fmt.Errorf("failed to get security group: %w", apierrors.FromK8sError(err, SecurityGroupResourceType)) + } + + return toSecurityGroupRecord(*cfSecurityGroup), nil +} + +func (r *SecurityGroupRepo) ListSecurityGroups(ctx context.Context, authInfo authorization.Info, message ListSecurityGroupMessage) (ListResult[SecurityGroupRecord], error) { + cfSecurityGroupList := &korifiv1alpha1.CFSecurityGroupList{} + message.Namespace = r.rootNamespace + pageInfo, err := r.klient.List(ctx, cfSecurityGroupList, message.toListOptions()...) + if err != nil { + return ListResult[SecurityGroupRecord]{}, fmt.Errorf("failed to list security-groups: %w", apierrors.FromK8sError(err, SecurityGroupResourceType)) + } + fmt.Println("Security Group List:", cfSecurityGroupList) + + //sgRecords := itx.FromSlice(cfSecurityGroupList.Items) + //return slices.Collect(it.Map(sgRecords, toSecurityGroupRecord)), nil + + records, err := it.TryCollect(it.MapError(itx.FromSlice(cfSecurityGroupList.Items), func(item korifiv1alpha1.CFSecurityGroup) (SecurityGroupRecord, error) { + return toSecurityGroupRecord(item), nil + })) + if err != nil { + return ListResult[SecurityGroupRecord]{}, err + } + + return ListResult[SecurityGroupRecord]{ + PageInfo: pageInfo, + Records: records, + }, nil +} + func (r *SecurityGroupRepo) CreateSecurityGroup(ctx context.Context, authInfo authorization.Info, message CreateSecurityGroupMessage) (SecurityGroupRecord, error) { + UUID := uuid.NewString() cfSecurityGroup := &korifiv1alpha1.CFSecurityGroup{ ObjectMeta: metav1.ObjectMeta{ Namespace: r.rootNamespace, - Name: uuid.NewString(), + Name: UUID, + Labels: map[string]string{ + korifiv1alpha1.GUIDLabelKey: UUID, + korifiv1alpha1.CFSecurityGroupDisplayNameLabel: message.DisplayName, + korifiv1alpha1.CFSecurityGroupGloballyEnabledRunningLabel: fmt.Sprintf("%t", message.GloballyEnabled.Running), + korifiv1alpha1.CFSecurityGroupGloballyEnabledStagingLabel: fmt.Sprintf("%t", message.GloballyEnabled.Staging), + }, }, Spec: korifiv1alpha1.CFSecurityGroupSpec{ DisplayName: message.DisplayName, @@ -114,6 +219,28 @@ func (r *SecurityGroupRepo) CreateSecurityGroup(ctx context.Context, authInfo au return toSecurityGroupRecord(*cfSecurityGroup), nil } +func (r *SecurityGroupRepo) BindSecurityGroup(ctx context.Context, authInfo authorization.Info, message BindSecurityGroupMessage) (SecurityGroupRecord, error) { + cfSecurityGroup := &korifiv1alpha1.CFSecurityGroup{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.rootNamespace, + Name: message.GUID, + }, + } + + if err := r.klient.Get(ctx, cfSecurityGroup); err != nil { + return SecurityGroupRecord{}, apierrors.FromK8sError(err, SecurityGroupResourceType) + } + + if err := r.klient.Patch(ctx, cfSecurityGroup, func() error { + message.apply(cfSecurityGroup) + return nil + }); err != nil { + return SecurityGroupRecord{}, apierrors.FromK8sError(err, SecurityGroupResourceType) + } + + return toSecurityGroupRecord(*cfSecurityGroup), nil +} + func toSecurityGroupRecord(cfSecurityGroup korifiv1alpha1.CFSecurityGroup) SecurityGroupRecord { runningSpaces := []string{} stagingSpaces := []string{} diff --git a/api/repositories/security_group_repository_test.go b/api/repositories/security_group_repository_test.go index ad1f96851..f8f811ada 100644 --- a/api/repositories/security_group_repository_test.go +++ b/api/repositories/security_group_repository_test.go @@ -1,10 +1,13 @@ package repositories_test import ( + "errors" + apierrors "code.cloudfoundry.org/korifi/api/errors" "code.cloudfoundry.org/korifi/api/repositories" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/tests/matchers" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,6 +27,81 @@ var _ = Describe("SecurityGroupRepo", func() { space = createSpaceWithCleanup(ctx, org.Name, prefixedGUID("space")) }) + Describe("GetSecurityGroup", func() { + var ( + securityGroup *korifiv1alpha1.CFSecurityGroup + securityGroupRecord repositories.SecurityGroupRecord + securityGroupGUID string + getErr error + ) + + BeforeEach(func() { + securityGroupGUID = uuid.NewString() + + securityGroup = &korifiv1alpha1.CFSecurityGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: securityGroupGUID, + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFSecurityGroupSpec{ + DisplayName: "test-security-group", + Rules: []korifiv1alpha1.SecurityGroupRule{ + { + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }, + }, + Spaces: map[string]korifiv1alpha1.SecurityGroupWorkloads{ + space.Name: {Running: true, Staging: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, securityGroup)).To(Succeed()) + }) + + JustBeforeEach(func() { + securityGroupRecord, getErr = repo.GetSecurityGroup(ctx, authInfo, securityGroupGUID) + }) + + It("errors with forbidden for users with no permissions", func() { + Expect(getErr).To(matchers.WrapErrorAssignableToTypeOf(apierrors.ForbiddenError{})) + }) + + When("the user is a CF admin", func() { + BeforeEach(func() { + createRoleBinding(ctx, userName, adminRole.Name, rootNamespace) + }) + + It("returns a security group record", func() { + Expect(getErr).ToNot(HaveOccurred()) + + Expect(securityGroupRecord.GUID).To(Equal(securityGroupGUID)) + Expect(securityGroupRecord.Name).To(Equal("test-security-group")) + Expect(securityGroupRecord.GloballyEnabled.Running).To(BeFalse()) + Expect(securityGroupRecord.GloballyEnabled.Staging).To(BeFalse()) + Expect(securityGroupRecord.RunningSpaces).To(ConsistOf(space.Name)) + Expect(securityGroupRecord.StagingSpaces).To(ConsistOf(space.Name)) + Expect(securityGroupRecord.Rules).To(ConsistOf(repositories.SecurityGroupRule{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + })) + }) + + When("the security group does not exist", func() { + BeforeEach(func() { + securityGroupGUID = "does-not-exist" + }) + + It("returns a not found error", func() { + notFoundErr := apierrors.NotFoundError{} + Expect(errors.As(getErr, ¬FoundErr)).To(BeTrue()) + }) + }) + }) + }) + Describe("CreateSecurityGroup", func() { var ( securityGroupRecord repositories.SecurityGroupRecord @@ -101,4 +179,86 @@ var _ = Describe("SecurityGroupRepo", func() { }) }) }) + + Describe("BindSecurityGroup", func() { + var ( + securityGroup *korifiv1alpha1.CFSecurityGroup + securityGroupRecord repositories.SecurityGroupRecord + bindMessage repositories.BindSecurityGroupMessage + bindErr error + ) + + BeforeEach(func() { + securityGroup = &korifiv1alpha1.CFSecurityGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFSecurityGroupSpec{ + DisplayName: "test-security-group", + Rules: []korifiv1alpha1.SecurityGroupRule{ + { + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }, + }, + }, + } + Expect(k8sClient.Create(ctx, securityGroup)).To(Succeed()) + + bindMessage = repositories.BindSecurityGroupMessage{ + GUID: securityGroup.Name, + Spaces: []string{space.Name}, + Workload: repositories.SecurityGroupRunningSpaceType, + } + }) + + JustBeforeEach(func() { + securityGroupRecord, bindErr = repo.BindSecurityGroup(ctx, authInfo, bindMessage) + }) + + It("errors with forbidden for users with no permissions", func() { + Expect(bindErr).To(matchers.WrapErrorAssignableToTypeOf(apierrors.ForbiddenError{})) + }) + + When("the user is a CF admin", func() { + BeforeEach(func() { + createRoleBinding(ctx, userName, adminRole.Name, rootNamespace) + }) + + It("binds the space to the cfSecurityGroup", func() { + Expect(bindErr).ToNot(HaveOccurred()) + + cfSecurityGroup := &korifiv1alpha1.CFSecurityGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: securityGroup.Name, + Namespace: rootNamespace, + }, + } + + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cfSecurityGroup), cfSecurityGroup)).To(Succeed()) + Expect(cfSecurityGroup.Spec.Spaces).To(Equal(map[string]korifiv1alpha1.SecurityGroupWorkloads{ + space.Name: {Running: true, Staging: false}, + })) + }) + + It("returns the correct record", func() { + Expect(bindErr).ToNot(HaveOccurred()) + Expect(securityGroupRecord.RunningSpaces).To(ConsistOf([]string{space.Name})) + Expect(securityGroupRecord.StagingSpaces).To(BeEmpty()) + }) + + When("the security group does not exist", func() { + BeforeEach(func() { + bindMessage.GUID = "does-not-ecist" + }) + + It("returns a not found error", func() { + notFoundErr := apierrors.NotFoundError{} + Expect(errors.As(bindErr, ¬FoundErr)).To(BeTrue()) + }) + }) + }) + }) }) diff --git a/controllers/api/v1alpha1/cfsecuritygroup_types.go b/controllers/api/v1alpha1/cfsecuritygroup_types.go index 13d8e2873..343239b55 100644 --- a/controllers/api/v1alpha1/cfsecuritygroup_types.go +++ b/controllers/api/v1alpha1/cfsecuritygroup_types.go @@ -8,9 +8,21 @@ import ( ) const ( - ProtocolTCP = "tcp" - ProtocolUDP = "udp" - ProtocolALL = "all" + ProtocolTCP = "tcp" + ProtocolUDP = "udp" + ProtocolALL = "all" + CFWorkloadTypeApp = "app" + CFWorkloadTypeBuild = "build" + CFSecurityGroupTypeGlobal = "global" + CFSecurityGroupTypeSpaceScoped = "space-scoped" + CFSecurityGroupTypeLabel = "korifi.cloudfoundry.org/security-group-type" + CFSecurityGroupDisplayNameLabel = "korifi.cloudfoundry.org/security-group-display-name" + CFSecurityGroupRunningSpaceGUIDsLabel = "korifi.cloudfoundry.org/security-group-running-space-guids" + CFSecurityGroupStagingSpaceGUIDsLabel = "korifi.cloudfoundry.org/security-group-staging-space-guids" + CFSecurityGroupGloballyEnabledRunningLabel = "korifi.cloudfoundry.org/security-group-globally-enabled-running" + CFSecurityGroupGloballyEnabledStagingLabel = "korifi.cloudfoundry.org/security-group-globally-enabled-staging" + CFSecurityGroupFinalizerName = "cfSecurityGroup.korifi.cloudfoundry.org" + CFWorkloadTypeLabelkey = "korifi.cloudfoundry.org/workload-type" ) type SecurityGroupRule struct { diff --git a/controllers/config/config.go b/controllers/config/config.go index 58a8ef35b..18e50355d 100644 --- a/controllers/config/config.go +++ b/controllers/config/config.go @@ -27,6 +27,7 @@ type ControllerConfig struct { Networking Networking `yaml:"networking"` ExperimentalManagedServicesEnabled bool `yaml:"experimentalManagedServicesEnabled"` + ExperimentalSecurityGroupsEnabled bool `yaml:"experimentalSecurityGroupsEnabled"` TrustInsecureServiceBrokers bool `yaml:"trustInsecureServiceBrokers"` DisableRouteController bool `yaml:"disableRouteController"` } diff --git a/controllers/controllers/networking/security_groups/controller.go b/controllers/controllers/networking/security_groups/controller.go new file mode 100644 index 000000000..94c56e1a2 --- /dev/null +++ b/controllers/controllers/networking/security_groups/controller.go @@ -0,0 +1,473 @@ +package securitygroups + +import ( + "context" + "fmt" + "net" + "strconv" + "strings" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tools" + "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/go-logr/logr" + v3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3" + "github.com/projectcalico/api/pkg/client/clientset_generated/clientset" + "github.com/projectcalico/api/pkg/lib/numorstring" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type Reconciler struct { + k8sClient client.Client + calicoClient clientset.Interface + log logr.Logger + rootNamespace string +} + +func NewReconciler( + client client.Client, + calicoClient clientset.Interface, + log logr.Logger, + rootNamespace string, +) *k8s.PatchingReconciler[korifiv1alpha1.CFSecurityGroup] { + return k8s.NewPatchingReconciler(log, client, &Reconciler{ + k8sClient: client, + calicoClient: calicoClient, + log: log, + rootNamespace: rootNamespace, + }) +} + +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) *builder.Builder { + return ctrl.NewControllerManagedBy(mgr). + For(&korifiv1alpha1.CFSecurityGroup{}). + Named("cfsecuritygroup") +} + +// +kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfsecuritygroups,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfsecuritygroups/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfsecuritygroups/finalizers,verbs=update +// +kubebuilder:rbac:groups=projectcalico.org,resources=networkpolicies;globalnetworkpolicies;tiers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=projectcalico.org,resources=tier.networkpolicies,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=projectcalico.org,resources=tier.globalnetworkpolicies,verbs=get;list;watch;create;update;patch;delete + +func (r *Reconciler) ReconcileResource(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup) (ctrl.Result, error) { + log := logr.FromContextOrDiscard(ctx) + + sg.Status.ObservedGeneration = sg.Generation + log.V(1).Info("set observed generation", "generation", sg.Status.ObservedGeneration) + + if !sg.GetDeletionTimestamp().IsZero() { + return r.finalizeCFSecurityGroup(ctx, sg) + } + + if len(sg.Spec.Spaces) > 0 { + sg.Labels = tools.SetMapValue(sg.Labels, korifiv1alpha1.CFSecurityGroupTypeLabel, korifiv1alpha1.CFSecurityGroupTypeSpaceScoped) + if err := r.reconcileNetworkPolicies(ctx, sg); err != nil { + return ctrl.Result{}, err + } + } + + if sg.Spec.GloballyEnabled.Running || sg.Spec.GloballyEnabled.Staging { + sg.Labels = tools.SetMapValue(sg.Labels, korifiv1alpha1.CFSecurityGroupTypeLabel, korifiv1alpha1.CFSecurityGroupTypeGlobal) + if err := r.reconcileGlobalNetworkPolicies(ctx, sg); err != nil { + return ctrl.Result{}, err + } + } + + if err := r.cleanOrphanedPolicies(ctx, sg); err != nil { + return ctrl.Result{}, err + } + + log.V(1).Info("CFSecurityGroup reconciled") + return ctrl.Result{}, nil +} + +func (r *Reconciler) finalizeCFSecurityGroup(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup) (ctrl.Result, error) { + logs := logr.FromContextOrDiscard(ctx).WithName("finalize-security-group") + + policies, err := r.calicoClient.ProjectcalicoV3().NetworkPolicies("").List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", fmt.Sprintf("default.%s", sg.Name)), + }) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list NetworkPolicies: %w", err) + } + + for _, policy := range policies.Items { + if err = r.k8sClient.Delete(ctx, &policy); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to delete NetworkPolicy %s: %w", policy.Name, err) + } + } + + globalPolicies, err := r.calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", fmt.Sprintf("default.%s", sg.Name)), + }) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list NetworkPolicies: %w", err) + } + + for _, globalPolicy := range globalPolicies.Items { + if err := r.k8sClient.Delete(ctx, &globalPolicy); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to delete NetworkPolicy %s: %w", globalPolicy.Name, err) + } + } + + if controllerutil.RemoveFinalizer(sg, korifiv1alpha1.CFSecurityGroupFinalizerName) { + logs.V(1).Info("finalizer removed") + } + return ctrl.Result{}, nil +} + +func (r *Reconciler) cleanOrphanedPolicies(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup) error { + policies, err := r.calicoClient.ProjectcalicoV3().NetworkPolicies("").List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", fmt.Sprintf("default.%s", sg.Name)), + LabelSelector: fmt.Sprintf("%s=%s", + korifiv1alpha1.CFSecurityGroupTypeLabel, korifiv1alpha1.CFSecurityGroupTypeSpaceScoped), + }) + if err != nil { + return fmt.Errorf("failed to list NetworkPolicies: %w", err) + } + + for _, policy := range policies.Items { + if _, exists := sg.Spec.Spaces[policy.Namespace]; !exists { + if err = r.calicoClient.ProjectcalicoV3().NetworkPolicies(policy.Namespace).Delete(ctx, policy.Name, metav1.DeleteOptions{}); err != nil { + return fmt.Errorf("failed to delete orphaned NetworkPolicy %s/%s: %w", policy.Namespace, policy.Name, err) + } + } + } + + if !sg.Spec.GloballyEnabled.Running && !sg.Spec.GloballyEnabled.Staging { + globalPolicy, err := r.calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Get(ctx, fmt.Sprintf("default.%s", sg.Name), metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return nil + } + + return fmt.Errorf("failed to get GlobalNetworkPolicy %s: %w", sg.Name, err) + } + + if err = r.calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Delete(ctx, globalPolicy.Name, metav1.DeleteOptions{}); err != nil { + return fmt.Errorf("failed to delete orphaned GlobalNetworkPolicy %s: %w", globalPolicy.Name, err) + } + } + + return nil +} + +func (r *Reconciler) reconcileNetworkPolicies(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup) error { + for space, workloads := range sg.Spec.Spaces { + if err := r.reconcileNetworkPolicyForSpace(ctx, sg, space, workloads); err != nil { + return err + } + } + return nil +} + +func (r *Reconciler) reconcileNetworkPolicyForSpace(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup, space string, workloads korifiv1alpha1.SecurityGroupWorkloads) error { + policy, err := r.calicoClient.ProjectcalicoV3().NetworkPolicies(space).Get(ctx, fmt.Sprintf("default.%s", sg.Name), metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return r.createNetworkPolicy(ctx, sg, space, workloads) + } + return fmt.Errorf("failed to get NetworkPolicy %s/%s: %w", space, sg.Name, err) + } + + err = securityGroupToNetworkPolicy(sg, workloads, policy) + if err != nil { + return err + } + + if _, err := r.calicoClient.ProjectcalicoV3().NetworkPolicies(space).Update(ctx, policy, metav1.UpdateOptions{}); err != nil { + return err + } + + return nil +} + +func (r *Reconciler) reconcileGlobalNetworkPolicies(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup) error { + policy, err := r.calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Get(ctx, fmt.Sprintf("default.%s", sg.Name), metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return r.createGlobalNetworkPolicy(ctx, sg) + } + return fmt.Errorf("failed to get NetworkPolicy %s: %w", sg.Name, err) + } + + err = r.securityGroupToGlobalNetworkPolicy(sg, policy) + if err != nil { + return err + } + + policy.Labels = tools.SetMapValue(policy.Labels, korifiv1alpha1.CFSecurityGroupTypeLabel, sg.Labels[korifiv1alpha1.CFSecurityGroupTypeLabel]) + if _, err := r.calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Update(ctx, policy, metav1.UpdateOptions{}); err != nil { + return err + } + + return nil +} + +func (r *Reconciler) createNetworkPolicy(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup, space string, workloads korifiv1alpha1.SecurityGroupWorkloads) error { + policy := &v3.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: sg.Name, + Namespace: space, + }, + } + + err := securityGroupToNetworkPolicy(sg, workloads, policy) + if err != nil { + return err + } + + policy.Labels = tools.SetMapValue(policy.Labels, korifiv1alpha1.CFSecurityGroupTypeLabel, sg.Labels[korifiv1alpha1.CFSecurityGroupTypeLabel]) + if _, err = r.calicoClient.ProjectcalicoV3().NetworkPolicies(space).Create(ctx, policy, metav1.CreateOptions{}); err != nil { + r.log.Error(err, "failed to create NetworkPolicy", "namespace", space, "name", sg.Name) + return err + } + + return nil +} + +func (r *Reconciler) createGlobalNetworkPolicy(ctx context.Context, sg *korifiv1alpha1.CFSecurityGroup) error { + policy := &v3.GlobalNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: sg.Name, + }, + } + + err := r.securityGroupToGlobalNetworkPolicy(sg, policy) + if err != nil { + return err + } + + policy.Labels = tools.SetMapValue(policy.Labels, korifiv1alpha1.CFSecurityGroupTypeLabel, sg.Labels[korifiv1alpha1.CFSecurityGroupTypeLabel]) + if _, err = r.calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Create(ctx, policy, metav1.CreateOptions{}); err != nil { + r.log.Error(err, "failed to create NetworkPolicy", "name", sg.Name) + return err + } + + return nil +} + +func securityGroupToNetworkPolicy(sg *korifiv1alpha1.CFSecurityGroup, workloads korifiv1alpha1.SecurityGroupWorkloads, policy *v3.NetworkPolicy) error { + egressRules, err := buildEgressRules(sg.Spec.Rules) + if err != nil { + return err + } + + policy.Spec.Egress = egressRules + policy.Spec.Selector = buildSelector(workloads) + policy.Spec.Types = []v3.PolicyType{v3.PolicyTypeEgress} + + return nil +} + +func (r *Reconciler) securityGroupToGlobalNetworkPolicy(sg *korifiv1alpha1.CFSecurityGroup, policy *v3.GlobalNetworkPolicy) error { + egressRules, err := buildEgressRules(sg.Spec.Rules) + if err != nil { + return err + } + + policy.Spec.Egress = egressRules + policy.Spec.Selector = buildSelector(sg.Spec.GloballyEnabled) + policy.Spec.Types = []v3.PolicyType{v3.PolicyTypeEgress} + policy.Spec.NamespaceSelector = "has(korifi.cloudfoundry.org/space-guid)" + + return nil +} + +func buildEgressRules(rules []korifiv1alpha1.SecurityGroupRule) ([]v3.Rule, error) { + var egressRules []v3.Rule + + for _, rule := range rules { + nets, err := buildRuleNets(rule) + if err != nil { + return []v3.Rule{}, err + } + + ports, err := buildRulePorts(rule) + if err != nil { + return []v3.Rule{}, err + } + + egressRules = append(egressRules, v3.Rule{ + Action: v3.Allow, + Protocol: &numorstring.Protocol{Type: 1, StrVal: getProtocol(rule.Protocol)}, + Destination: v3.EntityRule{ + Nets: nets, + Ports: ports, + }, + }) + } + + return egressRules, nil +} + +func buildRulePorts(rule korifiv1alpha1.SecurityGroupRule) ([]numorstring.Port, error) { + var ports []numorstring.Port + + if strings.Contains(rule.Ports, "-") { + port, err := parseRangePorts(rule.Ports) + if err != nil { + return nil, err + } + + return []numorstring.Port{port}, nil + } + + for _, portStr := range strings.Split(rule.Ports, ",") { + port, err := portStringToUint16(portStr) + if err != nil { + return nil, err + } + + ports = append(ports, numorstring.Port{MinPort: port, MaxPort: port}) + } + + return ports, nil +} + +func buildSelector(workloads korifiv1alpha1.SecurityGroupWorkloads) string { + var workloadTypes []string + + if workloads.Running { + workloadTypes = append(workloadTypes, korifiv1alpha1.CFWorkloadTypeApp) + } + if workloads.Staging { + workloadTypes = append(workloadTypes, korifiv1alpha1.CFWorkloadTypeBuild) + } + values := "'" + strings.Join(workloadTypes, "', '") + "'" + + return fmt.Sprintf("%s in { %s }", korifiv1alpha1.CFWorkloadTypeLabelkey, values) +} + +func parseRangePorts(ports string) (numorstring.Port, error) { + rangePorts := strings.Split(ports, "-") + if len(rangePorts) != 2 { + return numorstring.Port{}, fmt.Errorf("invalid port range format: %s", ports) + } + + start, err := portStringToUint16(rangePorts[0]) + if err != nil { + return numorstring.Port{}, err + } + + end, err := portStringToUint16(rangePorts[1]) + if err != nil { + return numorstring.Port{}, err + } + + return numorstring.Port{ + MinPort: start, + MaxPort: end, + }, nil +} + +func portStringToUint16(port string) (uint16, error) { + pStr := strings.TrimSpace(port) + if pStr == "" { + return 0, fmt.Errorf("port value cannot be empty") + } + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return 0, fmt.Errorf("invalid port %s: %w", pStr, err) + } + return uint16(p), nil +} + +func buildRuleNets(rule korifiv1alpha1.SecurityGroupRule) ([]string, error) { + if strings.Contains(rule.Destination, "-") { + var nets []string + parts := strings.Split(rule.Destination, "-") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid IP range format: %s", rule.Destination) + } + + cidrs, err := generateCIDRs(parts[0], parts[1]) + if err != nil { + return nil, err + } + nets = append(nets, cidrs...) + + return nets, nil + } + + if _, _, err := net.ParseCIDR(rule.Destination); err == nil { + return []string{rule.Destination}, nil + } + + if ip := net.ParseIP(rule.Destination); ip != nil { + return []string{fmt.Sprintf("%s/32", rule.Destination)}, nil + } + + return nil, fmt.Errorf("invalid destination: %s", rule.Destination) +} + +func generateCIDRs(startIP, endIP string) ([]string, error) { + start, err := ipToUint32(startIP) + if err != nil { + return nil, err + } + + end, err := ipToUint32(endIP) + if err != nil { + return nil, err + } + + if start > end { + return nil, fmt.Errorf("start IP %s must be less than or equal to end IP %s", startIP, endIP) + } + + var cidrs []string + for end >= start { + mask := uint32(0xFFFFFFFF) + length := 32 + + for mask > 0 { + nextMask := mask << 1 + if (start&nextMask) != start || (start|^nextMask) > end { + break + } + mask = nextMask + length-- + } + + cidrs = append(cidrs, fmt.Sprintf("%s/%d", uint32ToIP(start), length)) + + start |= ^mask + if start+1 < start { // Handle overflow + break + } + start++ + } + + return cidrs, nil +} + +func ipToUint32(ip string) (uint32, error) { + parsedIP := net.ParseIP(ip).To4() + if parsedIP == nil { + return 0, fmt.Errorf("invalid IPv4 address: %s", ip) + } + return uint32(parsedIP[0])<<24 | uint32(parsedIP[1])<<16 | uint32(parsedIP[2])<<8 | uint32(parsedIP[3]), nil +} + +func uint32ToIP(ip uint32) string { + return fmt.Sprintf("%d.%d.%d.%d", (ip>>24)&0xFF, (ip>>16)&0xFF, (ip>>8)&0xFF, ip&0xFF) +} + +func getProtocol(protocol string) string { + switch protocol { + case korifiv1alpha1.ProtocolTCP: + return numorstring.ProtocolTCP + case korifiv1alpha1.ProtocolUDP: + return numorstring.ProtocolUDP + default: + return numorstring.ProtocolTCP + } +} diff --git a/controllers/controllers/networking/security_groups/controller_test.go b/controllers/controllers/networking/security_groups/controller_test.go new file mode 100644 index 000000000..7f7610807 --- /dev/null +++ b/controllers/controllers/networking/security_groups/controller_test.go @@ -0,0 +1,417 @@ +package securitygroups_test + +import ( + "fmt" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/tools" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "code.cloudfoundry.org/korifi/tests/matchers" + v3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3" + "github.com/projectcalico/api/pkg/lib/numorstring" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("CFSecurityGroupReconciler Integration Tests", func() { + var cfSecurityGroup *korifiv1alpha1.CFSecurityGroup + + BeforeEach(func() { + cfSecurityGroup = &korifiv1alpha1.CFSecurityGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: rootNamespace, + }, + Spec: korifiv1alpha1.CFSecurityGroupSpec{ + DisplayName: "test-security-group", + Rules: []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }}, + }, + } + }) + + JustBeforeEach(func() { + Expect(adminClient.Create(ctx, cfSecurityGroup)).To(Succeed()) + }) + + It("sets the observed generation in the cfsecuritygroup status", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfSecurityGroup), cfSecurityGroup)).To(Succeed()) + g.Expect(cfSecurityGroup.Status.ObservedGeneration).To(BeEquivalentTo(cfSecurityGroup.Generation)) + }).Should(Succeed()) + }) + + It("sets the Ready condition to True", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(cfSecurityGroup), cfSecurityGroup)).To(Succeed()) + g.Expect(cfSecurityGroup.Status.Conditions).To(ContainElement(SatisfyAll( + HasType(Equal(korifiv1alpha1.StatusConditionReady)), + HasStatus(Equal(metav1.ConditionTrue)), + ))) + }).Should(Succeed()) + }) + + It("does not create a policy when there are no spaces appended", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(policy).To(BeNil()) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + }) + + It("does not create a global policy when the security group is not global", func() { + Eventually(func(g Gomega) { + globalPolicy, err := calicoFakeClient.ProjectcalicoV3().GlobalNetworkPolicies().Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(globalPolicy).To(BeNil()) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + }) + + When("we append a space to the security group", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Spaces = map[string]korifiv1alpha1.SecurityGroupWorkloads{testNamespace: {Running: true, Staging: false}} + }) + + It("creates a network policy", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Action).To(Equal(v3.Action("Allow"))) + g.Expect(policy.Spec.Types).To(Equal([]v3.PolicyType{"Egress"})) + }).Should(Succeed()) + }) + }) + + When("the security group is global", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.GloballyEnabled.Running = true + }) + + It("creates a global network policy", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().GlobalNetworkPolicies().Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Action).To(Equal(v3.Action("Allow"))) + g.Expect(policy.Spec.Types).To(Equal([]v3.PolicyType{"Egress"})) + g.Expect(policy.Spec.Selector).To(Equal("korifi.cloudfoundry.org/workload-type in { 'app' }")) + g.Expect(policy.Spec.NamespaceSelector).To(Equal("has(korifi.cloudfoundry.org/space-guid)")) + }).Should(Succeed()) + }) + }) + + Describe("correctly building the network policies", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Spaces = map[string]korifiv1alpha1.SecurityGroupWorkloads{testNamespace: {Running: true, Staging: false}} + }) + + When("the space is running", func() { + It("creates the proper selector", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Selector).To(Equal("korifi.cloudfoundry.org/workload-type in { 'app' }")) + }).Should(Succeed()) + }) + }) + + When("the space is staging", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Spaces = map[string]korifiv1alpha1.SecurityGroupWorkloads{testNamespace: {Running: false, Staging: true}} + }) + + It("creates the proper selector", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Selector).To(Equal("korifi.cloudfoundry.org/workload-type in { 'build' }")) + }).Should(Succeed()) + }) + }) + + When("the space is both staging and running", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Spaces = map[string]korifiv1alpha1.SecurityGroupWorkloads{testNamespace: {Running: true, Staging: true}} + }) + + It("creates the proper selector", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Selector).To(Equal("korifi.cloudfoundry.org/workload-type in { 'app', 'build' }")) + }).Should(Succeed()) + }) + }) + + When("the destination is an IP range", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Rules = []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1-192.168.1.255", + }} + }) + + It("creates multiple cidrs to cover the desired IP range", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Destination.Nets).To(ConsistOf( + "192.168.1.1/32", + "192.168.1.2/31", + "192.168.1.4/30", + "192.168.1.8/29", + "192.168.1.16/28", + "192.168.1.32/27", + "192.168.1.64/26", + "192.168.1.128/25", + )) + }).Should(Succeed()) + }) + }) + + When("the destination is a single ip", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Rules = []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1", + }} + }) + + It("creates only one cidr", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Destination.Nets).To(ConsistOf("192.168.1.1/32")) + }).Should(Succeed()) + }) + }) + + When("the destination is a cidr", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Rules = []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1/16", + }} + }) + + It("creates the correct cidr", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Destination.Nets).To(ConsistOf("192.168.1.1/16")) + }).Should(Succeed()) + }) + }) + + When("the rule has a single port", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Rules = []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80", + Destination: "192.168.1.1/16", + }} + }) + + It("creates the rule with the proper port", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Destination.Ports).To(Equal([]numorstring.Port{{ + MinPort: 80, + MaxPort: 80, + }})) + }).Should(Succeed()) + }) + }) + + When("the rule has a range of ports", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Rules = []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80-90", + Destination: "192.168.1.1/16", + }} + }) + + It("creates the rule with the correct port range", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Destination.Ports).To(Equal([]numorstring.Port{{ + MinPort: 80, + MaxPort: 90, + }})) + }).Should(Succeed()) + }) + }) + + When("the rule has a list of ports", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Rules = []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolTCP, + Ports: "80,90,100", + Destination: "192.168.1.1/16", + }} + }) + + It("creates the rule with the correct ports", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Destination.Ports).To(Equal([]numorstring.Port{ + { + MinPort: 80, + MaxPort: 80, + }, + { + MinPort: 90, + MaxPort: 90, + }, + { + MinPort: 100, + MaxPort: 100, + }, + })) + }).Should(Succeed()) + }) + }) + + When("we pass UDP as a protocol", func() { + BeforeEach(func() { + cfSecurityGroup.Spec.Rules = []korifiv1alpha1.SecurityGroupRule{{ + Protocol: korifiv1alpha1.ProtocolUDP, + Ports: "80", + Destination: "192.168.1.1/16", + }} + }) + + It("creates the rule with UDP protocol", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprint("default.", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(policy.Spec.Egress[0].Protocol).To(Equal(tools.PtrTo(numorstring.Protocol{Type: 1, StrVal: numorstring.ProtocolUDP}))) + }).Should(Succeed()) + }) + }) + }) + + When("removing a space from the security group", func() { + BeforeEach(func() { + _, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Create( + ctx, + &v3.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("default.%s", cfSecurityGroup.Name), + Namespace: testNamespace, + }, + }, + metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + It("removes the orphaned policies", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprintf("default.%s", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + g.Expect(policy).To(BeNil()) + }).Should(Succeed()) + }) + }) + + When("reverting a global security group", func() { + BeforeEach(func() { + _, err := calicoFakeClient.ProjectcalicoV3().GlobalNetworkPolicies().Create( + ctx, + &v3.GlobalNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("default.%s", cfSecurityGroup.Name), + Namespace: testNamespace, + }, + }, + metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + It("removes the orphaned policies", func() { + Eventually(func(g Gomega) { + globalPolicy, err := calicoFakeClient.ProjectcalicoV3().GlobalNetworkPolicies().Get(ctx, fmt.Sprintf("default.%s", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + g.Expect(globalPolicy).To(BeNil()) + }).Should(Succeed()) + }) + }) + + When("finalizing a security group", func() { + BeforeEach(func() { + _, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Create( + ctx, + &v3.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("default.%s", cfSecurityGroup.Name), + Namespace: testNamespace, + }, + }, + metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + _, err = calicoFakeClient.ProjectcalicoV3().GlobalNetworkPolicies().Create( + ctx, + &v3.GlobalNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("default.%s", cfSecurityGroup.Name), + Namespace: testNamespace, + }, + }, + metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + + JustBeforeEach(func() { + Expect(k8sManager.GetClient().Delete(ctx, cfSecurityGroup)).To(Succeed()) + }) + + It("deletes the security group", func() { + Eventually(func(g Gomega) { + err := adminClient.Get(ctx, client.ObjectKeyFromObject(cfSecurityGroup), cfSecurityGroup) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + }) + + It("deletes the network policy", func() { + Eventually(func(g Gomega) { + policy, err := calicoFakeClient.ProjectcalicoV3().NetworkPolicies(testNamespace).Get(ctx, fmt.Sprintf("default.%s", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + g.Expect(policy).To(BeNil()) + }).Should(Succeed()) + }) + + It("deletes the global network policy", func() { + Eventually(func(g Gomega) { + globalPolicy, err := calicoFakeClient.ProjectcalicoV3().GlobalNetworkPolicies().Get(ctx, fmt.Sprintf("default.%s", cfSecurityGroup.Name), metav1.GetOptions{}) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) + g.Expect(globalPolicy).To(BeNil()) + }).Should(Succeed()) + }) + }) +}) diff --git a/controllers/controllers/networking/security_groups/fake_calico_client.go b/controllers/controllers/networking/security_groups/fake_calico_client.go new file mode 100644 index 000000000..06c8ff9b4 --- /dev/null +++ b/controllers/controllers/networking/security_groups/fake_calico_client.go @@ -0,0 +1,961 @@ +package securitygroups + +import ( + "context" + "fmt" + "strings" + + v3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3" + in "github.com/projectcalico/api/pkg/client/clientset_generated/clientset/typed/projectcalico/v3" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/flowcontrol" +) + +type FakeCalicoClient struct { + networkPolicies map[string]*v3.NetworkPolicy + globalNetworkPolicies map[string]*v3.GlobalNetworkPolicy +} + +func NewFakeCalicoClient() *FakeCalicoClient { + return &FakeCalicoClient{ + networkPolicies: make(map[string]*v3.NetworkPolicy), + globalNetworkPolicies: make(map[string]*v3.GlobalNetworkPolicy), + } +} + +func (f *FakeCalicoClient) ProjectcalicoV3() in.ProjectcalicoV3Interface { + return &fakeProjectcalicoV3Client{f} +} + +func (f *FakeCalicoClient) Discovery() discovery.DiscoveryInterface { + return nil +} + +type fakeProjectcalicoV3Client struct { + f *FakeCalicoClient +} + +func (f *fakeProjectcalicoV3Client) BGPConfigurations() in.BGPConfigurationInterface { + return &fakeBGPConfigurationClient{} +} + +func (f *fakeProjectcalicoV3Client) BGPFilters() in.BGPFilterInterface { + return &fakeBGFilterClient{} +} + +func (f *fakeProjectcalicoV3Client) BGPPeers() in.BGPPeerInterface { + return &fakeBGPPeerClient{} +} + +func (f *fakeProjectcalicoV3Client) BlockAffinities() in.BlockAffinityInterface { + return &fakeBlockAffinityClient{} +} + +func (f *fakeProjectcalicoV3Client) CalicoNodeStatuses() in.CalicoNodeStatusInterface { + return &fakeCalicoNodeStatusClient{} +} + +func (f *fakeProjectcalicoV3Client) ClusterInformations() in.ClusterInformationInterface { + return &fakeClusterInformationClient{} +} + +func (f *fakeProjectcalicoV3Client) FelixConfigurations() in.FelixConfigurationInterface { + return &fakeFelixConfigurationClient{} +} + +func (f *fakeProjectcalicoV3Client) GlobalNetworkPolicies() in.GlobalNetworkPolicyInterface { + return &fakeGlobalNetworkPolicyClient{f.f} +} + +func (f *fakeProjectcalicoV3Client) GlobalNetworkSets() in.GlobalNetworkSetInterface { + return &fakeGlobalNetworkSetClient{} +} + +func (f *fakeProjectcalicoV3Client) HostEndpoints() in.HostEndpointInterface { + return &fakeHostEndpointClient{} +} + +func (f *fakeProjectcalicoV3Client) IPAMConfigurations() in.IPAMConfigurationInterface { + return &fakeIPAMConfigurationClient{} +} + +func (f *fakeProjectcalicoV3Client) IPPools() in.IPPoolInterface { + return &fakeIPPoolClient{} +} + +func (f *fakeProjectcalicoV3Client) IPReservations() in.IPReservationInterface { + return &fakeIPReservationClient{} +} + +func (f *fakeProjectcalicoV3Client) KubeControllersConfigurations() in.KubeControllersConfigurationInterface { + return &fakeKubeControllersConfigurationClient{} +} + +func (f *fakeProjectcalicoV3Client) NetworkPolicies(namespace string) in.NetworkPolicyInterface { + return &fakeNetworkPolicyClient{f.f, namespace} +} + +func (f *fakeProjectcalicoV3Client) NetworkSets(namespace string) in.NetworkSetInterface { + return &fakeNetworkSetClient{} +} + +func (f *fakeProjectcalicoV3Client) Profiles() in.ProfileInterface { + return &fakeProfileClient{} +} + +func (f *fakeProjectcalicoV3Client) StagedGlobalNetworkPolicies() in.StagedGlobalNetworkPolicyInterface { + return &fakeStagedGlobalNetworkPolicyClient{} +} + +func (f *fakeProjectcalicoV3Client) StagedKubernetesNetworkPolicies(s string) in.StagedKubernetesNetworkPolicyInterface { + return &fakeStagedKubernetesNetworkPolicyClient{} +} + +func (f *fakeProjectcalicoV3Client) StagedNetworkPolicies(s string) in.StagedNetworkPolicyInterface { + return &fakeStagedNetworkPolicyClient{} +} + +func (f *fakeProjectcalicoV3Client) Tiers() in.TierInterface { + return &fakeTierClient{} +} + +func (c *fakeProjectcalicoV3Client) RESTClient() rest.Interface { + return &fakeRESTInterfaceClient{} +} + +type fakeNetworkPolicyClient struct { + f *FakeCalicoClient + namespace string +} + +func (f *fakeNetworkPolicyClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.NetworkPolicy, error) { + key := fmt.Sprintf("%s/%s", f.namespace, name) + if policy, exists := f.f.networkPolicies[key]; exists { + return policy, nil + } + return nil, apierrors.NewNotFound(v3.Resource("NetworkPolicy"), name) +} + +func (f *fakeNetworkPolicyClient) Create(ctx context.Context, policy *v3.NetworkPolicy, opts metav1.CreateOptions) (*v3.NetworkPolicy, error) { + key := fmt.Sprintf("%s/default.%s", policy.Namespace, policy.Name) + f.f.networkPolicies[key] = policy + return policy, nil +} + +func (f *fakeNetworkPolicyClient) Update(ctx context.Context, policy *v3.NetworkPolicy, opts metav1.UpdateOptions) (*v3.NetworkPolicy, error) { + key := fmt.Sprintf("%s/%s", policy.Namespace, policy.Name) + f.f.networkPolicies[key] = policy + return policy, nil +} + +func (f *fakeNetworkPolicyClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + key := fmt.Sprintf("%s/%s", f.namespace, name) + if _, exists := f.f.networkPolicies[key]; exists { + delete(f.f.networkPolicies, key) + return nil + } + return apierrors.NewNotFound(v3.Resource("NetworkPolicy"), name) +} + +func (f *fakeNetworkPolicyClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.NetworkPolicyList, error) { + result := &v3.NetworkPolicyList{Items: []v3.NetworkPolicy{}} + for key, policy := range f.f.networkPolicies { + if f.namespace != "" && !strings.HasPrefix(key, f.namespace+"/") { + continue + } + + if opts.FieldSelector != "" { + if !strings.Contains(opts.FieldSelector, "metadata.name=") { + continue + } + name := strings.Split(strings.Split(opts.FieldSelector, "=")[1], ".")[1] + if policy.Name != fmt.Sprintf("default.%s", name) { + continue + } + } + + if opts.LabelSelector != "" && !matchesLabelSelector(policy.Labels, opts.LabelSelector) { + continue + } + result.Items = append(result.Items, *policy) + } + return result, nil +} + +func (f *fakeNetworkPolicyClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeNetworkPolicyClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeNetworkPolicyClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.NetworkPolicy, err error) { + return nil, nil +} + +type fakeGlobalNetworkPolicyClient struct { + f *FakeCalicoClient +} + +func (f *fakeGlobalNetworkPolicyClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.GlobalNetworkPolicy, error) { + if policy, exists := f.f.globalNetworkPolicies[name]; exists { + return policy, nil + } + return nil, apierrors.NewNotFound(v3.Resource("GlobalNetworkPolicy"), name) +} + +func (f *fakeGlobalNetworkPolicyClient) Create(ctx context.Context, policy *v3.GlobalNetworkPolicy, opts metav1.CreateOptions) (*v3.GlobalNetworkPolicy, error) { + key := fmt.Sprintf("default.%s", policy.Name) + f.f.globalNetworkPolicies[key] = policy + return policy, nil +} + +func (f *fakeGlobalNetworkPolicyClient) Update(ctx context.Context, policy *v3.GlobalNetworkPolicy, opts metav1.UpdateOptions) (*v3.GlobalNetworkPolicy, error) { + f.f.globalNetworkPolicies[policy.Name] = policy + return policy, nil +} + +func (f *fakeGlobalNetworkPolicyClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + if _, exists := f.f.globalNetworkPolicies[name]; exists { + delete(f.f.globalNetworkPolicies, name) + return nil + } + return apierrors.NewNotFound(v3.Resource("GlobalNetworkPolicy"), name) +} + +func (f *fakeGlobalNetworkPolicyClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.GlobalNetworkPolicyList, error) { + result := &v3.GlobalNetworkPolicyList{Items: []v3.GlobalNetworkPolicy{}} + for _, policy := range f.f.globalNetworkPolicies { + if opts.FieldSelector != "" { + if !strings.Contains(opts.FieldSelector, "metadata.name=") { + continue + } + name := strings.Split(strings.Split(opts.FieldSelector, "=")[1], ".")[1] + if policy.Name != fmt.Sprintf("default.%s", name) { + continue + } + } + + if opts.LabelSelector != "" && !matchesLabelSelector(policy.Labels, opts.LabelSelector) { + continue + } + result.Items = append(result.Items, *policy) + } + return result, nil +} + +func (f *fakeGlobalNetworkPolicyClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeGlobalNetworkPolicyClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeGlobalNetworkPolicyClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.GlobalNetworkPolicy, err error) { + return nil, nil +} + +func matchesLabelSelector(labels map[string]string, selector string) bool { + parts := strings.Split(selector, "=") + if len(parts) != 2 { + return false + } + key, value := parts[0], parts[1] + return labels[key] == value +} + +type fakeBGPConfigurationClient struct{} + +func (f *fakeBGPConfigurationClient) Create(ctx context.Context, bGPConfiguration *v3.BGPConfiguration, opts metav1.CreateOptions) (*v3.BGPConfiguration, error) { + return nil, nil +} + +func (f *fakeBGPConfigurationClient) Update(ctx context.Context, bGPConfiguration *v3.BGPConfiguration, opts metav1.UpdateOptions) (*v3.BGPConfiguration, error) { + return nil, nil +} + +func (f *fakeBGPConfigurationClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeBGPConfigurationClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeBGPConfigurationClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.BGPConfiguration, error) { + return nil, nil +} + +func (f *fakeBGPConfigurationClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.BGPConfigurationList, error) { + return nil, nil +} + +func (f *fakeBGPConfigurationClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeBGPConfigurationClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v3.BGPConfiguration, error) { + return nil, nil +} + +type fakeBGFilterClient struct{} + +func (f *fakeBGFilterClient) Create(ctx context.Context, bGPFilter *v3.BGPFilter, opts metav1.CreateOptions) (*v3.BGPFilter, error) { + return nil, nil +} + +func (f *fakeBGFilterClient) Update(ctx context.Context, bGPFilter *v3.BGPFilter, opts metav1.UpdateOptions) (*v3.BGPFilter, error) { + return nil, nil +} + +func (f *fakeBGFilterClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeBGFilterClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeBGFilterClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.BGPFilter, error) { + return nil, nil +} + +func (f *fakeBGFilterClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.BGPFilterList, error) { + return nil, nil +} + +func (f *fakeBGFilterClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeBGFilterClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.BGPFilter, err error) { + return nil, nil +} + +type fakeBGPPeerClient struct{} + +func (f *fakeBGPPeerClient) Create(ctx context.Context, bGPPeer *v3.BGPPeer, opts metav1.CreateOptions) (*v3.BGPPeer, error) { + return nil, nil +} + +func (f *fakeBGPPeerClient) Update(ctx context.Context, bGPPeer *v3.BGPPeer, opts metav1.UpdateOptions) (*v3.BGPPeer, error) { + return nil, nil +} + +func (f *fakeBGPPeerClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeBGPPeerClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeBGPPeerClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.BGPPeer, error) { + return nil, nil +} + +func (f *fakeBGPPeerClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.BGPPeerList, error) { + return nil, nil +} + +func (f *fakeBGPPeerClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeBGPPeerClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.BGPPeer, err error) { + return nil, nil +} + +type fakeBlockAffinityClient struct{} + +func (f *fakeBlockAffinityClient) Create(ctx context.Context, blockAffinity *v3.BlockAffinity, opts metav1.CreateOptions) (*v3.BlockAffinity, error) { + return nil, nil +} + +func (f *fakeBlockAffinityClient) Update(ctx context.Context, blockAffinity *v3.BlockAffinity, opts metav1.UpdateOptions) (*v3.BlockAffinity, error) { + return nil, nil +} + +func (f *fakeBlockAffinityClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeBlockAffinityClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeBlockAffinityClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.BlockAffinity, error) { + return nil, nil +} + +func (f *fakeBlockAffinityClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.BlockAffinityList, error) { + return nil, nil +} + +func (f *fakeBlockAffinityClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeBlockAffinityClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.BlockAffinity, err error) { + return nil, nil +} + +type fakeCalicoNodeStatusClient struct{} + +func (f *fakeCalicoNodeStatusClient) Create(ctx context.Context, calicoNodeStatus *v3.CalicoNodeStatus, opts metav1.CreateOptions) (*v3.CalicoNodeStatus, error) { + return nil, nil +} + +func (f *fakeCalicoNodeStatusClient) Update(ctx context.Context, calicoNodeStatus *v3.CalicoNodeStatus, opts metav1.UpdateOptions) (*v3.CalicoNodeStatus, error) { + return nil, nil +} + +func (f *fakeCalicoNodeStatusClient) UpdateStatus(ctx context.Context, calicoNodeStatus *v3.CalicoNodeStatus, opts metav1.UpdateOptions) (*v3.CalicoNodeStatus, error) { + return nil, nil +} + +func (f *fakeCalicoNodeStatusClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeCalicoNodeStatusClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeCalicoNodeStatusClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.CalicoNodeStatus, error) { + return nil, nil +} + +func (f *fakeCalicoNodeStatusClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.CalicoNodeStatusList, error) { + return nil, nil +} + +func (f *fakeCalicoNodeStatusClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeCalicoNodeStatusClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.CalicoNodeStatus, err error) { + return nil, nil +} + +type fakeClusterInformationClient struct{} + +func (f *fakeClusterInformationClient) Create(ctx context.Context, clusterInformation *v3.ClusterInformation, opts metav1.CreateOptions) (*v3.ClusterInformation, error) { + return nil, nil +} + +func (f *fakeClusterInformationClient) Update(ctx context.Context, clusterInformation *v3.ClusterInformation, opts metav1.UpdateOptions) (*v3.ClusterInformation, error) { + return nil, nil +} + +func (f *fakeClusterInformationClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeClusterInformationClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeClusterInformationClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.ClusterInformation, error) { + return nil, nil +} + +func (f *fakeClusterInformationClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.ClusterInformationList, error) { + return nil, nil +} + +func (f *fakeClusterInformationClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeClusterInformationClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.ClusterInformation, err error) { + return nil, nil +} + +type fakeFelixConfigurationClient struct{} + +func (f *fakeFelixConfigurationClient) Create(ctx context.Context, felixConfiguration *v3.FelixConfiguration, opts metav1.CreateOptions) (*v3.FelixConfiguration, error) { + return nil, nil +} + +func (f *fakeFelixConfigurationClient) Update(ctx context.Context, felixConfiguration *v3.FelixConfiguration, opts metav1.UpdateOptions) (*v3.FelixConfiguration, error) { + return nil, nil +} + +func (f *fakeFelixConfigurationClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeFelixConfigurationClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeFelixConfigurationClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.FelixConfiguration, error) { + return nil, nil +} + +func (f *fakeFelixConfigurationClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.FelixConfigurationList, error) { + return nil, nil +} + +func (f *fakeFelixConfigurationClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeFelixConfigurationClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.FelixConfiguration, err error) { + return nil, nil +} + +type fakeGlobalNetworkSetClient struct{} + +func (f *fakeGlobalNetworkSetClient) Create(ctx context.Context, globalNetworkSet *v3.GlobalNetworkSet, opts metav1.CreateOptions) (*v3.GlobalNetworkSet, error) { + return nil, nil +} + +func (f *fakeGlobalNetworkSetClient) Update(ctx context.Context, globalNetworkSet *v3.GlobalNetworkSet, opts metav1.UpdateOptions) (*v3.GlobalNetworkSet, error) { + return nil, nil +} + +func (f *fakeGlobalNetworkSetClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeGlobalNetworkSetClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeGlobalNetworkSetClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.GlobalNetworkSet, error) { + return nil, nil +} + +func (f *fakeGlobalNetworkSetClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.GlobalNetworkSetList, error) { + return nil, nil +} + +func (f *fakeGlobalNetworkSetClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeGlobalNetworkSetClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.GlobalNetworkSet, err error) { + return nil, nil +} + +type fakeHostEndpointClient struct{} + +func (f *fakeHostEndpointClient) Create(ctx context.Context, hostEndpoint *v3.HostEndpoint, opts metav1.CreateOptions) (*v3.HostEndpoint, error) { + return nil, nil +} + +func (f *fakeHostEndpointClient) Update(ctx context.Context, hostEndpoint *v3.HostEndpoint, opts metav1.UpdateOptions) (*v3.HostEndpoint, error) { + return nil, nil +} + +func (f *fakeHostEndpointClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeHostEndpointClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeHostEndpointClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.HostEndpoint, error) { + return nil, nil +} + +func (f *fakeHostEndpointClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.HostEndpointList, error) { + return nil, nil +} + +func (f *fakeHostEndpointClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeHostEndpointClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.HostEndpoint, err error) { + return nil, nil +} + +type fakeIPAMConfigurationClient struct{} + +func (f *fakeIPAMConfigurationClient) Create(ctx context.Context, iPAMConfiguration *v3.IPAMConfiguration, opts metav1.CreateOptions) (*v3.IPAMConfiguration, error) { + return nil, nil +} + +func (f *fakeIPAMConfigurationClient) Update(ctx context.Context, iPAMConfiguration *v3.IPAMConfiguration, opts metav1.UpdateOptions) (*v3.IPAMConfiguration, error) { + return nil, nil +} + +func (f *fakeIPAMConfigurationClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeIPAMConfigurationClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeIPAMConfigurationClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.IPAMConfiguration, error) { + return nil, nil +} + +func (f *fakeIPAMConfigurationClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.IPAMConfigurationList, error) { + return nil, nil +} + +func (f *fakeIPAMConfigurationClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeIPAMConfigurationClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.IPAMConfiguration, err error) { + return nil, nil +} + +type fakeIPPoolClient struct{} + +func (f *fakeIPPoolClient) Create(ctx context.Context, iPPool *v3.IPPool, opts metav1.CreateOptions) (*v3.IPPool, error) { + return nil, nil +} + +func (f *fakeIPPoolClient) Update(ctx context.Context, iPPool *v3.IPPool, opts metav1.UpdateOptions) (*v3.IPPool, error) { + return nil, nil +} + +func (f *fakeIPPoolClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeIPPoolClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeIPPoolClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.IPPool, error) { + return nil, nil +} + +func (f *fakeIPPoolClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.IPPoolList, error) { + return nil, nil +} + +func (f *fakeIPPoolClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeIPPoolClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.IPPool, err error) { + return nil, nil +} + +type fakeIPReservationClient struct{} + +func (f *fakeIPReservationClient) Create(ctx context.Context, iPReservation *v3.IPReservation, opts metav1.CreateOptions) (*v3.IPReservation, error) { + return nil, nil +} + +func (f *fakeIPReservationClient) Update(ctx context.Context, iPReservation *v3.IPReservation, opts metav1.UpdateOptions) (*v3.IPReservation, error) { + return nil, nil +} + +func (f *fakeIPReservationClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeIPReservationClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeIPReservationClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.IPReservation, error) { + return nil, nil +} + +func (f *fakeIPReservationClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.IPReservationList, error) { + return nil, nil +} + +func (f *fakeIPReservationClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeIPReservationClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.IPReservation, err error) { + return nil, nil +} + +type fakeKubeControllersConfigurationClient struct{} + +func (f *fakeKubeControllersConfigurationClient) Create(ctx context.Context, kubeControllersConfiguration *v3.KubeControllersConfiguration, opts metav1.CreateOptions) (*v3.KubeControllersConfiguration, error) { + return nil, nil +} + +func (f *fakeKubeControllersConfigurationClient) Update(ctx context.Context, kubeControllersConfiguration *v3.KubeControllersConfiguration, opts metav1.UpdateOptions) (*v3.KubeControllersConfiguration, error) { + return nil, nil +} + +func (f *fakeKubeControllersConfigurationClient) UpdateStatus(ctx context.Context, kubeControllersConfiguration *v3.KubeControllersConfiguration, opts metav1.UpdateOptions) (*v3.KubeControllersConfiguration, error) { + return nil, nil +} + +func (f *fakeKubeControllersConfigurationClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeKubeControllersConfigurationClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeKubeControllersConfigurationClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.KubeControllersConfiguration, error) { + return nil, nil +} + +func (f *fakeKubeControllersConfigurationClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.KubeControllersConfigurationList, error) { + return nil, nil +} + +func (f *fakeKubeControllersConfigurationClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeKubeControllersConfigurationClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.KubeControllersConfiguration, err error) { + return nil, nil +} + +type fakeNetworkSetClient struct{} + +func (f *fakeNetworkSetClient) Create(ctx context.Context, networkSet *v3.NetworkSet, opts metav1.CreateOptions) (*v3.NetworkSet, error) { + return nil, nil +} + +func (f *fakeNetworkSetClient) Update(ctx context.Context, networkSet *v3.NetworkSet, opts metav1.UpdateOptions) (*v3.NetworkSet, error) { + return nil, nil +} + +func (f *fakeNetworkSetClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeNetworkSetClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeNetworkSetClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.NetworkSet, error) { + return nil, nil +} + +func (f *fakeNetworkSetClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.NetworkSetList, error) { + return nil, nil +} + +func (f *fakeNetworkSetClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeNetworkSetClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.NetworkSet, err error) { + return nil, nil +} + +type fakeProfileClient struct{} + +func (f *fakeProfileClient) Create(ctx context.Context, profile *v3.Profile, opts metav1.CreateOptions) (*v3.Profile, error) { + return nil, nil +} + +func (f *fakeProfileClient) Update(ctx context.Context, profile *v3.Profile, opts metav1.UpdateOptions) (*v3.Profile, error) { + return nil, nil +} + +func (f *fakeProfileClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeProfileClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeProfileClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.Profile, error) { + return nil, nil +} + +func (f *fakeProfileClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.ProfileList, error) { + return nil, nil +} + +func (f *fakeProfileClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeProfileClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.Profile, err error) { + return nil, nil +} + +type fakeStagedGlobalNetworkPolicyClient struct{} + +func (f *fakeStagedGlobalNetworkPolicyClient) Create(ctx context.Context, stagedGlobalNetworkPolicy *v3.StagedGlobalNetworkPolicy, opts metav1.CreateOptions) (*v3.StagedGlobalNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedGlobalNetworkPolicyClient) Update(ctx context.Context, stagedGlobalNetworkPolicy *v3.StagedGlobalNetworkPolicy, opts metav1.UpdateOptions) (*v3.StagedGlobalNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedGlobalNetworkPolicyClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeStagedGlobalNetworkPolicyClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeStagedGlobalNetworkPolicyClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.StagedGlobalNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedGlobalNetworkPolicyClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.StagedGlobalNetworkPolicyList, error) { + return nil, nil +} + +func (f *fakeStagedGlobalNetworkPolicyClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeStagedGlobalNetworkPolicyClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.StagedGlobalNetworkPolicy, err error) { + return nil, nil +} + +type fakeStagedKubernetesNetworkPolicyClient struct{} + +func (f *fakeStagedKubernetesNetworkPolicyClient) Create(ctx context.Context, stagedKubernetesNetworkPolicy *v3.StagedKubernetesNetworkPolicy, opts metav1.CreateOptions) (*v3.StagedKubernetesNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedKubernetesNetworkPolicyClient) Update(ctx context.Context, stagedKubernetesNetworkPolicy *v3.StagedKubernetesNetworkPolicy, opts metav1.UpdateOptions) (*v3.StagedKubernetesNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedKubernetesNetworkPolicyClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeStagedKubernetesNetworkPolicyClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeStagedKubernetesNetworkPolicyClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.StagedKubernetesNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedKubernetesNetworkPolicyClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.StagedKubernetesNetworkPolicyList, error) { + return nil, nil +} + +func (f *fakeStagedKubernetesNetworkPolicyClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeStagedKubernetesNetworkPolicyClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.StagedKubernetesNetworkPolicy, err error) { + return nil, nil +} + +type fakeStagedNetworkPolicyClient struct{} + +func (f *fakeStagedNetworkPolicyClient) Create(ctx context.Context, stagedNetworkPolicy *v3.StagedNetworkPolicy, opts metav1.CreateOptions) (*v3.StagedNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedNetworkPolicyClient) Update(ctx context.Context, stagedNetworkPolicy *v3.StagedNetworkPolicy, opts metav1.UpdateOptions) (*v3.StagedNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedNetworkPolicyClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeStagedNetworkPolicyClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeStagedNetworkPolicyClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.StagedNetworkPolicy, error) { + return nil, nil +} + +func (f *fakeStagedNetworkPolicyClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.StagedNetworkPolicyList, error) { + return nil, nil +} + +func (f *fakeStagedNetworkPolicyClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeStagedNetworkPolicyClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.StagedNetworkPolicy, err error) { + return nil, nil +} + +type fakeTierClient struct{} + +func (f *fakeTierClient) Create(ctx context.Context, tier *v3.Tier, opts metav1.CreateOptions) (*v3.Tier, error) { + return nil, nil +} + +func (f *fakeTierClient) Update(ctx context.Context, tier *v3.Tier, opts metav1.UpdateOptions) (*v3.Tier, error) { + return nil, nil +} + +func (f *fakeTierClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return nil +} + +func (f *fakeTierClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + return nil +} + +func (f *fakeTierClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v3.Tier, error) { + return nil, nil +} + +func (f *fakeTierClient) List(ctx context.Context, opts metav1.ListOptions) (*v3.TierList, error) { + return nil, nil +} + +func (f *fakeTierClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + return nil, nil +} + +func (f *fakeTierClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v3.Tier, err error) { + return nil, nil +} + +type fakeRESTInterfaceClient struct{} + +func (f *fakeRESTInterfaceClient) GetRateLimiter() flowcontrol.RateLimiter { + return nil +} + +func (f *fakeRESTInterfaceClient) Verb(verb string) *rest.Request { + return nil +} + +func (f *fakeRESTInterfaceClient) Post() *rest.Request { + return nil +} + +func (f *fakeRESTInterfaceClient) Put() *rest.Request { + return nil +} + +func (f *fakeRESTInterfaceClient) Patch(pt types.PatchType) *rest.Request { + return nil +} + +func (f *fakeRESTInterfaceClient) Get() *rest.Request { + return nil +} + +func (f *fakeRESTInterfaceClient) Delete() *rest.Request { + return nil +} + +func (f *fakeRESTInterfaceClient) APIVersion() schema.GroupVersion { + return schema.GroupVersion{} +} diff --git a/controllers/controllers/networking/security_groups/package.go b/controllers/controllers/networking/security_groups/package.go deleted file mode 100644 index e6d2bfa3d..000000000 --- a/controllers/controllers/networking/security_groups/package.go +++ /dev/null @@ -1,3 +0,0 @@ -package securitygroups - -//+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfsecuritygroups,verbs=create; diff --git a/controllers/controllers/networking/security_groups/suite_test.go b/controllers/controllers/networking/security_groups/suite_test.go new file mode 100644 index 000000000..e3eff3c0a --- /dev/null +++ b/controllers/controllers/networking/security_groups/suite_test.go @@ -0,0 +1,141 @@ +package securitygroups_test + +import ( + "context" + "path/filepath" + "testing" + "time" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + securitygroups "code.cloudfoundry.org/korifi/controllers/controllers/networking/security_groups" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/tests/helpers" + "code.cloudfoundry.org/korifi/tools/k8s" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + projectcalicoV3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +var ( + ctx context.Context + stopManager context.CancelFunc + stopClientCache context.CancelFunc + testEnv *envtest.Environment + calicoFakeClient *securitygroups.FakeCalicoClient + adminClient client.Client + k8sManager manager.Manager + rootNamespace string + testNamespace string +) + +func TestSecurityGroupsControllers(t *testing.T) { + SetDefaultEventuallyTimeout(10 * time.Second) + SetDefaultEventuallyPollingInterval(250 * time.Millisecond) + + RegisterFailHandler(Fail) + RunSpecs(t, "CFSecurityGroup Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), zap.Level(zapcore.DebugLevel))) + + ctx = context.Background() + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "..", "..", "helm", "korifi", "controllers", "crds"), + }, + ErrorIfCRDPathMissing: true, + } + + _, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + + Expect(korifiv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(projectcalicoV3.AddToScheme(scheme.Scheme)).To(Succeed()) +}) + +var _ = BeforeEach(func() { + k8sManager = helpers.NewK8sManager(testEnv, filepath.Join("helm", "korifi", "controllers", "role.yaml")) + Expect(shared.SetupIndexWithManager(k8sManager)).To(Succeed()) + + adminClient, stopClientCache = helpers.NewCachedClient(testEnv.Config) + + rootNamespace = uuid.NewString() + + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: rootNamespace, + }, + })).To(Succeed()) + + calicoFakeClient = securitygroups.NewFakeCalicoClient() + err := securitygroups.NewReconciler( + k8sManager.GetClient(), + calicoFakeClient, + ctrl.Log.WithName("controllers").WithName("CFSecurityGroup"), + rootNamespace, + ).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + testNamespace = uuid.NewString() + + Expect(adminClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + })).To(Succeed()) + + cfOrg := &korifiv1alpha1.CFOrg{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testNamespace, + }, + Spec: korifiv1alpha1.CFOrgSpec{ + DisplayName: uuid.NewString(), + }, + } + Expect(adminClient.Create(ctx, cfOrg)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfOrg, func() { + cfOrg.Status.GUID = testNamespace + })).To(Succeed()) + + cfSpace := &korifiv1alpha1.CFSpace{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testNamespace, + }, + Spec: korifiv1alpha1.CFSpaceSpec{ + DisplayName: uuid.NewString(), + }, + } + Expect(adminClient.Create(ctx, cfSpace)).To(Succeed()) + Expect(k8s.Patch(ctx, adminClient, cfSpace, func() { + cfSpace.Status.GUID = testNamespace + })).To(Succeed()) +}) + +var _ = JustBeforeEach(func() { + stopManager = helpers.StartK8sManager(k8sManager) +}) + +var _ = AfterEach(func() { + stopManager() + stopClientCache() +}) + +var _ = AfterSuite(func() { + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/controllers/main.go b/controllers/main.go index 88d25511b..b0c79763f 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -29,6 +29,7 @@ import ( "code.cloudfoundry.org/korifi/controllers/config" "code.cloudfoundry.org/korifi/controllers/controllers/networking/domains" "code.cloudfoundry.org/korifi/controllers/controllers/networking/routes" + securitygroups "code.cloudfoundry.org/korifi/controllers/controllers/networking/security_groups" "code.cloudfoundry.org/korifi/controllers/controllers/services/bindings" managed_bindings "code.cloudfoundry.org/korifi/controllers/controllers/services/bindings/managed" upsi_bindings "code.cloudfoundry.org/korifi/controllers/controllers/services/bindings/upsi" @@ -65,11 +66,14 @@ import ( packageswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/packages" spaceswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/spaces" taskswebhook "code.cloudfoundry.org/korifi/controllers/webhooks/workloads/tasks" + projectcalicoV3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3" + "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/image" "code.cloudfoundry.org/korifi/version" buildv1alpha2 "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + "github.com/projectcalico/api/pkg/client/clientset_generated/clientset" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" k8sclient "k8s.io/client-go/kubernetes" @@ -99,6 +103,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(gatewayv1beta1.Install(scheme)) utilruntime.Must(korifiv1alpha1.AddToScheme(scheme)) + utilruntime.Must(projectcalicoV3.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -143,6 +148,11 @@ func main() { ctrl.Log.Info("starting Korifi controllers", "version", version.Version) conf := ctrl.GetConfigOrDie() + calicoClient, err := clientset.NewForConfig(conf) + if err != nil { + panic(fmt.Sprintf("unable to create Calico client: %v", err)) + } + k8sClient, err := k8sclient.NewForConfig(conf) if err != nil { panic(fmt.Sprintf("could not create k8s client: %v", err)) @@ -311,6 +321,18 @@ func main() { os.Exit(1) } + if controllerConfig.ExperimentalSecurityGroupsEnabled { + if err = securitygroups.NewReconciler( + mgr.GetClient(), + calicoClient, + controllersLog, + controllerConfig.CFRootNamespace, + ).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "CFSecurityGroup") + os.Exit(1) + } + } + if controllerConfig.ExperimentalManagedServicesEnabled { if err = brokers.NewReconciler( controllersClient, diff --git a/controllers/webhooks/finalizer/webhook.go b/controllers/webhooks/finalizer/webhook.go index 8ebac5da0..8c6002fe9 100644 --- a/controllers/webhooks/finalizer/webhook.go +++ b/controllers/webhooks/finalizer/webhook.go @@ -1,6 +1,6 @@ package finalizer -//+kubebuilder:webhook:path=/mutate-korifi-cloudfoundry-org-v1alpha1-controllers-finalizer,mutating=true,failurePolicy=fail,sideEffects=None,groups=korifi.cloudfoundry.org,resources=cfapps;cfspaces;cfpackages;cforgs;cfroutes;cfdomains;cfservicebindings;cfserviceinstances,verbs=create,versions=v1alpha1,name=mcffinalizer.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} +//+kubebuilder:webhook:path=/mutate-korifi-cloudfoundry-org-v1alpha1-controllers-finalizer,mutating=true,failurePolicy=fail,sideEffects=None,groups=korifi.cloudfoundry.org,resources=cfapps;cfspaces;cfpackages;cforgs;cfroutes;cfdomains;cfservicebindings;cfserviceinstances;cfsecuritygroups,verbs=create,versions=v1alpha1,name=mcffinalizer.korifi.cloudfoundry.org,admissionReviewVersions={v1,v1beta1} import ( "context" @@ -25,6 +25,7 @@ func NewControllersFinalizerWebhook() *ControllersFinalizerWebhook { "CFDomain": {FinalizerName: korifiv1alpha1.CFDomainFinalizerName, SetPolicy: k8s.Always}, "CFServiceInstance": {FinalizerName: korifiv1alpha1.CFServiceInstanceFinalizerName, SetPolicy: k8s.Always}, "CFServiceBinding": {FinalizerName: korifiv1alpha1.CFServiceBindingFinalizerName, SetPolicy: k8s.Always}, + "CFSecurityGroup": {FinalizerName: korifiv1alpha1.CFSecurityGroupFinalizerName, SetPolicy: k8s.Always}, }), } } diff --git a/go.mod b/go.mod index 9652a666b..49f391636 100644 --- a/go.mod +++ b/go.mod @@ -62,9 +62,11 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect + github.com/projectcalico/api v0.0.0-20250326193936-759a4c3213d1 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect diff --git a/go.sum b/go.sum index 0333e0125..b279606ac 100644 --- a/go.sum +++ b/go.sum @@ -303,6 +303,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jellydator/validation v1.1.0 h1:TBkx56y6dd0By2AhtStRdTIhDjtcuoSE9w6G6z7wQ4o= github.com/jellydator/validation v1.1.0/go.mod h1:AaCjfkQ4Ykdcb+YCwqCtaI3wDsf2UAGhJ06lJs0VgOw= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -404,6 +406,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/poy/eachers v0.0.0-20181020210610-23942921fe77/go.mod h1:x1vqpbcMW9T/KRcQ4b48diSiSVtYgvwQ5xzDByEg4WE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/projectcalico/api v0.0.0-20250326193936-759a4c3213d1 h1:n2lqKIGOvmham6sSmY7zQ1YCbD9E70vUkpdxPDDH9/w= +github.com/projectcalico/api v0.0.0-20250326193936-759a4c3213d1/go.mod h1:OORX6y/uicCv0g5dYqoaP3Lqntw/ACBEzrBWbR+jX74= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= diff --git a/helm/korifi/controllers/cf_roles/cf_admin.yaml b/helm/korifi/controllers/cf_roles/cf_admin.yaml index 4efb21eec..3b7ecbb8e 100644 --- a/helm/korifi/controllers/cf_roles/cf_admin.yaml +++ b/helm/korifi/controllers/cf_roles/cf_admin.yaml @@ -167,7 +167,12 @@ rules: resources: - cfsecuritygroups verbs: + - get - create + - delete + - list + - patch + - watch - apiGroups: - korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/configmap.yaml b/helm/korifi/controllers/configmap.yaml index 426866078..604d53292 100644 --- a/helm/korifi/controllers/configmap.yaml +++ b/helm/korifi/controllers/configmap.yaml @@ -39,5 +39,6 @@ data: gatewayNamespace: {{ .Release.Namespace }}-gateway gatewayName: korifi experimentalManagedServicesEnabled: {{ .Values.experimental.managedServices.enabled }} + experimentalSecurityGroupsEnabled: {{ .Values.experimental.securityGroups.enabled }} trustInsecureServiceBrokers: {{ .Values.experimental.managedServices.trustInsecureBrokers }} disableRouteController: {{ .Values.experimental.routing.disableRouteController }} diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml index 25f009105..14c373ef9 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_appworkloads.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: appworkloads.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_builderinfos.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_builderinfos.yaml index 423f559e3..febda5839 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_builderinfos.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_builderinfos.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: builderinfos.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_buildworkloads.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_buildworkloads.yaml index 5b15b824a..573273ebb 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_buildworkloads.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_buildworkloads.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: buildworkloads.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml index c24ee9dab..1a83825f4 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfapps.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfapps.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfbuilds.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfbuilds.yaml index 7d73663b7..1d970c781 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfbuilds.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfbuilds.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfbuilds.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfdomains.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfdomains.yaml index 69cac47dc..0c0c29e44 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfdomains.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfdomains.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cforgs.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cforgs.yaml index b88aedf67..b51678c04 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cforgs.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cforgs.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cforgs.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfpackages.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfpackages.yaml index a8b63f1f5..9cc2b0522 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfpackages.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfpackages.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfpackages.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml index d4f50721c..ecfa309c0 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfprocesses.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfprocesses.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfroutes.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfroutes.yaml index 9b1abc885..2be0eff83 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfroutes.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfroutes.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfroutes.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfsecuritygroups.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfsecuritygroups.yaml index 8e428f080..c9fbe1917 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfsecuritygroups.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfsecuritygroups.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfsecuritygroups.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml index 3cf66c84f..439342bf9 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebindings.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfservicebindings.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebrokers.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebrokers.yaml index 3dcc17103..9858c418a 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebrokers.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfservicebrokers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfservicebrokers.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml index 2ce62fe52..228432618 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfserviceinstances.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceofferings.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceofferings.yaml index 36aee942f..f1356479f 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceofferings.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceofferings.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfserviceofferings.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml index 910f5c1af..d00bcf142 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceplans.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfserviceplans.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfspaces.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfspaces.yaml index 35c292112..b0e97cd69 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfspaces.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfspaces.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cfspaces.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cftasks.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cftasks.yaml index 6d5ffafc2..b2a6aa8f4 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cftasks.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cftasks.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: cftasks.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_runnerinfos.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_runnerinfos.yaml index 9520d0270..9f3c21bba 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_runnerinfos.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_runnerinfos.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: runnerinfos.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_taskworkloads.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_taskworkloads.yaml index 29c19aa1f..22b1dac7f 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_taskworkloads.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_taskworkloads.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.18.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: taskworkloads.korifi.cloudfoundry.org spec: group: korifi.cloudfoundry.org diff --git a/helm/korifi/controllers/manifests.yaml b/helm/korifi/controllers/manifests.yaml index 1fbe3f08b..d17f3203b 100644 --- a/helm/korifi/controllers/manifests.yaml +++ b/helm/korifi/controllers/manifests.yaml @@ -113,6 +113,7 @@ webhooks: - cfdomains - cfservicebindings - cfserviceinstances + - cfsecuritygroups sideEffects: None - admissionReviewVersions: - v1 diff --git a/helm/korifi/controllers/role.yaml b/helm/korifi/controllers/role.yaml index 2cb29be2c..c2e86bc51 100644 --- a/helm/korifi/controllers/role.yaml +++ b/helm/korifi/controllers/role.yaml @@ -107,6 +107,7 @@ rules: - cfpackages - cfprocesses - cfroutes + - cfsecuritygroups - cfservicebindings - cfservicebrokers - cfserviceinstances @@ -134,6 +135,7 @@ rules: - cforgs/finalizers - cfprocesses/finalizers - cfroutes/finalizers + - cfsecuritygroups/finalizers - cfservicebindings/finalizers - cfserviceinstances/finalizers - cfspaces/finalizers @@ -158,6 +160,7 @@ rules: - cfpackages/status - cfprocesses/status - cfroutes/status + - cfsecuritygroups/status - cfservicebindings/status - cfservicebrokers/status - cfserviceinstances/status @@ -199,12 +202,6 @@ rules: - list - patch - watch -- apiGroups: - - korifi.cloudfoundry.org - resources: - - cfsecuritygroups - verbs: - - create - apiGroups: - korifi.cloudfoundry.org resources: @@ -247,6 +244,22 @@ rules: - podsecuritypolicies verbs: - use +- apiGroups: + - projectcalico.org + resources: + - globalnetworkpolicies + - networkpolicies + - tier.globalnetworkpolicies + - tier.networkpolicies + - tiers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: diff --git a/kpack-image-builder/controllers/buildworkload_controller.go b/kpack-image-builder/controllers/buildworkload_controller.go index 7b66e51b7..973ad1c07 100644 --- a/kpack-image-builder/controllers/buildworkload_controller.go +++ b/kpack-image-builder/controllers/buildworkload_controller.go @@ -685,7 +685,8 @@ func (r *BuildWorkloadReconciler) reconcileKpackImage( } desiredKpackImage.Labels = map[string]string{ - BuildWorkloadLabelKey: buildWorkload.Name, + BuildWorkloadLabelKey: buildWorkload.Name, + korifiv1alpha1.CFWorkloadTypeLabelkey: korifiv1alpha1.CFWorkloadTypeBuild, } desiredKpackImage.Spec = buildv1alpha2.ImageSpec{ diff --git a/kpack-image-builder/controllers/buildworkload_controller_test.go b/kpack-image-builder/controllers/buildworkload_controller_test.go index 25d29eae1..8f8cb596f 100644 --- a/kpack-image-builder/controllers/buildworkload_controller_test.go +++ b/kpack-image-builder/controllers/buildworkload_controller_test.go @@ -247,7 +247,8 @@ var _ = Describe("BuildWorkloadReconciler", func() { Name: appGUID, Namespace: namespaceGUID, Labels: map[string]string{ - controllers.BuildWorkloadLabelKey: buildWorkloadGUID, + controllers.BuildWorkloadLabelKey: buildWorkloadGUID, + korifiv1alpha1.CFWorkloadTypeLabelkey: korifiv1alpha1.CFWorkloadTypeBuild, }, }, Spec: buildv1alpha2.ImageSpec{ @@ -278,7 +279,8 @@ var _ = Describe("BuildWorkloadReconciler", func() { Name: appGUID, Namespace: namespaceGUID, Labels: map[string]string{ - controllers.BuildWorkloadLabelKey: buildWorkloadGUID, + controllers.BuildWorkloadLabelKey: buildWorkloadGUID, + korifiv1alpha1.CFWorkloadTypeLabelkey: korifiv1alpha1.CFWorkloadTypeBuild, }, }, Spec: buildv1alpha2.ImageSpec{ @@ -1100,11 +1102,12 @@ var _ = Describe("BuildWorkloadReconciler", func() { Name: "build", Namespace: namespaceGUID, Labels: map[string]string{ - korifiv1alpha1.CFAppGUIDLabelKey: appGUID, - buildv1alpha2.ImageLabel: appGUID, - buildv1alpha2.ImageGenerationLabel: "1", - buildv1alpha2.BuildNumberLabel: "1", - controllers.BuildWorkloadLabelKey: buildWorkload.Name, + korifiv1alpha1.CFAppGUIDLabelKey: appGUID, + buildv1alpha2.ImageLabel: appGUID, + buildv1alpha2.ImageGenerationLabel: "1", + buildv1alpha2.BuildNumberLabel: "1", + controllers.BuildWorkloadLabelKey: buildWorkload.Name, + korifiv1alpha1.CFWorkloadTypeLabelkey: korifiv1alpha1.CFWorkloadTypeBuild, }, }, } @@ -1286,7 +1289,8 @@ func buildKpackImageObject(name string, namespace string, source korifiv1alpha1. Name: name, Namespace: namespace, Labels: map[string]string{ - controllers.BuildWorkloadLabelKey: name, + controllers.BuildWorkloadLabelKey: name, + korifiv1alpha1.CFWorkloadTypeLabelkey: korifiv1alpha1.CFWorkloadTypeBuild, }, }, Spec: buildv1alpha2.ImageSpec{ diff --git a/scripts/assets/kind-config.yaml b/scripts/assets/kind-config.yaml index d495c045d..8a6de3de3 100644 --- a/scripts/assets/kind-config.yaml +++ b/scripts/assets/kind-config.yaml @@ -36,3 +36,6 @@ nodes: - containerPort: 30055 hostPort: 30055 protocol: TCP +networking: + disableDefaultCNI: true + podSubnet: 10.244.0.0/16 \ No newline at end of file diff --git a/scripts/deploy-on-kind.sh b/scripts/deploy-on-kind.sh index c5a9b91ff..e2f53470b 100755 --- a/scripts/deploy-on-kind.sh +++ b/scripts/deploy-on-kind.sh @@ -262,6 +262,17 @@ function create_cluster_builder() { kubectl wait --for=condition=ready clusterbuilder --all=true --timeout=15m } +function install_calico() { + curl -s https://raw.githubusercontent.com/projectcalico/calico/v3.30.2/manifests/operator-crds.yaml -o $SCRIPT_DIR/calico-operator-crds.yaml + curl -s https://raw.githubusercontent.com/projectcalico/calico/v3.30.2/manifests/tigera-operator.yaml -o $SCRIPT_DIR/calico-tigera-operator.yaml + curl -s https://raw.githubusercontent.com/projectcalico/calico/v3.30.2/manifests/custom-resources.yaml -o $SCRIPT_DIR/calico-custom-resources.yaml + sed -i 's/cidr: 192.168.0.0\/16/cidr: 10.244.0.0\/16/g' $SCRIPT_DIR/calico-custom-resources.yaml + + kubectl apply -f $SCRIPT_DIR/calico-operator-crds.yaml --server-side + kubectl apply -f $SCRIPT_DIR/calico-tigera-operator.yaml --server-side + kubectl apply -f $SCRIPT_DIR/calico-custom-resources.yaml +} + function main() { make -C "$ROOT_DIR" bin/yq @@ -269,6 +280,7 @@ function main() { validate_registry_params ensure_kind_cluster "$CLUSTER_NAME" ensure_local_registry + install_calico install_dependencies create_namespaces create_registry_secret diff --git a/statefulset-runner/controllers/appworkload/appworkload_to_stset.go b/statefulset-runner/controllers/appworkload/appworkload_to_stset.go index 216c1fcae..7879a3f80 100644 --- a/statefulset-runner/controllers/appworkload/appworkload_to_stset.go +++ b/statefulset-runner/controllers/appworkload/appworkload_to_stset.go @@ -204,11 +204,12 @@ func (r *AppWorkloadToStatefulsetConverter) Convert(appWorkload *korifiv1alpha1. } labels := map[string]string{ - controllers.LabelGUID: appWorkload.Spec.GUID, - LabelProcessType: appWorkload.Spec.ProcessType, - LabelVersion: appWorkload.Spec.Version, - LabelAppGUID: appWorkload.Spec.AppGUID, - LabelAppWorkloadGUID: appWorkload.Name, + controllers.LabelGUID: appWorkload.Spec.GUID, + LabelProcessType: appWorkload.Spec.ProcessType, + LabelVersion: appWorkload.Spec.Version, + LabelAppGUID: appWorkload.Spec.AppGUID, + LabelAppWorkloadGUID: appWorkload.Name, + korifiv1alpha1.CFWorkloadTypeLabelkey: korifiv1alpha1.CFWorkloadTypeApp, } statefulSet.Spec.Template.Labels = labels diff --git a/statefulset-runner/controllers/appworkload/appworkload_to_stset_test.go b/statefulset-runner/controllers/appworkload/appworkload_to_stset_test.go index 7f96e6a7e..0475dced2 100644 --- a/statefulset-runner/controllers/appworkload/appworkload_to_stset_test.go +++ b/statefulset-runner/controllers/appworkload/appworkload_to_stset_test.go @@ -227,6 +227,11 @@ var _ = Describe("AppWorkload to StatefulSet Converter", func() { Expect(statefulSet.Spec.Template.Labels).To(HaveKeyWithValue(appworkload.LabelVersion, "version_1234")) }) + It("should set workload type as a label", func() { + Expect(statefulSet.Labels).To(HaveKeyWithValue(korifiv1alpha1.CFWorkloadTypeLabelkey, korifiv1alpha1.CFWorkloadTypeApp)) + Expect(statefulSet.Spec.Template.Labels).To(HaveKeyWithValue(korifiv1alpha1.CFWorkloadTypeLabelkey, korifiv1alpha1.CFWorkloadTypeApp)) + }) + It("should set guid as a label selector", func() { Expect(statefulSet.Spec.Selector.MatchLabels).To(HaveKeyWithValue(controllers.LabelGUID, "guid_1234")) }) diff --git a/tests/e2e/e2e_suite_test.go b/tests/e2e/e2e_suite_test.go index 774b83360..53643ca06 100644 --- a/tests/e2e/e2e_suite_test.go +++ b/tests/e2e/e2e_suite_test.go @@ -248,6 +248,10 @@ type securityGroupResource struct { Rules []payloads.SecurityGroupRule } +type relationshipDataResource struct { + Data []payloads.RelationshipData `json:"data"` +} + type serviceBrokerResource struct { resource `json:",inline"` URL string `json:"url"` diff --git a/tests/e2e/security_groups_test.go b/tests/e2e/security_groups_test.go index 44ddb1de5..3914cb3af 100644 --- a/tests/e2e/security_groups_test.go +++ b/tests/e2e/security_groups_test.go @@ -14,42 +14,50 @@ var _ = Describe("Security Group", func() { resp *resty.Response securityGroupName string securityGroupGUID string + respResource securityGroupResource + resultErr cfErrs + err error ) - Describe("Create", func() { - var ( - respResource securityGroupResource - resultErr cfErrs - ) + BeforeEach(func() { + securityGroupName = generateGUID("create") + resultErr = cfErrs{} - BeforeEach(func() { - securityGroupName = generateGUID("create") - resultErr = cfErrs{} - }) + resp, err = adminClient.R(). + SetBody(securityGroupResource{ + Name: securityGroupName, + Rules: []payloads.SecurityGroupRule{ + { + Protocol: "tcp", + Ports: "80", + Destination: "192.168.1.1", + }, + }, + }). + SetResult(&respResource). + SetError(&resultErr). + Post("/v3/security_groups") + + Expect(err).NotTo(HaveOccurred()) + Expect(resp).To(HaveRestyStatusCode(http.StatusCreated)) + securityGroupGUID = respResource.GUID + }) + Describe("Get", func() { JustBeforeEach(func() { - var err error - resp, err = adminClient.R(). - SetBody(securityGroupResource{ - Name: securityGroupName, - Rules: []payloads.SecurityGroupRule{ - { - Protocol: "tcp", - Ports: "80", - Destination: "192.168.1.1", - }, - }, - }). - SetResult(&respResource). - SetError(&resultErr). - Post("/v3/security_groups") + resp, err = adminClient.R().SetResult(&respResource).Get("/v3/security_groups/" + securityGroupGUID) Expect(err).NotTo(HaveOccurred()) + }) - securityGroupGUID = respResource.GUID + It("retrieves the security group", func() { + Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) + Expect(respResource.GUID).To(Equal(securityGroupGUID)) + Expect(respResource.Name).To(Equal(securityGroupName)) }) + }) - It("creates the domain", func() { - Expect(resp).To(HaveRestyStatusCode(http.StatusCreated)) + Describe("Create", func() { + It("creates the security group", func() { Expect(respResource.GUID).To(Equal(securityGroupGUID)) Expect(respResource.Name).To(Equal(securityGroupName)) Expect(respResource.Rules).To(Equal([]payloads.SecurityGroupRule{ @@ -61,4 +69,33 @@ var _ = Describe("Security Group", func() { })) }) }) + + Describe("Bind", func() { + var ( + bindResp relationshipDataResource + spaceGUID string + ) + + JustBeforeEach(func() { + spaceGUID = createSpace(generateGUID("space1"), commonTestOrgGUID) + resp, err = adminClient.R(). + SetBody(relationshipDataResource{ + Data: []payloads.RelationshipData{{ + GUID: spaceGUID, + }}, + }). + SetResult(&bindResp). + SetError(&resultErr). + Post("/v3/security_groups/" + securityGroupGUID + "/relationships/running_spaces") + + Expect(err).NotTo(HaveOccurred()) + }) + + It("binds the space to the security group", func() { + Expect(resp).To(HaveRestyStatusCode(http.StatusOK)) + Expect(bindResp.Data).To(ContainElement(payloads.RelationshipData{ + GUID: spaceGUID, + })) + }) + }) })