mirror of https://github.com/istio/istio.io.git
Copy istio.io test framework from istio/istio (#6521)
This commit is contained in:
parent
e97c81b700
commit
a7c0724624
|
@ -0,0 +1,130 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"istio.io/istio/pkg/test/env"
|
||||
"istio.io/istio/pkg/test/scopes"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
)
|
||||
|
||||
const (
|
||||
snippetsFileExtension = ".snippets.txt"
|
||||
snippetsFileHeaderFormat = `# Created by %s. DO NOT EDIT THIS FILE MANUALLY!
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
// Step builds a step of the test pipeline.
|
||||
type Step interface {
|
||||
// run this step.
|
||||
run(ctx Context)
|
||||
}
|
||||
|
||||
// Builder builds a test of a documented workflow from http://istio.io.
|
||||
type Builder struct {
|
||||
snippetsFileName string
|
||||
steps []Step
|
||||
cleanupSteps []Step
|
||||
}
|
||||
|
||||
// NewBuilder returns an instance of an example test. The name of the snippets file must be provided.
|
||||
// If the snippets file name does not end with ".snippets.txt", the extension will be appended automatically.
|
||||
func NewBuilder(snippetsFileName string) *Builder {
|
||||
if snippetsFileName == "" {
|
||||
panic("must provide the snippets file name")
|
||||
}
|
||||
|
||||
// Add the appropriate suffix if it's missing.
|
||||
if !strings.HasSuffix(snippetsFileName, snippetsFileExtension) {
|
||||
snippetsFileName += snippetsFileExtension
|
||||
}
|
||||
|
||||
return &Builder{
|
||||
snippetsFileName: snippetsFileName,
|
||||
}
|
||||
}
|
||||
|
||||
// Add a step to be run.
|
||||
func (b *Builder) Add(steps ...Step) *Builder {
|
||||
b.steps = append(b.steps, steps...)
|
||||
return b
|
||||
}
|
||||
|
||||
// Defer registers a function to be executed when the test completes.
|
||||
func (b *Builder) Defer(steps ...Step) *Builder {
|
||||
b.cleanupSteps = append(b.cleanupSteps, steps...)
|
||||
return b
|
||||
}
|
||||
|
||||
// BuildAndRun is a utility method for building and running the test function in one step.
|
||||
func (b *Builder) BuildAndRun(ctx framework.TestContext) {
|
||||
b.Build()(ctx)
|
||||
}
|
||||
|
||||
// Build a run function for the test
|
||||
func (b *Builder) Build() func(ctx framework.TestContext) {
|
||||
return func(ctx framework.TestContext) {
|
||||
scopes.CI.Infof("Executing test %s (%d steps)", ctx.Name(), len(b.steps))
|
||||
|
||||
snippetFile, err := os.Create(filepath.Join(ctx.WorkDir(), b.snippetsFileName))
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed creating snippets file: %v", err)
|
||||
}
|
||||
defer func() { _ = snippetFile.Close() }()
|
||||
|
||||
// Write the header to the snippets file.
|
||||
if _, err := snippetFile.WriteString(fmt.Sprintf(snippetsFileHeaderFormat, ctx.Name())); err != nil {
|
||||
ctx.Fatalf("failed writing header to snippets file: %v", err)
|
||||
}
|
||||
|
||||
eCtx := Context{
|
||||
TestContext: ctx,
|
||||
snippetFile: snippetFile,
|
||||
snippetMap: make(map[string]string),
|
||||
}
|
||||
|
||||
// create a symbolic link to samples/, for easy access
|
||||
samplesSymlink := path.Join(ctx.WorkDir(), "samples")
|
||||
if _, err := os.Stat(samplesSymlink); os.IsNotExist(err) {
|
||||
err = os.Symlink(path.Join(env.IstioSrc, "samples"), samplesSymlink)
|
||||
if err != nil {
|
||||
scopes.CI.Warnf("Could not create symlink to samples/ directory at %s", samplesSymlink)
|
||||
} else {
|
||||
defer func() {
|
||||
_ = os.Remove(samplesSymlink)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Run cleanup functions at the end.
|
||||
defer func() {
|
||||
for _, step := range b.cleanupSteps {
|
||||
step.run(eCtx)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, step := range b.steps {
|
||||
step.run(eCtx)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/framework/components/environment/kube"
|
||||
)
|
||||
|
||||
// Context for the currently executing test.
|
||||
type Context struct {
|
||||
framework.TestContext
|
||||
snippetFile io.Writer
|
||||
|
||||
// Maintain the set of all snippets added so far to avoid duplicates.
|
||||
snippetMap map[string]string
|
||||
}
|
||||
|
||||
// addSnippet adds the content of the given snippet to the snippet file.
|
||||
func (ctx Context) addSnippet(snippetName, inputName, content string) {
|
||||
// Ensure we don't duplicate snippet names.
|
||||
if prevInput := ctx.snippetMap[snippetName]; prevInput != "" {
|
||||
ctx.Fatalf("Duplicate snippet %s in input %s. Previous input: %s.", snippetName, inputName, prevInput)
|
||||
}
|
||||
ctx.snippetMap[snippetName] = inputName
|
||||
|
||||
// Write the snippet.
|
||||
if _, err := ctx.snippetFile.Write([]byte(content)); err != nil {
|
||||
ctx.Fatalf("Failed writing snippet %s: %v", snippetName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// KubeEnv casts the test environment as a *kube.Environment. If the cast fails, fails the test.
|
||||
func (ctx Context) KubeEnv() *kube.Environment {
|
||||
e, ok := ctx.Environment().(*kube.Environment)
|
||||
if !ok {
|
||||
ctx.Fatalf("test framework unable to get Kubernetes environment")
|
||||
}
|
||||
return e
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
var _ Step = Func(nil)
|
||||
|
||||
// Func is a Step that runs the provided function.
|
||||
type Func func(Context)
|
||||
|
||||
func (f Func) run(ctx Context) {
|
||||
f(ctx)
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"istio.io/istio/pkg/test/env"
|
||||
"istio.io/istio/pkg/test/util/tmpl"
|
||||
)
|
||||
|
||||
type Input interface {
|
||||
InputSelector
|
||||
Name() string
|
||||
ReadAll() (string, error)
|
||||
}
|
||||
|
||||
type InputSelector interface {
|
||||
SelectInput(Context) Input
|
||||
}
|
||||
|
||||
var _ Input = Path("")
|
||||
var _ InputSelector = Path("")
|
||||
|
||||
// TODO(nmittler): Rename to File
|
||||
type Path string
|
||||
|
||||
func (p Path) Name() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (p Path) ReadAll() (string, error) {
|
||||
content, err := ioutil.ReadFile(string(p))
|
||||
return string(content), err
|
||||
}
|
||||
|
||||
func (p Path) SelectInput(ctx Context) Input {
|
||||
ctx.Helper()
|
||||
return p
|
||||
}
|
||||
|
||||
var _ Input = Inline{}
|
||||
var _ InputSelector = Inline{}
|
||||
|
||||
type Inline struct {
|
||||
FileName string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t Inline) Name() string {
|
||||
return t.FileName
|
||||
}
|
||||
|
||||
func (t Inline) ReadAll() (string, error) {
|
||||
return t.Value, nil
|
||||
}
|
||||
|
||||
func (t Inline) SelectInput(ctx Context) Input {
|
||||
ctx.Helper()
|
||||
return t
|
||||
}
|
||||
|
||||
func BookInfo(relativePath string) Input {
|
||||
return Path(filepath.Join(env.IstioSrc, "samples/bookinfo/platform/kube/"+relativePath))
|
||||
}
|
||||
|
||||
func InputSelectorFunc(fn func(ctx Context) Input) InputSelector {
|
||||
return &inputSelector{fn: fn}
|
||||
}
|
||||
|
||||
type inputSelector struct {
|
||||
fn func(Context) Input
|
||||
}
|
||||
|
||||
func (s *inputSelector) SelectInput(ctx Context) Input {
|
||||
ctx.Helper()
|
||||
return s.fn(ctx)
|
||||
}
|
||||
|
||||
var _ InputSelector = IfMinikube{}
|
||||
|
||||
// IfMinikube is a FileSelector that chooses Input based on whether the environment is configured for Minikube.
|
||||
type IfMinikube struct {
|
||||
// Then is selected when the environment is configured for Minikube.
|
||||
Then InputSelector
|
||||
// Else is selected whtn the environment is NOT configured for Minikube.
|
||||
Else InputSelector
|
||||
}
|
||||
|
||||
func (s IfMinikube) SelectInput(ctx Context) Input {
|
||||
ctx.Helper()
|
||||
if ctx.KubeEnv().Settings().Minikube {
|
||||
return s.Then.SelectInput(ctx)
|
||||
}
|
||||
return s.Else.SelectInput(ctx)
|
||||
}
|
||||
|
||||
func Evaluate(selector InputSelector, data map[string]interface{}) InputSelector {
|
||||
return InputSelectorFunc(func(ctx Context) Input {
|
||||
ctx.Helper()
|
||||
|
||||
input := selector.SelectInput(ctx)
|
||||
|
||||
// Read the input template.
|
||||
templateContent, err := input.ReadAll()
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed reading input %s: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
// Evaluate the input file as a template.
|
||||
output, err := tmpl.Evaluate(templateContent, data)
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed evaluating template %s: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
return Inline{
|
||||
FileName: input.Name(),
|
||||
Value: output,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"istio.io/istio/pkg/test/kube"
|
||||
)
|
||||
|
||||
var _ Step = SinglePodWait("")
|
||||
var _ Step = MultiPodWait("")
|
||||
|
||||
// PodWait is a Step that waits for pods to be deployed.
|
||||
type PodWait func(ctx Context) kube.PodFetchFunc
|
||||
|
||||
func (s PodWait) run(ctx Context) {
|
||||
ctx.Helper()
|
||||
|
||||
if _, err := ctx.KubeEnv().WaitUntilPodsAreReady(s(ctx)); err != nil {
|
||||
ctx.Fatal("failed waiting for pods to start: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SinglePodWait waits for a single pod matching the given label and selectors.
|
||||
func SinglePodWait(namespace string, selectors ...string) PodWait {
|
||||
return func(ctx Context) kube.PodFetchFunc {
|
||||
ctx.Helper()
|
||||
return ctx.KubeEnv().NewSinglePodFetch(namespace, selectors...)
|
||||
}
|
||||
}
|
||||
|
||||
/// MultiPodWait waits for multiple pods that match the given labels and selectors.
|
||||
func MultiPodWait(namespace string, selectors ...string) PodWait {
|
||||
return func(ctx Context) kube.PodFetchFunc {
|
||||
ctx.Helper()
|
||||
return ctx.KubeEnv().NewPodFetch(namespace, selectors...)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,736 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"istio.io/istio/pkg/test/framework/components/environment"
|
||||
"istio.io/istio/pkg/test/scopes"
|
||||
)
|
||||
|
||||
// outputStream enumerates the selectable output streams.
|
||||
type outputStream string
|
||||
|
||||
const (
|
||||
outputStreamStdout outputStream = "stdout"
|
||||
outputStreamStderr outputStream = "stderr"
|
||||
outputStreamAll outputStream = "all"
|
||||
)
|
||||
|
||||
// Map of supported output streams.
|
||||
var outputStreams = map[outputStream]struct{}{
|
||||
outputStreamStdout: {},
|
||||
outputStreamStderr: {},
|
||||
outputStreamAll: {},
|
||||
}
|
||||
|
||||
const (
|
||||
snippetStartToken = "# $snippet"
|
||||
verifyToken = "# $verify"
|
||||
snippetOutputToken = "# $snippetoutput"
|
||||
snippetEndToken = "# $endsnippet"
|
||||
syntaxKey = "syntax"
|
||||
outputIsKey = "outputis"
|
||||
outputSnippetKey = "outputsnippet"
|
||||
outputStreamKey = "outputstream"
|
||||
sourceKey = "source"
|
||||
verifierKey = "verifier"
|
||||
defaultCommandSyntax = "bash"
|
||||
defaultOutputSyntax = "text"
|
||||
commandLinePrefix = "$ "
|
||||
testOutputDirEnvVar = "TEST_OUTPUT_DIR"
|
||||
kubeConfigEnvVar = "KUBECONFIG"
|
||||
outputSnippetExtension = "_output"
|
||||
stdoutExtension = ".stdout.txt"
|
||||
stderrExtension = ".stderr.txt"
|
||||
)
|
||||
|
||||
var (
|
||||
// Matcher for links to the Istio github repository.
|
||||
githubLinkMatch = regexp.MustCompile("@.*@")
|
||||
)
|
||||
|
||||
var _ Step = Script{}
|
||||
|
||||
// Script is a test Step that parses an input script which may contain a mix of raw shell commands
|
||||
// and snippets.
|
||||
//
|
||||
// Snippets must be surrounded by the tokens "# $snippet" and "# $endsnippet", each of which must be
|
||||
// on placed their own line and must be at the start of that line. For example:
|
||||
//
|
||||
// # $snippet dostuff syntax="bash"
|
||||
// $ kubectl apply -f @some/path/relative/to/istio/src/dir.yaml@
|
||||
// # $endsnippet
|
||||
//
|
||||
// This will run the command
|
||||
// `kubectl apply -f some/path/relative/to/istio/src/dir.yaml` and also generate
|
||||
// the snippet.
|
||||
//
|
||||
// Snippets that reference files in `https://github.com/istio/istio`, as shown above, should
|
||||
// surround those links with `@`. The reference will be converted into an actual link
|
||||
// when rendering the page on `istio.io`.
|
||||
//
|
||||
// The `$snippet` line supports a number of fields separated by whitespace. The first field is the
|
||||
// name of the snippet and is required. After the name, a number of arguments in the form of
|
||||
// <key>="<value>" may be provided. The following arguments are supported:
|
||||
//
|
||||
// - syntax:
|
||||
// Sets the syntax for the command text. This is used to properly highlight the output
|
||||
// on istio.io. Defaults to "bash".
|
||||
// - outputis:
|
||||
// Sets the syntax for the output of the last command in the snippet. Snippet output
|
||||
// will only be generated if this is set. By default, the commands and output will be merged
|
||||
// into a single snippet. This can be overridden with outputsnippet.
|
||||
// - outputsnippet:
|
||||
// A boolean value, which if "true" indicates that the output for the last command of
|
||||
// the snippet should appear in a separate snippet. The name of the generated snippet
|
||||
// will append "_output" to the name of the current snippet. Defaults to "false".
|
||||
// - outputstream:
|
||||
// Indicates which command output stream should be used as the snippet output.
|
||||
// Can be one of "stdout", "stderr", or "all". Defaults to "all". The command output
|
||||
// stream is ignored if using a custom $snippetoutput (see below).
|
||||
//
|
||||
// ==== Verifying Command Output ====
|
||||
//
|
||||
// You can add verification logic to the output of the last command run within the snippet block
|
||||
// by adding one or more `# $verify` block. Each `verify` block specified will have to succeed
|
||||
// in order for the command to be considered successful. For example:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// # $verify verifier="contains" source="stdout"
|
||||
// stdout must contain this string!
|
||||
// # $verify verifier="contains" source="stderr"
|
||||
// and stderr must contain this string!
|
||||
// # $endsnippet
|
||||
//
|
||||
// The following arguments are supported by `$verify`:
|
||||
//
|
||||
// - verifier:
|
||||
// Sets the verification algorithm to be used. Defaults to "token". The following\
|
||||
// values are supported:
|
||||
// - "token":
|
||||
// Performs a token-based comparison of expected and actual output. The syntax
|
||||
// supports the wildcard `?` character to skip comparison for a given token.
|
||||
// - "contains":
|
||||
// verifies that the output contains the given string.
|
||||
// - "notContains":
|
||||
// verifies that the output does not contain the given string.
|
||||
// - "lineRegex":
|
||||
// matches each line of the output against a regex for that line. The number of
|
||||
// lines of the output must match the number of regexes provided. Each regex
|
||||
// must be on a separate line.
|
||||
// - source:
|
||||
// Indicates which command output stream should be used as the input for the verifier.
|
||||
// Can be one of "stdout", "stderr", or "all". Defaults to "all".
|
||||
//
|
||||
// ==== Generating Snippet Output ====
|
||||
//
|
||||
// You can indicate that a snippet should display output with the argument `outputis`:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash" outputis="text"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// # $endsnippet
|
||||
//
|
||||
// This will run the command `kubectl apply -f somefile.yaml` and add the output of the command to the
|
||||
// generated snippet:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash" outputis="text"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// this is the actual output of the command
|
||||
// # $endsnippet
|
||||
//
|
||||
// You can create a separate snippet for the output with the `outputsnippet` argument. When specifying
|
||||
// `outputsnippet="true"`, a second snippet with the suffix "_output" will be generated:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash" outputis="text" outputsnippet="true"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// # $endsnippet
|
||||
//
|
||||
// Generates:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// # $endsnippet
|
||||
//
|
||||
// # $snippet mysnippetoutput syntax="text"
|
||||
// this is the actual output of the command
|
||||
// # $endsnippet
|
||||
//
|
||||
// You can specify which output stream of the command to use for the output with the `outputstream`
|
||||
// argument (see table above for possible values). This can be useful in cases where a command writes
|
||||
// to both stdout and stderr (which can cause unreliable ordering between the two streams). For example:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash" outputis="text" outputstream="stdout"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// # $endsnippet
|
||||
//
|
||||
// Generates:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash" outputis="text"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// this is the command's stdout
|
||||
// # $endsnippet
|
||||
//
|
||||
// You can configure the snippet to ignore the actual output of the command and to use a custom output
|
||||
// value instead by specifying a `$snippetoutput block. This is useful for omitting parts of the
|
||||
// expected output. For example:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash" outputis="text"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// $ $snippetoutput
|
||||
// ... // Omitting parts of the output
|
||||
// hello world
|
||||
// # $endsnippet
|
||||
//
|
||||
// Generates:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash" outputis="text"
|
||||
// $ kubectl apply -f somefile.yaml
|
||||
// ... // Omitting parts of the output
|
||||
// hello world
|
||||
// # $endsnippet
|
||||
//
|
||||
// === Customizing Snippet Commands ===
|
||||
//
|
||||
// You can execute different commands than are used in the generated snippets. This is
|
||||
// useful when you need to add additional logic for handling things like retries, which
|
||||
// wouldn't be desirable in the online documentation. This is done by evaluating the
|
||||
// input as a golang template and applying different parameters for the command and
|
||||
// snippet. For example:
|
||||
//
|
||||
// {{- $curlOptions := "--retry 10 --retry-connrefused --retry-delay 5 " -}}
|
||||
// {{- if .isSnippet -}}
|
||||
// {{- $curlOptions = "" -}}
|
||||
// {{- end -}}
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash"
|
||||
// $ curl {{ $curlOptions }} http://www.google.com
|
||||
// # $endsnippet
|
||||
//
|
||||
// When the Script is created, you can evaluate the input differently:
|
||||
//
|
||||
// istioio.Script{
|
||||
// Input: istioio.Evaluate(istioio.Path("scripts/myfile.txt"), map[string]interface{}{
|
||||
// "isSnippet": false,
|
||||
// }),
|
||||
// SnippetInput: istioio.Evaluate(istioio.Path("scripts/myfile.txt"), map[string]interface{}{
|
||||
// "isSnippet": true,
|
||||
// }),
|
||||
// }
|
||||
//
|
||||
// This will run the command:
|
||||
//
|
||||
// curl --retry 10 --retry-connrefused --retry-delay 5 http://www.google.com
|
||||
//
|
||||
// And will generate the snippet:
|
||||
//
|
||||
// # $snippet mysnippet syntax="bash"
|
||||
// $ curl http://www.google.com
|
||||
// # $endsnippet
|
||||
//
|
||||
// === Environment ===
|
||||
//
|
||||
// To simplify common tasks, the following environment variables are set when the command is executed:
|
||||
//
|
||||
// - TEST_OUTPUT_DIR:
|
||||
// Set to the working directory of the current test. By default, scripts are run from this
|
||||
// directory. This variable is useful for cases where the execution `WorkDir` has been set,
|
||||
// but the script needs to access files in the test working directory.
|
||||
// - KUBECONFIG:
|
||||
// Set to the value from the test framework. This is necessary to make kubectl commands execute
|
||||
// with the configuration specified on the command line.
|
||||
//
|
||||
type Script struct {
|
||||
// Input for the parser.
|
||||
Input InputSelector
|
||||
|
||||
// SnippetInput allows for using a separate Input for generation of snippet files.
|
||||
// This allows, for example, applying different template parameters when generating
|
||||
// the commands shown in the snippet file.
|
||||
SnippetInput InputSelector
|
||||
|
||||
// Shell to use when running the command. By default "bash" will be used.
|
||||
Shell string
|
||||
|
||||
// WorkDir specifies the working directory when executing the script.
|
||||
WorkDir string
|
||||
|
||||
// Env user-provided environment variables for the generated Command.
|
||||
Env map[string]string
|
||||
}
|
||||
|
||||
func (s Script) run(ctx Context) {
|
||||
if s.SnippetInput == nil {
|
||||
// No snippet input was defined, just use the input for snippet generation.
|
||||
s.SnippetInput = s.Input
|
||||
}
|
||||
|
||||
s.runCommand(ctx)
|
||||
s.createSnippets(ctx)
|
||||
}
|
||||
|
||||
func (s Script) runCommand(ctx Context) {
|
||||
input := s.Input.SelectInput(ctx)
|
||||
content, err := input.ReadAll()
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed reading command input %s: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
// Generate the body of the command.
|
||||
commandLines := make([]string, 0)
|
||||
lines := strings.Split(content, "\n")
|
||||
for index := 0; index < len(lines); index++ {
|
||||
line := lines[index]
|
||||
if isStartOfSnippet(line) {
|
||||
// Parse the snippet and advance the index past it.
|
||||
sinfo := parseSnippet(ctx, &index, lines)
|
||||
|
||||
// Gather all the command lines from the snippet.
|
||||
snippetCommands := sinfo.getCommandLines()
|
||||
if sinfo.needsOutput() {
|
||||
// Copy stderr and stdout to output files so that we can validate later.
|
||||
snippetCommands[len(snippetCommands)-1] += fmt.Sprintf(" 1> >(tee %s) 2> >(tee %s >&2)",
|
||||
sinfo.getStdoutFile(),
|
||||
sinfo.getStderrFile())
|
||||
}
|
||||
|
||||
// Copy the commands from the snippet.
|
||||
for _, snippetCommand := range snippetCommands {
|
||||
commandLines = append(commandLines, filterCommandLine(snippetCommand))
|
||||
}
|
||||
} else {
|
||||
// Not a snippet, just copy the line directly to the command.
|
||||
commandLines = append(commandLines, filterCommandLine(line))
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the command lines together.
|
||||
command := strings.TrimSpace(strings.Join(commandLines, "\n"))
|
||||
|
||||
// Now run the command...
|
||||
scopes.CI.Infof("Running command script %s", input.Name())
|
||||
|
||||
// Copy the command to workDir.
|
||||
_, fileName := filepath.Split(input.Name())
|
||||
if err := ioutil.WriteFile(path.Join(ctx.WorkDir(), fileName), []byte(command), 0644); err != nil {
|
||||
ctx.Fatalf("failed copying command %s to workDir: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
// Get the shell.
|
||||
shell := s.Shell
|
||||
if shell == "" {
|
||||
shell = "bash"
|
||||
}
|
||||
|
||||
// Create the command.
|
||||
cmd := exec.Command(shell)
|
||||
cmd.Dir = s.getWorkDir(ctx)
|
||||
cmd.Env = s.getEnv(ctx)
|
||||
cmd.Stdin = strings.NewReader(command)
|
||||
|
||||
// Run the command and get the output.
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// Copy the command output from the script to workDir
|
||||
outputFileName := fileName + "_output.txt"
|
||||
if err := ioutil.WriteFile(filepath.Join(ctx.WorkDir(), outputFileName), bytes.TrimSpace(output), 0644); err != nil {
|
||||
ctx.Fatalf("failed copying output for command %s: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.Fatalf("script %s returned an error: %v. Output:\n%s", input.Name(), err, string(output))
|
||||
}
|
||||
}
|
||||
|
||||
func (s Script) createSnippets(ctx Context) {
|
||||
input := s.SnippetInput.SelectInput(ctx)
|
||||
content, err := input.ReadAll()
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed reading snippet input %s: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
scopes.CI.Infof("Creating snippets for input: %s", input.Name())
|
||||
|
||||
// Process the input line-by-line
|
||||
lines := strings.Split(content, "\n")
|
||||
for index := 0; index < len(lines); index++ {
|
||||
line := lines[index]
|
||||
|
||||
if !isStartOfSnippet(line) {
|
||||
// Not the start of a snippet, skip this line.
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the snippet and advance the index past it.
|
||||
sinfo := parseSnippet(ctx, &index, lines)
|
||||
|
||||
// Verify the output for this snippet.
|
||||
sinfo.verify()
|
||||
|
||||
// Verify the output, if configured to do so.
|
||||
snippetOutput := ""
|
||||
if sinfo.outputIs != "" {
|
||||
if len(sinfo.customOutputLines) > 0 {
|
||||
// Use the custom output defined in the snippet.
|
||||
snippetOutput = sinfo.getCustomOutput()
|
||||
} else {
|
||||
// Read the output for the snippet.
|
||||
snippetOutput = sinfo.readOutput()
|
||||
}
|
||||
}
|
||||
|
||||
// Join the command lines for the snippet into a single string.
|
||||
commands := strings.Join(filterCommentLines(sinfo.getCommandLines()), "\n")
|
||||
|
||||
// Check to see if the snippet specifies that output should be in a separate snippet.
|
||||
if sinfo.outputSnippet {
|
||||
// Create the snippet for the command.
|
||||
Snippet{
|
||||
Name: sinfo.name,
|
||||
Syntax: sinfo.syntax,
|
||||
Input: Inline{
|
||||
FileName: sinfo.name,
|
||||
Value: commands,
|
||||
},
|
||||
}.run(ctx)
|
||||
|
||||
// Create a separate snippet for the output.
|
||||
outputSnippetName := sinfo.name + outputSnippetExtension
|
||||
Snippet{
|
||||
Name: outputSnippetName,
|
||||
Syntax: sinfo.outputIs,
|
||||
Input: Inline{
|
||||
FileName: outputSnippetName,
|
||||
Value: snippetOutput,
|
||||
},
|
||||
}.run(ctx)
|
||||
} else {
|
||||
// Combine the command and output in the same snippet.
|
||||
Snippet{
|
||||
Name: sinfo.name,
|
||||
Syntax: sinfo.syntax,
|
||||
OutputIs: sinfo.outputIs,
|
||||
Input: Inline{
|
||||
FileName: sinfo.name,
|
||||
Value: strings.TrimSpace(commands + "\n" + snippetOutput),
|
||||
},
|
||||
}.run(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s Script) getWorkDir(ctx Context) string {
|
||||
if s.WorkDir != "" {
|
||||
// User-specified work dir for the script.
|
||||
return s.WorkDir
|
||||
}
|
||||
return ctx.WorkDir()
|
||||
}
|
||||
|
||||
func (s Script) getEnv(ctx Context) []string {
|
||||
// Start with the environment for the current process.
|
||||
e := os.Environ()
|
||||
|
||||
// Copy the user-specified environment (if set) and add the k8s config.
|
||||
customVars := map[string]string{
|
||||
// Set the output dir for the test.
|
||||
testOutputDirEnvVar: ctx.WorkDir(),
|
||||
}
|
||||
ctx.Environment().Case(environment.Kube, func() {
|
||||
customVars[kubeConfigEnvVar] = ctx.KubeEnv().Settings().KubeConfig
|
||||
})
|
||||
for k, v := range s.Env {
|
||||
customVars[k] = v
|
||||
}
|
||||
|
||||
// Append the custom vars to the list.
|
||||
for name, value := range customVars {
|
||||
e = append(e, fmt.Sprintf("%s=%s", name, value))
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func isStartOfSnippet(line string) bool {
|
||||
return strings.HasPrefix(line, snippetStartToken)
|
||||
}
|
||||
|
||||
func parseSnippet(ctx Context, lineIndex *int, lines []string) snippetInfo {
|
||||
ctx.Helper()
|
||||
|
||||
// Remove the start token
|
||||
trimmedLine := strings.TrimPrefix(lines[*lineIndex], snippetStartToken)
|
||||
|
||||
// Parse the start line.
|
||||
info := snippetInfo{
|
||||
ctx: ctx,
|
||||
outputSource: outputStreamAll,
|
||||
syntax: defaultCommandSyntax,
|
||||
}
|
||||
for _, fields := range strings.Fields(trimmedLine) {
|
||||
arg := strings.TrimSpace(fields)
|
||||
if arg == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Assume the first non-empty argument to be the snippet name.
|
||||
if info.name == "" {
|
||||
info.name = arg
|
||||
continue
|
||||
}
|
||||
|
||||
key, value, err := parseArg(arg)
|
||||
if err != nil {
|
||||
ctx.Fatalf("snippet %s: failed parsing snippet start line %s: %v",
|
||||
info.name, trimmedLine, err)
|
||||
}
|
||||
|
||||
switch key {
|
||||
case syntaxKey:
|
||||
info.syntax = value
|
||||
case outputIsKey:
|
||||
info.outputIs = value
|
||||
case outputSnippetKey:
|
||||
outputSnippet, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed parsing arg %s for snippet %s: %v", arg, info.name, err)
|
||||
}
|
||||
info.outputSnippet = outputSnippet
|
||||
case outputStreamKey:
|
||||
info.outputSource = outputStream(value)
|
||||
if _, ok := outputStreams[info.outputSource]; !ok {
|
||||
ctx.Fatalf("snippet %s: unsupported %s: %s. %s must be in %v",
|
||||
info.name, key, value, info.outputSource, outputStreams)
|
||||
}
|
||||
default:
|
||||
ctx.Fatalf("unsupported snippet attribute: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if info.name == "" {
|
||||
ctx.Fatalf("snippet missing name")
|
||||
}
|
||||
|
||||
if info.outputIs == "" && info.outputSnippet {
|
||||
info.outputIs = defaultOutputSyntax
|
||||
}
|
||||
|
||||
// Read the body lines for the snippet.
|
||||
foundEnd := false
|
||||
processingCustomOutput := false
|
||||
var currentVerifier *verifierInfo
|
||||
finishCurrentVerifier := func() {
|
||||
if currentVerifier != nil {
|
||||
info.verifiers = append(info.verifiers, *currentVerifier)
|
||||
currentVerifier = nil
|
||||
}
|
||||
}
|
||||
for *lineIndex++; !foundEnd && *lineIndex < len(lines); *lineIndex++ {
|
||||
line := lines[*lineIndex]
|
||||
if strings.HasPrefix(line, snippetEndToken) {
|
||||
// Found the end of the snippet.
|
||||
foundEnd = true
|
||||
} else if strings.HasPrefix(line, snippetOutputToken) {
|
||||
processingCustomOutput = true
|
||||
finishCurrentVerifier()
|
||||
} else if strings.HasPrefix(line, verifyToken) {
|
||||
finishCurrentVerifier()
|
||||
currentVerifier = &verifierInfo{
|
||||
name: tokenVerifierKey,
|
||||
verifier: verifyTokens,
|
||||
outputSource: outputStreamAll,
|
||||
}
|
||||
|
||||
// Parse the start line of the output.
|
||||
for _, arg := range strings.Fields(line[len(verifyToken):]) {
|
||||
arg = strings.TrimSpace(arg)
|
||||
if arg != "" {
|
||||
key, value, err := parseArg(arg)
|
||||
if err != nil {
|
||||
ctx.Fatalf("snippet %s: failed parsing snippet output line %s: %v",
|
||||
info.name, trimmedLine, err)
|
||||
}
|
||||
|
||||
switch key {
|
||||
case verifierKey:
|
||||
currentVerifier.name = value
|
||||
currentVerifier.verifier = verifiers[value]
|
||||
if currentVerifier.verifier == nil {
|
||||
ctx.Fatalf("snippet %s: contains invalid snippet output verifier: %s. Must be in %v",
|
||||
info.name, value, verifiers)
|
||||
}
|
||||
case sourceKey:
|
||||
currentVerifier.outputSource = outputStream(value)
|
||||
if _, ok := outputStreams[currentVerifier.outputSource]; !ok {
|
||||
ctx.Fatalf("snippet %s: unsupported %s: %s. %s must be in %v",
|
||||
info.name, sourceKey, value, currentVerifier.outputSource, outputStreams)
|
||||
}
|
||||
default:
|
||||
ctx.Fatalf("unsupported snippet output attribute: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if currentVerifier != nil {
|
||||
currentVerifier.expectedOutput = append(currentVerifier.expectedOutput, line)
|
||||
} else if processingCustomOutput {
|
||||
info.customOutputLines = append(info.customOutputLines, line)
|
||||
} else {
|
||||
info.commandLines = append(info.commandLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the current verifier, if one exists.
|
||||
finishCurrentVerifier()
|
||||
|
||||
if !foundEnd {
|
||||
ctx.Fatalf("snippet %s missing end token", info.name)
|
||||
}
|
||||
|
||||
// Back up the index to point to the endSnippet line, so that the outer loop will
|
||||
// increment properly.
|
||||
*lineIndex--
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
type verifierInfo struct {
|
||||
name string
|
||||
verifier verifier
|
||||
expectedOutput []string
|
||||
outputSource outputStream
|
||||
}
|
||||
|
||||
func (i verifierInfo) verify(sinfo *snippetInfo) {
|
||||
expectedOutput := strings.TrimSpace(strings.Join(i.expectedOutput, "\n"))
|
||||
|
||||
// Read the output file for the last command in the snippet.
|
||||
actualOutput := sinfo.readFrom(i.outputSource)
|
||||
|
||||
// If the snippet provided expected output, validate that the actual output
|
||||
// from the command matches.
|
||||
if expectedOutput != "" {
|
||||
scopes.CI.Infof("Verifying results for snippet %s with verifier %s", sinfo.name, i.name)
|
||||
i.verifier(sinfo.ctx, sinfo.name, expectedOutput, actualOutput)
|
||||
}
|
||||
}
|
||||
|
||||
type snippetInfo struct {
|
||||
ctx Context
|
||||
name string
|
||||
syntax string
|
||||
outputIs string
|
||||
outputSnippet bool
|
||||
customOutputLines []string
|
||||
outputSource outputStream
|
||||
commandLines []string
|
||||
verifiers []verifierInfo
|
||||
}
|
||||
|
||||
func (i snippetInfo) verify() {
|
||||
for _, v := range i.verifiers {
|
||||
v.verify(&i)
|
||||
}
|
||||
}
|
||||
|
||||
func (i snippetInfo) readOutput() string {
|
||||
return i.readFrom(i.outputSource)
|
||||
}
|
||||
|
||||
func (i snippetInfo) readFrom(source outputStream) string {
|
||||
switch source {
|
||||
case outputStreamStdout:
|
||||
return i.readFile(i.getStdoutFile())
|
||||
case outputStreamStderr:
|
||||
return i.readFile(i.getStderrFile())
|
||||
case outputStreamAll:
|
||||
// Concatenate the stdout and stderr.
|
||||
return i.readFile(i.getStdoutFile()) + i.readFile(i.getStderrFile())
|
||||
default:
|
||||
i.ctx.Fatalf("snippet %s: attempting to read from invalid output source: %s", i.name, source)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (i snippetInfo) readFile(path string) string {
|
||||
// Read the output file for the last command in the snippet.
|
||||
output, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
i.ctx.Fatalf("snippet %s: failed reading output file %s: %v", i.name, path, err)
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func (i snippetInfo) getStdoutFile() string {
|
||||
return filepath.Join(i.ctx.WorkDir(), i.name+stdoutExtension)
|
||||
}
|
||||
|
||||
func (i snippetInfo) getStderrFile() string {
|
||||
return filepath.Join(i.ctx.WorkDir(), i.name+stderrExtension)
|
||||
}
|
||||
|
||||
func (i snippetInfo) getCommandLines() []string {
|
||||
return append([]string{}, i.commandLines...)
|
||||
}
|
||||
|
||||
func (i snippetInfo) getCustomOutput() string {
|
||||
return strings.Join(i.customOutputLines, "\n")
|
||||
}
|
||||
|
||||
func (i snippetInfo) needsOutput() bool {
|
||||
return len(i.commandLines) > 0 && (i.outputIs != "" || len(i.verifiers) > 0)
|
||||
}
|
||||
|
||||
func filterCommentLines(lines []string) []string {
|
||||
out := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(line, "#") {
|
||||
out = append(out, line)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// filterCommandLine scrubs the given command line so that it is ready for execution.
|
||||
func filterCommandLine(line string) string {
|
||||
// Remove surrounding @'s for github links.
|
||||
line = githubLinkMatch.ReplaceAllStringFunc(line, func(input string) string {
|
||||
return input[1 : len(input)-1]
|
||||
})
|
||||
|
||||
// Remove any command line prefixes.
|
||||
return strings.TrimPrefix(line, commandLinePrefix)
|
||||
}
|
||||
|
||||
func parseArg(arg string) (string, string, error) {
|
||||
// All subsequent arguments must be of the form key=value
|
||||
keyAndValue := strings.Split(arg, "=")
|
||||
if len(keyAndValue) != 2 {
|
||||
return "", "", fmt.Errorf("invalid argument %s", arg)
|
||||
}
|
||||
|
||||
key := keyAndValue[0]
|
||||
value := strings.ReplaceAll(keyAndValue[1], "\"", "")
|
||||
return key, value, nil
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
startSnippetLineFormat = "# $snippet %s%s\n"
|
||||
endSnippetLine = "# $endsnippet\n"
|
||||
syntaxFormat = " syntax=\"%s\""
|
||||
outputisFormat = " outputis=\"%s\""
|
||||
)
|
||||
|
||||
var _ Step = Snippet{}
|
||||
|
||||
// LineFilter allows applying a filter to the content generated in the snippets.
|
||||
type LineFilter func(content string) (include bool, result string)
|
||||
|
||||
type Snippet struct {
|
||||
// Input chooses the input source to be used for the snippet.
|
||||
Input InputSelector
|
||||
|
||||
// Name of the generated snippet file. If not provided, uses the same name as the File.
|
||||
Name string
|
||||
|
||||
// Syntax for the snippet. Will not be included if not specified.
|
||||
Syntax string
|
||||
|
||||
// OutputIs value for the snippet. Will not be included if not specified.
|
||||
OutputIs string
|
||||
}
|
||||
|
||||
func (s Snippet) run(ctx Context) {
|
||||
ctx.Helper()
|
||||
|
||||
input := s.Input.SelectInput(ctx)
|
||||
snippetName := s.getName(input)
|
||||
|
||||
if snippetName == "" {
|
||||
ctx.Fatalf("snippet must be given a name")
|
||||
}
|
||||
|
||||
content, err := input.ReadAll()
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed writing snippet %s: %v", snippetName, err)
|
||||
}
|
||||
|
||||
// Create the text metadata for the snippet, if provided.
|
||||
snippetMetadata := ""
|
||||
if s.Syntax != "" {
|
||||
snippetMetadata += fmt.Sprintf(syntaxFormat, s.Syntax)
|
||||
}
|
||||
if s.OutputIs != "" {
|
||||
snippetMetadata += fmt.Sprintf(outputisFormat, s.OutputIs)
|
||||
}
|
||||
|
||||
// Start the snippet with the named snippet annotation.
|
||||
snippetContent := fmt.Sprintf(startSnippetLineFormat, snippetName, snippetMetadata)
|
||||
|
||||
// Add the content
|
||||
snippetContent += content + "\n"
|
||||
|
||||
// End the snippet
|
||||
snippetContent += endSnippetLine + "\n"
|
||||
|
||||
// Write out the snippet.
|
||||
ctx.addSnippet(s.Name, input.Name(), snippetContent)
|
||||
}
|
||||
|
||||
func (s Snippet) getName(input Input) string {
|
||||
if s.Name != "" {
|
||||
return s.Name
|
||||
}
|
||||
return filepath.Base(input.Name())
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/framework/components/environment"
|
||||
"istio.io/istio/pkg/test/framework/label"
|
||||
)
|
||||
|
||||
var _ Step = Subtest{}
|
||||
|
||||
// Subtest is a Step for injecting a subtest in the Step pipeline.
|
||||
type Subtest struct {
|
||||
// Name of the subtest. This will be the actual name of the go test that's run.
|
||||
Name string
|
||||
|
||||
// Labels labels to attach to the subtest.
|
||||
Labels []label.Instance
|
||||
|
||||
// RequiresEnvironment specifies restrictions on environment for the subtest.
|
||||
RequiresEnvironment environment.Name
|
||||
|
||||
// Func the test function for the subtest.
|
||||
Func func(ctx framework.TestContext)
|
||||
}
|
||||
|
||||
func (s Subtest) run(ctx Context) {
|
||||
ctx.NewSubTest(s.Name).Label(s.Labels...).RequiresEnvironment(s.RequiresEnvironment).Run(s.Func)
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/scanner"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenVerifierKey = "token"
|
||||
)
|
||||
|
||||
// verifier for output of a shell command.
|
||||
type verifier func(ctx Context, name, expectedOutput, actualOutput string)
|
||||
|
||||
// verifiers supported by in the command scripts.
|
||||
var verifiers = map[string]verifier{
|
||||
tokenVerifierKey: verifyTokens,
|
||||
"contains": verifyContains,
|
||||
"notContains": verifyNotContains,
|
||||
"lineRegex": verifyLineRegex,
|
||||
}
|
||||
|
||||
// verifyTokens tokenizes the output and compares against the tokens from the given file.
|
||||
func verifyTokens(ctx Context, name, expectedOutput, actualOutput string) {
|
||||
// Tokenize the content and the file.
|
||||
expectedTokenLines := tokenize(expectedOutput)
|
||||
actualTokenLines := tokenize(actualOutput)
|
||||
|
||||
if len(expectedTokenLines) != len(actualTokenLines) {
|
||||
ctx.Fatalf("verification failed for command %s: line count: (expected %d, found %d). Expected:\n%s\nto match:\n%s",
|
||||
name, len(expectedTokenLines), len(actualTokenLines), actualOutput, expectedOutput)
|
||||
}
|
||||
|
||||
for lineIndex := 0; lineIndex < len(expectedTokenLines); lineIndex++ {
|
||||
expectedTokens := expectedTokenLines[lineIndex]
|
||||
actualTokens := actualTokenLines[lineIndex]
|
||||
|
||||
if len(expectedTokens) != len(actualTokens) {
|
||||
ctx.Fatalf("verification failed for command %s [line %d]: token count (expected %d, found %d). Expected:\n%s\nto match:\n%s",
|
||||
name, lineIndex, len(expectedTokens), len(actualTokens), actualOutput, expectedOutput)
|
||||
}
|
||||
|
||||
for tokenIndex := 0; tokenIndex < len(expectedTokens); tokenIndex++ {
|
||||
expectedToken := expectedTokens[tokenIndex]
|
||||
if expectedToken == "?" {
|
||||
// The value was a wildcard, matches anything.
|
||||
continue
|
||||
}
|
||||
|
||||
actualToken := actualTokens[tokenIndex]
|
||||
if expectedToken != actualToken {
|
||||
ctx.Fatalf("verification failed for command %s [line %d]: token %d (expected %s, found %s). Expected:\n%s\nto match:\n%s",
|
||||
name, lineIndex, tokenIndex, expectedToken, actualToken, actualOutput, expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tokenize(content string) [][]string {
|
||||
inputLines := strings.Split(strings.TrimSpace(content), "\n")
|
||||
tokenLines := make([][]string, 0)
|
||||
|
||||
for _, inputLine := range inputLines {
|
||||
tokens := make([]string, 0)
|
||||
|
||||
var s scanner.Scanner
|
||||
|
||||
// Configure the token characters.
|
||||
s.IsIdentRune = func(ch rune, i int) bool {
|
||||
return ch == '_' || ch == '-' || ch == '.' || unicode.IsLetter(ch) || unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
s.Init(strings.NewReader(inputLine))
|
||||
for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
|
||||
tokens = append(tokens, s.TokenText())
|
||||
}
|
||||
tokenLines = append(tokenLines, tokens)
|
||||
}
|
||||
return tokenLines
|
||||
}
|
||||
|
||||
func verifyContains(ctx Context, name, expectedOutput, actualOutput string) {
|
||||
if !strings.Contains(actualOutput, expectedOutput) {
|
||||
ctx.Fatalf("verification failed for command %s: output does not contain expected text.\nExpected:\n%s\nOutput:\n%s",
|
||||
name, expectedOutput, actualOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyNotContains(ctx Context, name, expectedOutput, actualOutput string) {
|
||||
if strings.Contains(actualOutput, expectedOutput) {
|
||||
ctx.Fatalf("verification failed for command %s: output contains not expected text.\nNot Expected:\n%s\nOutput:\n%s",
|
||||
name, expectedOutput, actualOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyLineRegex(ctx Context, name, expectedOutput, actualOutput string) {
|
||||
expectedOutputLines := strings.Split(strings.TrimSpace(expectedOutput), "\n")
|
||||
actualOutputLines := strings.Split(strings.TrimSpace(actualOutput), "\n")
|
||||
|
||||
if len(expectedOutputLines) != len(actualOutputLines) {
|
||||
ctx.Fatalf("verification failed for command %s: line count: (expected %d, found %d). Expected:\n%s\nto match:\n%s",
|
||||
name, len(expectedOutputLines), len(actualOutputLines), actualOutput, expectedOutput)
|
||||
}
|
||||
|
||||
for lineIndex := 0; lineIndex < len(expectedOutputLines); lineIndex++ {
|
||||
if match, _ := regexp.MatchString(expectedOutputLines[lineIndex], actualOutputLines[lineIndex]); !match {
|
||||
ctx.Fatalf("verification failed for command %s: output does not match expected regex.\nExpected:\n%s\nOutput:\n%s",
|
||||
name, expectedOutputLines[lineIndex], actualOutputLines[lineIndex])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright Istio 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 istioio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"istio.io/istio/pkg/test/util/yml"
|
||||
)
|
||||
|
||||
var _ Step = YamlResources{}
|
||||
|
||||
// YamlResources is a test step that generates individual snippets for resources within a given
|
||||
// YAML document.
|
||||
type YamlResources struct {
|
||||
// BaseName for the generated snippets. If not provided, the base name of Input will be used.
|
||||
BaseName string
|
||||
|
||||
// Input YAML document.
|
||||
Input InputSelector
|
||||
|
||||
// ResourceNames provides the names of the resources for which to generate snippets.
|
||||
ResourceNames []string
|
||||
}
|
||||
|
||||
func (s YamlResources) run(ctx Context) {
|
||||
input := s.Input.SelectInput(ctx)
|
||||
|
||||
content, err := input.ReadAll()
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed reading YAML input %s: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
parts, err := yml.Parse(content)
|
||||
if err != nil {
|
||||
ctx.Fatalf("failed parsing YAML input %s: %v", input.Name(), err)
|
||||
}
|
||||
|
||||
// Get the base name for the snippets.
|
||||
baseName := s.BaseName
|
||||
if baseName == "" {
|
||||
baseName = filepath.Base(input.Name())
|
||||
}
|
||||
|
||||
for _, resourceName := range s.ResourceNames {
|
||||
found := false
|
||||
snippetName := fmt.Sprintf("%s_%s.yaml", baseName, resourceName)
|
||||
|
||||
for _, part := range parts {
|
||||
if part.Descriptor.Metadata.Name == resourceName {
|
||||
found = true
|
||||
|
||||
// Generate a snippet for this resource.
|
||||
Snippet{
|
||||
Name: snippetName,
|
||||
Syntax: "yaml",
|
||||
Input: Inline{
|
||||
FileName: snippetName,
|
||||
Value: part.Contents,
|
||||
},
|
||||
}.run(ctx)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
ctx.Fatalf("failed to find YAML resource %s from input %s", resourceName, input.Name)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,7 +48,8 @@ To write an `istio.io` follow these steps:
|
|||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/framework/components/environment"
|
||||
"istio.io/istio/pkg/test/framework/components/istio"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
```
|
||||
|
||||
1. Create a function called `TestMain`, following the example below. This
|
||||
|
|
|
@ -18,7 +18,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
)
|
||||
|
||||
// https://istio.io/docs/ops/configuration/mesh/app-health-check/
|
||||
|
|
|
@ -17,8 +17,9 @@ package security
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
)
|
||||
|
||||
//https://istio.io/docs/examples/bookinfo/
|
||||
|
|
|
@ -18,7 +18,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
)
|
||||
|
||||
// TestAuthorizationForHTTPServices simulates the task in https://www.istio.io/docs/tasks/security/authz-http/
|
||||
|
|
|
@ -18,7 +18,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
)
|
||||
|
||||
//https://istio.io/docs/tasks/security/mtls-migration/
|
||||
|
|
|
@ -21,8 +21,9 @@ import (
|
|||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/framework/components/environment/kube"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
"istio.io/istio/pkg/test/util/curl"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -18,7 +18,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
)
|
||||
|
||||
// https://preliminary.istio.io/docs/tasks/traffic-management/mirroring/
|
||||
|
|
|
@ -18,7 +18,8 @@ import (
|
|||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/framework/components/environment/kube"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -18,7 +18,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"istio.io/istio/pkg/test/framework"
|
||||
"istio.io/istio/pkg/test/istioio"
|
||||
|
||||
"istio.io/istio.io/pkg/test/istioio"
|
||||
)
|
||||
|
||||
func TestTrafficShifting(t *testing.T) {
|
||||
|
|
Loading…
Reference in New Issue