api/repositories/deployment_repository.go
package repositories
import (
"context"
"fmt"
"strconv"
"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"
"code.cloudfoundry.org/korifi/version"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/meta"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const DeploymentResourceType = "Deployment"
type DeploymentRepo struct {
userClientFactory authorization.UserK8sClientFactory
namespaceRetriever NamespaceRetriever
}
type DeploymentRecord struct {
GUID string
CreatedAt time.Time
UpdatedAt *time.Time
DropletGUID string
Status DeploymentStatus
}
type DeploymentStatusValue string
const (
DeploymentStatusValueActive DeploymentStatusValue = "ACTIVE"
DeploymentStatusValueFinalized DeploymentStatusValue = "FINALIZED"
)
type DeploymentStatusReason string
const (
DeploymentStatusReasonDeploying DeploymentStatusReason = "DEPLOYING"
DeploymentStatusReasonDeployed DeploymentStatusReason = "DEPLOYED"
)
type DeploymentStatus struct {
Value DeploymentStatusValue
Reason DeploymentStatusReason
}
type CreateDeploymentMessage struct {
AppGUID string
DropletGUID string
}
func NewDeploymentRepo(
userClientFactory authorization.UserK8sClientFactory,
namespaceRetriever NamespaceRetriever,
) *DeploymentRepo {
return &DeploymentRepo{
userClientFactory: userClientFactory,
namespaceRetriever: namespaceRetriever,
}
}
func (r *DeploymentRepo) GetDeployment(ctx context.Context, authInfo authorization.Info, deploymentGUID string) (DeploymentRecord, error) {
ns, err := r.namespaceRetriever.NamespaceFor(ctx, deploymentGUID, AppResourceType)
if err != nil {
return DeploymentRecord{}, err
}
userClient, err := r.userClientFactory.BuildClient(authInfo)
if err != nil {
return DeploymentRecord{}, fmt.Errorf("get-deployment failed to create user client: %w", err)
}
app := &korifiv1alpha1.CFApp{}
err = userClient.Get(ctx, client.ObjectKey{Namespace: ns, Name: deploymentGUID}, app)
if err != nil {
return DeploymentRecord{}, apierrors.FromK8sError(err, DeploymentResourceType)
}
return appToDeploymentRecord(app), nil
}
func (r *DeploymentRepo) CreateDeployment(ctx context.Context, authInfo authorization.Info, message CreateDeploymentMessage) (DeploymentRecord, error) {
ns, err := r.namespaceRetriever.NamespaceFor(ctx, message.AppGUID, AppResourceType)
if err != nil {
return DeploymentRecord{}, err
}
userClient, err := r.userClientFactory.BuildClient(authInfo)
if err != nil {
return DeploymentRecord{}, fmt.Errorf("create-deployment failed to create user client: %w", err)
}
app := &korifiv1alpha1.CFApp{}
err = userClient.Get(ctx, client.ObjectKey{Namespace: ns, Name: message.AppGUID}, app)
if err != nil {
return DeploymentRecord{}, apierrors.FromK8sError(err, DeploymentResourceType)
}
if err = ensureSupport(ctx, userClient, app); err != nil {
return DeploymentRecord{}, err
}
dropletGUID := app.Spec.CurrentDropletRef.Name
if message.DropletGUID != "" {
dropletGUID = message.DropletGUID
}
appRev := app.Annotations[korifiv1alpha1.CFAppRevisionKey]
newRev, err := bumpAppRev(appRev)
if err != nil {
return DeploymentRecord{}, fmt.Errorf("expected app-rev to be an integer: %w", err)
}
err = k8s.PatchResource(ctx, userClient, app, func() {
app.Spec.CurrentDropletRef.Name = dropletGUID
if app.Annotations == nil {
app.Annotations = map[string]string{}
}
app.Annotations[korifiv1alpha1.CFAppRevisionKey] = newRev
app.Spec.DesiredState = korifiv1alpha1.StartedState
})
if err != nil {
return DeploymentRecord{}, apierrors.FromK8sError(err, DeploymentResourceType)
}
return appToDeploymentRecord(app), nil
}
func bumpAppRev(appRev string) (string, error) {
r, err := strconv.Atoi(appRev)
if err != nil {
return "", err
}
return strconv.Itoa(r + 1), nil
}
func appToDeploymentRecord(cfApp *korifiv1alpha1.CFApp) DeploymentRecord {
deploymentRecord := DeploymentRecord{
GUID: cfApp.Name,
CreatedAt: cfApp.CreationTimestamp.Time,
UpdatedAt: getLastUpdatedTime(cfApp),
DropletGUID: cfApp.Spec.CurrentDropletRef.Name,
Status: DeploymentStatus{
Value: DeploymentStatusValueActive,
Reason: DeploymentStatusReasonDeploying,
},
}
if meta.IsStatusConditionTrue(cfApp.Status.Conditions, korifiv1alpha1.StatusConditionReady) {
deploymentRecord.Status = DeploymentStatus{
Value: DeploymentStatusValueFinalized,
Reason: DeploymentStatusReasonDeployed,
}
}
return deploymentRecord
}
func ensureSupport(ctx context.Context, userClient client.Client, app *korifiv1alpha1.CFApp) error {
log := logr.FromContextOrDiscard(ctx).WithName("repo.deployment.ensureSupport")
var appWorkloadsList korifiv1alpha1.AppWorkloadList
err := userClient.List(ctx, &appWorkloadsList, client.InNamespace(app.Namespace), client.MatchingLabels{
korifiv1alpha1.CFAppGUIDLabelKey: app.Name,
})
if err != nil {
return apierrors.FromK8sError(err, DeploymentResourceType)
}
checker := version.NewChecker("v0.7.1")
for i := range appWorkloadsList.Items {
appWorkload := appWorkloadsList.Items[i]
newer, err := checker.ObjectIsNewer(&appWorkload)
if newer {
continue
}
if err != nil {
log.Info("failed comparining version of appWorkload",
"ns", appWorkload.Namespace,
"name", appWorkload.Name,
"version", appWorkload.Annotations[version.KorifiCreationVersionKey],
)
}
return apierrors.NewUnprocessableEntityError(nil, "App instances created with an older version of Korifi can't use the rolling strategy. Please restart/restage/re-push app before using the rolling strategy")
}
return nil
}