Support network in functions

Signed-off-by: Eddie Zaneski <eddiezane@gmail.com>
This commit is contained in:
Eddie Zaneski
2020-02-07 15:39:10 -07:00
parent 91da8525c1
commit 6cdcb1f436
6 changed files with 295 additions and 21 deletions

View File

@@ -40,6 +40,10 @@ func GetRunFnRunner(name string) *RunFnRunner {
r.Command.Flags().StringVar(
&r.Image, "image", "",
"run this image as a function instead of discovering them.")
r.Command.Flags().BoolVar(
&r.Network, "network", false, "enable network access for functions that declare it")
r.Command.Flags().StringVar(
&r.NetworkName, "network-name", "bridge", "the docker network to run the container in")
return r
}
@@ -56,6 +60,8 @@ type RunFnRunner struct {
FnPaths []string
Image string
RunFns runfn.RunFns
Network bool
NetworkName string
}
func (r *RunFnRunner) runE(c *cobra.Command, args []string) error {
@@ -188,6 +194,8 @@ func (r *RunFnRunner) preRunE(c *cobra.Command, args []string) error {
Output: output,
Input: input,
Path: path,
Network: r.Network,
NetworkName: r.NetworkName,
}
// don't consider args for the function

View File

@@ -25,6 +25,8 @@ func TestRunFnCommand_preRunE(t *testing.T) {
input io.Reader
output io.Writer
functionPaths []string
network bool
networkName string
}{
{
name: "config map",
@@ -86,6 +88,40 @@ metadata:
data: {}
kind: ConfigMap
apiVersion: v1
`,
},
{
name: "network enabled",
args: []string{"run", "dir", "--image", "foo:bar", "--network"},
path: "dir",
network: true,
networkName: "bridge",
expected: `
metadata:
name: function-input
annotations:
config.kubernetes.io/function: |
container: {image: 'foo:bar'}
data: {}
kind: ConfigMap
apiVersion: v1
`,
},
{
name: "with network name",
args: []string{"run", "dir", "--image", "foo:bar", "--network", "--network-name", "foo"},
path: "dir",
network: true,
networkName: "foo",
expected: `
metadata:
name: function-input
annotations:
config.kubernetes.io/function: |
container: {image: 'foo:bar'}
data: {}
kind: ConfigMap
apiVersion: v1
`,
},
{
@@ -206,6 +242,20 @@ apiVersion: v1
t.FailNow()
}
// check if Network was set
if tt.network {
if !assert.Equal(t, tt.network, r.RunFns.Network) {
t.FailNow()
}
if !assert.Equal(t, tt.networkName, r.RunFns.NetworkName) {
t.FailNow()
}
} else {
if !assert.Equal(t, false, r.RunFns.Network) {
t.FailNow()
}
}
// check if FunctionPaths were set
if tt.functionPaths == nil {
// make Equal work against flag default

View File

@@ -408,6 +408,17 @@ const (
var functionAnnotationKeys = []string{FunctionAnnotationKey, oldFunctionAnnotationKey}
// GetFunction parses the config function from the object if it is found
func GetFunction(n *yaml.RNode, meta yaml.ResourceMeta) (*yaml.RNode, error) {
for _, s := range functionAnnotationKeys {
fn := meta.Annotations[s]
if fn != "" {
return yaml.Parse(fn)
}
}
return n.Pipe(yaml.Lookup("metadata", "configFn"))
}
// GetContainerName returns the container image for an API if one exists
func GetContainerName(n *yaml.RNode) (string, string) {
meta, _ := n.GetMeta()
@@ -415,14 +426,10 @@ func GetContainerName(n *yaml.RNode) (string, string) {
// path to the function, this will be mounted into the container
path := meta.Annotations[kioutil.PathAnnotation]
// check previous keys for backwards compatibility
for _, s := range functionAnnotationKeys {
functionAnnotation := meta.Annotations[s]
if functionAnnotation != "" {
annotationContent, _ := yaml.Parse(functionAnnotation)
image, _ := annotationContent.Pipe(yaml.Lookup("container", "image"))
return image.YNode().Value, path
}
fn, _ := GetFunction(n, meta)
if fn != nil {
image, _ := fn.Pipe(yaml.Lookup("container", "image"))
return yaml.GetValue(image), path
}
container := meta.Annotations["config.kubernetes.io/container"]
@@ -434,5 +441,19 @@ func GetContainerName(n *yaml.RNode) (string, string) {
if err != nil || yaml.IsMissingOrNull(image) {
return "", path
}
return image.YNode().Value, path
return yaml.GetValue(image), path
}
// GetContainerNetworkRequired returns whether or not networking is required for the container
func GetContainerNetworkRequired(n *yaml.RNode) (bool, error) {
meta, err := n.GetMeta()
if err != nil {
return false, err
}
f, err := GetFunction(n, meta)
if err != nil {
return false, err
}
networkRequired, _ := f.Pipe(yaml.Lookup("container", "network", "required"))
return yaml.GetValue(networkRequired) == "true", nil
}

View File

@@ -309,6 +309,79 @@ metadata:
`, b.String())
}
func Test_GetFunction(t *testing.T) {
var tests = []struct {
name string
resource string
expectedFn string
missingFn bool
}{
// fn annotation
{
name: "fn annotation",
resource: `
apiVersion: v1beta1
kind: Example
metadata:
annotations:
config.kubernetes.io/function: |-
container: foo:v1.0.0
`,
expectedFn: `container: foo:v1.0.0`,
},
// legacy fn style
{name: "legacy fn meta",
resource: `
apiVersion: v1beta1
kind: Example
metadata:
configFn:
container: foo:v1.0.0
`,
expectedFn: `container: foo:v1.0.0`,
},
// no fn
{name: "no fn",
resource: `
apiVersion: v1beta1
kind: Example
metadata:
annotations: {}
`,
missingFn: true,
},
// test network, etc...
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
resource := yaml.MustParse(tt.resource)
meta, err := resource.GetMeta()
if !assert.NoError(t, err) {
t.FailNow()
}
fn, err := GetFunction(resource, meta)
if !assert.NoError(t, err) {
t.FailNow()
}
if tt.missingFn {
if !assert.Nil(t, fn) {
t.FailNow()
}
} else {
if !assert.Equal(t, strings.TrimSpace(fn.MustString()), strings.TrimSpace(tt.expectedFn)) {
t.FailNow()
}
}
})
}
}
func Test_GetContainerName(t *testing.T) {
// make sure gcr.io works
n, err := yaml.Parse(`apiVersion: v1beta1
@@ -364,6 +437,76 @@ metadata:
assert.Equal(t, "", c)
}
func Test_GetContainerNetworkRequired(t *testing.T) {
tests := []struct {
input string
required bool
}{
{
input: `apiVersion: v1
kind: Foo
metadata:
name: foo
configFn:
container:
image: gcr.io/kustomize-functions/example-tshirt:v0.1.0
network:
required: true
`,
required: true,
},
{
input: `apiVersion: v1
kind: Foo
metadata:
name: foo
configFn:
container:
image: gcr.io/kustomize-functions/example-tshirt:v0.1.0
network:
required: false
`,
required: false,
},
{
input: `apiVersion: v1
kind: Foo
metadata:
name: foo
configFn:
container:
image: gcr.io/kustomize-functions/example-tshirt:v0.1.0
`,
required: false,
},
{
input: `apiVersion: v1
kind: Foo
metadata:
name: foo
annotations:
config.kubernetes.io/function: |
container:
image: gcr.io/kustomize-functions/example-tshirt:v0.1.0
network:
required: true
`,
required: true,
},
}
for _, tc := range tests {
cfg, err := yaml.Parse(tc.input)
if !assert.NoError(t, err) {
return
}
required, err := GetContainerNetworkRequired(cfg)
assert.NoError(t, err)
assert.Equal(t, tc.required, required)
}
}
func TestFilter_Filter_defaultNaming(t *testing.T) {
cfg, err := yaml.Parse(`apiVersion: apps/v1
kind: Deployment

View File

@@ -44,6 +44,12 @@ type RunFns struct {
// Input can be set to read the Resources from Input rather than from a directory
Input io.Reader
// Network enables network access for functions that declare it
Network bool
// NetworkName is the name of the docker network to use for the container
NetworkName string
// Output can be set to write the result to Output rather than back to the directory
Output io.Writer
@@ -52,7 +58,7 @@ type RunFns struct {
NoFunctionsFromInput *bool
// for testing purposes only
containerFilterProvider func(string, string, *yaml.RNode) kio.Filter
containerFilterProvider func(string, string, string, *yaml.RNode) kio.Filter
}
// Execute runs the command
@@ -119,7 +125,10 @@ func (r RunFns) getFilters(nodes []*yaml.RNode) ([]kio.Filter, error) {
fltrs = append(fltrs, f...)
// explicit filters from a list of directories
f = r.getFunctionsFromFunctions()
f, err = r.getFunctionsFromFunctions()
if err != nil {
return nil, err
}
fltrs = append(fltrs, f...)
return fltrs, nil
@@ -160,8 +169,22 @@ func (r RunFns) getFunctionsFromInput(nodes []*yaml.RNode) ([]kio.Filter, error)
sortFns(buff)
for i := range buff.Nodes {
api := buff.Nodes[i]
network := ""
img, path := filters.GetContainerName(api)
fltrs = append(fltrs, r.containerFilterProvider(img, path, api))
required, err := filters.GetContainerNetworkRequired(api)
if err != nil {
return nil, err
}
if required {
if !r.Network {
// TODO(eddizane): Provide error info about which function needs the network
return fltrs, errors.Errorf("network required but not enabled with --network")
}
network = r.NetworkName
}
fltrs = append(fltrs, r.containerFilterProvider(img, path, network, api))
}
return fltrs, nil
}
@@ -182,8 +205,22 @@ func (r RunFns) getFunctionsFromFunctionPaths() ([]kio.Filter, error) {
}
for i := range buff.Nodes {
api := buff.Nodes[i]
network := ""
img, path := filters.GetContainerName(api)
c := r.containerFilterProvider(img, path, api)
required, err := filters.GetContainerNetworkRequired(api)
if err != nil {
return nil, err
}
if required {
if !r.Network {
// TODO(eddiezane): Provide error info about which function needs the network
return fltrs, errors.Errorf("network required but not enabled with --network")
}
network = r.NetworkName
}
c := r.containerFilterProvider(img, path, network, api)
cf, ok := c.(*filters.ContainerFilter)
if ok {
// functions provided by FunctionPaths are globally scoped
@@ -196,12 +233,26 @@ func (r RunFns) getFunctionsFromFunctionPaths() ([]kio.Filter, error) {
// getFunctionsFromFunctions returns the set of explicitly provided functions as
// Filters
func (r RunFns) getFunctionsFromFunctions() []kio.Filter {
func (r RunFns) getFunctionsFromFunctions() ([]kio.Filter, error) {
var fltrs []kio.Filter
for i := range r.Functions {
api := r.Functions[i]
network := ""
img, path := filters.GetContainerName(api)
c := r.containerFilterProvider(img, path, api)
required, err := filters.GetContainerNetworkRequired(api)
if err != nil {
return nil, err
}
if required {
if !r.Network {
// TODO(eddizane): Provide error info about which function needs the network
return fltrs, errors.Errorf("network required but not enabled with --network")
}
network = r.NetworkName
}
c := r.containerFilterProvider(img, path, network, api)
cf, ok := c.(*filters.ContainerFilter)
if ok {
// functions provided by Functions are globally scoped
@@ -209,7 +260,7 @@ func (r RunFns) getFunctionsFromFunctions() []kio.Filter {
}
fltrs = append(fltrs, c)
}
return fltrs
return fltrs, nil
}
// sortFns sorts functions so that functions with the longest paths come first
@@ -278,10 +329,11 @@ func (r *RunFns) init() {
// if containerFilterProvider hasn't been set, use the default
if r.containerFilterProvider == nil {
r.containerFilterProvider = func(image, path string, api *yaml.RNode) kio.Filter {
r.containerFilterProvider = func(image, path, network string, api *yaml.RNode) kio.Filter {
cf := &filters.ContainerFilter{
Image: image,
Config: api,
Network: network,
StorageMounts: r.StorageMounts,
GlobalScope: r.GlobalScope,
}

View File

@@ -50,7 +50,7 @@ kind:
if !assert.NoError(t, err) {
return
}
filter := instance.containerFilterProvider("example.com:version", "", api)
filter := instance.containerFilterProvider("example.com:version", "", "", api)
assert.Equal(t, &filters.ContainerFilter{Image: "example.com:version", Config: api}, filter)
}
@@ -69,7 +69,7 @@ kind:
if !assert.NoError(t, err) {
return
}
filter := instance.containerFilterProvider("example.com:version", "", api)
filter := instance.containerFilterProvider("example.com:version", "", "", api)
assert.Equal(t, &filters.ContainerFilter{
Image: "example.com:version", Config: api, GlobalScope: true}, filter)
}
@@ -659,8 +659,8 @@ func setupTest(t *testing.T) string {
// getFilterProvider fakes the creation of a filter, replacing the ContainerFiler with
// a filter to s/kind: Deployment/kind: StatefulSet/g.
// this can be used to simulate running a filter.
func getFilterProvider(t *testing.T) func(string, string, *yaml.RNode) kio.Filter {
return func(s, _ string, node *yaml.RNode) kio.Filter {
func getFilterProvider(t *testing.T) func(string, string, string, *yaml.RNode) kio.Filter {
return func(s, _, _ string, node *yaml.RNode) kio.Filter {
// parse the filter from the input
filter := yaml.YFilter{}
b := &bytes.Buffer{}