Skip to content

Commit

Permalink
Add initial helloworld integration test
Browse files Browse the repository at this point in the history
This is a WIP of adding an integration test to cover #59, which is a
simple TaskRun which has no inputs or outputs but just runs a container
that echoes helloworld.

At this point we have the logic to:
- Create the Task and TaskRun
- Wait for the TaskRun to have a condition (TODO: actually check the
  conditions, waiting to rebase onto #86)
- Get the Build associated (not yet created, will be via #86)
- Get the Pod for the build
- TODO: get the logs for that Pod (we've been looking at
  https://github.com/knative/build/blob/e8c2cb6eb5cb09d9737ca9e6da4a1c68af3247b2/pkg/logs/logs.go#L36:6
  for inspiration)

This also changes namespaces to be test specific, so each test can
create a namespace and tear it down, without needing to keep track of
resources created and delete them individually.

Co-authored-by: Aaron Prindle <aprindle@google.com>
  • Loading branch information
bobcatfish and aaron-prindle committed Oct 5, 2018
1 parent 385f2d9 commit 1e2d24c
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 109 deletions.
18 changes: 5 additions & 13 deletions pkg/apis/pipeline/v1alpha1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package v1alpha1

import (
buildv1alpha1 "github.com/knative/build/pkg/apis/build/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand All @@ -27,8 +26,8 @@ type TaskSpec struct {
// +optional
Inputs *Inputs `json:"inputs,omitempty"`
// +optional
Outputs *Outputs `json:"outputs,omitempty"`
BuildSpec BuildSpec `json:"buildSpec"`
Outputs *Outputs `json:"outputs,omitempty"`
BuildSpec buildv1alpha1.BuildSpec `json:"buildSpec"`
}

// TaskStatus defines the observed state of Task
Expand Down Expand Up @@ -70,6 +69,9 @@ type Inputs struct {
// used as the name of the volume containing this context which will be mounted
// into the container executed by the Build/Task, e.g. a Source with the
// name "workspace" would be mounted into "/workspace".
//
// TODO(#62): Something is wrong here, this should be a reference to a resource,
// could just be that the names and comments are out of date.
type Source struct {
// name of the source should match the name of the SourceBinding in the pipeline
Name string `json:"name"`
Expand Down Expand Up @@ -101,16 +103,6 @@ type TestResult struct {
Path string `json:"path"`
}

// BuildSpec describes how to create a Build for this Task.
// A BuildSpec will contain either a Template or a series of Steps.
type BuildSpec struct {
// Trying to emulate https://github.com/knative/build/blob/master/pkg/apis/build/v1alpha1/build_types.go
// +optional
Steps []corev1.Container `json:"steps,omitempty"`
// +optional
Template buildv1alpha1.TemplateInstantiationSpec `json:"template,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// TaskList contains a list of Task
Expand Down
8 changes: 3 additions & 5 deletions pkg/apis/pipeline/v1alpha1/taskrun_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

buildv1alpha1 "github.com/knative/build/pkg/apis/build/v1alpha1"
)

// TaskRunSpec defines the desired state of TaskRun
Expand All @@ -36,7 +34,8 @@ type TaskRunSpec struct {

// TaskRunInputs holds the input values that this task was invoked with.
type TaskRunInputs struct {
Resources []PipelineResourceVersion `json:"resourcesVersion"`
// +optional
Resources []PipelineResourceVersion `json:"resourcesVersion,omitempty"`
// +optional
Params []Param `json:"params,omitempty"`
}
Expand Down Expand Up @@ -127,8 +126,7 @@ type TaskRun struct {
// +optional
Spec TaskRunSpec `json:"spec,omitempty"`
// +optional
//TODO(aaron-prindle) change back to TaskRunStatus
Status buildv1alpha1.BuildStatus `json:"status,omitempty"`
Status TaskRunStatus `json:"status,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
25 changes: 0 additions & 25 deletions pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 51 additions & 15 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Tests

To run tests:

```shell
# Unit tests
go test ./...

# Integration tests (against your current kube cluster)
go test -v -count=1 -tags=e2e ./test
```

## Unit tests

Unit tests live side by side with the code they are testing and can be run with:
Expand All @@ -12,6 +22,7 @@ _By default `go test` will not run [the integration tests](#integration-tests),
`-tags=e2e` to be enabled._

### Unit testing Controllers

Kubernetes client-go provides a number of fake clients and objects for unit testing. The ones we will be using are:

1. [fake kubernetes client](k8s.io/client-go/kubernetes/fake): Provides a fake REST interface to interact with Kubernetes API
Expand All @@ -21,21 +32,21 @@ You can create a fake PipelineClient for the Controller under test like [this](.

This [pipelineClient](./../pkg/client/clientset/versioned/clientset.go#L34) is initialized with no runtime objects. You can also initialie the client with kubernetes objects and can interact with them using the `pipelineClient.Pipeline()`

```
```go
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

obj := *v1alpha1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.PipelineRunSpec{
PipelineRef: v1alpha1.PipelineRef{
Name: "test-pipeline",
APIVersion: "a1",
},
PipelineRef: v1alpha1.PipelineRef{
Name: "test-pipeline",
APIVersion: "a1",
},
}}
pipelineClient := fakepipelineclientset.NewSimpleClientset(obj)
objs := pipelineClient.Pipeline().PipelineRuns("namespace").List(v1.ListOptions{})
Expand All @@ -51,21 +62,21 @@ so that the [listers](./../pkg/client/listers) can access these.

To add test `PipelineRun` objects to the listers, you can

```
```go
pipelineClient := fakepipelineclientset.NewSimpleClientset()
sharedInfomer := informers.NewSharedInformerFactory(pipelineClient, 0)
pipelineRunsInformer := sharedInfomer.Pipeline().V1alpha1().PipelineRuns()

obj := *v1alpha1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.PipelineRunSpec{
PipelineRef: v1alpha1.PipelineRef{
Name: "test-pipeline",
APIVersion: "a1",
},
PipelineRef: v1alpha1.PipelineRef{
Name: "test-pipeline",
APIVersion: "a1",
},
}}
pipelineRunsInformer.Informer().GetIndexer().Add(obj)
```
Expand Down Expand Up @@ -165,7 +176,6 @@ func tearDown(clients *test.Clients) {

_See [clients.go](./clients.go)._


#### Generate random names

You can use the function `AppendRandomString` to create random names for `crd`s or anything else,
Expand All @@ -177,6 +187,32 @@ namespace := test.AppendRandomString('arendelle')

_See [randstring.go](./randstring.go)._

#### Check Pipeline resources

After creating Pipeline resources or making changes to them, you will need to wait for the system
to realize those changes. You can use polling methods to check the resources reach the desired state.

The `WaitFor*` functions use the kubernetes [`wait` package](https://godoc.org/k8s.io/apimachinery/pkg/util/wait).
To poll they use [`PollImmediate`](https://godoc.org/k8s.io/apimachinery/pkg/util/wait#PollImmediate)
and the return values of the function you provide behave the same as
[`ConditionFunc`](https://godoc.org/k8s.io/apimachinery/pkg/util/wait#ConditionFunc):
a `bool` to indicate if the function should stop or continue polling, and an `error` to indicate if
there has been an error.

For example, you can poll a `TaskRun` object to wait for it to have a `Status.Condition`:

```go
err = WaitForTaskRunState(c, hwTaskRunName, func(tr *v1alpha1.TaskRun) (bool, error) {
if len(tr.Status.Conditions) > 0 {
return true, nil
}
return false, nil
}, "TaskRunHasCondition")
```

_[Metrics will be emitted](https://github.com/knative/pkg/tree/master/test#emit-metrics)
for these `Wait` method tracking how long test poll for._

## Presubmit tests

[`presubmit-tests.sh`](./presubmit-tests.sh) is the entry point for all tests
Expand Down
33 changes: 24 additions & 9 deletions test/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,30 @@ import (

"github.com/knative/build-pipeline/pkg/client/clientset/versioned"
"github.com/knative/build-pipeline/pkg/client/clientset/versioned/typed/pipeline/v1alpha1"
buildversioned "github.com/knative/build/pkg/client/clientset/versioned"
buildv1alpha1 "github.com/knative/build/pkg/client/clientset/versioned/typed/build/v1alpha1"
knativetest "github.com/knative/pkg/test"
)

// Clients holds instances of interfaces for making requests to the Pipeline controllers.
type Clients struct {
KubeClient *knativetest.KubeClient
// clients holds instances of interfaces for making requests to the Pipeline controllers.
type clients struct {
KubeClient *knativetest.KubeClient

PipelineClient v1alpha1.PipelineInterface
TaskClient v1alpha1.TaskInterface
TaskRunClient v1alpha1.TaskRunInterface

BuildClient buildv1alpha1.BuildInterface
}

// NewClients instantiates and returns several clientsets required for making requests to the
// newClients instantiates and returns several clientsets required for making requests to the
// Pipeline cluster specified by the combination of clusterName and configPath. Clients can
// make requests within namespace.
func NewClients(configPath, clusterName, namespace string) (*Clients, error) {
func newClients(configPath, clusterName, namespace string) (*clients, error) {
var err error
clients := &Clients{}
c := &clients{}

clients.KubeClient, err = knativetest.NewKubeClient(configPath, clusterName)
c.KubeClient, err = knativetest.NewKubeClient(configPath, clusterName)
if err != nil {
return nil, fmt.Errorf("failed to create kubeclient from config file at %s: %s", configPath, err)
}
Expand All @@ -50,7 +57,15 @@ func NewClients(configPath, clusterName, namespace string) (*Clients, error) {
if err != nil {
return nil, fmt.Errorf("failed to create pipeline clientset from config file at %s: %s", configPath, err)
}
clients.PipelineClient = cs.PipelineV1alpha1().Pipelines(namespace)
c.PipelineClient = cs.PipelineV1alpha1().Pipelines(namespace)
c.TaskClient = cs.PipelineV1alpha1().Tasks(namespace)
c.TaskRunClient = cs.PipelineV1alpha1().TaskRuns(namespace)

bcs, err := buildversioned.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create build clientset from config file at %s: %s", configPath, err)
}
c.BuildClient = bcs.BuildV1alpha1().Builds(namespace)

return clients, nil
return c, nil
}
55 changes: 55 additions & 0 deletions test/crd_checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2018 The Knative Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package test

import (
"context"
"fmt"
"time"

"github.com/knative/build-pipeline/pkg/apis/pipeline/v1alpha1"
"go.opencensus.io/trace"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
)

const (
interval = 1 * time.Second
// Currently using a super short timeout b/c tests are expected to fail so this way
// we can get to that failure faster - knative/serving is currently using `6 * time.Minute`
// which we could use, or we could use timeouts more specific to what each `Task` is
// actually expected to do.
timeout = 10 * time.Second
)

// WaitForTaskRunState polls the status of the TaskRun called name from client every
// interval until inState returns `true` indicating it is done, returns an
// error or timeout. desc will be used to name the metric that is emitted to
// track how long it took for name to get into the state checked by inState.
func WaitForTaskRunState(c *clients, name string, inState func(r *v1alpha1.TaskRun) (bool, error), desc string) error {
metricName := fmt.Sprintf("WaitForTaskRunState/%s/%s", name, desc)
_, span := trace.StartSpan(context.Background(), metricName)
defer span.End()

return wait.PollImmediate(interval, timeout, func() (bool, error) {
r, err := c.TaskRunClient.Get(name, metav1.GetOptions{})
if err != nil {
return true, err
}
return inState(r)
})
}
Loading

0 comments on commit 1e2d24c

Please sign in to comment.