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

     1  package podbuilder
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/docker/distribution/reference"
     9  	appsv1 "k8s.io/api/apps/v1"
    10  	v1 "k8s.io/api/core/v1"
    11  	"k8s.io/apimachinery/pkg/api/validation"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/types"
    14  
    15  	"github.com/windmilleng/tilt/internal/container"
    16  	"github.com/windmilleng/tilt/internal/k8s"
    17  	"github.com/windmilleng/tilt/pkg/model"
    18  )
    19  
    20  const fakeContainerID = container.ID("myTestContainer")
    21  
    22  func FakeContainerID() container.ID {
    23  	return FakeContainerIDAtIndex(0)
    24  }
    25  
    26  func FakeContainerIDAtIndex(index int) container.ID {
    27  	indexSuffix := ""
    28  	if index != 0 {
    29  		indexSuffix = fmt.Sprintf("-%d", index)
    30  	}
    31  	return container.ID(fmt.Sprintf("%s%s", fakeContainerID, indexSuffix))
    32  }
    33  
    34  func FakeContainerIDSet(size int) map[container.ID]bool {
    35  	result := container.NewIDSet()
    36  	for i := 0; i < size; i++ {
    37  		result[FakeContainerIDAtIndex(i)] = true
    38  	}
    39  	return result
    40  }
    41  
    42  // Builds Pod objects for testing
    43  //
    44  // The pod model should be internally well-formed (e.g., the containers
    45  // in the PodSpec object should match the containers in the PodStatus object).
    46  //
    47  // The pod model should also be consistent with the Manifest (e.g., if the Manifest
    48  // specifies a Deployment with labels in a PodTemplateSpec, then any Pods should also
    49  // have those labels).
    50  //
    51  // The PodBuilder is responsible for making sure we create well-formed Pods for
    52  // testing. Tests should never modify the pod directly, but instead use the PodBuilder
    53  // methods to ensure that the pod is consistent.
    54  type PodBuilder struct {
    55  	t        testing.TB
    56  	manifest model.Manifest
    57  
    58  	podID          string
    59  	phase          string
    60  	creationTime   time.Time
    61  	deletionTime   time.Time
    62  	restartCount   int
    63  	extraPodLabels map[string]string
    64  	deploymentUID  types.UID
    65  
    66  	// keyed by container index -- i.e. the first container will have image: imageRefs[0] and ID: cIDs[0], etc.
    67  	// If there's no entry at index i, we'll use a dummy value.
    68  	imageRefs map[int]string
    69  	cIDs      map[int]string
    70  	cReady    map[int]bool
    71  }
    72  
    73  func New(t testing.TB, manifest model.Manifest) PodBuilder {
    74  	return PodBuilder{
    75  		t:              t,
    76  		manifest:       manifest,
    77  		imageRefs:      make(map[int]string),
    78  		cIDs:           make(map[int]string),
    79  		cReady:         make(map[int]bool),
    80  		extraPodLabels: make(map[string]string),
    81  	}
    82  }
    83  
    84  func (b PodBuilder) WithPodLabel(key, val string) PodBuilder {
    85  	b.extraPodLabels[key] = val
    86  	return b
    87  }
    88  
    89  func (b PodBuilder) ManifestName() model.ManifestName {
    90  	return b.manifest.Name
    91  }
    92  
    93  func (b PodBuilder) RestartCount() int {
    94  	return b.restartCount
    95  }
    96  
    97  func (b PodBuilder) WithRestartCount(restartCount int) PodBuilder {
    98  	b.restartCount = restartCount
    99  	return b
   100  }
   101  
   102  func (b PodBuilder) WithPodID(podID string) PodBuilder {
   103  	msgs := validation.NameIsDNSSubdomain(podID, false)
   104  	if len(msgs) != 0 {
   105  		b.t.Fatalf("pod id %q is invalid: %s", podID, msgs)
   106  	}
   107  	b.podID = podID
   108  	return b
   109  }
   110  
   111  func (b PodBuilder) WithPhase(phase string) PodBuilder {
   112  	b.phase = phase
   113  	return b
   114  }
   115  
   116  func (b PodBuilder) WithImage(image string) PodBuilder {
   117  	return b.WithImageAtIndex(image, 0)
   118  }
   119  
   120  func (b PodBuilder) WithImageAtIndex(image string, index int) PodBuilder {
   121  	b.imageRefs[index] = image
   122  	return b
   123  }
   124  
   125  func (b PodBuilder) WithContainerID(cID container.ID) PodBuilder {
   126  	return b.WithContainerIDAtIndex(cID, 0)
   127  }
   128  
   129  func (b PodBuilder) WithContainerIDAtIndex(cID container.ID, index int) PodBuilder {
   130  	if cID == "" {
   131  		b.cIDs[index] = ""
   132  	} else {
   133  		b.cIDs[index] = fmt.Sprintf("%s%s", k8s.ContainerIDPrefix, cID)
   134  	}
   135  	return b
   136  }
   137  
   138  func (b PodBuilder) WithContainerReady(ready bool) PodBuilder {
   139  	return b.WithContainerReadyAtIndex(ready, 0)
   140  }
   141  
   142  func (b PodBuilder) WithContainerReadyAtIndex(ready bool, index int) PodBuilder {
   143  	b.cReady[index] = ready
   144  	return b
   145  }
   146  
   147  func (b PodBuilder) WithCreationTime(creationTime time.Time) PodBuilder {
   148  	b.creationTime = creationTime
   149  	return b
   150  }
   151  
   152  func (b PodBuilder) WithDeletionTime(deletionTime time.Time) PodBuilder {
   153  	b.deletionTime = deletionTime
   154  	return b
   155  }
   156  
   157  func (b PodBuilder) PodID() string {
   158  	if b.podID != "" {
   159  		return b.podID
   160  	}
   161  	return "fakePodID"
   162  }
   163  
   164  func (b PodBuilder) buildPodUID() types.UID {
   165  	return types.UID(fmt.Sprintf("%s-fakeUID", b.PodID()))
   166  }
   167  
   168  func (b PodBuilder) WithDeploymentUID(deploymentUID types.UID) PodBuilder {
   169  	b.deploymentUID = deploymentUID
   170  	return b
   171  }
   172  
   173  func (b PodBuilder) buildReplicaSetName() string {
   174  	return fmt.Sprintf("%s-replicaset", b.manifest.Name)
   175  }
   176  
   177  func (b PodBuilder) buildReplicaSetUID() types.UID {
   178  	return types.UID(fmt.Sprintf("%s-fakeUID", b.buildReplicaSetName()))
   179  }
   180  
   181  func (b PodBuilder) buildDeploymentName() string {
   182  	return b.manifest.Name.String()
   183  }
   184  
   185  func (b PodBuilder) DeploymentUID() types.UID {
   186  	if b.deploymentUID != "" {
   187  		return b.deploymentUID
   188  	}
   189  	return types.UID(fmt.Sprintf("%s-fakeUID", b.buildDeploymentName()))
   190  }
   191  
   192  func (b PodBuilder) buildDeployment() *appsv1.Deployment {
   193  	return &appsv1.Deployment{
   194  		ObjectMeta: metav1.ObjectMeta{
   195  			Name:   b.buildDeploymentName(),
   196  			Labels: k8s.NewTiltLabelMap(),
   197  			UID:    b.DeploymentUID(),
   198  		},
   199  	}
   200  }
   201  
   202  func (b PodBuilder) buildReplicaSet() *appsv1.ReplicaSet {
   203  	dep := b.buildDeployment()
   204  	return &appsv1.ReplicaSet{
   205  		ObjectMeta: metav1.ObjectMeta{
   206  			Name:   b.buildReplicaSetName(),
   207  			UID:    b.buildReplicaSetUID(),
   208  			Labels: k8s.NewTiltLabelMap(),
   209  			OwnerReferences: []metav1.OwnerReference{
   210  				k8s.RuntimeObjToOwnerRef(dep),
   211  			},
   212  		},
   213  	}
   214  }
   215  
   216  func (b PodBuilder) buildCreationTime() metav1.Time {
   217  	if !b.creationTime.IsZero() {
   218  		return metav1.Time{Time: b.creationTime}
   219  	}
   220  	return metav1.Time{Time: time.Now()}
   221  }
   222  
   223  func (b PodBuilder) buildDeletionTime() *metav1.Time {
   224  	if !b.deletionTime.IsZero() {
   225  		return &metav1.Time{Time: b.deletionTime}
   226  	}
   227  	return nil
   228  }
   229  
   230  func (b PodBuilder) buildLabels(tSpec *v1.PodTemplateSpec) map[string]string {
   231  	labels := k8s.NewTiltLabelMap()
   232  	for k, v := range tSpec.Labels {
   233  		labels[k] = v
   234  	}
   235  	for k, v := range b.extraPodLabels {
   236  		labels[k] = v
   237  	}
   238  	return labels
   239  }
   240  
   241  func (b PodBuilder) buildImage(imageSpec string, index int) string {
   242  	image, ok := b.imageRefs[index]
   243  	if ok {
   244  		return image
   245  	}
   246  
   247  	imageSpecRef := container.MustParseNamed(imageSpec)
   248  
   249  	// Use the pod ID as the image tag. This is kind of weird, but gets at the semantics
   250  	// we want (e.g., a new pod ID indicates that this is a new build).
   251  	// Tests that don't want this behavior should replace the image with setImage(pod, imageName)
   252  	return fmt.Sprintf("%s:%s", imageSpecRef.Name(), b.PodID())
   253  }
   254  
   255  func (b PodBuilder) buildContainerID(index int) string {
   256  	cID, ok := b.cIDs[index]
   257  	if ok {
   258  		return cID
   259  	}
   260  
   261  	return fmt.Sprintf("%s%s", k8s.ContainerIDPrefix, FakeContainerIDAtIndex(index))
   262  }
   263  
   264  func (b PodBuilder) buildPhase() v1.PodPhase {
   265  	if b.phase == "" {
   266  		return v1.PodPhase("Running")
   267  	}
   268  	return v1.PodPhase(b.phase)
   269  }
   270  
   271  func (b PodBuilder) buildContainerStatuses(spec v1.PodSpec) []v1.ContainerStatus {
   272  	result := make([]v1.ContainerStatus, len(spec.Containers))
   273  	for i, cSpec := range spec.Containers {
   274  		restartCount := 0
   275  		if i == 0 {
   276  			restartCount = b.restartCount
   277  		}
   278  		ready, ok := b.cReady[i]
   279  		// if not specified, default to true
   280  		ready = !ok || ready
   281  
   282  		result[i] = v1.ContainerStatus{
   283  			Name:         cSpec.Name,
   284  			Image:        b.buildImage(cSpec.Image, i),
   285  			Ready:        ready,
   286  			ContainerID:  b.buildContainerID(i),
   287  			RestartCount: int32(restartCount),
   288  		}
   289  	}
   290  	return result
   291  }
   292  
   293  func (b PodBuilder) validateImageRefs(numContainers int) {
   294  	for index, img := range b.imageRefs {
   295  		if index >= numContainers {
   296  			b.t.Fatalf("Image %q specified at index %d. Pod only has %d containers", img, index, numContainers)
   297  		}
   298  	}
   299  }
   300  
   301  func (b PodBuilder) validateContainerIDs(numContainers int) {
   302  	for index, cID := range b.cIDs {
   303  		if index >= numContainers {
   304  			b.t.Fatalf("Container ID %q specified at index %d. Pod only has %d containers", cID, index, numContainers)
   305  		}
   306  	}
   307  }
   308  
   309  // Simulates a Pod -> ReplicaSet -> Deployment ref tree
   310  func (b PodBuilder) ObjectTreeEntities() []k8s.K8sEntity {
   311  	pod := b.Build()
   312  	rs := b.buildReplicaSet()
   313  	dep := b.buildDeployment()
   314  	return []k8s.K8sEntity{
   315  		k8s.NewK8sEntity(pod),
   316  		k8s.NewK8sEntity(rs),
   317  		k8s.NewK8sEntity(dep),
   318  	}
   319  }
   320  
   321  func (b PodBuilder) Build() *v1.Pod {
   322  	entities, err := parseYAMLFromManifest(b.manifest)
   323  	if err != nil {
   324  		b.t.Fatal(fmt.Errorf("PodBuilder YAML parser: %v", err))
   325  	}
   326  
   327  	tSpecs, err := k8s.ExtractPodTemplateSpec(entities)
   328  	if err != nil {
   329  		b.t.Fatal(fmt.Errorf("PodBuilder extract pod templates: %v", err))
   330  	}
   331  
   332  	if len(tSpecs) != 1 {
   333  		b.t.Fatalf("PodBuilder only works with Manifests with exactly 1 PodTemplateSpec: %v", tSpecs)
   334  	}
   335  
   336  	tSpec := tSpecs[0]
   337  	spec := tSpec.Spec
   338  	numContainers := len(spec.Containers)
   339  	b.validateImageRefs(numContainers)
   340  	b.validateContainerIDs(numContainers)
   341  
   342  	for i, container := range spec.Containers {
   343  		container.Image = b.buildImage(container.Image, i)
   344  		spec.Containers[i] = container
   345  	}
   346  
   347  	return &v1.Pod{
   348  		ObjectMeta: metav1.ObjectMeta{
   349  			Name:              b.PodID(),
   350  			CreationTimestamp: b.buildCreationTime(),
   351  			DeletionTimestamp: b.buildDeletionTime(),
   352  			Labels:            b.buildLabels(tSpec),
   353  			UID:               b.buildPodUID(),
   354  			OwnerReferences: []metav1.OwnerReference{
   355  				k8s.RuntimeObjToOwnerRef(b.buildReplicaSet()),
   356  			},
   357  		},
   358  		Spec: spec,
   359  		Status: v1.PodStatus{
   360  			Phase:             b.buildPhase(),
   361  			ContainerStatuses: b.buildContainerStatuses(spec),
   362  		},
   363  	}
   364  }
   365  
   366  func imageNameForManifest(manifestName string) reference.Named {
   367  	return container.MustParseNamed(manifestName)
   368  }
   369  
   370  func parseYAMLFromManifest(m model.Manifest) ([]k8s.K8sEntity, error) {
   371  	return k8s.ParseYAMLFromString(m.K8sTarget().YAML)
   372  }