cloudfoundry-incubator/eirini

View on GitHub
k8s/stset/desire.go

Summary

Maintainability
A
3 hrs
Test Coverage
B
88%
package stset

import (
    "context"

    "code.cloudfoundry.org/eirini/api"
    "code.cloudfoundry.org/eirini/k8s/shared"
    "code.cloudfoundry.org/eirini/k8s/utils"
    "code.cloudfoundry.org/eirini/k8s/utils/dockerutils"
    "code.cloudfoundry.org/lager"
    "github.com/hashicorp/go-multierror"
    "github.com/pkg/errors"
    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    k8serrors "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

//counterfeiter:generate . SecretsClient
//counterfeiter:generate . StatefulSetCreator
//counterfeiter:generate . LRPToStatefulSetConverter
//counterfeiter:generate . PodDisruptionBudgetUpdater

type LRPToStatefulSetConverter interface {
    Convert(statefulSetName string, lrp *api.LRP, privateRegistrySecret *corev1.Secret) (*appsv1.StatefulSet, error)
}

type SecretsClient interface {
    Create(ctx context.Context, namespace string, secret *corev1.Secret) (*corev1.Secret, error)
    SetOwner(ctx context.Context, secret *corev1.Secret, owner metav1.Object) (*corev1.Secret, error)
    Delete(ctx context.Context, namespace string, name string) error
}

type StatefulSetCreator interface {
    Create(ctx context.Context, namespace string, statefulSet *appsv1.StatefulSet) (*appsv1.StatefulSet, error)
}

type PodDisruptionBudgetUpdater interface {
    Update(ctx context.Context, stset *appsv1.StatefulSet, lrp *api.LRP) error
}

type Desirer struct {
    logger                     lager.Logger
    secrets                    SecretsClient
    statefulSets               StatefulSetCreator
    lrpToStatefulSetConverter  LRPToStatefulSetConverter
    podDisruptionBudgetCreator PodDisruptionBudgetUpdater
}

func NewDesirer(
    logger lager.Logger,
    secrets SecretsClient,
    statefulSets StatefulSetCreator,
    lrpToStatefulSetConverter LRPToStatefulSetConverter,
    podDisruptionBudgetCreator PodDisruptionBudgetUpdater,
) Desirer {
    return Desirer{
        logger:                     logger,
        secrets:                    secrets,
        statefulSets:               statefulSets,
        lrpToStatefulSetConverter:  lrpToStatefulSetConverter,
        podDisruptionBudgetCreator: podDisruptionBudgetCreator,
    }
}

func (d *Desirer) Desire(ctx context.Context, namespace string, lrp *api.LRP, opts ...shared.Option) error {
    logger := d.logger.Session("desire", lager.Data{"guid": lrp.GUID, "version": lrp.Version, "namespace": namespace})

    statefulSetName, err := utils.GetStatefulsetName(lrp)
    if err != nil {
        return err
    }

    privateRegistrySecret, err := d.createRegistryCredsSecretIfRequired(ctx, namespace, lrp)
    if err != nil {
        return err
    }

    st, err := d.lrpToStatefulSetConverter.Convert(statefulSetName, lrp, privateRegistrySecret)
    if err != nil {
        return err
    }

    st.Namespace = namespace

    err = shared.ApplyOpts(st, opts...)
    if err != nil {
        return err
    }

    stSet, err := d.statefulSets.Create(ctx, namespace, st)
    if err != nil {
        var statusErr *k8serrors.StatusError
        if errors.As(err, &statusErr) && statusErr.Status().Reason == metav1.StatusReasonAlreadyExists {
            logger.Debug("statefulset-already-exists", lager.Data{"error": err.Error()})

            return nil
        }

        return d.cleanupAndError(ctx, errors.Wrap(err, "failed to create statefulset"), privateRegistrySecret)
    }

    if err := d.setSecretOwner(ctx, privateRegistrySecret, stSet); err != nil {
        logger.Error("failed-to-set-owner-to-the-registry-secret", err)

        return errors.Wrap(err, "failed to set owner to the registry secret")
    }

    if err := d.podDisruptionBudgetCreator.Update(ctx, stSet, lrp); err != nil {
        logger.Error("failed-to-create-pod-disruption-budget", err)

        return errors.Wrap(err, "failed to create pod disruption budget")
    }

    return nil
}

func (d *Desirer) setSecretOwner(ctx context.Context, privateRegistrySecret *corev1.Secret, stSet *appsv1.StatefulSet) error {
    if privateRegistrySecret == nil {
        return nil
    }

    _, err := d.secrets.SetOwner(ctx, privateRegistrySecret, stSet)

    return err
}

func (d *Desirer) createRegistryCredsSecretIfRequired(ctx context.Context, namespace string, lrp *api.LRP) (*corev1.Secret, error) {
    if lrp.PrivateRegistry == nil {
        return nil, nil // nolint:nilnil
    }

    secret, err := generateRegistryCredsSecret(lrp)
    if err != nil {
        return nil, errors.Wrap(err, "failed to generate private registry secret for statefulset")
    }

    secret, err = d.secrets.Create(ctx, namespace, secret)

    return secret, errors.Wrap(err, "failed to create private registry secret for statefulset")
}

func (d *Desirer) cleanupAndError(ctx context.Context, stsetCreationError error, privateRegistrySecret *corev1.Secret) error {
    resultError := multierror.Append(nil, stsetCreationError)

    if privateRegistrySecret != nil {
        err := d.secrets.Delete(ctx, privateRegistrySecret.Namespace, privateRegistrySecret.Name)
        if err != nil {
            resultError = multierror.Append(resultError, errors.Wrap(err, "failed to cleanup registry secret"))
        }
    }

    return resultError
}

func generateRegistryCredsSecret(lrp *api.LRP) (*corev1.Secret, error) {
    dockerConfig := dockerutils.NewDockerConfig(
        lrp.PrivateRegistry.Server,
        lrp.PrivateRegistry.Username,
        lrp.PrivateRegistry.Password,
    )

    dockerConfigJSON, err := dockerConfig.JSON()
    if err != nil {
        return nil, errors.Wrap(err, "failed to generate privete registry config")
    }

    return &corev1.Secret{
        ObjectMeta: metav1.ObjectMeta{
            GenerateName: PrivateRegistrySecretGenerateName,
        },
        Type: corev1.SecretTypeDockerConfigJson,
        StringData: map[string]string{
            dockerutils.DockerConfigKey: dockerConfigJSON,
        },
    }, nil
}