Skip to content

Commit

Permalink
Implement custom tester functionality in Skaffold (#5451)
Browse files Browse the repository at this point in the history
* Adding new test runner for custom tester.

* Adding new custom tester config to Skaffold comfig.

* Adding usage example for newly added custon tester.

* Extracting structure tests run logic into a seperate method.

* Moving structure test's dependency file extraction logic to NewTester() to reduce code redundancy.

* Revert "Moving structure test's dependency file extraction logic to NewTester() to reduce code redundancy."

This reverts commit 5e1d859.

* Moving structure test's dependency file extraction logic to NewTester() to reduce code redundancy.

* Making the custom command a required field.[D

* Adding new error codes for custom tester.

* Updating custom test example.

* Removing errors file.

* Updating custom test example.

* Adding generated schema files.

* Added custom test example to `integration/run_test.go`.

* Added custom test example in `integration/examples directory`.

* Updating the apiVersion in example skaffold.yaml.

* Updating the apiVersion in example skaffold.yaml.

* Removing the custom-test form examples folder as the newly added custom test fields are not available in the current version of the skaffold config.

* Updates per review feedback.

* Added schema validation & updated example.

* Adding tests for error scenarios.

* Updated the test exit code.

* Special casing error tests for Windows platform.

* Updating the container name for custom test example.

* Updated custom-test example container pods to continue running.

* Added tests for TestDependencies.

* Adding tests for custom test schema validation.

* Updated unit tests per review feedback.

* Updated unit tests to use mock by overriding util.RunCmd method.

* Updating windows specific unit test.

* Added another instance of custom command to the integration test.

* Updated custom test dependencies.

* Updating custom test example with command dependencies.
  • Loading branch information
PriyaModali authored Mar 4, 2021
1 parent 7a04427 commit 2e55bec
Show file tree
Hide file tree
Showing 15 changed files with 794 additions and 8 deletions.
81 changes: 79 additions & 2 deletions docs/content/en/schemas/v2beta13.json
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,74 @@
"description": "*beta* tags images with a configurable template string.",
"x-intellij-html-description": "<em>beta</em> tags images with a configurable template string."
},
"CustomTest": {
"required": [
"command"
],
"properties": {
"command": {
"type": "string",
"description": "custom command to be executed. If the command exits with a non-zero return code, the test will be considered to have failed.",
"x-intellij-html-description": "custom command to be executed. If the command exits with a non-zero return code, the test will be considered to have failed."
},
"dependencies": {
"$ref": "#/definitions/CustomTestDependencies",
"description": "additional test-specific file dependencies; changes to these files will re-run this test.",
"x-intellij-html-description": "additional test-specific file dependencies; changes to these files will re-run this test."
},
"timeoutSeconds": {
"type": "integer",
"description": "sets the wait time for skaffold for the command to complete. If unset or 0, Skaffold will wait until the command completes.",
"x-intellij-html-description": "sets the wait time for skaffold for the command to complete. If unset or 0, Skaffold will wait until the command completes."
}
},
"preferredOrder": [
"command",
"timeoutSeconds",
"dependencies"
],
"additionalProperties": false,
"description": "describes the custom test command provided by the user. Custom tests are run after an image build whenever build or test dependencies are changed.",
"x-intellij-html-description": "describes the custom test command provided by the user. Custom tests are run after an image build whenever build or test dependencies are changed."
},
"CustomTestDependencies": {
"properties": {
"command": {
"type": "string",
"description": "represents a command that skaffold executes to obtain dependencies. The output of this command *must* be a valid JSON array.",
"x-intellij-html-description": "represents a command that skaffold executes to obtain dependencies. The output of this command <em>must</em> be a valid JSON array."
},
"ignore": {
"items": {
"type": "string"
},
"type": "array",
"description": "specifies the paths that should be ignored by skaffold's file watcher. If a file exists in both `paths` and in `ignore`, it will be ignored, and will be excluded from both retest and file synchronization. Will only work in conjunction with `paths`.",
"x-intellij-html-description": "specifies the paths that should be ignored by skaffold's file watcher. If a file exists in both <code>paths</code> and in <code>ignore</code>, it will be ignored, and will be excluded from both retest and file synchronization. Will only work in conjunction with <code>paths</code>.",
"default": "[]"
},
"paths": {
"items": {
"type": "string"
},
"type": "array",
"description": "should be set to the file dependencies for this command, so that the skaffold file watcher knows when to retest and perform file synchronization.",
"x-intellij-html-description": "should be set to the file dependencies for this command, so that the skaffold file watcher knows when to retest and perform file synchronization.",
"default": "[]",
"examples": [
"[\"src/test/**\"]"
]
}
},
"preferredOrder": [
"command",
"paths",
"ignore"
],
"additionalProperties": false,
"description": "used to specify dependencies for custom test command. `paths` should be specified for file watching to work as expected.",
"x-intellij-html-description": "used to specify dependencies for custom test command. <code>paths</code> should be specified for file watching to work as expected."
},
"DateTimeTagger": {
"properties": {
"format": {
Expand Down Expand Up @@ -2910,6 +2978,14 @@
"image"
],
"properties": {
"custom": {
"items": {
"$ref": "#/definitions/CustomTest"
},
"type": "array",
"description": "the set of custom tests to run after an artifact is built.",
"x-intellij-html-description": "the set of custom tests to run after an artifact is built."
},
"image": {
"type": "string",
"description": "artifact on which to run those tests.",
Expand All @@ -2933,11 +3009,12 @@
},
"preferredOrder": [
"image",
"custom",
"structureTests"
],
"additionalProperties": false,
"description": "a list of structure tests to run on images that Skaffold builds.",
"x-intellij-html-description": "a list of structure tests to run on images that Skaffold builds."
"description": "a list of tests to run on images that Skaffold builds.",
"x-intellij-html-description": "a list of tests to run on images that Skaffold builds."
}
}
}
12 changes: 12 additions & 0 deletions integration/examples/custom-tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.15 as builder
COPY main.go .
# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
ARG SKAFFOLD_GO_GCFLAGS
RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /app main.go

FROM alpine:3
# Define GOTRACEBACK to mark this container as using the Go language runtime
# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/).
ENV GOTRACEBACK=single
CMD ["./app"]
COPY --from=builder /app .
32 changes: 32 additions & 0 deletions integration/examples/custom-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
### Example: Running custom tests on built images

This example shows how to run _custom tests_ on newly built images in the skaffold dev loop.

Custom tests are associated with single image artifacts. When test dependencies change, no build will happen but tests would get re-run. Tests are configured in the `skaffold.yaml` in the `test` stanza, e.g.

```yaml
test:
- image: skaffold-example
custom:
- command: <command>
timeoutSeconds: <timeout in seconds>
dependencies: <dependencies for this command>
paths: <file dependencies>
- <paths glob>
```
As tests take time, you might prefer to configure tests using [profiles](https://skaffold.dev/docs/https://skaffold.dev/docs/environment/profiles/) so that they can be automatically enabled or disabled, e.g.
If the `command` exits with a non-zero return code then the test will have failed, and deployment will not continue.

```yaml
profiles:
- name: test
test:
- image: skaffold-example
custom:
- command: <command>
timeoutSeconds: <timeout in seconds>
dependencies: <dependencies for this command>
paths: <file dependencies>
- <paths glob>
```
8 changes: 8 additions & 0 deletions integration/examples/custom-tests/k8s-pod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Pod
metadata:
name: custom-test
spec:
containers:
- name: custom-test
image: custom-test-example
39 changes: 39 additions & 0 deletions integration/examples/custom-tests/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright 2021 The Skaffold 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 main

import (
"fmt"
"math/rand"
"time"
)

func MinInt(a, b int) int {
if a < b {
return a
}
return b
}

func main() {
rand.Seed(time.Now().UnixNano())
for {
x := rand.Intn(100)
y := rand.Intn(100)

min := MinInt(x, y)
fmt.Println("Min of ", x, " and ", y, " is: ", min)
time.Sleep(time.Second * 1)
}
}
53 changes: 53 additions & 0 deletions integration/examples/custom-tests/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
Copyright 2021 The Skaffold 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 main

import (
"fmt"
"testing"
)

func TestMinIntBasic(tb *testing.T) {
fmt.Println("Running Basic test.")
min := MinInt(5, -5)
if min != -5 {
tb.Errorf("MinInt(5, -5) returned %d; expecting -5", min)
}
}

func TestMinIntTableDriven(tdt *testing.T) {
var tests = []struct {
x, y int
want int
}{
{0, 0, 0},
{1, 0, 0},
{0, 1, 0},
{0, -1, -1},
{-1, 0, -1},
{-2, -5, -5},
{-5, -2, -5},
}

fmt.Println("Running Table driven test.")
for _, t := range tests {
testname := fmt.Sprintf("TestMinInt(): %d,%d", t.x, t.y)
tdt.Run(testname, func(tdt *testing.T) {
min := MinInt(t.x, t.y)
if min != t.want {
tdt.Errorf("MinInt(%d, %d) returned %d; expecting %d", t.x, t.y, min, t.want)
}
})
}
}
21 changes: 21 additions & 0 deletions integration/examples/custom-tests/skaffold.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: skaffold/v2beta13
kind: Config
build:
artifacts:
- image: custom-test-example
test:
- image: custom-test-example
custom:
- command: ./test.sh
timeoutSeconds: 60
dependencies:
paths:
- "*_test.go"
- "test.sh"
- command: echo Hello world!!
dependencies:
command: echo [\"main_test.go\"]
deploy:
kubectl:
manifests:
- k8s-*
21 changes: 21 additions & 0 deletions integration/examples/custom-tests/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

# Copyright 2021 The Skaffold 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.

set -e

echo "go custom test $@"

go test .
5 changes: 5 additions & 0 deletions integration/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func TestRun(t *testing.T) {
dir: "examples/structure-tests",
pods: []string{"getting-started"},
},
{
description: "custom-tests",
dir: "examples/custom-tests",
pods: []string{"custom-test"},
},
{
description: "microservices",
dir: "examples/microservices",
Expand Down
35 changes: 34 additions & 1 deletion pkg/skaffold/schema/latest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,12 +468,15 @@ type ResourceRequirement struct {
ResourceStorage string `yaml:"resourceStorage,omitempty"`
}

// TestCase is a list of structure tests to run on images that Skaffold builds.
// TestCase is a list of tests to run on images that Skaffold builds.
type TestCase struct {
// ImageName is the artifact on which to run those tests.
// For example: `gcr.io/k8s-skaffold/example`.
ImageName string `yaml:"image" yamltags:"required"`

// CustomTests lists the set of custom tests to run after an artifact is built.
CustomTests []CustomTest `yaml:"custom,omitempty"`

// StructureTests lists the [Container Structure Tests](https://github.com/GoogleContainerTools/container-structure-test)
// to run on that artifact.
// For example: `["./test/*"]`.
Expand Down Expand Up @@ -1016,6 +1019,36 @@ type CustomDependencies struct {
Ignore []string `yaml:"ignore,omitempty"`
}

// CustomTest describes the custom test command provided by the user.
// Custom tests are run after an image build whenever build or test dependencies are changed.
type CustomTest struct {
// Command is the custom command to be executed. If the command exits with a non-zero return
// code, the test will be considered to have failed.
Command string `yaml:"command" yamltags:"required"`

// TimeoutSeconds sets the wait time for skaffold for the command to complete.
// If unset or 0, Skaffold will wait until the command completes.
TimeoutSeconds int `yaml:"timeoutSeconds,omitempty"`

// Dependencies are additional test-specific file dependencies; changes to these files will re-run this test.
Dependencies *CustomTestDependencies `yaml:"dependencies,omitempty"`
}

// CustomTestDependencies is used to specify dependencies for custom test command.
// `paths` should be specified for file watching to work as expected.
type CustomTestDependencies struct {
// Command represents a command that skaffold executes to obtain dependencies. The output of this command *must* be a valid JSON array.
Command string `yaml:"command,omitempty" yamltags:"oneOf=dependency"`

// Paths should be set to the file dependencies for this command, so that the skaffold file watcher knows when to retest and perform file synchronization.
// For example: `["src/test/**"]`
Paths []string `yaml:"paths,omitempty" yamltags:"oneOf=dependency" skaffold:"filepath"`

// Ignore specifies the paths that should be ignored by skaffold's file watcher. If a file exists in both `paths` and in `ignore`, it will be ignored, and will be excluded from both retest and file synchronization.
// Will only work in conjunction with `paths`.
Ignore []string `yaml:"ignore,omitempty" skaffold:"filepath"`
}

// DockerfileDependency *beta* is used to specify a custom build artifact that is built from a Dockerfile. This allows skaffold to determine dependencies from the Dockerfile.
type DockerfileDependency struct {
// Path locates the Dockerfile relative to workspace.
Expand Down
Loading

0 comments on commit 2e55bec

Please sign in to comment.