github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/testutils/manifestbuilder/manifestbuilder.go (about)

     1  package manifestbuilder
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"k8s.io/apimachinery/pkg/labels"
     8  
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/tilt-dev/tilt/internal/controllers/apis/cmdimage"
    12  	"github.com/tilt-dev/tilt/internal/controllers/apis/dockerimage"
    13  	"github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate"
    14  	"github.com/tilt-dev/tilt/internal/k8s"
    15  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    16  	"github.com/tilt-dev/tilt/pkg/model"
    17  )
    18  
    19  // Builds Manifest objects for testing.
    20  //
    21  // To create well-formed manifests, we want to make sure that:
    22  // - The relationships between targets are internally consistent
    23  //   (e.g., if there's an ImageTarget and a K8sTarget in the manifest, then
    24  //    the K8sTarget should depend on the ImageTarget).
    25  // - Any filepaths in the manifest are scoped to the
    26  //   test directory (e.g., we're not trying to watch random directories
    27  //   outside the test environment).
    28  
    29  type ManifestBuilder struct {
    30  	f    Fixture
    31  	name model.ManifestName
    32  
    33  	k8sPodReadiness    model.PodReadinessMode
    34  	k8sYAML            string
    35  	k8sPodSelectors    []labels.Set
    36  	k8sImageLocators   []v1alpha1.KubernetesImageLocator
    37  	dcConfigPaths      []string
    38  	localCmd           string
    39  	localServeCmd      string
    40  	localDeps          []string
    41  	localAllowParallel bool
    42  	resourceDeps       []string
    43  	triggerMode        model.TriggerMode
    44  
    45  	iTargets []model.ImageTarget
    46  }
    47  
    48  func New(f Fixture, name model.ManifestName) ManifestBuilder {
    49  	k8sPodReadiness := model.PodReadinessWait
    50  
    51  	// TODO(nick): A better solution would be to identify whether this
    52  	// is a workload based on the YAML.
    53  	if name == model.UnresourcedYAMLManifestName {
    54  		k8sPodReadiness = model.PodReadinessIgnore
    55  	}
    56  
    57  	return ManifestBuilder{
    58  		f:               f,
    59  		name:            name,
    60  		k8sPodReadiness: k8sPodReadiness,
    61  	}
    62  }
    63  
    64  func (b ManifestBuilder) WithNamedJSONPathImageLocator(name, path string) ManifestBuilder {
    65  	b.k8sImageLocators = append(b.k8sImageLocators, v1alpha1.KubernetesImageLocator{
    66  		ObjectSelector: k8s.MustNameSelector(name).ToSpec(),
    67  		Path:           path,
    68  	})
    69  	return b
    70  }
    71  
    72  func (b ManifestBuilder) WithK8sPodReadiness(pr model.PodReadinessMode) ManifestBuilder {
    73  	b.k8sPodReadiness = pr
    74  	return b
    75  }
    76  
    77  func (b ManifestBuilder) WithK8sYAML(yaml string) ManifestBuilder {
    78  	b.k8sYAML = yaml
    79  	return b
    80  }
    81  
    82  func (b ManifestBuilder) WithK8sPodSelectors(podSelectors []labels.Set) ManifestBuilder {
    83  	b.k8sPodSelectors = podSelectors
    84  	return b
    85  }
    86  
    87  func (b ManifestBuilder) WithDockerCompose() ManifestBuilder {
    88  	b.dcConfigPaths = []string{b.f.JoinPath("docker-compose.yml")}
    89  	return b
    90  }
    91  
    92  func (b ManifestBuilder) WithLocalResource(cmd string, deps []string) ManifestBuilder {
    93  	b.localCmd = cmd
    94  	b.localDeps = deps
    95  	return b
    96  }
    97  
    98  func (b ManifestBuilder) WithLocalServeCmd(cmd string) ManifestBuilder {
    99  	b.localServeCmd = cmd
   100  	return b
   101  }
   102  
   103  func (b ManifestBuilder) WithLocalAllowParallel(v bool) ManifestBuilder {
   104  	b.localAllowParallel = v
   105  	return b
   106  }
   107  
   108  func (b ManifestBuilder) WithTriggerMode(tm model.TriggerMode) ManifestBuilder {
   109  	b.triggerMode = tm
   110  	return b
   111  }
   112  
   113  func (b ManifestBuilder) WithImageTarget(iTarg model.ImageTarget) ManifestBuilder {
   114  	b.iTargets = append(b.iTargets, iTarg)
   115  	return b
   116  }
   117  
   118  func (b ManifestBuilder) WithImageTargets(iTargs ...model.ImageTarget) ManifestBuilder {
   119  	b.iTargets = append(b.iTargets, iTargs...)
   120  	return b
   121  }
   122  
   123  func (b ManifestBuilder) WithLiveUpdate(lu v1alpha1.LiveUpdateSpec) ManifestBuilder {
   124  	return b.WithLiveUpdateAtIndex(lu, 0)
   125  }
   126  
   127  func (b ManifestBuilder) WithLiveUpdateAtIndex(lu v1alpha1.LiveUpdateSpec, index int) ManifestBuilder {
   128  	if len(b.iTargets) <= index {
   129  		b.f.T().Fatalf("WithLiveUpdateAtIndex: index %d out of range -- (manifestBuilder has %d image targets)", index, len(b.iTargets))
   130  	}
   131  
   132  	iTarg := b.iTargets[index]
   133  	iTarg.LiveUpdateSpec = lu
   134  	if !liveupdate.IsEmptySpec(lu) {
   135  		iTarg.LiveUpdateName = liveupdate.GetName(b.name, iTarg.ID())
   136  	}
   137  	b.iTargets[index] = iTarg
   138  	return b
   139  }
   140  
   141  func (b ManifestBuilder) WithResourceDeps(deps ...string) ManifestBuilder {
   142  	b.resourceDeps = deps
   143  	return b
   144  }
   145  
   146  func (b ManifestBuilder) Build() model.Manifest {
   147  	var m model.Manifest
   148  
   149  	// Adjust images to use their API server object names, when appropriate.
   150  	// Currently,
   151  	for index, iTarget := range b.iTargets {
   152  		if iTarget.IsDockerBuild() {
   153  			iTarget.DockerImageName = dockerimage.GetName(b.name, iTarget.ID())
   154  		} else if iTarget.IsCustomBuild() {
   155  			iTarget.CmdImageName = cmdimage.GetName(b.name, iTarget.ID())
   156  		}
   157  
   158  		if len(b.dcConfigPaths) != 0 {
   159  			if iTarget.IsDockerBuild() {
   160  				dbi := iTarget.DockerBuildInfo()
   161  				dbi.DockerImageSpec.Cluster = v1alpha1.ClusterNameDocker
   162  				iTarget.BuildDetails = dbi
   163  			} else if iTarget.IsCustomBuild() {
   164  				cbi := iTarget.CustomBuildInfo()
   165  				cbi.CmdImageSpec.Cluster = v1alpha1.ClusterNameDocker
   166  				iTarget.BuildDetails = cbi
   167  			}
   168  		}
   169  
   170  		if liveupdate.IsEmptySpec(iTarget.LiveUpdateSpec) {
   171  			iTarget.LiveUpdateReconciler = false
   172  		} else {
   173  			iTarget.LiveUpdateReconciler = true
   174  		}
   175  		b.iTargets[index] = iTarget
   176  	}
   177  
   178  	var rds []model.ManifestName
   179  	for _, dep := range b.resourceDeps {
   180  		rds = append(rds, model.ManifestName(dep))
   181  	}
   182  
   183  	if b.k8sYAML != "" {
   184  		k8sTarget := k8s.MustTarget(model.TargetName(b.name), b.k8sYAML)
   185  		k8sTarget.KubernetesApplySpec.KubernetesDiscoveryTemplateSpec = &v1alpha1.KubernetesDiscoveryTemplateSpec{
   186  			ExtraSelectors: k8s.SetsAsLabelSelectors(b.k8sPodSelectors),
   187  		}
   188  		k8sTarget.ImageLocators = append(k8sTarget.ImageLocators, b.k8sImageLocators...)
   189  		k8sTarget.PodReadinessMode = b.k8sPodReadiness
   190  
   191  		m = assembleK8s(
   192  			model.Manifest{Name: b.name, ResourceDependencies: rds},
   193  			k8sTarget,
   194  			b.iTargets...)
   195  	} else if len(b.dcConfigPaths) > 0 {
   196  		m = assembleDC(
   197  			model.Manifest{Name: b.name, ResourceDependencies: rds},
   198  			model.DockerComposeTarget{
   199  				Spec: v1alpha1.DockerComposeServiceSpec{
   200  					Service: string(b.name),
   201  					Project: v1alpha1.DockerComposeProject{
   202  						ConfigPaths: b.dcConfigPaths,
   203  					},
   204  				},
   205  				Name: model.TargetName(b.name),
   206  			},
   207  			b.iTargets...)
   208  	} else if b.localCmd != "" || b.localServeCmd != "" {
   209  		updateCmd := model.ToHostCmd(b.localCmd)
   210  		updateCmd.Dir = b.f.Path()
   211  
   212  		serveCmd := model.ToHostCmd(b.localServeCmd)
   213  		serveCmd.Dir = b.f.Path()
   214  
   215  		lt := model.NewLocalTarget(
   216  			model.TargetName(b.name),
   217  			updateCmd,
   218  			serveCmd,
   219  			b.localDeps).
   220  			WithAllowParallel(b.localAllowParallel)
   221  		m = model.Manifest{Name: b.name, ResourceDependencies: rds}.WithDeployTarget(lt)
   222  
   223  		m = m.WithDisableSource(&v1alpha1.DisableSource{
   224  			ConfigMap: &v1alpha1.ConfigMapDisableSource{
   225  				Name: fmt.Sprintf("%s-disable", b.name),
   226  				Key:  "isDisabled",
   227  			},
   228  		})
   229  	} else {
   230  		b.f.T().Fatalf("No deploy target specified: %s", b.name)
   231  		return model.Manifest{}
   232  	}
   233  	m = m.WithTriggerMode(b.triggerMode)
   234  
   235  	err := m.InferImageProperties()
   236  	require.NoError(b.f.T(), err)
   237  
   238  	err = m.InferLiveUpdateSelectors()
   239  	require.NoError(b.f.T(), err)
   240  	return m
   241  }
   242  
   243  type Fixture interface {
   244  	T() testing.TB
   245  	Path() string
   246  	JoinPath(ps ...string) string
   247  	MkdirAll(p string)
   248  }