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

Helm integration test framework #237

Merged
merged 18 commits into from
Feb 20, 2019
Merged

Helm integration test framework #237

merged 18 commits into from
Feb 20, 2019

Conversation

yorinasub17
Copy link
Contributor

This introduces helm integration tests by providing helm.Install and helm.Delete functions. Additionally, this extends the k8s module with additional functions that map better to helm. Specifically, when you install a chart, it is not obvious what name is chosen for the chart. However, you are always guaranteed to have a bunch of labels defined on the resources, so it is more common to query Kubernetes resources using the labels than directly by name with helm.

Here is the list of new functions introduced:

  • helm.Install: Install a helm chart onto a Kubernetes cluster
  • helm.Delete: Delete a deployed release from Tiller
  • k8s.ListPods: List pods in a given namespace
  • k8s.ListServices: List services in a given namespace
  • k8s.WaitUntilNumPodsCreated: Waits until the number of pods matching the given filters reaches the desired count.
  • k8s.Tunnel: A struct that can be used to create and manage port forward tunnel to a Pod.

Finally, I had to further separate out the helm tests because Tiller overloaded minikube when running with the kubernetes tests.

// RunHelmCommandAndGetOutputE runs helm with the given arguments and options and returns stdout/stderr.
func RunHelmCommandAndGetOutputE(t *testing.T, options *Options, cmd string, additionalArgs ...string) (string, error) {
args := []string{cmd}
args = getCommonArgs(options, args...)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This refactor was necessary because only install and upgrade take the --set, --set-file, and -f options.

@@ -223,10 +269,18 @@ workflows:
tags:
only: /^v.*/

- helm_test:
Copy link
Member

Choose a reason for hiding this comment

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

Will this run in parallel?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

**NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm
tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm
can overload the minikube system and thus interfere with the other kubernetes tests. To avoid overloading the system, we
run the kubernetes tests and helm tests separately from the others.
Copy link
Member

Choose a reason for hiding this comment

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

What does "overload the system" mean here? Too much CPU or memory consumption? What are the symptoms? Is it just overloading it in CircleCI, and would work OK on larger servers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll update the comment, but here is an example failing test: https://circleci.com/gh/gruntwork-io/terratest/869

Of the 28 tests, 19 fail. They all fail with:

Post https://10.142.2.94:8443/apis/authorization.k8s.io/v1/selfsubjectaccessreviews: dial tcp 10.142.2.94:8443: connect: connection refused

This indicates that either:

  • minikube crashed
  • minikube failed to respond

I don't think this would be a problem on beefy machines, as this does NOT happen when I run the kubernetes+helm tests locally, so I believe this is an issue with CircleCI. My best guess is memory. The executors have 8GB of RAM, but my laptop is 16GB. I don't think it's CPU because I am on a 13inch no touchbar model, which is a dual core machine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

New verbiage:

// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube
// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with
// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm
// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We
// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.

// Install will install the selected helm chart with the provided options under the given release name. This will fail
// the test if there is an error.
func Install(t *testing.T, options *Options, chartDir string, releaseName string) {
require.NoError(t, InstallE(t, options, chartDir, releaseName))
Copy link
Member

Choose a reason for hiding this comment

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

I like this clean, one-line approach 👍


// GetConfigPath will return a sensible default if the config path is not set on the options.
func (kubectlOptions *KubectlOptions) GetConfigPath(t *testing.T) (string, error) {
var err error
Copy link
Member

Choose a reason for hiding this comment

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

Why declare this here, separately?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add comment, but this is so that I can update kubeConfigPath in the if block below. Otherwise, it complains saying err is undefined.

t,
statusMsg,
60,
1*time.Second,
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't you use the passed-in retry settings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Woops copy pasta from when I was experimenting in helm-kubernetes-services. Will fix.

return "svc"
default:
// This should not happen
return ""
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps return a clear error message in the string?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok

ResourceName string
Out io.Writer
StopChan chan struct{}
ReadyChan chan struct{}
Copy link
Member

Choose a reason for hiding this comment

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

Are all these fields are used outside of this package (esp the channels)? If not, you can make them lowercase to make them private.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah ok. I didn't know that convention. Will update!


// GetAvailablePortE retrieves an available port on the host machine. This delegates the port selection to the golang net
// library by starting a server and then checking the port that the server is using.
func GetAvailablePortE(t *testing.T) (int, error) {
Copy link
Member

Choose a reason for hiding this comment

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

If you run this in a concurrent context, couldn't a race condition lead to the same port being selected? i.e., goroutine one listens on port 12345, it's available, so it returns the port number, and in the defer stops listening. Right after the defer, but before the port number is returned, another goroutine happens to listen on 12345, see it's available, and returns it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you are right. Let me see if there is a way to make this work correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok resolved this by changing it so that the available port selection happens in ForwardPortE when local port is 0. This way, we can enforce a mutex lock between getting the available port and spawning the listener. See 7440db7 .


// Open a tunnel to pod from any available port locally
localPort := GetAvailablePort(t)
tunnel := NewTunnel(options, ResourceTypePod, "nginx-pod", localPort, 80)
Copy link
Member

Choose a reason for hiding this comment

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

Nice, powerful API hiding a lot of complexity 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can't take credit for it since it is copied from helm 😄 But yes I like this API.

30,
10*time.Second,
func(statusCode int, body string) bool {
return statusCode == 200
Copy link
Member

Choose a reason for hiding this comment

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

Check the body contains the text "nginx"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok

@yorinasub17
Copy link
Contributor Author

Ok I believe I addressed all the comments except for the concurrency issue with GetAvailablePort. I will look into that tomorrow.

@yorinasub17
Copy link
Contributor Author

Also verified the private var conversion for Tunnel struct still works with helm-kubernetes-services.

@yorinasub17
Copy link
Contributor Author

Ok I believe I addressed all the review comments, and build is passing, so going to go ahead and merge + release this!

@yorinasub17 yorinasub17 merged commit 9430200 into master Feb 20, 2019
@yorinasub17 yorinasub17 deleted the yori-helm-integration branch February 20, 2019 01:31
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.

None yet

2 participants