Skip to content

Commit

Permalink
Implement space deletion job
Browse files Browse the repository at this point in the history
- Refactor org deletion helper to handle all types.

[#2604]

Co-authored-by: Dave Walter <walterda@vmware.com>
  • Loading branch information
acosta11 and davewalter committed Jun 30, 2023
1 parent 4be9c86 commit 9b4ff9f
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 25 deletions.
67 changes: 45 additions & 22 deletions api/handlers/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"code.cloudfoundry.org/korifi/api/authorization"
apierrors "code.cloudfoundry.org/korifi/api/errors"
"code.cloudfoundry.org/korifi/api/presenter"
"code.cloudfoundry.org/korifi/api/repositories"
"code.cloudfoundry.org/korifi/api/routing"

"github.com/go-logr/logr"
Expand All @@ -34,12 +35,14 @@ const JobResourceType = "Job"
type Job struct {
serverURL url.URL
orgRepo CFOrgRepository
spaceRepo CFSpaceRepository
}

func NewJob(serverURL url.URL, orgRepo CFOrgRepository) *Job {
func NewJob(serverURL url.URL, orgRepo CFOrgRepository, spaceRepo CFSpaceRepository) *Job {
return &Job{
serverURL: serverURL,
orgRepo: orgRepo,
spaceRepo: spaceRepo,
}
}

Expand All @@ -66,10 +69,10 @@ func (h *Job) get(r *http.Request) (*routing.Response, error) {
switch jobType {
case syncSpacePrefix:
jobResponse = presenter.ForManifestApplyJob(jobGUID, resourceGUID, h.serverURL)
case appDeletePrefix, spaceDeletePrefix, routeDeletePrefix, domainDeletePrefix, roleDeletePrefix:
case appDeletePrefix, routeDeletePrefix, domainDeletePrefix, roleDeletePrefix:
jobResponse = presenter.ForJob(jobGUID, []presenter.JobResponseError{}, presenter.StateComplete, jobType, h.serverURL)
case orgDeletePrefix:
jobResponse, err = h.handleOrgDelete(r.Context(), resourceGUID, jobGUID)
case orgDeletePrefix, spaceDeletePrefix:
jobResponse, err = h.handleDeleteJob(r.Context(), resourceGUID, jobGUID, jobType)
if err != nil {
return nil, err
}
Expand All @@ -84,49 +87,69 @@ func (h *Job) get(r *http.Request) (*routing.Response, error) {
return routing.NewResponse(http.StatusOK).WithBody(jobResponse), nil
}

func (h *Job) handleOrgDelete(ctx context.Context, resourceGUID, jobGUID string) (presenter.JobResponse, error) {
func (h *Job) handleDeleteJob(ctx context.Context, resourceGUID, jobGUID, jobType string) (presenter.JobResponse, error) {
authInfo, _ := authorization.InfoFromContext(ctx)
log := logr.FromContextOrDiscard(ctx).WithName("handlers.job.get.handleOrgDelete")
log := logr.FromContextOrDiscard(ctx).WithName("handlers.job.get.handleDeleteJob")

var (
org repositories.OrgRecord
space repositories.SpaceRecord
err error
resourceName string
resourceType string
deletedAt string
)

switch jobType {
case orgDeletePrefix:
org, err = h.orgRepo.GetOrg(ctx, authInfo, resourceGUID)
resourceName = org.Name
resourceType = "Org"
deletedAt = org.DeletedAt
case spaceDeletePrefix:
space, err = h.spaceRepo.GetSpace(ctx, authInfo, resourceGUID)
resourceName = space.Name
resourceType = "Space"
deletedAt = space.DeletedAt
}

org, err := h.orgRepo.GetOrg(ctx, authInfo, resourceGUID)
if err != nil {
switch err.(type) {
case apierrors.NotFoundError, apierrors.ForbiddenError:
return presenter.ForJob(
jobGUID,
[]presenter.JobResponseError{},
presenter.StateComplete,
orgDeletePrefix,
jobType,
h.serverURL,
), nil
default:
return presenter.JobResponse{}, apierrors.LogAndReturn(
log,
apierrors.ForbiddenAsNotFound(err),
"failed to fetch org from Kubernetes",
"OrgGUID", resourceGUID,
err,
"failed to fetch "+resourceType+" from Kubernetes",
resourceType+"GUID", resourceGUID,
)
}
}

// This logic can be refactored into a generic helper for all resource types.
if org.DeletedAt == "" {
if deletedAt == "" {
return presenter.JobResponse{}, apierrors.LogAndReturn(
log,
apierrors.NewNotFoundError(fmt.Errorf("job %q not found", jobGUID), JobResourceType),
"org not marked for deletion",
"OrgGUID", resourceGUID,
resourceType+" not marked for deletion",
resourceType+"GUID", resourceGUID,
)
}

deletionTime, err := time.Parse(time.RFC3339Nano, org.DeletedAt)
deletionTime, err := time.Parse(time.RFC3339Nano, deletedAt)
if err != nil {
return presenter.JobResponse{}, apierrors.LogAndReturn(
log,
err,
"failed to parse org deletion time",
"name", org.Name,
"timestamp", org.DeletedAt,
"failed to parse "+resourceType+" deletion time",
"name", resourceName,
"timestamp", deletedAt,
)
}

Expand All @@ -135,19 +158,19 @@ func (h *Job) handleOrgDelete(ctx context.Context, resourceGUID, jobGUID string)
jobGUID,
[]presenter.JobResponseError{},
presenter.StateProcessing,
orgDeletePrefix,
jobType,
h.serverURL,
), nil
} else {
return presenter.ForJob(
jobGUID,
[]presenter.JobResponseError{{
Code: 10008,
Detail: fmt.Sprintf("CFOrg deletion timed out. Check for lingering resources in the %q namespace", org.GUID),
Detail: fmt.Sprintf("%s deletion timed out. Check for remaining resources in the %q namespace", resourceType, resourceGUID),
Title: "CF-UnprocessableEntity",
}},
presenter.StateFailed,
orgDeletePrefix,
jobType,
h.serverURL,
), nil
}
Expand Down
109 changes: 106 additions & 3 deletions api/handlers/job_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ var _ = Describe("Job", func() {
jobGUID string
req *http.Request
orgRepo *fake.OrgRepository
spaceRepo *fake.SpaceRepository
)

BeforeEach(func() {
spaceGUID = uuid.NewString()

orgRepo = new(fake.OrgRepository)
apiHandler := handlers.NewJob(*serverURL, orgRepo)
spaceRepo = new(fake.SpaceRepository)
apiHandler := handlers.NewJob(*serverURL, orgRepo, spaceRepo)
routerBuilder.LoadRoutes(apiHandler)
})

Expand Down Expand Up @@ -73,7 +75,6 @@ var _ = Describe("Job", func() {
)))
},
Entry("app delete", "app.delete", "cf-app-guid"),
Entry("space delete", "space.delete", "cf-space-guid"),
Entry("route delete", "route.delete", "cf-route-guid"),
Entry("domain delete", "domain.delete", "cf-domain-guid"),
Entry("role delete", "role.delete", "cf-role-guid"),
Expand Down Expand Up @@ -150,7 +151,7 @@ var _ = Describe("Job", func() {
MatchJSONPath("$.state", "FAILED"),
MatchJSONPath("$.errors", ConsistOf(map[string]interface{}{
"code": float64(10008),
"detail": fmt.Sprintf("CFOrg deletion timed out. Check for lingering resources in the %q namespace", resourceGUID),
"detail": fmt.Sprintf("Org deletion timed out. Check for remaining resources in the %q namespace", resourceGUID),
"title": "CF-UnprocessableEntity",
})),
)))
Expand Down Expand Up @@ -190,5 +191,107 @@ var _ = Describe("Job", func() {
})
})
})

