api/handlers/space.go
package handlers
import (
"context"
"net/http"
"net/url"
"time"
"code.cloudfoundry.org/korifi/api/authorization"
apierrors "code.cloudfoundry.org/korifi/api/errors"
"code.cloudfoundry.org/korifi/api/payloads"
"code.cloudfoundry.org/korifi/api/presenter"
"code.cloudfoundry.org/korifi/api/repositories"
"code.cloudfoundry.org/korifi/api/routing"
"github.com/go-logr/logr"
)
const (
SpacesPath = "/v3/spaces"
SpacePath = "/v3/spaces/{guid}"
)
//counterfeiter:generate -o fake -fake-name CFSpaceRepository . CFSpaceRepository
type CFSpaceRepository interface {
CreateSpace(context.Context, authorization.Info, repositories.CreateSpaceMessage) (repositories.SpaceRecord, error)
ListSpaces(context.Context, authorization.Info, repositories.ListSpacesMessage) ([]repositories.SpaceRecord, error)
GetSpace(context.Context, authorization.Info, string) (repositories.SpaceRecord, error)
DeleteSpace(context.Context, authorization.Info, repositories.DeleteSpaceMessage) error
PatchSpaceMetadata(context.Context, authorization.Info, repositories.PatchSpaceMetadataMessage) (repositories.SpaceRecord, error)
GetDeletedAt(context.Context, authorization.Info, string) (*time.Time, error)
}
type Space struct {
spaceRepo CFSpaceRepository
apiBaseURL url.URL
requestValidator RequestValidator
}
func NewSpace(apiBaseURL url.URL, spaceRepo CFSpaceRepository, requestValidator RequestValidator) *Space {
return &Space{
apiBaseURL: apiBaseURL,
spaceRepo: spaceRepo,
requestValidator: requestValidator,
}
}
func (h *Space) create(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.space.create")
var payload payloads.SpaceCreate
if err := h.requestValidator.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return nil, apierrors.LogAndReturn(logger, err, "Failed to decode and validate payload")
}
space := payload.ToMessage()
record, err := h.spaceRepo.CreateSpace(r.Context(), authInfo, space)
if err != nil {
return nil, apierrors.LogAndReturn(
logger,
apierrors.AsUnprocessableEntity(err, "Invalid organization. Ensure the organization exists and you have access to it.", apierrors.NotFoundError{}),
"Failed to create space",
"Space Name", space.Name,
)
}
return routing.NewResponse(http.StatusCreated).WithBody(presenter.ForSpace(record, h.apiBaseURL)), nil
}
func (h *Space) list(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.space.list")
spaceList := new(payloads.SpaceList)
if err := h.requestValidator.DecodeAndValidateURLValues(r, spaceList); err != nil {
return nil, apierrors.LogAndReturn(logger, err, "Failed to decode and validate request values")
}
spaces, err := h.spaceRepo.ListSpaces(r.Context(), authInfo, spaceList.ToMessage())
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "Failed to fetch spaces")
}
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForList(presenter.ForSpace, spaces, h.apiBaseURL, *r.URL)), nil
}
//nolint:dupl
func (h *Space) update(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.space.update")
spaceGUID := routing.URLParam(r, "guid")
space, err := h.spaceRepo.GetSpace(r.Context(), authInfo, spaceGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "Failed to fetch org from Kubernetes", "GUID", spaceGUID)
}
var payload payloads.SpacePatch
if err = h.requestValidator.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return nil, apierrors.LogAndReturn(logger, err, "failed to decode payload")
}
space, err = h.spaceRepo.PatchSpaceMetadata(r.Context(), authInfo, payload.ToMessage(spaceGUID, space.OrganizationGUID))
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "Failed to patch space metadata", "GUID", spaceGUID)
}
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForSpace(space, h.apiBaseURL)), nil
}
func (h *Space) delete(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.space.delete")
spaceGUID := routing.URLParam(r, "guid")
spaceRecord, err := h.spaceRepo.GetSpace(r.Context(), authInfo, spaceGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "Failed to fetch space", "SpaceGUID", spaceGUID)
}
deleteSpaceMessage := repositories.DeleteSpaceMessage{
GUID: spaceRecord.GUID,
OrganizationGUID: spaceRecord.OrganizationGUID,
}
err = h.spaceRepo.DeleteSpace(r.Context(), authInfo, deleteSpaceMessage)
if err != nil {
return nil, apierrors.LogAndReturn(logger, err, "Failed to delete space", "SpaceGUID", spaceGUID)
}
return routing.NewResponse(http.StatusAccepted).WithHeader("Location", presenter.JobURLForRedirects(spaceGUID, presenter.SpaceDeleteOperation, h.apiBaseURL)), nil
}
func (h *Space) get(r *http.Request) (*routing.Response, error) {
authInfo, _ := authorization.InfoFromContext(r.Context())
logger := logr.FromContextOrDiscard(r.Context()).WithName("handlers.space.get")
spaceGUID := routing.URLParam(r, "guid")
space, err := h.spaceRepo.GetSpace(r.Context(), authInfo, spaceGUID)
if err != nil {
return nil, apierrors.LogAndReturn(logger, apierrors.ForbiddenAsNotFound(err), "Failed to fetch space", "spaceGUID", spaceGUID)
}
return routing.NewResponse(http.StatusOK).WithBody(presenter.ForSpace(space, h.apiBaseURL)), nil
}
func (h *Space) UnauthenticatedRoutes() []routing.Route {
return nil
}
func (h *Space) AuthenticatedRoutes() []routing.Route {
return []routing.Route{
{Method: "GET", Pattern: SpacesPath, Handler: h.list},
{Method: "POST", Pattern: SpacesPath, Handler: h.create},
{Method: "PATCH", Pattern: SpacePath, Handler: h.update},
{Method: "DELETE", Pattern: SpacePath, Handler: h.delete},
{Method: "GET", Pattern: SpacePath, Handler: h.get},
}
}