From 63e9c67db8cc73a28e37073bd87587884da0eba2 Mon Sep 17 00:00:00 2001 From: Alexander Campbell Date: Thu, 25 May 2017 14:23:00 -0700 Subject: [PATCH] kubectl: refactor addFromEnvFile, write tests --- pkg/kubectl/BUILD | 1 + pkg/kubectl/env_file.go | 80 ++++++++++++++++++--------- pkg/kubectl/env_file_test.go | 103 +++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 pkg/kubectl/env_file_test.go diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index b269c52d7de..9e953c04dde 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -14,6 +14,7 @@ go_test( "cluster_test.go", "configmap_test.go", "deployment_test.go", + "env_file_test.go", "generate_test.go", "kubectl_test.go", "namespace_test.go", diff --git a/pkg/kubectl/env_file.go b/pkg/kubectl/env_file.go index bbdcdc657ab..ce24c710441 100644 --- a/pkg/kubectl/env_file.go +++ b/pkg/kubectl/env_file.go @@ -28,6 +28,47 @@ import ( "k8s.io/apimachinery/pkg/util/validation" ) +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// proccessEnvFileLine returns a blank key if the line is empty or a comment. +// The value will be retrieved from the environment if necessary. +func proccessEnvFileLine(line []byte, filePath string, + currentLine int) (key, value string, err error) { + + if !utf8.Valid(line) { + return ``, ``, fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v", + filePath, currentLine+1, line) + } + + // We trim UTF8 BOM from the first line of the file but no others + if currentLine == 0 { + line = bytes.TrimPrefix(line, utf8bom) + } + + // trim the line from all leading whitespace first + line = bytes.TrimLeftFunc(line, unicode.IsSpace) + + // If the line is empty or a comment, we return a blank key/value pair. + if len(line) == 0 || line[0] == '#' { + return ``, ``, nil + } + + data := strings.SplitN(string(line), "=", 2) + key = data[0] + if errs := validation.IsCIdentifier(key); len(errs) != 0 { + return ``, ``, fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";")) + } + + if len(data) == 2 { + value = data[1] + } else { + // No value (no `=` in the line) is a signal to obtain the value + // from the environment. + value = os.Getenv(key) + } + return +} + // addFromEnvFile processes an env file allows a generic addTo to handle the // collection of key value pairs or returns an error. func addFromEnvFile(filePath string, addTo func(key, value string) error) error { @@ -39,38 +80,23 @@ func addFromEnvFile(filePath string, addTo func(key, value string) error) error scanner := bufio.NewScanner(f) currentLine := 0 - utf8bom := []byte{0xEF, 0xBB, 0xBF} for scanner.Scan() { + // Proccess the current line, retrieving a key/value pair if + // possible. scannedBytes := scanner.Bytes() - if !utf8.Valid(scannedBytes) { - return fmt.Errorf("env file %s contains invalid utf8 bytes at line %d: %v", filePath, currentLine+1, scannedBytes) + key, value, err := proccessEnvFileLine(scannedBytes, filePath, currentLine) + if err != nil { + return err } - // We trim UTF8 BOM - if currentLine == 0 { - scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom) - } - // trim the line from all leading whitespace first - line := strings.TrimLeftFunc(string(scannedBytes), unicode.IsSpace) currentLine++ - // line is not empty, and not starting with '#' - if len(line) > 0 && !strings.HasPrefix(line, "#") { - data := strings.SplitN(line, "=", 2) - key := data[0] - if errs := validation.IsCIdentifier(key); len(errs) != 0 { - return fmt.Errorf("%q is not a valid key name: %s", key, strings.Join(errs, ";")) - } - value := "" - if len(data) > 1 { - // pass the value through, no trimming - value = data[1] - } else { - // a pass-through variable is given - value = os.Getenv(key) - } - if err = addTo(key, value); err != nil { - return err - } + if len(key) == 0 { + // no key means line was empty or a comment + continue + } + + if err = addTo(key, value); err != nil { + return err } } return nil diff --git a/pkg/kubectl/env_file_test.go b/pkg/kubectl/env_file_test.go new file mode 100644 index 00000000000..10e37238de2 --- /dev/null +++ b/pkg/kubectl/env_file_test.go @@ -0,0 +1,103 @@ +/* +Copyright 2017 The Kubernetes 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 kubectl + +import ( + "os" + "strings" + "testing" +) + +// Test the cases of proccessEnvFileLine that can be run without touching the +// environment. +func Test_processEnvFileLine(t *testing.T) { + testCases := []struct { + name string + line []byte + currentLine int + expectedKey string + expectedValue string + expectedErr string + }{ + {"the utf8bom is trimmed on the first line", + append(utf8bom, 'a', '=', 'c'), 0, "a", "c", ""}, + + {"the utf8bom is NOT trimmed on the second line", + append(utf8bom, 'a', '=', 'c'), 1, "", "", "not a valid key name"}, + + {"no key is returned on a comment line", + []byte{' ', '#', 'c'}, 0, "", "", ""}, + + {"no key is returned on a blank line", + []byte{' ', ' ', '\t'}, 0, "", "", ""}, + + {"key is returned even with no value", + []byte{' ', 'x', '='}, 0, "x", "", ""}, + } + for _, c := range testCases { + key, value, err := proccessEnvFileLine(c.line, `filename`, c.currentLine) + t.Logf("Testing that %s.", c.name) + if c.expectedKey != key { + t.Errorf("\texpected key %q, received %q", c.expectedKey, key) + } + if c.expectedValue != value { + t.Errorf("\texpected value %q, received %q", c.expectedValue, value) + } + if len(c.expectedErr) == 0 { + if err != nil { + t.Errorf("\tunexpected err %v", err) + } + } else { + if !strings.Contains(err.Error(), c.expectedErr) { + t.Errorf("\terr %v doesn't match expected %q", err, c.expectedErr) + } + } + } +} + +// proccessEnvFileLine needs to fetch the value from the environment if no +// equals sign is provided. +// For example: +// +// my_key1=alpha +// my_key2=beta +// my_key3 +// +// In this file, my_key3 must be fetched from the environment. +// Test this capability. +func Test_processEnvFileLine_readEnvironment(t *testing.T) { + const realKey = "k8s_test_env_file_key" + const realValue = `my_value` + + // Just in case, these two lines ensure the environment is restored to + // its original state. + original := os.Getenv(realKey) + defer func() { os.Setenv(realKey, original) }() + + os.Setenv(realKey, `my_value`) + + key, value, err := proccessEnvFileLine([]byte(realKey), `filename`, 3) + if err != nil { + t.Fatal(err) + } + if key != realKey { + t.Errorf(`expected key %q, received %q`, realKey, key) + } + if value != realValue { + t.Errorf(`expected value %q, received %q`, realValue, value) + } +}