Describe("space delete", func() {
const (
operation = "space.delete"
resourceGUID = "cf-space-guid"
)

BeforeEach(func() {
jobGUID = operation + "~" + resourceGUID
})

When("the space deletion is in progress", func() {
BeforeEach(func() {
spaceRepo.GetSpaceReturns(repositories.SpaceRecord{
GUID: "cf-space-guid",
DeletedAt: time.Now().Format(time.RFC3339Nano),
}, nil)
})

It("returns a processing status", func() {
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.guid", jobGUID),
MatchJSONPath("$.links.self.href", defaultServerURL+"/v3/jobs/"+jobGUID),
MatchJSONPath("$.operation", operation),
MatchJSONPath("$.state", "PROCESSING"),
MatchJSONPath("$.errors", BeEmpty()),
)))
})
})

When("the space does not exist", func() {
BeforeEach(func() {
spaceRepo.GetSpaceReturns(repositories.SpaceRecord{}, apierrors.NewNotFoundError(nil, repositories.SpaceResourceType))
})

It("returns a complete status", func() {
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.guid", jobGUID),
MatchJSONPath("$.links.self.href", defaultServerURL+"/v3/jobs/"+jobGUID),
MatchJSONPath("$.operation", operation),
MatchJSONPath("$.state", "COMPLETE"),
MatchJSONPath("$.errors", BeEmpty()),
)))
})
})

