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

Add istio.io docs test for security/authn-policy #16140

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/test/framework/test.go
Expand Up @@ -63,19 +63,22 @@ func NewTest(t *testing.T) *Test {

// Label applies the given labels to this test.
func (t *Test) Label(labels ...label.Instance) *Test {
t.goTest.Helper()
t.labels = append(t.labels, labels...)
return t
}

// RequiresEnvironment ensures that the current environment matches what the suite expects. Otherwise it stops test
// execution and skips the test.
func (t *Test) RequiresEnvironment(name environment.Name) *Test {
t.goTest.Helper()
t.requiredEnv = name
return t
}

// Run the test, supplied as a lambda.
func (t *Test) Run(fn func(ctx TestContext)) {
t.goTest.Helper()
t.runInternal(fn, false)
}

Expand Down Expand Up @@ -139,6 +142,8 @@ func (t *Test) runInternal(fn func(ctx TestContext), parallel bool) {
panic(fmt.Sprintf("Attempting to run test `%s` more than once", testName))
}

t.goTest.Helper()

if t.parent != nil {
// Create a new subtest under the parent's test.
parentGoTest := t.parent.goTest
Expand All @@ -154,6 +159,8 @@ func (t *Test) runInternal(fn func(ctx TestContext), parallel bool) {
}

func (t *Test) doRun(ctx *testContext, fn func(ctx TestContext), parallel bool) {
t.goTest.Helper()

// Initial setup if we're running in Parallel.
if parallel {
// Inform the parent, who will need to call ctx.Done asynchronously.
Expand Down
24 changes: 21 additions & 3 deletions pkg/test/istio.io/examples/example.go
Expand Up @@ -56,12 +56,26 @@ func New(t *testing.T, name string) Example {

// AddScript adds a directive to run a script
func (example *Example) AddScript(namespace string, script string, output outputType) {
example.t.Helper()

//fullPath := getFullPath(istioPath + script)
Copy link
Member

Choose a reason for hiding this comment

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

This line can be removed.

example.steps = append(example.steps, newStepScript("./"+script, output))
fullPath := "./"+script
stats, err := os.Stat(fullPath)
Copy link
Member

Choose a reason for hiding this comment

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

Should we move this validation to a common function and use it for files as well?

if os.IsNotExist(err) {
example.t.Fatalf("Script %q was not found", script)
}
if !stats.Mode().IsRegular() || stats.Mode().Perm() & 0x1 == 0 {
example.t.Fatalf("Script %q is not executable (mode: %s)",
script, stats.Mode().Perm().String())
}

example.steps = append(example.steps, newStepScript(fullPath, output))
}

// AddFile adds an existing file
func (example *Example) AddFile(namespace string, path string) {
example.t.Helper()

fullPath := getFullPath(istioPath + path)
example.steps = append(example.steps, newStepFile(namespace, fullPath))
}
Expand All @@ -72,6 +86,8 @@ type testFunc func(t *testing.T)
// Exec registers a callback to be invoked synchronously. This is typically used for
// validation logic to ensure command-lines worked as intended
func (example *Example) Exec(testFunction testFunc) {
example.t.Helper()

example.steps = append(example.steps, newStepFunction(testFunction))
}

Expand All @@ -80,12 +96,12 @@ func (example *Example) Exec(testFunction testFunc) {
func getFullPath(path string) string {
gopath := os.Getenv("GOPATH")
return gopath + "/src/" + path

}

// Run runs the scripts and capture output
// Note that this overrides os.Stdout/os.Stderr and is not thread-safe
func (example *Example) Run() {
example.t.Helper()

//override stdout and stderr for test. Is there a better way of doing this?

Expand All @@ -99,7 +115,7 @@ func (example *Example) Run() {
//f, err := os.Create(
//os.StdOut =

example.t.Log(fmt.Sprintf("Executing test %s (%d steps)", example.name, len(example.steps)))
example.t.Log(fmt.Sprintf("Executing example %s (%d steps)", example.name, len(example.steps)))

//create directory if it doesn't exist
if _, err := os.Stat(example.name); os.IsNotExist(err) {
Expand All @@ -112,6 +128,8 @@ func (example *Example) Run() {
framework.
NewTest(example.t).
Run(func(ctx framework.TestContext) {
example.t.Helper()

kubeEnv, ok := ctx.Environment().(*kube.Environment)
if !ok {
example.t.Fatalf("test framework unable to get Kubernetes environment")
Expand Down
6 changes: 6 additions & 0 deletions pkg/test/istio.io/examples/testType.go
Expand Up @@ -41,6 +41,8 @@ func newStepFile(namespace string, path string) testStep {
}

func (test fileTestType) Run(env *kube.Environment, t *testing.T) (string, error) {
t.Helper()

t.Logf(fmt.Sprintf("Executing %s\n", test.path))
if err := env.Apply(test.namespace, test.path); err != nil {
return "", err
Expand All @@ -64,6 +66,8 @@ type functionTestType struct {
}

func (test functionTestType) Run(env *kube.Environment, t *testing.T) (string, error) {
t.Helper()

t.Logf(fmt.Sprintf("Executing function\n"))
test.testFunction(t)
return "", nil
Expand All @@ -88,6 +92,8 @@ type scriptTestType struct {
}

func (test scriptTestType) Run(env *kube.Environment, t *testing.T) (string, error) {
t.Helper()

t.Logf(fmt.Sprintf("Executing %s\n", test.script))
cmd := exec.Command(test.script)

Expand Down
168 changes: 168 additions & 0 deletions pkg/test/istio.io/tasks/security/authn-policy/authn-policy_test.go
@@ -0,0 +1,168 @@
// Copyright 2019 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
package tests

import (
"testing"

"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/istio.io/examples"
)

var (
ist istio.Instance
)

func TestMain(m *testing.M) {
framework.NewSuite("authn-policy", m).
SetupOnEnv(environment.Kube, istio.Setup(&ist, setupConfig)).
RequireEnvironment(environment.Kube).
Run()
}

func setupConfig(cfg *istio.Config) {
if cfg == nil {
return
}
// This is redundant, but setting it explicitly to match the docs as it's explicitly required
// in the docs.
cfg.Values["global.mtls.enabled"] = "false"
}

// https://preliminary.istio.io/docs/tasks/security/authn-policy/
Copy link
Member

Choose a reason for hiding this comment

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

These links are likely to change. Not sure we want to be adding them to the code. That said, it would be nice to have a convention to map between the test and the doc.

// https://github.com/istio/istio.io/blob/master/content/docs/tasks/security/authn-policy/index.md
func TestAuthnPolicy(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps it would be better to create separate test functions for each example?

ex := examples.New(t, "Setup")

ex.AddScript("", "create-namespaces.sh", examples.TextOutput)
ex.AddFile("foo", "samples/httpbin/httpbin.yaml")
ex.AddFile("foo", "samples/sleep/sleep.yaml")
ex.AddFile("bar", "samples/httpbin/httpbin.yaml")
ex.AddFile("bar", "samples/sleep/sleep.yaml")
ex.AddFile("legacy", "samples/httpbin/httpbin.yaml")
ex.AddFile("legacy", "samples/sleep/sleep.yaml")

// This is missing from the docs, but it is necessary before continuing.
Copy link
Member

Choose a reason for hiding this comment

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

The idea of having a validation function for scripts (i.e. should return error, should not return error, should have text...) was suggested yesterday. Might be a nice to have that idea for files as well. Waiting for containers to be ready and verifying reachability are likely common activities.

ex.AddScript("", "wait-for-containers.sh", examples.TextOutput)
ex.AddScript("", "verify-reachability.sh", examples.TextOutput)

// TODO: Update the docs to use commands that succeed or fail, to check the authentication
// policies and destination rules, and use the same commands here.
ex.Run()

ex = examples.New(t, "Globally enabling Istio mutual TLS")

ex.AddScript("", "part1-configure-authentication-meshpolicy.sh", examples.TextOutput)
// TODO: Update the docs to add instructions to wait until the policy has been propagated,
// and use the same commands here.

// TODO: Check the output of the command. Fail if curl doesn't fail.
ex.AddScript("", "part1-verify-reachability-from-istio.sh", examples.TextOutput)
ex.AddScript("", "part1-configure-destinationrule-default.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part1-verify-reachability-from-istio.sh", examples.TextOutput)
// TODO: Fail if curl doesn't fail.
ex.AddScript("", "part1-verify-reachability-from-non-istio.sh", examples.TextOutput)

// TODO: Fail if curl doesn't fail.
ex.AddScript("", "part1-verify-reachability-to-legacy.sh", examples.TextOutput)
ex.AddScript("", "part1-configure-destinationrule-httpbin-legacy.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part1-verify-reachability-to-legacy.sh", examples.TextOutput)

// TODO: Fail if curl doesn't fail.
ex.AddScript("", "part1-verify-reachability-to-k8s-api.sh", examples.TextOutput)
ex.AddScript("", "part1-configure-destinationrule-api-server.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part1-verify-reachability-to-k8s-api.sh", examples.TextOutput)

ex.AddScript("", "part1-cleanup.sh", examples.TextOutput)

ex.Run()

ex = examples.New(t, "Enable mutual TLS per namespace or service")

ex.AddScript("", "part2-configure-authentication-policy-default.sh", examples.TextOutput)
ex.AddScript("", "part2-configure-destinationrule-default.sh", examples.TextOutput)
// TODO: Update the docs to add instructions to wait until the policy has been propagated,
// and use the same commands here.

// TODO: Fail if curl from foo or bar to any other namespace fails.
// TODO: Fail if curl from legacy to foo succeeds.
ex.AddScript("", "part2-verify-reachability.sh", examples.TextOutput)
ex.AddScript("", "part2-configure-authentication-policy-httpbin.sh", examples.TextOutput)
ex.AddScript("", "part2-configure-destinationrule-httpbin.sh", examples.TextOutput)
// TODO: Fail if curl from foo or bar to any other namespace fails.
// TODO: Fail if curl from legacy to foo OR bar succeeds.
ex.AddScript("", "part2-verify-reachability.sh", examples.TextOutput)

ex.AddScript("", "part2-configure-authentication-policy-httpbin-port.sh", examples.TextOutput)
ex.AddScript("", "part2-configure-destinationrule-httpbin-port.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part2-verify-reachability-to-bar-port-8000.sh", examples.TextOutput)

ex.AddScript("", "part2-configure-authentication-policy-overwrite-example.sh", examples.TextOutput)
ex.AddScript("", "part2-configure-destinationrule-overwrite-example.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part2-verify-reachability-to-foo-port-8000.sh", examples.TextOutput)

ex.AddScript("", "part2-cleanup.sh", examples.TextOutput)

ex.Run()

ex = examples.New(t, "End-user authentication")

ex.AddScript("", "part3-configure-gateway-httpbin.sh", examples.TextOutput)
ex.AddScript("", "part3-configure-virtualservice-httpbin.sh", examples.TextOutput)
// TODO: Update the docs to add instructions to wait until the gateway is ready,
// and use the same commands here.

// TODO: Fail if curl fails.
ex.AddScript("", "part3-verify-reachability-headers-without-token.sh", examples.TextOutput)
ex.AddScript("", "part3-configure-authentication-policy-jwt-example.sh", examples.TextOutput)
// TODO: Fail if curl succeeds.
ex.AddScript("", "part3-verify-reachability-headers-without-token.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part3-verify-reachability-headers-with-token.sh", examples.TextOutput)

// TODO: Add the test that runs security/tools/jwt/samples/gen-jwt.py against
// security/tools/jwt/samples/key.pem.
// This requires having Python and the jwcrypto library installed locally.

ex.AddScript("", "part3-configure-authentication-policy-jwt-example-exclude.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part3-verify-reachability-useragent-without-token.sh", examples.TextOutput)
// TODO: Fail if curl succeeds.
ex.AddScript("", "part3-verify-reachability-headers-without-token.sh", examples.TextOutput)

ex.AddScript("", "part3-configure-authentication-policy-jwt-example-include.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part3-verify-reachability-useragent-without-token.sh", examples.TextOutput)
// TODO: Fail if curl succeeds.
ex.AddScript("", "part3-verify-reachability-ip-without-token.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part3-verify-reachability-ip-with-token.sh", examples.TextOutput)

ex.AddScript("", "part3-configure-authentication-policy-jwt-mtls.sh", examples.TextOutput)
ex.AddScript("", "part3-configure-destinationrule-httpbin.sh", examples.TextOutput)
// TODO: Fail if curl fails.
ex.AddScript("", "part3-verify-reachability-from-istio-with-token.sh", examples.TextOutput)
// TODO: Fail if curl succeeds.
ex.AddScript("", "part3-verify-reachability-from-non-istio-with-token.sh", examples.TextOutput)

ex.AddScript("", "part3-cleanup.sh", examples.TextOutput)

ex.Run()
}
@@ -0,0 +1,5 @@
#!/bin/bash
set -e
kubectl create ns foo
kubectl create ns bar
kubectl create ns legacy
@@ -0,0 +1,5 @@
#!/bin/bash
kubectl delete meshpolicy default
kubectl delete destinationrules httpbin-legacy -n legacy
kubectl delete destinationrules api-server -n istio-system
kubectl delete destinationrules default -n istio-system
@@ -0,0 +1,11 @@
#!/bin/bash
set -e
kubectl apply -f - <<EOF
apiVersion: "authentication.istio.io/v1alpha1"
kind: "MeshPolicy"
metadata:
name: "default"
spec:
peers:
- mtls: {}
EOF
@@ -0,0 +1,14 @@
#!/bin/bash
set -e
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: "api-server"
namespace: istio-system
spec:
host: "kubernetes.default.svc.cluster.local"
trafficPolicy:
tls:
mode: DISABLE
EOF
@@ -0,0 +1,14 @@
#!/bin/bash
set -e
kubectl apply -f - <<EOF
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
name: "default"
namespace: "istio-system"
spec:
host: "*.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
EOF
@@ -0,0 +1,14 @@
#!/bin/bash
set -e
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need some mechanism to not have these first two lines show up in the output that's generated for use on istio.io. One option is to not include the lines in these files, and instead have the test framework insert them autoamtically before executing the script. The alternative is to automatically remove these lines when producing the final output.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this particular case, I can replace this with bash's -e command-line parameter which has the same effect:

#!/bin/bash -e

We could then filter out all comments (any line matching ^#).

We also need a way to capture the output of commands, both for asserting on it in tests, and for documentation. Many examples in the docs show the output of curl, for instance. I think the script's expected output should be contained in the script itself, in comments, similarly to Python doctest. Maybe something like:

#!/bin/bash -e
# expect exit status 22
# expect output begin
# 401
# expect output end

kubectl exec $(kubectl get pod -l app=sleep -n legacy -o jsonpath={.items..metadata.name}) -c sleep -n legacy -- curl http://httpbin.foo:8000/ip -s -o /dev/null -w "%{http_code}\n"

This doesn't allow capturing the output per command, but it's close to what we need.

Copy link
Member

Choose a reason for hiding this comment

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

Having the test automatically insert them sounds good to me.

kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: "httpbin-legacy"
namespace: "legacy"
spec:
host: "httpbin.legacy.svc.cluster.local"
trafficPolicy:
tls:
mode: DISABLE
EOF
@@ -0,0 +1,2 @@
#!/bin/bash
for from in "foo" "bar"; do for to in "foo" "bar"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
@@ -0,0 +1,2 @@
#!/bin/bash
rlenglet marked this conversation as resolved.
Show resolved Hide resolved
for from in "legacy"; do for to in "foo" "bar"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
@@ -0,0 +1,3 @@
#!/bin/bash
TOKEN=$(kubectl describe secret $(kubectl get secrets | grep default-token | cut -f1 -d ' ' | head -1) | grep -E '^token' | cut -f2 -d':' | tr -d '\t')
kubectl exec $(kubectl get pod -l app=sleep -n foo -o jsonpath={.items..metadata.name}) -c sleep -n foo -- curl https://kubernetes.default/api --header "Authorization: Bearer $TOKEN" --insecure -s -o /dev/null -w "%{http_code}\n"
@@ -0,0 +1,2 @@
#!/bin/bash
for from in "foo" "bar"; do for to in "legacy"; do kubectl exec $(kubectl get pod -l app=sleep -n ${from} -o jsonpath={.items..metadata.name}) -c sleep -n ${from} -- curl "http://httpbin.${to}:8000/ip" -s -o /dev/null -w "sleep.${from} to httpbin.${to}: %{http_code}\n"; done; done
@@ -0,0 +1,5 @@
#!/bin/bash
kubectl delete policy default overwrite-example -n foo
kubectl delete policy httpbin -n bar
kubectl delete destinationrules default overwrite-example -n foo
kubectl delete destinationrules httpbin -n bar