From e82e46e5de5614a5cc72a1f1db56c8301565d332 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Mon, 11 Jan 2021 11:56:24 +0100 Subject: [PATCH] Copy past golang/expansion from kubernetes/kubernetes Try to stop depending on kubernetes/kubenetes. Copy golang/expansion into the virtual-kubelet repo. The upstream code looks super stable, so there is little harm to copy it here. Signed-off-by: Miek Gieben --- internal/expansion/LICENSE | 27 +++ internal/expansion/README.md | 4 + internal/expansion/expand.go | 102 +++++++++++ internal/expansion/expand_test.go | 285 ++++++++++++++++++++++++++++++ internal/podutils/env.go | 2 +- 5 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 internal/expansion/LICENSE create mode 100644 internal/expansion/README.md create mode 100644 internal/expansion/expand.go create mode 100644 internal/expansion/expand_test.go mode change 100755 => 100644 internal/podutils/env.go diff --git a/internal/expansion/LICENSE b/internal/expansion/LICENSE new file mode 100644 index 000000000..744875676 --- /dev/null +++ b/internal/expansion/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/expansion/README.md b/internal/expansion/README.md new file mode 100644 index 000000000..faac1a60e --- /dev/null +++ b/internal/expansion/README.md @@ -0,0 +1,4 @@ +Copied from +https://github.com/kubernetes/kubernetes/tree/master/third_party/forked/golang/expansion . + +This is to eliminate a direct dependency on kubernetes/kubernetes. diff --git a/internal/expansion/expand.go b/internal/expansion/expand.go new file mode 100644 index 000000000..6bf0ea8ce --- /dev/null +++ b/internal/expansion/expand.go @@ -0,0 +1,102 @@ +package expansion + +import ( + "bytes" +) + +const ( + operator = '$' + referenceOpener = '(' + referenceCloser = ')' +) + +// syntaxWrap returns the input string wrapped by the expansion syntax. +func syntaxWrap(input string) string { + return string(operator) + string(referenceOpener) + input + string(referenceCloser) +} + +// MappingFuncFor returns a mapping function for use with Expand that +// implements the expansion semantics defined in the expansion spec; it +// returns the input string wrapped in the expansion syntax if no mapping +// for the input is found. +func MappingFuncFor(context ...map[string]string) func(string) string { + return func(input string) string { + for _, vars := range context { + val, ok := vars[input] + if ok { + return val + } + } + + return syntaxWrap(input) + } +} + +// Expand replaces variable references in the input string according to +// the expansion spec using the given mapping function to resolve the +// values of variables. +func Expand(input string, mapping func(string) string) string { + var buf bytes.Buffer + checkpoint := 0 + for cursor := 0; cursor < len(input); cursor++ { + if input[cursor] == operator && cursor+1 < len(input) { + // Copy the portion of the input string since the last + // checkpoint into the buffer + buf.WriteString(input[checkpoint:cursor]) + + // Attempt to read the variable name as defined by the + // syntax from the input string + read, isVar, advance := tryReadVariableName(input[cursor+1:]) + + if isVar { + // We were able to read a variable name correctly; + // apply the mapping to the variable name and copy the + // bytes into the buffer + buf.WriteString(mapping(read)) + } else { + // Not a variable name; copy the read bytes into the buffer + buf.WriteString(read) + } + + // Advance the cursor in the input string to account for + // bytes consumed to read the variable name expression + cursor += advance + + // Advance the checkpoint in the input string + checkpoint = cursor + 1 + } + } + + // Return the buffer and any remaining unwritten bytes in the + // input string. + return buf.String() + input[checkpoint:] +} + +// tryReadVariableName attempts to read a variable name from the input +// string and returns the content read from the input, whether that content +// represents a variable name to perform mapping on, and the number of bytes +// consumed in the input string. +// +// The input string is assumed not to contain the initial operator. +func tryReadVariableName(input string) (string, bool, int) { + switch input[0] { + case operator: + // Escaped operator; return it. + return input[0:1], false, 1 + case referenceOpener: + // Scan to expression closer + for i := 1; i < len(input); i++ { + if input[i] == referenceCloser { + return input[1:i], true, i + 1 + } + } + + // Incomplete reference; return it. + return string(operator) + string(referenceOpener), false, 1 + default: + // Not the beginning of an expression, ie, an operator + // that doesn't begin an expression. Return the operator + // and the first rune in the string. + return (string(operator) + string(input[0])), false, 1 + } +} diff --git a/internal/expansion/expand_test.go b/internal/expansion/expand_test.go new file mode 100644 index 000000000..55a7f4e26 --- /dev/null +++ b/internal/expansion/expand_test.go @@ -0,0 +1,285 @@ +package expansion + +import ( + "testing" + + api "k8s.io/kubernetes/pkg/apis/core" +) + +func TestMapReference(t *testing.T) { + envs := []api.EnvVar{ + { + Name: "FOO", + Value: "bar", + }, + { + Name: "ZOO", + Value: "$(FOO)-1", + }, + { + Name: "BLU", + Value: "$(ZOO)-2", + }, + } + + declaredEnv := map[string]string{ + "FOO": "bar", + "ZOO": "$(FOO)-1", + "BLU": "$(ZOO)-2", + } + + serviceEnv := map[string]string{} + + mapping := MappingFuncFor(declaredEnv, serviceEnv) + + for _, env := range envs { + declaredEnv[env.Name] = Expand(env.Value, mapping) + } + + expectedEnv := map[string]string{ + "FOO": "bar", + "ZOO": "bar-1", + "BLU": "bar-1-2", + } + + for k, v := range expectedEnv { + if e, a := v, declaredEnv[k]; e != a { + t.Errorf("Expected %v, got %v", e, a) + } else { + delete(declaredEnv, k) + } + } + + if len(declaredEnv) != 0 { + t.Errorf("Unexpected keys in declared env: %v", declaredEnv) + } +} + +func TestMapping(t *testing.T) { + context := map[string]string{ + "VAR_A": "A", + "VAR_B": "B", + "VAR_C": "C", + "VAR_REF": "$(VAR_A)", + "VAR_EMPTY": "", + } + mapping := MappingFuncFor(context) + + doExpansionTest(t, mapping) +} + +func TestMappingDual(t *testing.T) { + context := map[string]string{ + "VAR_A": "A", + "VAR_EMPTY": "", + } + context2 := map[string]string{ + "VAR_B": "B", + "VAR_C": "C", + "VAR_REF": "$(VAR_A)", + } + mapping := MappingFuncFor(context, context2) + + doExpansionTest(t, mapping) +} + +func doExpansionTest(t *testing.T, mapping func(string) string) { + cases := []struct { + name string + input string + expected string + }{ + { + name: "whole string", + input: "$(VAR_A)", + expected: "A", + }, + { + name: "repeat", + input: "$(VAR_A)-$(VAR_A)", + expected: "A-A", + }, + { + name: "beginning", + input: "$(VAR_A)-1", + expected: "A-1", + }, + { + name: "middle", + input: "___$(VAR_B)___", + expected: "___B___", + }, + { + name: "end", + input: "___$(VAR_C)", + expected: "___C", + }, + { + name: "compound", + input: "$(VAR_A)_$(VAR_B)_$(VAR_C)", + expected: "A_B_C", + }, + { + name: "escape & expand", + input: "$$(VAR_B)_$(VAR_A)", + expected: "$(VAR_B)_A", + }, + { + name: "compound escape", + input: "$$(VAR_A)_$$(VAR_B)", + expected: "$(VAR_A)_$(VAR_B)", + }, + { + name: "mixed in escapes", + input: "f000-$$VAR_A", + expected: "f000-$VAR_A", + }, + { + name: "backslash escape ignored", + input: "foo\\$(VAR_C)bar", + expected: "foo\\Cbar", + }, + { + name: "backslash escape ignored", + input: "foo\\\\$(VAR_C)bar", + expected: "foo\\\\Cbar", + }, + { + name: "lots of backslashes", + input: "foo\\\\\\\\$(VAR_A)bar", + expected: "foo\\\\\\\\Abar", + }, + { + name: "nested var references", + input: "$(VAR_A$(VAR_B))", + expected: "$(VAR_A$(VAR_B))", + }, + { + name: "nested var references second type", + input: "$(VAR_A$(VAR_B)", + expected: "$(VAR_A$(VAR_B)", + }, + { + name: "value is a reference", + input: "$(VAR_REF)", + expected: "$(VAR_A)", + }, + { + name: "value is a reference x 2", + input: "%%$(VAR_REF)--$(VAR_REF)%%", + expected: "%%$(VAR_A)--$(VAR_A)%%", + }, + { + name: "empty var", + input: "foo$(VAR_EMPTY)bar", + expected: "foobar", + }, + { + name: "unterminated expression", + input: "foo$(VAR_Awhoops!", + expected: "foo$(VAR_Awhoops!", + }, + { + name: "expression without operator", + input: "f00__(VAR_A)__", + expected: "f00__(VAR_A)__", + }, + { + name: "shell special vars pass through", + input: "$?_boo_$!", + expected: "$?_boo_$!", + }, + { + name: "bare operators are ignored", + input: "$VAR_A", + expected: "$VAR_A", + }, + { + name: "undefined vars are passed through", + input: "$(VAR_DNE)", + expected: "$(VAR_DNE)", + }, + { + name: "multiple (even) operators, var undefined", + input: "$$$$$$(BIG_MONEY)", + expected: "$$$(BIG_MONEY)", + }, + { + name: "multiple (even) operators, var defined", + input: "$$$$$$(VAR_A)", + expected: "$$$(VAR_A)", + }, + { + name: "multiple (odd) operators, var undefined", + input: "$$$$$$$(GOOD_ODDS)", + expected: "$$$$(GOOD_ODDS)", + }, + { + name: "multiple (odd) operators, var defined", + input: "$$$$$$$(VAR_A)", + expected: "$$$A", + }, + { + name: "missing open expression", + input: "$VAR_A)", + expected: "$VAR_A)", + }, + { + name: "shell syntax ignored", + input: "${VAR_A}", + expected: "${VAR_A}", + }, + { + name: "trailing incomplete expression not consumed", + input: "$(VAR_B)_______$(A", + expected: "B_______$(A", + }, + { + name: "trailing incomplete expression, no content, is not consumed", + input: "$(VAR_C)_______$(", + expected: "C_______$(", + }, + { + name: "operator at end of input string is preserved", + input: "$(VAR_A)foobarzab$", + expected: "Afoobarzab$", + }, + { + name: "shell escaped incomplete expr", + input: "foo-\\$(VAR_A", + expected: "foo-\\$(VAR_A", + }, + { + name: "lots of $( in middle", + input: "--$($($($($--", + expected: "--$($($($($--", + }, + { + name: "lots of $( in beginning", + input: "$($($($($--foo$(", + expected: "$($($($($--foo$(", + }, + { + name: "lots of $( at end", + input: "foo0--$($($($(", + expected: "foo0--$($($($(", + }, + { + name: "escaped operators in variable names are not escaped", + input: "$(foo$$var)", + expected: "$(foo$$var)", + }, + { + name: "newline not expanded", + input: "\n", + expected: "\n", + }, + } + + for _, tc := range cases { + expanded := Expand(tc.input, mapping) + if e, a := tc.expected, expanded; e != a { + t.Errorf("%v: expected %q, got %q", tc.name, e, a) + } + } +} diff --git a/internal/podutils/env.go b/internal/podutils/env.go old mode 100755 new mode 100644 index 7cc8bbe49..2914589f1 --- a/internal/podutils/env.go +++ b/internal/podutils/env.go @@ -20,6 +20,7 @@ import ( "sort" "strings" + "github.com/virtual-kubelet/virtual-kubelet/internal/expansion" "github.com/virtual-kubelet/virtual-kubelet/internal/manager" "github.com/virtual-kubelet/virtual-kubelet/log" corev1 "k8s.io/api/core/v1" @@ -32,7 +33,6 @@ import ( v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper" fieldpath "k8s.io/kubernetes/pkg/fieldpath" "k8s.io/kubernetes/pkg/kubelet/envvars" - "k8s.io/kubernetes/third_party/forked/golang/expansion" "k8s.io/utils/pointer" )