github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/dockercomposeservice/logs.go (about)

     1  package dockercomposeservice
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	apierrors "k8s.io/apimachinery/pkg/api/errors"
     9  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    10  	"k8s.io/apimachinery/pkg/types"
    11  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    12  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    13  
    14  	"github.com/tilt-dev/tilt/internal/controllers/apicmp"
    15  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    16  )
    17  
    18  // Each DockerComposeService object owns a DockerComposeLogStream object of the same name.
    19  //
    20  // NOTE(nick): I think this might be the wrong API. Currently we model DockerCompose as two objects:
    21  //
    22  //  1. A service object with a Spec that tells us how to create the service and
    23  //     a Status that contains the container state
    24  //  2. A log object with a Spec that tells us how to watch logs.
    25  //
    26  // I think the better way to model this would be to more clearly separate "spinning up a service"
    27  // from "watching a service", so we'd actually have:
    28  //
    29  //  1. A service object with a Spec that tells us how to create the service and
    30  //     a Status that tells us whether the creation succeeded.
    31  //  2. A monitor object with a status that contains both the container state and the log state.
    32  //
    33  // But moving to this system will be easier once everything is in the API server.
    34  func (r *Reconciler) manageOwnedLogStream(ctx context.Context, nn types.NamespacedName, obj *v1alpha1.DockerComposeService) (reconcile.Result, error) {
    35  	if obj != nil && (obj.Status.ApplyError != "" || obj.Status.ContainerID == "") {
    36  		isDisabled := obj.Status.DisableStatus != nil &&
    37  			obj.Status.DisableStatus.State == v1alpha1.DisableStateDisabled
    38  		if !isDisabled {
    39  			// If the DockerCompose is in an error state or hasn't deployed anything,
    40  			// don't reconcile the log object.
    41  			return reconcile.Result{}, nil
    42  		}
    43  	}
    44  
    45  	var existing v1alpha1.DockerComposeLogStream
    46  	err := r.ctrlClient.Get(ctx, nn, &existing)
    47  	isNotFound := apierrors.IsNotFound(err)
    48  	if err != nil && !isNotFound {
    49  		return reconcile.Result{},
    50  			fmt.Errorf("failed to fetch managed DockerComposeLogStream objects for DockerComposeService %s: %v",
    51  				nn.Name, err)
    52  	}
    53  
    54  	desired, err := r.toDesiredLogStream(obj)
    55  	if err != nil {
    56  		return reconcile.Result{}, fmt.Errorf("generating dockercomposelogstream: %v", err)
    57  	}
    58  
    59  	if isNotFound {
    60  		if desired == nil {
    61  			return reconcile.Result{}, nil // Nothing to do.
    62  		}
    63  
    64  		err := r.ctrlClient.Create(ctx, desired)
    65  		if err != nil {
    66  			if apierrors.IsAlreadyExists(err) {
    67  				return reconcile.Result{RequeueAfter: time.Second}, nil
    68  			}
    69  			return reconcile.Result{}, fmt.Errorf("creating dockercomposelogstream: %v", err)
    70  		}
    71  		return reconcile.Result{}, nil
    72  	}
    73  
    74  	if desired == nil {
    75  		err := r.ctrlClient.Delete(ctx, &existing)
    76  		if err != nil && !apierrors.IsNotFound(err) {
    77  			return reconcile.Result{}, fmt.Errorf("deleting dockercomposelogstream: %v", err)
    78  		}
    79  		return reconcile.Result{}, nil
    80  	}
    81  
    82  	if !apicmp.DeepEqual(existing.Spec, desired.Spec) ||
    83  		!apicmp.DeepEqual(existing.ObjectMeta.Annotations, desired.ObjectMeta.Annotations) {
    84  		existing = *existing.DeepCopy()
    85  		existing.ObjectMeta.Annotations = desired.ObjectMeta.Annotations
    86  		existing.Spec = desired.Spec
    87  		err = r.ctrlClient.Update(ctx, &existing)
    88  		if err != nil {
    89  			if apierrors.IsConflict(err) {
    90  				return reconcile.Result{RequeueAfter: time.Second}, nil
    91  			}
    92  			return reconcile.Result{}, fmt.Errorf("updating dockercomposelogstream: %v", err)
    93  		}
    94  	}
    95  
    96  	return reconcile.Result{}, nil
    97  }
    98  
    99  // Construct the desired DockerComposeLogStream
   100  func (r *Reconciler) toDesiredLogStream(obj *v1alpha1.DockerComposeService) (*v1alpha1.DockerComposeLogStream, error) {
   101  	if obj == nil {
   102  		return nil, nil
   103  	}
   104  
   105  	if obj.Status.DisableStatus != nil && obj.Status.DisableStatus.State == v1alpha1.DisableStateDisabled {
   106  		return nil, nil
   107  	}
   108  
   109  	desired := &v1alpha1.DockerComposeLogStream{
   110  		ObjectMeta: metav1.ObjectMeta{
   111  			Name: obj.Name,
   112  			Annotations: map[string]string{
   113  				v1alpha1.AnnotationManifest: obj.Annotations[v1alpha1.AnnotationManifest],
   114  				v1alpha1.AnnotationSpanID:   obj.Annotations[v1alpha1.AnnotationSpanID],
   115  			},
   116  		},
   117  		Spec: v1alpha1.DockerComposeLogStreamSpec{
   118  			Service: obj.Spec.Service,
   119  			Project: obj.Spec.Project,
   120  		},
   121  	}
   122  
   123  	err := controllerutil.SetControllerReference(obj, desired, r.ctrlClient.Scheme())
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	return desired, nil
   128  }