github.com/grahambrereton-form3/tilt@v0.10.18/pkg/model/manifest.go (about)

     1  package model
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"k8s.io/apimachinery/pkg/labels"
     8  
     9  	"github.com/docker/distribution/reference"
    10  
    11  	"github.com/windmilleng/tilt/internal/container"
    12  	"github.com/windmilleng/tilt/internal/sliceutils"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/google/go-cmp/cmp/cmpopts"
    16  )
    17  
    18  // TODO(nick): We should probably get rid of ManifestName completely and just use TargetName everywhere.
    19  type ManifestName string
    20  
    21  func (m ManifestName) String() string         { return string(m) }
    22  func (m ManifestName) TargetName() TargetName { return TargetName(m) }
    23  
    24  // NOTE: If you modify Manifest, make sure to modify `Manifest.Equal` appropriately
    25  type Manifest struct {
    26  	// Properties for all manifests.
    27  	Name ManifestName
    28  
    29  	// Info needed to build an image. (This struct contains details of DockerBuild, FastBuild... etc.)
    30  	ImageTargets []ImageTarget
    31  
    32  	// Info needed to deploy. Can be k8s yaml, docker compose, etc.
    33  	deployTarget TargetSpec
    34  
    35  	// How updates are triggered:
    36  	// - automatically, when we detect a change
    37  	// - manually, when the user tells us to
    38  	TriggerMode TriggerMode
    39  
    40  	// The resource in this manifest will not be built until all of its dependencies have been
    41  	// ready at least once.
    42  	ResourceDependencies []ManifestName
    43  }
    44  
    45  func (m Manifest) ID() TargetID {
    46  	return TargetID{
    47  		Type: TargetTypeManifest,
    48  		Name: m.Name.TargetName(),
    49  	}
    50  }
    51  
    52  func (m Manifest) DependencyIDs() []TargetID {
    53  	result := []TargetID{}
    54  	for _, iTarget := range m.ImageTargets {
    55  		result = append(result, iTarget.ID())
    56  	}
    57  	if !m.deployTarget.ID().Empty() {
    58  		result = append(result, m.deployTarget.ID())
    59  	}
    60  	return result
    61  }
    62  
    63  func (m Manifest) WithImageTarget(iTarget ImageTarget) Manifest {
    64  	m.ImageTargets = []ImageTarget{iTarget}
    65  	return m
    66  }
    67  
    68  func (m Manifest) WithImageTargets(iTargets []ImageTarget) Manifest {
    69  	m.ImageTargets = append([]ImageTarget{}, iTargets...)
    70  	return m
    71  }
    72  
    73  func (m Manifest) ImageTargetAt(i int) ImageTarget {
    74  	if i < len(m.ImageTargets) {
    75  		return m.ImageTargets[i]
    76  	}
    77  	return ImageTarget{}
    78  }
    79  
    80  type DockerBuildArgs map[string]string
    81  
    82  func (m Manifest) LocalTarget() LocalTarget {
    83  	ret, _ := m.deployTarget.(LocalTarget)
    84  	return ret
    85  }
    86  
    87  func (m Manifest) IsLocal() bool {
    88  	_, ok := m.deployTarget.(LocalTarget)
    89  	return ok
    90  }
    91  
    92  func (m Manifest) DockerComposeTarget() DockerComposeTarget {
    93  	ret, _ := m.deployTarget.(DockerComposeTarget)
    94  	return ret
    95  }
    96  
    97  func (m Manifest) IsDC() bool {
    98  	_, ok := m.deployTarget.(DockerComposeTarget)
    99  	return ok
   100  }
   101  
   102  func (m Manifest) K8sTarget() K8sTarget {
   103  	ret, _ := m.deployTarget.(K8sTarget)
   104  	return ret
   105  }
   106  
   107  func (m Manifest) IsK8s() bool {
   108  	_, ok := m.deployTarget.(K8sTarget)
   109  	return ok
   110  }
   111  
   112  func (m Manifest) IsUnresourcedYAMLManifest() bool {
   113  	return m.Name == UnresourcedYAMLManifestName
   114  }
   115  
   116  func (m Manifest) DeployTarget() TargetSpec {
   117  	return m.deployTarget
   118  }
   119  
   120  func (m Manifest) WithDeployTarget(t TargetSpec) Manifest {
   121  	switch typedTarget := t.(type) {
   122  	case K8sTarget:
   123  		typedTarget.Name = m.Name.TargetName()
   124  		t = typedTarget
   125  	case DockerComposeTarget:
   126  		typedTarget.Name = m.Name.TargetName()
   127  		t = typedTarget
   128  	}
   129  	m.deployTarget = t
   130  	return m
   131  }
   132  
   133  func (m Manifest) WithTriggerMode(mode TriggerMode) Manifest {
   134  	m.TriggerMode = mode
   135  	return m
   136  }
   137  
   138  func (m Manifest) TargetSpecs() []TargetSpec {
   139  	result := []TargetSpec{}
   140  	for _, t := range m.ImageTargets {
   141  		result = append(result, t)
   142  	}
   143  	result = append(result, m.deployTarget)
   144  	return result
   145  }
   146  
   147  func (m Manifest) IsImageDeployed(iTarget ImageTarget) bool {
   148  	id := iTarget.ID()
   149  	for _, depID := range m.DeployTarget().DependencyIDs() {
   150  		if depID == id {
   151  			return true
   152  		}
   153  	}
   154  	return false
   155  }
   156  
   157  func (m Manifest) LocalPaths() []string {
   158  	// TODO(matt?) DC syncs should probably stored somewhere more consistent with Docker/Fast Build
   159  	switch di := m.deployTarget.(type) {
   160  	case DockerComposeTarget:
   161  		return di.LocalPaths()
   162  	default:
   163  		paths := []string{}
   164  		for _, iTarget := range m.ImageTargets {
   165  			paths = append(paths, iTarget.LocalPaths()...)
   166  		}
   167  		return sliceutils.DedupedAndSorted(paths)
   168  	}
   169  }
   170  
   171  func (m Manifest) Validate() error {
   172  	if m.Name == "" {
   173  		return fmt.Errorf("[validate] manifest missing name: %+v", m)
   174  	}
   175  
   176  	for _, iTarget := range m.ImageTargets {
   177  		err := iTarget.Validate()
   178  		if err != nil {
   179  			return err
   180  		}
   181  	}
   182  
   183  	if m.deployTarget != nil {
   184  		err := m.deployTarget.Validate()
   185  		if err != nil {
   186  			return err
   187  		}
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  func (m1 Manifest) Equal(m2 Manifest) bool {
   194  	primitivesEq, dockerEq, k8sEq, dcEq, localEq, depsEq := m1.fieldGroupsEqual(m2)
   195  	return primitivesEq && dockerEq && k8sEq && dcEq && localEq && depsEq
   196  }
   197  
   198  // ChangesInvalidateBuild checks whether the changes from old => new manifest
   199  // invalidate our build of the old one; i.e. if we're replacing `old` with `new`,
   200  // should we perform a full rebuild?
   201  func ChangesInvalidateBuild(old, new Manifest) bool {
   202  	_, dockerEq, k8sEq, dcEq, localEq, _ := old.fieldGroupsEqual(new)
   203  
   204  	// We only need to update for this manifest if any of the field-groups
   205  	// affecting build+deploy have changed (i.e. a change in primitives doesn't matter)
   206  	return !dockerEq || !k8sEq || !dcEq || !localEq
   207  
   208  }
   209  func (m1 Manifest) fieldGroupsEqual(m2 Manifest) (primitivesEq, dockerEq, k8sEq, dcEq, localEq, depsEq bool) {
   210  	primitivesEq = m1.Name == m2.Name && m1.TriggerMode == m2.TriggerMode
   211  
   212  	dockerEq = DeepEqual(m1.ImageTargets, m2.ImageTargets)
   213  
   214  	dc1 := m1.DockerComposeTarget()
   215  	dc2 := m2.DockerComposeTarget()
   216  	dcEq = DeepEqual(dc1, dc2)
   217  
   218  	k8s1 := m1.K8sTarget()
   219  	k8s2 := m2.K8sTarget()
   220  	k8sEq = DeepEqual(k8s1, k8s2)
   221  
   222  	lt1 := m1.LocalTarget()
   223  	lt2 := m2.LocalTarget()
   224  	localEq = DeepEqual(lt1, lt2)
   225  
   226  	depsEq = DeepEqual(m1.ResourceDependencies, m2.ResourceDependencies)
   227  
   228  	return primitivesEq, dockerEq, dcEq, k8sEq, localEq, depsEq
   229  }
   230  
   231  func (m Manifest) ManifestName() ManifestName {
   232  	return m.Name
   233  }
   234  
   235  func (m Manifest) Empty() bool {
   236  	return m.Equal(Manifest{})
   237  }
   238  
   239  func RefSelectorsForManifests(manifests []Manifest) []container.RefSelector {
   240  	var res []container.RefSelector
   241  	for _, m := range manifests {
   242  		for _, iTarg := range m.ImageTargets {
   243  			sel := container.NameSelector(iTarg.DeploymentRef).WithNameMatch()
   244  			res = append(res, sel)
   245  		}
   246  	}
   247  	return res
   248  }
   249  
   250  var _ TargetSpec = Manifest{}
   251  
   252  type Sync struct {
   253  	LocalPath     string
   254  	ContainerPath string
   255  }
   256  
   257  type Dockerignore struct {
   258  	// The path to evaluate the dockerignore contents relative to
   259  	LocalPath string
   260  	Contents  string
   261  }
   262  
   263  type LocalGitRepo struct {
   264  	LocalPath string
   265  }
   266  
   267  func (LocalGitRepo) IsRepo() {}
   268  
   269  type Run struct {
   270  	// Required. The command to run.
   271  	Cmd Cmd
   272  	// Optional. If not specified, this command runs on every change.
   273  	// If specified, we only run the Cmd if the changed file matches a trigger.
   274  	Triggers PathSet
   275  }
   276  
   277  func (r Run) WithTriggers(paths []string, baseDir string) Run {
   278  	if len(paths) > 0 {
   279  		r.Triggers = PathSet{
   280  			Paths:         paths,
   281  			BaseDirectory: baseDir,
   282  		}
   283  	} else {
   284  		r.Triggers = PathSet{}
   285  	}
   286  	return r
   287  }
   288  
   289  type Cmd struct {
   290  	Argv []string
   291  }
   292  
   293  func (c Cmd) IsShellStandardForm() bool {
   294  	return len(c.Argv) == 3 && c.Argv[0] == "sh" && c.Argv[1] == "-c" && !strings.Contains(c.Argv[2], "\n")
   295  }
   296  
   297  // Get the script when the shell is in standard form.
   298  // Panics if the command is not in shell standard form.
   299  func (c Cmd) ShellStandardScript() string {
   300  	if !c.IsShellStandardForm() {
   301  		panic(fmt.Sprintf("Not in shell standard form: %+v", c))
   302  	}
   303  	return c.Argv[2]
   304  }
   305  
   306  func (c Cmd) EntrypointStr() string {
   307  	if c.IsShellStandardForm() {
   308  		return fmt.Sprintf("ENTRYPOINT %s", c.Argv[2])
   309  	}
   310  
   311  	quoted := make([]string, len(c.Argv))
   312  	for i, arg := range c.Argv {
   313  		quoted[i] = fmt.Sprintf("%q", arg)
   314  	}
   315  	return fmt.Sprintf("ENTRYPOINT [%s]", strings.Join(quoted, ", "))
   316  }
   317  
   318  func (c Cmd) RunStr() string {
   319  	if c.IsShellStandardForm() {
   320  		return fmt.Sprintf("RUN %s", c.Argv[2])
   321  	}
   322  
   323  	quoted := make([]string, len(c.Argv))
   324  	for i, arg := range c.Argv {
   325  		quoted[i] = fmt.Sprintf("%q", arg)
   326  	}
   327  	return fmt.Sprintf("RUN [%s]", strings.Join(quoted, ", "))
   328  }
   329  func (c Cmd) String() string {
   330  	if c.IsShellStandardForm() {
   331  		return c.Argv[2]
   332  	}
   333  
   334  	quoted := make([]string, len(c.Argv))
   335  	for i, arg := range c.Argv {
   336  		if strings.Contains(arg, " ") {
   337  			quoted[i] = fmt.Sprintf("%q", arg)
   338  		} else {
   339  			quoted[i] = arg
   340  		}
   341  	}
   342  	return fmt.Sprintf("%s", strings.Join(quoted, " "))
   343  }
   344  
   345  func (c Cmd) Empty() bool {
   346  	return len(c.Argv) == 0
   347  }
   348  
   349  func ToShellCmd(cmd string) Cmd {
   350  	if cmd == "" {
   351  		return Cmd{}
   352  	}
   353  	return Cmd{Argv: []string{"sh", "-c", cmd}}
   354  }
   355  
   356  func ToShellCmds(cmds []string) []Cmd {
   357  	res := make([]Cmd, len(cmds))
   358  	for i, cmd := range cmds {
   359  		res[i] = ToShellCmd(cmd)
   360  	}
   361  	return res
   362  }
   363  
   364  func ToRun(cmd Cmd) Run {
   365  	return Run{Cmd: cmd}
   366  }
   367  
   368  func ToRuns(cmds []Cmd) []Run {
   369  	res := make([]Run, len(cmds))
   370  	for i, cmd := range cmds {
   371  		res[i] = ToRun(cmd)
   372  	}
   373  	return res
   374  }
   375  
   376  type PortForward struct {
   377  	// The port to connect to inside the deployed container.
   378  	// If 0, we will connect to the first containerPort.
   379  	ContainerPort int
   380  
   381  	// The port to expose on the current machine.
   382  	LocalPort int
   383  
   384  	// Optional host to bind to on the current machine (localhost by default)
   385  	Host string
   386  }
   387  
   388  var imageTargetAllowUnexported = cmp.AllowUnexported(ImageTarget{})
   389  var dcTargetAllowUnexported = cmp.AllowUnexported(DockerComposeTarget{})
   390  var labelRequirementAllowUnexported = cmp.AllowUnexported(labels.Requirement{})
   391  var k8sTargetAllowUnexported = cmp.AllowUnexported(K8sTarget{})
   392  var localTargetAllowUnexported = cmp.AllowUnexported(LocalTarget{})
   393  var selectorAllowUnexported = cmp.AllowUnexported(container.RefSelector{})
   394  
   395  var dockerRefEqual = cmp.Comparer(func(a, b reference.Named) bool {
   396  	aNil := a == nil
   397  	bNil := b == nil
   398  	if aNil && bNil {
   399  		return true
   400  	}
   401  
   402  	if aNil != bNil {
   403  		return false
   404  	}
   405  
   406  	return a.String() == b.String()
   407  })
   408  
   409  func DeepEqual(x, y interface{}) bool {
   410  	return cmp.Equal(x, y,
   411  		cmpopts.EquateEmpty(),
   412  		imageTargetAllowUnexported,
   413  		dcTargetAllowUnexported,
   414  		labelRequirementAllowUnexported,
   415  		k8sTargetAllowUnexported,
   416  		localTargetAllowUnexported,
   417  		selectorAllowUnexported,
   418  		dockerRefEqual)
   419  }