github.com/grahambrereton-form3/tilt@v0.10.18/internal/engine/image_build_and_deployer.go (about)

     1  package engine
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os/exec"
     8  	"time"
     9  
    10  	"github.com/docker/distribution/reference"
    11  	"github.com/opentracing/opentracing-go"
    12  	"github.com/pkg/errors"
    13  	v1 "k8s.io/api/core/v1"
    14  	"k8s.io/apimachinery/pkg/types"
    15  
    16  	"github.com/windmilleng/tilt/internal/analytics"
    17  	"github.com/windmilleng/tilt/internal/build"
    18  	"github.com/windmilleng/tilt/internal/container"
    19  	"github.com/windmilleng/tilt/internal/dockerfile"
    20  	"github.com/windmilleng/tilt/internal/k8s"
    21  	"github.com/windmilleng/tilt/internal/store"
    22  	"github.com/windmilleng/tilt/internal/synclet/sidecar"
    23  	"github.com/windmilleng/tilt/pkg/logger"
    24  	"github.com/windmilleng/tilt/pkg/model"
    25  )
    26  
    27  var _ BuildAndDeployer = &ImageBuildAndDeployer{}
    28  
    29  type KINDPusher interface {
    30  	PushToKIND(ctx context.Context, ref reference.NamedTagged, w io.Writer) error
    31  }
    32  
    33  type cmdKINDPusher struct {
    34  	clusterName k8s.ClusterName
    35  }
    36  
    37  func (p *cmdKINDPusher) PushToKIND(ctx context.Context, ref reference.NamedTagged, w io.Writer) error {
    38  	cmd := exec.CommandContext(ctx, "kind", "load", "docker-image", ref.String(), "--name", string(p.clusterName))
    39  	cmd.Stdout = w
    40  	cmd.Stderr = w
    41  
    42  	return cmd.Run()
    43  }
    44  
    45  func NewKINDPusher(clusterName k8s.ClusterName) KINDPusher {
    46  	return &cmdKINDPusher{
    47  		clusterName: clusterName,
    48  	}
    49  }
    50  
    51  type ImageBuildAndDeployer struct {
    52  	ib               build.ImageBuilder
    53  	icb              *imageAndCacheBuilder
    54  	k8sClient        k8s.Client
    55  	env              k8s.Env
    56  	runtime          container.Runtime
    57  	analytics        *analytics.TiltAnalytics
    58  	injectSynclet    bool
    59  	clock            build.Clock
    60  	kp               KINDPusher
    61  	syncletContainer sidecar.SyncletContainer
    62  }
    63  
    64  func NewImageBuildAndDeployer(
    65  	b build.ImageBuilder,
    66  	cacheBuilder build.CacheBuilder,
    67  	customBuilder build.CustomBuilder,
    68  	k8sClient k8s.Client,
    69  	env k8s.Env,
    70  	analytics *analytics.TiltAnalytics,
    71  	updMode UpdateMode,
    72  	c build.Clock,
    73  	runtime container.Runtime,
    74  	kp KINDPusher,
    75  	syncletContainer sidecar.SyncletContainer,
    76  ) *ImageBuildAndDeployer {
    77  	return &ImageBuildAndDeployer{
    78  		ib:               b,
    79  		icb:              NewImageAndCacheBuilder(b, cacheBuilder, customBuilder, updMode),
    80  		k8sClient:        k8sClient,
    81  		env:              env,
    82  		analytics:        analytics,
    83  		clock:            c,
    84  		runtime:          runtime,
    85  		kp:               kp,
    86  		syncletContainer: syncletContainer,
    87  	}
    88  }
    89  
    90  // Turn on synclet injection. Should be called before any builds.
    91  func (ibd *ImageBuildAndDeployer) SetInjectSynclet(inject bool) {
    92  	ibd.injectSynclet = inject
    93  }
    94  
    95  func (ibd *ImageBuildAndDeployer) BuildAndDeploy(ctx context.Context, st store.RStore, specs []model.TargetSpec, stateSet store.BuildStateSet) (resultSet store.BuildResultSet, err error) {
    96  	iTargets, kTargets := extractImageAndK8sTargets(specs)
    97  	if len(kTargets) != 1 {
    98  		return store.BuildResultSet{}, SilentRedirectToNextBuilderf("ImageBuildAndDeployer does not support these specs")
    99  	}
   100  
   101  	kTarget := kTargets[0]
   102  	span, ctx := opentracing.StartSpanFromContext(ctx, "daemon-ImageBuildAndDeployer-BuildAndDeploy")
   103  	span.SetTag("target", kTarget.Name)
   104  	defer span.Finish()
   105  
   106  	startTime := time.Now()
   107  	defer func() {
   108  		incremental := "0"
   109  		for _, state := range stateSet {
   110  			if state.HasImage() {
   111  				incremental = "1"
   112  			}
   113  		}
   114  		tags := map[string]string{"incremental": incremental}
   115  		ibd.analytics.Timer("build.image", time.Since(startTime), tags)
   116  	}()
   117  
   118  	q, err := NewImageTargetQueue(ctx, iTargets, stateSet, ibd.ib.ImageExists)
   119  	if err != nil {
   120  		return store.BuildResultSet{}, err
   121  	}
   122  
   123  	// each image target has two stages: one for build, and one for push
   124  	numStages := q.CountDirty()*2 + 1
   125  
   126  	ps := build.NewPipelineState(ctx, numStages, ibd.clock)
   127  	defer func() { ps.End(ctx, err) }()
   128  
   129  	var anyInPlaceBuild bool
   130  
   131  	iTargetMap := model.ImageTargetsByID(iTargets)
   132  	err = q.RunBuilds(func(target model.TargetSpec, state store.BuildState, depResults []store.BuildResult) (store.BuildResult, error) {
   133  		iTarget, ok := target.(model.ImageTarget)
   134  		if !ok {
   135  			return nil, fmt.Errorf("Not an image target: %T", target)
   136  		}
   137  
   138  		iTarget, err := injectImageDependencies(iTarget, iTargetMap, depResults)
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  
   143  		ref, err := ibd.icb.Build(ctx, iTarget, state, ps)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  
   148  		ref, err = ibd.push(ctx, ref, ps, iTarget, kTarget)
   149  		if err != nil {
   150  			return nil, err
   151  		}
   152  
   153  		anyInPlaceBuild = anyInPlaceBuild ||
   154  			!iTarget.AnyFastBuildInfo().Empty() || !iTarget.AnyLiveUpdateInfo().Empty()
   155  		return store.NewImageBuildResult(iTarget.ID(), ref), nil
   156  	})
   157  	if err != nil {
   158  		return store.BuildResultSet{}, err
   159  	}
   160  
   161  	// (If we pass an empty list of refs here (as we will do if only deploying
   162  	// yaml), we just don't inject any image refs into the yaml, nbd.
   163  	return ibd.deploy(ctx, st, ps, iTargetMap, kTarget, q.results, anyInPlaceBuild)
   164  }
   165  
   166  func (ibd *ImageBuildAndDeployer) push(ctx context.Context, ref reference.NamedTagged, ps *build.PipelineState, iTarget model.ImageTarget, kTarget model.K8sTarget) (reference.NamedTagged, error) {
   167  	ps.StartPipelineStep(ctx, "Pushing %s", reference.FamiliarString(ref))
   168  	defer ps.EndPipelineStep(ctx)
   169  
   170  	cbSkip := false
   171  	if iTarget.IsCustomBuild() {
   172  		cbSkip = iTarget.CustomBuildInfo().DisablePush
   173  	}
   174  
   175  	// We can also skip the push of the image if it isn't used
   176  	// in any k8s resources! (e.g., it's consumed by another image).
   177  	if ibd.canAlwaysSkipPush() || !isImageDeployedToK8s(iTarget, kTarget) || cbSkip {
   178  		ps.Printf(ctx, "Skipping push")
   179  		return ref, nil
   180  	}
   181  
   182  	var err error
   183  	if ibd.env == k8s.EnvKIND {
   184  		ps.Printf(ctx, "Pushing to KIND")
   185  		err := ibd.kp.PushToKIND(ctx, ref, ps.Writer(ctx))
   186  		if err != nil {
   187  			return nil, fmt.Errorf("Error pushing to KIND: %v", err)
   188  		}
   189  	} else {
   190  		ps.Printf(ctx, "Pushing with Docker client")
   191  		writer := ps.Writer(ctx)
   192  		ctx = logger.WithLogger(ctx, logger.NewLogger(logger.InfoLvl, writer))
   193  		ref, err = ibd.ib.PushImage(ctx, ref, writer)
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  	}
   198  
   199  	return ref, nil
   200  }
   201  
   202  // Returns: the entities deployed and the namespace of the pod with the given image name/tag.
   203  func (ibd *ImageBuildAndDeployer) deploy(ctx context.Context, st store.RStore, ps *build.PipelineState,
   204  	iTargetMap map[model.TargetID]model.ImageTarget, kTarget model.K8sTarget, results store.BuildResultSet, needsSynclet bool) (store.BuildResultSet, error) {
   205  	ps.StartPipelineStep(ctx, "Deploying")
   206  	defer ps.EndPipelineStep(ctx)
   207  
   208  	ps.StartBuildStep(ctx, "Injecting images into Kubernetes YAML")
   209  
   210  	newK8sEntities, err := ibd.createEntitiesToDeploy(ctx, iTargetMap, kTarget, results, needsSynclet)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	ctx, l := ibd.indentLogger(ctx)
   216  
   217  	l.Infof("Applying via kubectl:")
   218  	for _, displayName := range kTarget.DisplayNames {
   219  		l.Infof("   %s", displayName)
   220  	}
   221  
   222  	deployed, err := ibd.k8sClient.Upsert(ctx, newK8sEntities)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	// TODO(nick): Do something with this result
   228  	uids := []types.UID{}
   229  	for _, entity := range deployed {
   230  		uid := entity.UID()
   231  		if uid == "" {
   232  			return nil, fmt.Errorf("Entity not deployed correctly: %v", entity)
   233  		}
   234  		uids = append(uids, entity.UID())
   235  	}
   236  	results[kTarget.ID()] = store.NewK8sDeployResult(kTarget.ID(), uids, deployed)
   237  
   238  	return results, nil
   239  }
   240  
   241  func (ibd *ImageBuildAndDeployer) indentLogger(ctx context.Context) (context.Context, logger.Logger) {
   242  	l := logger.Get(ctx)
   243  	writer := logger.NewPrefixedWriter(logger.Blue(l).Sprint("  │ "), l.Writer(logger.InfoLvl))
   244  	l = logger.NewLogger(logger.InfoLvl, writer)
   245  	return logger.WithLogger(ctx, l), l
   246  }
   247  
   248  func (ibd *ImageBuildAndDeployer) createEntitiesToDeploy(ctx context.Context,
   249  	iTargetMap map[model.TargetID]model.ImageTarget, k8sTarget model.K8sTarget,
   250  	results store.BuildResultSet, needsSynclet bool) ([]k8s.K8sEntity, error) {
   251  	newK8sEntities := []k8s.K8sEntity{}
   252  
   253  	// TODO(nick): The parsed YAML should probably be a part of the model?
   254  	// It doesn't make much sense to re-parse it and inject labels on every deploy.
   255  	entities, err := k8s.ParseYAMLFromString(k8sTarget.YAML)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	depIDs := k8sTarget.DependencyIDs()
   261  	injectedDepIDs := map[model.TargetID]bool{}
   262  	for _, e := range entities {
   263  		injectedSynclet := false
   264  		e, err = k8s.InjectLabels(e, []model.LabelPair{
   265  			k8s.TiltManagedByLabel(),
   266  		})
   267  		if err != nil {
   268  			return nil, errors.Wrap(err, "deploy")
   269  		}
   270  
   271  		// For development, image pull policy should never be set to "Always",
   272  		// even if it might make sense to use "Always" in prod. People who
   273  		// set "Always" for development are shooting their own feet.
   274  		e, err = k8s.InjectImagePullPolicy(e, v1.PullIfNotPresent)
   275  		if err != nil {
   276  			return nil, err
   277  		}
   278  
   279  		// StatefulSet pods should be managed in parallel. See:
   280  		// https://github.com/windmilleng/tilt/issues/1962
   281  		e = k8s.InjectParallelPodManagementPolicy(e)
   282  
   283  		// When working with a local k8s cluster, we set the pull policy to Never,
   284  		// to ensure that k8s fails hard if the image is missing from docker.
   285  		policy := v1.PullIfNotPresent
   286  		if ibd.canAlwaysSkipPush() {
   287  			policy = v1.PullNever
   288  		}
   289  
   290  		for _, depID := range depIDs {
   291  			ref := store.ImageFromBuildResult(results[depID])
   292  			if ref == nil {
   293  				return nil, fmt.Errorf("Internal error: missing image build result for dependency ID: %s", depID)
   294  			}
   295  
   296  			iTarget := iTargetMap[depID]
   297  			selector := iTarget.ConfigurationRef
   298  			matchInEnvVars := iTarget.MatchInEnvVars
   299  
   300  			var replaced bool
   301  			e, replaced, err = k8s.InjectImageDigest(e, selector, ref, matchInEnvVars, policy)
   302  			if err != nil {
   303  				return nil, err
   304  			}
   305  			if replaced {
   306  				injectedDepIDs[depID] = true
   307  
   308  				if !iTarget.OverrideCmd.Empty() {
   309  					e, err = k8s.InjectCommand(e, ref, iTarget.OverrideCmd)
   310  					if err != nil {
   311  						return nil, err
   312  					}
   313  				}
   314  
   315  				if ibd.injectSynclet && needsSynclet && !injectedSynclet {
   316  					injectedRefSelector := container.NewRefSelector(ref).WithExactMatch()
   317  
   318  					var sidecarInjected bool
   319  					e, sidecarInjected, err = sidecar.InjectSyncletSidecar(e, injectedRefSelector, ibd.syncletContainer)
   320  					if err != nil {
   321  						return nil, err
   322  					}
   323  					if !sidecarInjected {
   324  						return nil, fmt.Errorf("Could not inject synclet: %v", e)
   325  					}
   326  					injectedSynclet = true
   327  				}
   328  			}
   329  		}
   330  		newK8sEntities = append(newK8sEntities, e)
   331  	}
   332  
   333  	for _, depID := range depIDs {
   334  		if !injectedDepIDs[depID] {
   335  			return nil, fmt.Errorf("Docker image missing from yaml: %s", depID)
   336  		}
   337  	}
   338  
   339  	return newK8sEntities, nil
   340  }
   341  
   342  // If we're using docker-for-desktop as our k8s backend,
   343  // we don't need to push to the central registry.
   344  // The k8s will use the image already available
   345  // in the local docker daemon.
   346  func (ibd *ImageBuildAndDeployer) canAlwaysSkipPush() bool {
   347  	return ibd.env.UsesLocalDockerRegistry() && ibd.runtime == container.RuntimeDocker
   348  }
   349  
   350  // Create a new ImageTarget with the dockerfiles rewritten
   351  // with the injected images.
   352  func injectImageDependencies(iTarget model.ImageTarget, iTargetMap map[model.TargetID]model.ImageTarget, deps []store.BuildResult) (model.ImageTarget, error) {
   353  	if len(deps) == 0 {
   354  		return iTarget, nil
   355  	}
   356  
   357  	df := dockerfile.Dockerfile("")
   358  	switch bd := iTarget.BuildDetails.(type) {
   359  	case model.DockerBuild:
   360  		df = dockerfile.Dockerfile(bd.Dockerfile)
   361  	case model.FastBuild:
   362  		df = dockerfile.Dockerfile(bd.BaseDockerfile)
   363  	default:
   364  		return model.ImageTarget{}, fmt.Errorf("image %q has no valid buildDetails", iTarget.ConfigurationRef)
   365  	}
   366  
   367  	ast, err := dockerfile.ParseAST(df)
   368  	if err != nil {
   369  		return model.ImageTarget{}, errors.Wrap(err, "injectImageDependencies")
   370  	}
   371  
   372  	for _, dep := range deps {
   373  		image := store.ImageFromBuildResult(dep)
   374  		if image == nil {
   375  			return model.ImageTarget{}, fmt.Errorf("Internal error: image is nil")
   376  		}
   377  		id := dep.TargetID()
   378  		modified, err := ast.InjectImageDigest(iTargetMap[id].ConfigurationRef, image)
   379  		if err != nil {
   380  			return model.ImageTarget{}, errors.Wrap(err, "injectImageDependencies")
   381  		} else if !modified {
   382  			return model.ImageTarget{}, fmt.Errorf("Could not inject image %q into Dockerfile of image %q", image, iTarget.ConfigurationRef)
   383  		}
   384  	}
   385  
   386  	newDf, err := ast.Print()
   387  	if err != nil {
   388  		return model.ImageTarget{}, errors.Wrap(err, "injectImageDependencies")
   389  	}
   390  
   391  	switch bd := iTarget.BuildDetails.(type) {
   392  	case model.DockerBuild:
   393  		bd.Dockerfile = newDf.String()
   394  		iTarget = iTarget.WithBuildDetails(bd)
   395  	case model.FastBuild:
   396  		bd.BaseDockerfile = newDf.String()
   397  		iTarget = iTarget.WithBuildDetails(bd)
   398  	}
   399  
   400  	return iTarget, nil
   401  }