cloudfoundry/cf-k8s-controllers

View on GitHub
api/repositories/space_repository.go

Summary

Maintainability
A
0 mins
Test Coverage
package repositories

import (
    "context"
    "fmt"
    "time"

    "code.cloudfoundry.org/korifi/api/authorization"
    apierrors "code.cloudfoundry.org/korifi/api/errors"
    korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1"
    "code.cloudfoundry.org/korifi/tools/k8s"

    "github.com/google/uuid"
    k8serrors "k8s.io/apimachinery/pkg/api/errors"
    "k8s.io/apimachinery/pkg/api/meta"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

const (
    SpacePrefix       = "cf-space-"
    SpaceResourceType = "Space"
)

type CreateSpaceMessage struct {
    Name             string
    OrganizationGUID string
}

type ListSpacesMessage struct {
    Names             []string
    GUIDs             []string
    OrganizationGUIDs []string
}

type DeleteSpaceMessage struct {
    GUID             string
    OrganizationGUID string
}

type PatchSpaceMetadataMessage struct {
    MetadataPatch
    GUID    string
    OrgGUID string
}

type SpaceRecord struct {
    Name             string
    GUID             string
    OrganizationGUID string
    Labels           map[string]string
    Annotations      map[string]string
    CreatedAt        time.Time
    UpdatedAt        *time.Time
    DeletedAt        *time.Time
}

type SpaceRepo struct {
    orgRepo            *OrgRepo
    namespaceRetriever NamespaceRetriever
    userClientFactory  authorization.UserK8sClientFactory
    nsPerms            *authorization.NamespacePermissions
    conditionAwaiter   Awaiter[*korifiv1alpha1.CFSpace]
}

func NewSpaceRepo(
    namespaceRetriever NamespaceRetriever,
    orgRepo *OrgRepo,
    userClientFactory authorization.UserK8sClientFactory,
    nsPerms *authorization.NamespacePermissions,
    conditionAwaiter Awaiter[*korifiv1alpha1.CFSpace],
) *SpaceRepo {
    return &SpaceRepo{
        orgRepo:            orgRepo,
        namespaceRetriever: namespaceRetriever,
        userClientFactory:  userClientFactory,
        nsPerms:            nsPerms,
        conditionAwaiter:   conditionAwaiter,
    }
}

func (r *SpaceRepo) CreateSpace(ctx context.Context, info authorization.Info, message CreateSpaceMessage) (SpaceRecord, error) {
    _, err := r.orgRepo.GetOrg(ctx, info, message.OrganizationGUID)
    if err != nil {
        return SpaceRecord{}, fmt.Errorf("failed to get parent organization: %w", err)
    }

    userClient, err := r.userClientFactory.BuildClient(info)
    if err != nil {
        return SpaceRecord{}, fmt.Errorf("failed to build user client: %w", err)
    }

    cfSpace := &korifiv1alpha1.CFSpace{
        ObjectMeta: metav1.ObjectMeta{
            Name:      SpacePrefix + uuid.NewString(),
            Namespace: message.OrganizationGUID,
        },
        Spec: korifiv1alpha1.CFSpaceSpec{
            DisplayName: message.Name,
        },
    }
    err = userClient.Create(ctx, cfSpace)
    if err != nil {
        return SpaceRecord{}, apierrors.FromK8sError(err, SpaceResourceType)
    }

    cfSpace, err = r.conditionAwaiter.AwaitCondition(ctx, userClient, cfSpace, korifiv1alpha1.StatusConditionReady)
    if err != nil {
        return SpaceRecord{}, apierrors.FromK8sError(err, SpaceResourceType)
    }

    return cfSpaceToSpaceRecord(cfSpace), nil
}

func (r *SpaceRepo) ListSpaces(ctx context.Context, info authorization.Info, message ListSpacesMessage) ([]SpaceRecord, error) {
    userClient, err := r.userClientFactory.BuildClient(info)
    if err != nil {
        return []SpaceRecord{}, fmt.Errorf("failed to build user client: %w", err)
    }

    authorizedOrgNamespaces, err := r.nsPerms.GetAuthorizedOrgNamespaces(ctx, info)
    if err != nil {
        return nil, err
    }

    cfSpaces := []korifiv1alpha1.CFSpace{}

    authorizedSpaceNamespaces, err := r.nsPerms.GetAuthorizedSpaceNamespaces(ctx, info)
    if err != nil {
        return nil, err
    }

    preds := []func(korifiv1alpha1.CFSpace) bool{
        func(s korifiv1alpha1.CFSpace) bool { return authorizedSpaceNamespaces[s.Name] },
        func(s korifiv1alpha1.CFSpace) bool {
            return meta.IsStatusConditionTrue(s.Status.Conditions, korifiv1alpha1.StatusConditionReady)
        },
        SetPredicate(message.GUIDs, func(s korifiv1alpha1.CFSpace) string { return s.Name }),
        SetPredicate(message.Names, func(s korifiv1alpha1.CFSpace) string { return s.Spec.DisplayName }),
    }

    orgGUIDs := NewSet(message.OrganizationGUIDs...)
    for org := range authorizedOrgNamespaces {
        if len(orgGUIDs) > 0 && !orgGUIDs.Includes(org) {
            continue
        }

        cfSpaceList := new(korifiv1alpha1.CFSpaceList)

        err = userClient.List(ctx, cfSpaceList, client.InNamespace(org))
        if k8serrors.IsForbidden(err) {
            continue
        }
        if err != nil {
            return nil, apierrors.FromK8sError(err, SpaceResourceType)
        }

        cfSpaces = append(cfSpaces, Filter(cfSpaceList.Items, preds...)...)
    }

    var records []SpaceRecord
    for i := range cfSpaces {
        records = append(records, cfSpaceToSpaceRecord(&cfSpaces[i]))
    }

    return records, nil
}

func (r *SpaceRepo) GetSpace(ctx context.Context, info authorization.Info, spaceGUID string) (SpaceRecord, error) {
    ns, err := r.namespaceRetriever.NamespaceFor(ctx, spaceGUID, SpaceResourceType)
    if err != nil {
        return SpaceRecord{}, err
    }

    userClient, err := r.userClientFactory.BuildClient(info)
    if err != nil {
        return SpaceRecord{}, fmt.Errorf("get-space failed to build user client: %w", err)
    }

    cfSpace := &korifiv1alpha1.CFSpace{}
    err = userClient.Get(ctx, client.ObjectKey{Namespace: ns, Name: spaceGUID}, cfSpace)
    if err != nil {
        return SpaceRecord{}, fmt.Errorf("failed to get space: %w", apierrors.FromK8sError(err, SpaceResourceType))
    }

    return cfSpaceToSpaceRecord(cfSpace), nil
}

func cfSpaceToSpaceRecord(cfSpace *korifiv1alpha1.CFSpace) SpaceRecord {
    return SpaceRecord{
        Name:             cfSpace.Spec.DisplayName,
        GUID:             cfSpace.Name,
        OrganizationGUID: cfSpace.Namespace,
        Annotations:      cfSpace.Annotations,
        Labels:           cfSpace.Labels,
        CreatedAt:        cfSpace.CreationTimestamp.Time,
        UpdatedAt:        getLastUpdatedTime(cfSpace),
        DeletedAt:        golangTime(cfSpace.DeletionTimestamp),
    }
}

func (r *SpaceRepo) DeleteSpace(ctx context.Context, info authorization.Info, message DeleteSpaceMessage) error {
    userClient, err := r.userClientFactory.BuildClient(info)
    if err != nil {
        return fmt.Errorf("failed to build user client: %w", err)
    }

    err = userClient.Delete(ctx, &korifiv1alpha1.CFSpace{
        ObjectMeta: metav1.ObjectMeta{
            Name:      message.GUID,
            Namespace: message.OrganizationGUID,
        },
    })

    return apierrors.FromK8sError(err, SpaceResourceType)
}

func (r *SpaceRepo) PatchSpaceMetadata(ctx context.Context, authInfo authorization.Info, message PatchSpaceMetadataMessage) (SpaceRecord, error) {
    userClient, err := r.userClientFactory.BuildClient(authInfo)
    if err != nil {
        return SpaceRecord{}, fmt.Errorf("failed to build user client: %w", err)
    }

    cfSpace := new(korifiv1alpha1.CFSpace)
    err = userClient.Get(ctx, client.ObjectKey{Namespace: message.OrgGUID, Name: message.GUID}, cfSpace)
    if err != nil {
        return SpaceRecord{}, fmt.Errorf("failed to get space: %w", apierrors.FromK8sError(err, SpaceResourceType))
    }

    err = k8s.PatchResource(ctx, userClient, cfSpace, func() {
        message.Apply(cfSpace)
    })
    if err != nil {
        return SpaceRecord{}, apierrors.FromK8sError(err, SpaceResourceType)
    }

    return cfSpaceToSpaceRecord(cfSpace), nil
}

func (r *SpaceRepo) GetDeletedAt(ctx context.Context, authInfo authorization.Info, spaceGUID string) (*time.Time, error) {
    space, err := r.GetSpace(ctx, authInfo, spaceGUID)
    if err != nil {
        return nil, err
    }
    return space.DeletedAt, nil
}