Modify inject to warn when file is un-injectable (#1603)

If an input file is un-injectable, existing inject behavior is to simply
output a copy of the input.

Introduce a report, printed to stderr, that communicates the end state
of the inject command. Currently this includes checking for hostNetwork
and unsupported resources.

Malformed YAML documents will continue to cause no YAML output, and return
error code 1.

This change also modifies integration tests to handle stdout and stderr separately.

example outputs...

some pods injected, none with host networking:

```
hostNetwork: pods do not use host networking...............................[ok]
supported: at least one resource injected..................................[ok]

Summary: 4 of 8 YAML document(s) injected
  deploy/emoji
  deploy/voting
  deploy/web
  deploy/vote-bot
```

some pods injected, one host networking:

```
hostNetwork: pods do not use host networking...............................[warn] -- deploy/vote-bot uses "hostNetwork: true"
supported: at least one resource injected..................................[ok]

Summary: 3 of 8 YAML document(s) injected
  deploy/emoji
  deploy/voting
  deploy/web
```

no pods injected:

```
hostNetwork: pods do not use host networking...............................[warn] -- deploy/emoji, deploy/voting, deploy/web, deploy/vote-bot use "hostNetwork: true"
supported: at least one resource injected..................................[warn] -- no supported objects found

Summary: 0 of 8 YAML document(s) injected
```

TODO: check for UDP and other init containers

Part of #1516

Signed-off-by: Andrew Seigner <siggy@buoyant.io>
This commit is contained in:
Andrew Seigner 2018-09-10 10:34:25 -07:00 committed by GitHub
parent 828ea29321
commit c5a719da47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 213 additions and 41 deletions

View File

@ -10,8 +10,6 @@ import (
)
const (
lineWidth = 80
okStatus = "[ok]"
retryStatus = "[retry]"
failStatus = "[FAIL]"
)

View File

@ -32,6 +32,10 @@ const (
ControlPlanePodName = "controller"
// The name of the variable used to pass the pod's namespace.
PodNamespaceEnvVarName = "LINKERD2_PROXY_POD_NAMESPACE"
// for inject reports
hostNetworkDesc = "hostNetwork: pods do not use host networking"
unsupportedDesc = "supported: at least one resource injected"
)
type injectOptions struct {
@ -42,6 +46,17 @@ type injectOptions struct {
*proxyConfigOptions
}
type injectReport struct {
name string
hostNetwork bool
unsupportedResource bool
}
// objMeta provides a generic struct to parse the names of Kubernetes objects
type objMeta struct {
metaV1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
}
func newInjectOptions() *injectOptions {
return &injectOptions{
inboundPort: 4143,
@ -117,14 +132,19 @@ func read(path string) ([]io.Reader, error) {
// Returns the integer representation of os.Exit code; 0 on success and 1 on failure.
func runInjectCmd(inputs []io.Reader, errWriter, outWriter io.Writer, options *injectOptions) int {
postInjectBuf := &bytes.Buffer{}
reportBuf := &bytes.Buffer{}
for _, input := range inputs {
err := InjectYAML(input, postInjectBuf, options)
err := InjectYAML(input, postInjectBuf, reportBuf, options)
if err != nil {
fmt.Fprintf(errWriter, "Error injecting linkerd proxy: %v\n", err)
return 1
}
_, err = io.Copy(outWriter, postInjectBuf)
// print error report after yaml output, for better visibility
io.Copy(errWriter, reportBuf)
if err != nil {
fmt.Fprintf(errWriter, "Error printing YAML: %v\n", err)
return 1
@ -156,11 +176,12 @@ func injectObjectMeta(t *metaV1.ObjectMeta, k8sLabels map[string]string, options
* and init-container injected. If the pod is unsuitable for having them
* injected, return false.
*/
func injectPodSpec(t *v1.PodSpec, identity k8s.TLSIdentity, controlPlaneDNSNameOverride string, options *injectOptions) bool {
func injectPodSpec(t *v1.PodSpec, identity k8s.TLSIdentity, controlPlaneDNSNameOverride string, options *injectOptions, report *injectReport) bool {
// Pods with `hostNetwork=true` share a network namespace with the host. The
// init-container would destroy the iptables configuration on the host, so
// skip the injection in this case.
if t.HostNetwork {
report.hostNetwork = true
return false
}
@ -332,9 +353,11 @@ func injectPodSpec(t *v1.PodSpec, identity k8s.TLSIdentity, controlPlaneDNSNameO
}
// InjectYAML takes an input stream of YAML, outputting injected YAML to out.
func InjectYAML(in io.Reader, out io.Writer, options *injectOptions) error {
func InjectYAML(in io.Reader, out io.Writer, report io.Writer, options *injectOptions) error {
reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(in, 4096))
injectReports := []injectReport{}
// Iterate over all YAML objects in the input
for {
// Read a single YAML object
@ -346,19 +369,24 @@ func InjectYAML(in io.Reader, out io.Writer, options *injectOptions) error {
return err
}
result, err := injectResource(bytes, options)
ir := injectReport{}
result, err := injectResource(bytes, options, &ir)
if err != nil {
return err
}
out.Write(result)
out.Write([]byte("---\n"))
injectReports = append(injectReports, ir)
}
generateReport(injectReports, report)
return nil
}
func injectList(b []byte, options *injectOptions) ([]byte, error) {
func injectList(b []byte, options *injectOptions, report *injectReport) ([]byte, error) {
var sourceList v1.List
if err := yaml.Unmarshal(b, &sourceList); err != nil {
return nil, err
@ -367,7 +395,7 @@ func injectList(b []byte, options *injectOptions) ([]byte, error) {
items := []runtime.RawExtension{}
for _, item := range sourceList.Items {
result, err := injectResource(item.Raw, options)
result, err := injectResource(item.Raw, options, report)
if err != nil {
return nil, err
}
@ -387,8 +415,8 @@ func injectList(b []byte, options *injectOptions) ([]byte, error) {
return yaml.Marshal(sourceList)
}
func injectResource(bytes []byte, options *injectOptions) ([]byte, error) {
// The Kuberentes API is versioned and each version has an API modeled
func injectResource(bytes []byte, options *injectOptions, report *injectReport) ([]byte, error) {
// The Kubernetes API is versioned and each version has an API modeled
// with its own distinct Go types. If we tell `yaml.Unmarshal()` which
// version we support then it will provide a representation of that
// object using the given type if possible. However, it only allows us
@ -405,6 +433,13 @@ func injectResource(bytes []byte, options *injectOptions) ([]byte, error) {
return nil, err
}
// retrieve the `metadata/name` field for reporting later
var om objMeta
if err := yaml.Unmarshal(bytes, &om); err != nil {
return nil, err
}
report.name = fmt.Sprintf("%s/%s", strings.ToLower(meta.Kind), om.Name)
// obj and podTemplateSpec will reference zero or one the following
// objects, depending on the type.
var obj interface{}
@ -510,7 +545,9 @@ func injectResource(bytes []byte, options *injectOptions) ([]byte, error) {
// Lists are a little different than the other types. There's no immediate
// pod template. Because of this, we do a recursive call for each element
// in the list (instead of just marshaling the injected pod template).
return injectList(bytes, options)
// TODO: generate an injectReport per list item
return injectList(bytes, options, report)
}
@ -534,7 +571,7 @@ func injectResource(bytes []byte, options *injectOptions) ([]byte, error) {
ControllerNamespace: controlPlaneNamespace,
}
if injectPodSpec(podSpec, identity, DNSNameOverride, options) {
if injectPodSpec(podSpec, identity, DNSNameOverride, options, report) {
injectObjectMeta(objectMeta, k8sLabels, options)
var err error
output, err = yaml.Marshal(obj)
@ -542,6 +579,8 @@ func injectResource(bytes []byte, options *injectOptions) ([]byte, error) {
return nil, err
}
}
} else {
report.unsupportedResource = true
}
return output, nil
@ -589,3 +628,57 @@ func walk(path string) ([]io.Reader, error) {
return in, nil
}
func generateReport(injectReports []injectReport, output io.Writer) {
injected := []string{}
hostNetwork := []string{}
for _, r := range injectReports {
if !r.hostNetwork && !r.unsupportedResource {
injected = append(injected, r.name)
} else if r.hostNetwork {
hostNetwork = append(hostNetwork, r.name)
}
}
// leading newline to separate from yaml output on stdout
output.Write([]byte("\n"))
hostNetworkPrefix := fmt.Sprintf("%s%s", hostNetworkDesc, getFiller(hostNetworkDesc))
if len(hostNetwork) == 0 {
output.Write([]byte(fmt.Sprintf("%s%s\n", hostNetworkPrefix, okStatus)))
} else {
verb := "uses"
if len(hostNetwork) > 1 {
verb = "use"
}
output.Write([]byte(fmt.Sprintf("%s%s -- %s %s \"hostNetwork: true\"\n", hostNetworkPrefix, warnStatus, strings.Join(hostNetwork, ", "), verb)))
}
unsupportedPrefix := fmt.Sprintf("%s%s", unsupportedDesc, getFiller(unsupportedDesc))
if len(injected) > 0 {
output.Write([]byte(fmt.Sprintf("%s%s\n", unsupportedPrefix, okStatus)))
} else {
output.Write([]byte(fmt.Sprintf("%s%s -- no supported objects found\n", unsupportedPrefix, warnStatus)))
}
summary := fmt.Sprintf("Summary: %d of %d YAML document(s) injected", len(injected), len(injectReports))
output.Write([]byte(fmt.Sprintf("\n%s\n", summary)))
for _, i := range injected {
output.Write([]byte(fmt.Sprintf(" %s\n", i)))
}
// trailing newline to separate from kubectl output if piping
output.Write([]byte("\n"))
}
func getFiller(text string) string {
filler := ""
for i := 0; i < lineWidth-len(text)-len(okStatus)-len("\n"); i++ {
filler = filler + "."
}
return filler
}

View File

@ -45,8 +45,9 @@ func TestInjectYAML(t *testing.T) {
read := bufio.NewReader(file)
output := new(bytes.Buffer)
report := new(bytes.Buffer)
err = InjectYAML(read, output, tc.testInjectOptions)
err = InjectYAML(read, output, report, tc.testInjectOptions)
if err != nil {
t.Errorf("Unexpected error injecting YAML: %v\n", err)
}
@ -80,6 +81,7 @@ func TestRunInjectCmd(t *testing.T) {
{
inputFileName: "inject_gettest_deployment.good.input.yml",
stdOutGoldenFileName: "inject_gettest_deployment.good.golden.yml",
stdErrGoldenFileName: "inject_gettest_deployment.good.golden.stderr",
exitCode: 0,
},
}
@ -123,9 +125,20 @@ func TestInjectFilePath(t *testing.T) {
resource string
resourceFile string
expectedFile string
stdErrFile string
}{
{resource: "nginx", resourceFile: filepath.Join(resourceFolder, "nginx.yaml"), expectedFile: filepath.Join(expectedFolder, "injected_nginx.yaml")},
{resource: "redis", resourceFile: filepath.Join(resourceFolder, "db/redis.yaml"), expectedFile: filepath.Join(expectedFolder, "injected_redis.yaml")},
{
resource: "nginx",
resourceFile: filepath.Join(resourceFolder, "nginx.yaml"),
expectedFile: filepath.Join(expectedFolder, "injected_nginx.yaml"),
stdErrFile: filepath.Join(expectedFolder, "injected_nginx.stderr"),
},
{
resource: "redis",
resourceFile: filepath.Join(resourceFolder, "db/redis.yaml"),
expectedFile: filepath.Join(expectedFolder, "injected_redis.yaml"),
stdErrFile: filepath.Join(expectedFolder, "injected_redis.stderr"),
},
}
for i, testCase := range testCases {
@ -135,8 +148,9 @@ func TestInjectFilePath(t *testing.T) {
t.Fatal("Unexpected error: ", err)
}
errBuf := &bytes.Buffer{}
actual := &bytes.Buffer{}
if exitCode := runInjectCmd(in, actual, actual, options); exitCode != 0 {
if exitCode := runInjectCmd(in, errBuf, actual, options); exitCode != 0 {
t.Fatal("Unexpected error. Exit code from runInjectCmd: ", exitCode)
}
@ -144,6 +158,11 @@ func TestInjectFilePath(t *testing.T) {
if expected != actual.String() {
t.Errorf("Result mismatch.\nExpected: %s\nActual: %s", expected, actual.String())
}
stdErr := readOptionalTestFile(t, testCase.stdErrFile)
if stdErr != errBuf.String() {
t.Errorf("Result mismatch.\nExpected: %s\nActual: %s", stdErr, errBuf.String())
}
})
}
})
@ -154,8 +173,9 @@ func TestInjectFilePath(t *testing.T) {
t.Fatal("Unexpected error: ", err)
}
errBuf := &bytes.Buffer{}
actual := &bytes.Buffer{}
if exitCode := runInjectCmd(in, actual, actual, options); exitCode != 0 {
if exitCode := runInjectCmd(in, errBuf, actual, options); exitCode != 0 {
t.Fatal("Unexpected error. Exit code from runInjectCmd: ", exitCode)
}
@ -163,6 +183,11 @@ func TestInjectFilePath(t *testing.T) {
if expected != actual.String() {
t.Errorf("Result mismatch.\nExpected: %s\nActual: %s", expected, actual.String())
}
stdErr := readOptionalTestFile(t, filepath.Join(expectedFolder, "injected_nginx_redis.stderr"))
if stdErr != errBuf.String() {
t.Errorf("Result mismatch.\nExpected: %s\nActual: %s", stdErr, errBuf.String())
}
})
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"text/template"
@ -133,7 +134,7 @@ func render(config installConfig, w io.Writer, options *installOptions) error {
// Special case for linkerd-proxy running in the Prometheus pod.
injectOptions.proxyOutboundCapacity[config.PrometheusImage] = prometheusProxyOutboundCapacity
return InjectYAML(buf, w, injectOptions)
return InjectYAML(buf, w, ioutil.Discard, injectOptions)
}
func validate(options *installOptions) error {

View File

@ -14,7 +14,13 @@ import (
"github.com/spf13/cobra"
)
const defaultNamespace = "linkerd"
const (
defaultNamespace = "linkerd"
lineWidth = 80
okStatus = "[ok]"
warnStatus = "[warn]"
)
var controlPlaneNamespace string
var apiAddr string // An empty value means "use the Kubernetes configuration"

View File

@ -0,0 +1,7 @@
hostNetwork: pods do not use host networking...............................[ok]
supported: at least one resource injected..................................[ok]
Summary: 1 of 1 YAML document(s) injected
deployment/nginx

View File

@ -0,0 +1,14 @@
hostNetwork: pods do not use host networking...............................[ok]
supported: at least one resource injected..................................[ok]
Summary: 1 of 1 YAML document(s) injected
deployment/redis
hostNetwork: pods do not use host networking...............................[ok]
supported: at least one resource injected..................................[ok]
Summary: 1 of 1 YAML document(s) injected
deployment/nginx

View File

@ -0,0 +1,7 @@
hostNetwork: pods do not use host networking...............................[ok]
supported: at least one resource injected..................................[ok]
Summary: 1 of 1 YAML document(s) injected
deployment/redis

View File

@ -0,0 +1,8 @@
hostNetwork: pods do not use host networking...............................[ok]
supported: at least one resource injected..................................[ok]
Summary: 2 of 2 YAML document(s) injected
deployment/get-test-deploy-injected-1
deployment/get-test-deploy-injected-2

View File

@ -39,7 +39,7 @@ func (s *server) Get(dest *destination.GetDestination, stream destination.Destin
return err
}
log.Debug("Get update: %v", update)
log.Debugf("Get update: %v", update)
stream.Send(update)
}

View File

@ -26,7 +26,7 @@ func TestMain(m *testing.M) {
//////////////////////
func TestEgressHttp(t *testing.T) {
out, err := TestHelper.LinkerdRun("inject", "testdata/proxy.yaml")
out, _, err := TestHelper.LinkerdRun("inject", "testdata/proxy.yaml")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

View File

@ -46,7 +46,7 @@ var (
//////////////////////
func TestCliGet(t *testing.T) {
out, err := TestHelper.LinkerdRun("inject", "testdata/to_be_injected_application.yaml")
out, _, err := TestHelper.LinkerdRun("inject", "testdata/to_be_injected_application.yaml")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
@ -81,7 +81,7 @@ func TestCliGet(t *testing.T) {
}
t.Run("get pods from --all-namespaces", func(t *testing.T) {
out, err = TestHelper.LinkerdRun("get", "pods", "--all-namespaces")
out, _, err = TestHelper.LinkerdRun("get", "pods", "--all-namespaces")
if err != nil {
t.Fatalf("Unexpected error: %v output:\n%s", err, out)
@ -94,7 +94,7 @@ func TestCliGet(t *testing.T) {
})
t.Run("get pods from the linkerd namespace", func(t *testing.T) {
out, err = TestHelper.LinkerdRun("get", "pods", "-n", TestHelper.GetLinkerdNamespace())
out, _, err = TestHelper.LinkerdRun("get", "pods", "-n", TestHelper.GetLinkerdNamespace())
if err != nil {
t.Fatalf("Unexpected error: %v output:\n%s", err, out)

View File

@ -54,7 +54,7 @@ func TestVersionPreInstall(t *testing.T) {
}
func TestCheckPreInstall(t *testing.T) {
out, err := TestHelper.LinkerdRun("check", "--pre", "--expected-version", TestHelper.GetVersion())
out, _, err := TestHelper.LinkerdRun("check", "--pre", "--expected-version", TestHelper.GetVersion())
if err != nil {
t.Fatalf("Check command failed\n%s", out)
}
@ -72,7 +72,7 @@ func TestInstall(t *testing.T) {
linkerdDeployReplicas["ca"] = 1
}
out, err := TestHelper.LinkerdRun(cmd...)
out, _, err := TestHelper.LinkerdRun(cmd...)
if err != nil {
t.Fatalf("linkerd install command failed\n%s", out)
}
@ -131,7 +131,7 @@ func TestCheckPostInstall(t *testing.T) {
var out string
var err error
overallErr := TestHelper.RetryFor(30*time.Second, func() error {
out, err = TestHelper.LinkerdRun("check", "--expected-version", TestHelper.GetVersion())
out, _, err = TestHelper.LinkerdRun("check", "--expected-version", TestHelper.GetVersion())
return err
})
if overallErr != nil {
@ -185,11 +185,16 @@ func TestInject(t *testing.T) {
cmd = append(cmd, []string{"--tls", "optional"}...)
}
out, err := TestHelper.LinkerdRun(cmd...)
out, injectReport, err := TestHelper.LinkerdRun(cmd...)
if err != nil {
t.Fatalf("linkerd inject command failed\n%s", out)
}
err = TestHelper.ValidateOutput(injectReport, "inject.report.golden")
if err != nil {
t.Fatalf("Received unexpected output\n%s", err.Error())
}
prefixedNs := TestHelper.GetTestNamespace("smoke-test")
out, err = TestHelper.KubectlApply(out, prefixedNs)
if err != nil {
@ -215,7 +220,7 @@ func TestInject(t *testing.T) {
func TestCheckProxy(t *testing.T) {
prefixedNs := TestHelper.GetTestNamespace("smoke-test")
out, err := TestHelper.LinkerdRun(
out, _, err := TestHelper.LinkerdRun(
"check",
"--proxy",
"--expected-version",

View File

@ -122,7 +122,7 @@ func TestCliStatForLinkerdNamespace(t *testing.T) {
} {
t.Run("linkerd "+strings.Join(tt.args, " "), func(t *testing.T) {
err := TestHelper.RetryFor(20*time.Second, func() error {
out, err := TestHelper.LinkerdRun(tt.args...)
out, _, err := TestHelper.LinkerdRun(tt.args...)
if err != nil {
t.Fatalf("Unexpected stat error: %s\n%s", err, out)
}

View File

@ -73,7 +73,7 @@ var (
//////////////////////
func TestCliTap(t *testing.T) {
out, err := TestHelper.LinkerdRun("inject", "testdata/tap_application.yaml")
out, _, err := TestHelper.LinkerdRun("inject", "testdata/tap_application.yaml")
if err != nil {
t.Fatalf("linkerd inject command failed\n%s", out)
}

8
test/testdata/inject.report.golden vendored Normal file
View File

@ -0,0 +1,8 @@
hostNetwork: pods do not use host networking...............................[ok]
supported: at least one resource injected..................................[ok]
Summary: 2 of 4 YAML document(s) injected
deployment/smoke-test-terminus
deployment/smoke-test-gateway

View File

@ -1,6 +1,7 @@
package testutil
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
@ -73,9 +74,9 @@ func NewTestHelper() *TestHelper {
tls: *tls,
}
version, err := testHelper.LinkerdRun("version", "--client", "--short")
version, _, err := testHelper.LinkerdRun("version", "--client", "--short")
if err != nil {
exit(1, "error getting linkerd version")
exit(1, "error getting linkerd version: "+err.Error())
}
testHelper.version = strings.TrimSpace(version)
@ -117,19 +118,18 @@ func (h *TestHelper) TLS() bool {
}
// CombinedOutput executes a shell command and returns the output.
func (h *TestHelper) CombinedOutput(name string, arg ...string) (string, error) {
func (h *TestHelper) CombinedOutput(name string, arg ...string) (string, string, error) {
command := exec.Command(name, arg...)
bytes, err := command.CombinedOutput()
if err != nil {
return string(bytes), err
}
var stderr bytes.Buffer
command.Stderr = &stderr
return string(bytes), nil
stdout, err := command.Output()
return string(stdout), stderr.String(), err
}
// LinkerdRun executes a linkerd command appended with the --linkerd-namespace
// flag.
func (h *TestHelper) LinkerdRun(arg ...string) (string, error) {
func (h *TestHelper) LinkerdRun(arg ...string) (string, string, error) {
withNamespace := append(arg, "--linkerd-namespace", h.namespace)
return h.CombinedOutput(h.linkerd, withNamespace...)
}
@ -179,7 +179,7 @@ func (h *TestHelper) ValidateOutput(out, fixtureFile string) error {
// CheckVersion validates the the output of the "linkerd version" command.
func (h *TestHelper) CheckVersion(serverVersion string) error {
out, err := h.LinkerdRun("version")
out, _, err := h.LinkerdRun("version")
if err != nil {
return fmt.Errorf("Unexpected error: %s\n%s", err.Error(), out)
}