When("the space deletion times out", func() {
BeforeEach(func() {
spaceRepo.GetSpaceReturns(repositories.SpaceRecord{
GUID: "cf-space-guid",
DeletedAt: (time.Now().Add(-180 * time.Second)).Format(time.RFC3339Nano),
}, nil)
})

It("returns a failed status", func() {
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.guid", jobGUID),
MatchJSONPath("$.links.self.href", defaultServerURL+"/v3/jobs/"+jobGUID),
MatchJSONPath("$.operation", operation),
MatchJSONPath("$.state", "FAILED"),
MatchJSONPath("$.errors", ConsistOf(map[string]interface{}{
"code": float64(10008),
"detail": fmt.Sprintf("Space deletion timed out. Check for remaining resources in the %q namespace", resourceGUID),
"title": "CF-UnprocessableEntity",
})),
)))
})
})

When("the user does not have permission to see the space", func() {
BeforeEach(func() {
spaceRepo.GetSpaceReturns(repositories.SpaceRecord{}, apierrors.NewForbiddenError(nil, repositories.SpaceResourceType))
})

It("returns a complete status", func() {
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.guid", jobGUID),
MatchJSONPath("$.links.self.href", defaultServerURL+"/v3/jobs/"+jobGUID),
MatchJSONPath("$.operation", operation),
MatchJSONPath("$.state", "COMPLETE"),
MatchJSONPath("$.errors", BeEmpty()),
)))
})
})

When("the space has not been marked for deletion", func() {
BeforeEach(func() {
spaceRepo.GetSpaceReturns(repositories.SpaceRecord{
GUID: resourceGUID,
}, nil)
})

It("returns a not found error", func() {
Expect(rr).To(HaveHTTPStatus(http.StatusNotFound))
Expect(rr).To(HaveHTTPBody(SatisfyAll(
MatchJSONPath("$.errors[0].code", float64(10010)),
MatchJSONPath("$.errors[0].detail", "Job not found. Ensure it exists and you have access to it."),
MatchJSONPath("$.errors[0].title", "CF-ResourceNotFound"),
)))
})
})
})
})
})
1 change: 1 addition & 0 deletions api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ func main() {
handlers.NewJob(
*serverURL,
orgRepo,
spaceRepo,
),
handlers.NewLogCache(
appRepo,
Expand Down
7 changes: 7 additions & 0 deletions api/repositories/space_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type SpaceRecord struct {
Annotations map[string]string
CreatedAt string
UpdatedAt string
DeletedAt string
}

type SpaceRepo struct {
Expand Down Expand Up @@ -236,6 +237,11 @@ func (r *SpaceRepo) GetSpace(ctx context.Context, info authorization.Info, space

func cfSpaceToSpaceRecord(cfSpace korifiv1alpha1.CFSpace) SpaceRecord {
updatedAtTime, _ := getTimeLastUpdatedTimestamp(&cfSpace.ObjectMeta)
deletedAtTime := ""
if cfSpace.DeletionTimestamp != nil {
deletedAtTime = formatTimestamp(*cfSpace.DeletionTimestamp)
}

return SpaceRecord{
Name: cfSpace.Spec.DisplayName,
GUID: cfSpace.Name,
Expand All @@ -244,6 +250,7 @@ func cfSpaceToSpaceRecord(cfSpace korifiv1alpha1.CFSpace) SpaceRecord {
Labels: cfSpace.Labels,
CreatedAt: formatTimestamp(cfSpace.CreationTimestamp),
UpdatedAt: updatedAtTime,
DeletedAt: deletedAtTime,
}
}

Expand Down

0 comments on commit 9b4ff9f

Please sign in to comment.