Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Pod await logic #590

Merged
merged 6 commits into from
Jul 3, 2019
Merged

Conversation

lblackstone
Copy link
Member

Fixes #450

@lblackstone lblackstone force-pushed the lblackstone/pod-await-refactor branch 7 times, most recently from a5cdb43 to a01c1e0 Compare June 26, 2019 22:27
@lblackstone lblackstone marked this pull request as ready for review June 27, 2019 03:10
@lblackstone lblackstone force-pushed the lblackstone/pod-await-refactor branch from 448a412 to 30e329d Compare June 27, 2019 03:14
Copy link
Contributor

@metral metral left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM. Let's get some other eyes on here to review given the size of the PR

pkg/await/core_pod.go Show resolved Hide resolved
pkg/await/fixtures/pod.go Show resolved Hide resolved
pkg/await/states/pod_test.go Show resolved Hide resolved
@lblackstone lblackstone force-pushed the lblackstone/pod-await-refactor branch from 2970ee2 to d5a20f3 Compare July 2, 2019 15:58
@lblackstone lblackstone merged commit 46b1bce into master Jul 3, 2019
@pulumi-bot pulumi-bot deleted the lblackstone/pod-await-refactor branch July 3, 2019 19:54
lblackstone added a commit that referenced this pull request Jul 3, 2019
Copy link
Contributor

@hausdorff hausdorff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is with great regret that I must report this is vastly superior to what I originally wrote.

In fact, just today I remarked to @lukehoban that you occasionally hear stories of people writing some random Go and wishing they had a feature of another language, only to rewrite it and find out (a) they didn't need it, and (b) it's simpler without it. I told Luke I had never had this experience and I couldn't tell whether that's because I'm a really bad Go programmer or not.

Well, now that we've rewritten it, the jury is in: I am just a bad Go programmer.

Anyway, a few questions/suggestions below.

@@ -497,7 +497,7 @@ func Test_Apps_Deployment(t *testing.T) {
object: deploymentProgressing(inputNamespace, deploymentInputName, revision1),
subErrors: []string{
"Minimum number of live Pods was not attained",
`1 Pods failed to run because: [ImagePullBackOff] Back-off pulling image "sdkjlsdlkj"`,
`containers with unready status: [nginx] -- Back-off pulling image "sdkjlsdlkj"`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a huge thing but as a general rule I think we should keep the ImagePullBackOff because the standard error codes are easier to Google. It's also easier for people who know how this stuff works to understand what's going on.


// Message stores a log string and the severity for the log message.
type Message struct {
S string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start, and it's ok if the answer is no -- but if there is additional structured information we can pull out of the S and into the Message struct, I think we should do that. Is there anything that seems likely to fit that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not off the top of my head. Will keep it in mind as I continue refactoring.

}

// Empty returns true if the Message is uninitialized, false otherwise.
func (m Message) Empty() bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: why make Empty a method on Message instead of making Result#Message a pointer (in which case you could check if it's "empty" by checking whether it's nil).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message is used in a bunch of places. Can't recall the specific reason, but it was easier this way when I wrote it.

scheduleErrorCounts := map[string]int{}
containerErrorCounts := map[string]int{}
for _, pod := range sia.pods {
func (sia *statefulsetInitAwaiter) aggregatePodErrors() logging.Messages {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal, but an alternative design you might consider here is to have it take a lambda as an argument instead of passing an allocated list back, and then allocating another list, again, in response, which is what we're doing in a lot of the callsites.

messages := sia.aggregatePodErrors(func() {
    sia.config.logMessage(message)
})

}
messages = append(messages, checker.Update(pod).Warnings()...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

@@ -61,3 +62,149 @@ func PodBasic() *podBasic {
},
}
}

// PodBase returns Pod struct with basic data initialized.
func PodBase(name, namespace string) *corev1.Pod {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why build the pods up explicitly in code instead of just deserializing JSON? Seems like it's advantageous to have the verbatim resource definitions as the basis of these tests, and as it's written I can't quite tell if they are exactly correct. Also, the status field for pod in particular is way way fiddly. I consider the comment from @metral below evidence of all of this. :)


func NewPodChecker() *StateChecker {
return &StateChecker{
conditions: []Condition{podScheduled, podInitialized, podReady},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol this is way better than the way we're doing it now.

result := Result{Description: fmt.Sprintf("Waiting for Pod %q to be scheduled", fqName(pod))}

for _, condition := range pod.Status.Conditions {
if condition.Type == v1.PodScheduled {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop-and-check-type logic seems error-prone, and I think we should consider moving this into its own function, something like:

result := Result{Description: fmt.Sprintf("Waiting for Pod %q to be scheduled", fqName(pod))}
matchCondition(
	pod.Status.Conditions,
	v1.PodScheduled,
	func(v1.PodCondition) { result.Ok = true }, // onTrue
	func(condition v1.PodCondition) { 
	 	msg := statusFromCondition(condition)
		if len(msg) > 0 {
			result.Message = logging.StatusMessage(msg)
		}
	}) // onFalse

Same thing, perhaps, for container status?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a filter function to flatten out the logic, and structured the conditional checking similarly to your suggestion.

"github.com/pulumi/pulumi-kubernetes/pkg/logging"
)

// Result specifies the result of a Condition applied to an input object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to mention that this is a v1.PodCondition, and write a bit more about what this is actually for -- e.g., it's the Result of a check that a Pod has some v1.PodCondition that is true, with some metadata useful for rendering messages about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually isn't specific to PodConditions; it's a generic condition that is used to determine readiness of the relevant resource.

// Condition is a function that checks a state and returns a Result.
type Condition func(s interface{}) Result

// StateChecker holds the data required to generically implement await logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be nice to make this clearer, too. StateChecker checks that a Pod has a set of v1.PodConditions that match the desired state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, StateChecker isn't specific to Pod; it will be used for every resource type once I'm done with refactoring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplify Pod awaiter
3 participants