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 }