github.com/john-lin/cni@v0.6.0-rc1.0.20170712150331-b69e640cc0e2/libcni/api_test.go (about)

     1  // Copyright 2016 CNI authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package libcni_test
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"net"
    22  	"os"
    23  	"path/filepath"
    24  
    25  	"github.com/containernetworking/cni/libcni"
    26  	"github.com/containernetworking/cni/pkg/skel"
    27  	"github.com/containernetworking/cni/pkg/types"
    28  	"github.com/containernetworking/cni/pkg/types/current"
    29  	noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
    30  
    31  	. "github.com/onsi/ginkgo"
    32  	. "github.com/onsi/gomega"
    33  )
    34  
    35  type pluginInfo struct {
    36  	debugFilePath string
    37  	debug         *noop_debug.Debug
    38  	config        string
    39  	stdinData     []byte
    40  }
    41  
    42  type portMapping struct {
    43  	HostPort      int    `json:"hostPort"`
    44  	ContainerPort int    `json:"containerPort"`
    45  	Protocol      string `json:"protocol"`
    46  }
    47  
    48  func stringInList(s string, list []string) bool {
    49  	for _, item := range list {
    50  		if s == item {
    51  			return true
    52  		}
    53  	}
    54  	return false
    55  }
    56  
    57  func newPluginInfo(configValue, prevResult string, injectDebugFilePath bool, result string, runtimeConfig map[string]interface{}, capabilities []string) pluginInfo {
    58  	debugFile, err := ioutil.TempFile("", "cni_debug")
    59  	Expect(err).NotTo(HaveOccurred())
    60  	Expect(debugFile.Close()).To(Succeed())
    61  	debugFilePath := debugFile.Name()
    62  
    63  	debug := &noop_debug.Debug{
    64  		ReportResult: result,
    65  	}
    66  	Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
    67  
    68  	// config is what would be in the plugin's on-disk configuration
    69  	// without runtime injected keys
    70  	config := fmt.Sprintf(`{"type": "noop", "some-key": "%s"`, configValue)
    71  	if prevResult != "" {
    72  		config += fmt.Sprintf(`, "prevResult": %s`, prevResult)
    73  	}
    74  	if injectDebugFilePath {
    75  		config += fmt.Sprintf(`, "debugFile": "%s"`, debugFilePath)
    76  	}
    77  	if len(capabilities) > 0 {
    78  		config += `, "capabilities": {`
    79  		for i, c := range capabilities {
    80  			if i > 0 {
    81  				config += ", "
    82  			}
    83  			config += fmt.Sprintf(`"%s": true`, c)
    84  		}
    85  		config += "}"
    86  	}
    87  	config += "}"
    88  
    89  	// stdinData is what the runtime should pass to the plugin's stdin,
    90  	// including injected keys like 'name', 'cniVersion', and 'runtimeConfig'
    91  	newConfig := make(map[string]interface{})
    92  	err = json.Unmarshal([]byte(config), &newConfig)
    93  	Expect(err).NotTo(HaveOccurred())
    94  	newConfig["name"] = "some-list"
    95  	newConfig["cniVersion"] = "0.3.1"
    96  
    97  	// Only include standard runtime config and capability args that this plugin advertises
    98  	newRuntimeConfig := make(map[string]interface{})
    99  	for key, value := range runtimeConfig {
   100  		if stringInList(key, capabilities) {
   101  			newRuntimeConfig[key] = value
   102  		}
   103  	}
   104  	if len(newRuntimeConfig) > 0 {
   105  		newConfig["runtimeConfig"] = newRuntimeConfig
   106  	}
   107  
   108  	stdinData, err := json.Marshal(newConfig)
   109  	Expect(err).NotTo(HaveOccurred())
   110  
   111  	return pluginInfo{
   112  		debugFilePath: debugFilePath,
   113  		debug:         debug,
   114  		config:        config,
   115  		stdinData:     stdinData,
   116  	}
   117  }
   118  
   119  var _ = Describe("Invoking plugins", func() {
   120  	Describe("Capabilities", func() {
   121  		var (
   122  			debugFilePath string
   123  			debug         *noop_debug.Debug
   124  			pluginConfig  []byte
   125  			cniConfig     libcni.CNIConfig
   126  			runtimeConfig *libcni.RuntimeConf
   127  			netConfig     *libcni.NetworkConfig
   128  		)
   129  
   130  		BeforeEach(func() {
   131  			debugFile, err := ioutil.TempFile("", "cni_debug")
   132  			Expect(err).NotTo(HaveOccurred())
   133  			Expect(debugFile.Close()).To(Succeed())
   134  			debugFilePath = debugFile.Name()
   135  
   136  			debug = &noop_debug.Debug{}
   137  			Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
   138  
   139  			pluginConfig = []byte(`{ "type": "noop", "cniVersion": "0.3.1", "capabilities": { "portMappings": true, "somethingElse": true, "noCapability": false } }`)
   140  			netConfig, err = libcni.ConfFromBytes(pluginConfig)
   141  			Expect(err).NotTo(HaveOccurred())
   142  
   143  			cniConfig = libcni.CNIConfig{Path: []string{filepath.Dir(pluginPaths["noop"])}}
   144  
   145  			runtimeConfig = &libcni.RuntimeConf{
   146  				ContainerID: "some-container-id",
   147  				NetNS:       "/some/netns/path",
   148  				IfName:      "some-eth0",
   149  				Args:        [][2]string{{"DEBUG", debugFilePath}},
   150  				CapabilityArgs: map[string]interface{}{
   151  					"portMappings": []portMapping{
   152  						{HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
   153  					},
   154  					"somethingElse": []string{"foobar", "baz"},
   155  					"noCapability":  true,
   156  					"notAdded":      []bool{true, false},
   157  				},
   158  			}
   159  		})
   160  
   161  		AfterEach(func() {
   162  			Expect(os.RemoveAll(debugFilePath)).To(Succeed())
   163  		})
   164  
   165  		It("adds correct runtime config for capabilities to stdin", func() {
   166  			_, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
   167  			Expect(err).NotTo(HaveOccurred())
   168  
   169  			debug, err = noop_debug.ReadDebug(debugFilePath)
   170  			Expect(err).NotTo(HaveOccurred())
   171  			Expect(debug.Command).To(Equal("ADD"))
   172  
   173  			conf := make(map[string]interface{})
   174  			err = json.Unmarshal(debug.CmdArgs.StdinData, &conf)
   175  			Expect(err).NotTo(HaveOccurred())
   176  
   177  			// We expect runtimeConfig keys only for portMappings and somethingElse
   178  			rawRc := conf["runtimeConfig"]
   179  			rc, ok := rawRc.(map[string]interface{})
   180  			Expect(ok).To(Equal(true))
   181  			expectedKeys := []string{"portMappings", "somethingElse"}
   182  			Expect(len(rc)).To(Equal(len(expectedKeys)))
   183  			for _, key := range expectedKeys {
   184  				_, ok := rc[key]
   185  				Expect(ok).To(Equal(true))
   186  			}
   187  		})
   188  
   189  		It("adds no runtimeConfig when the plugin advertises no used capabilities", func() {
   190  			// Replace CapabilityArgs with ones we know the plugin
   191  			// doesn't support
   192  			runtimeConfig.CapabilityArgs = map[string]interface{}{
   193  				"portMappings22": []portMapping{
   194  					{HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
   195  				},
   196  				"somethingElse22": []string{"foobar", "baz"},
   197  			}
   198  
   199  			_, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
   200  			Expect(err).NotTo(HaveOccurred())
   201  
   202  			debug, err = noop_debug.ReadDebug(debugFilePath)
   203  			Expect(err).NotTo(HaveOccurred())
   204  			Expect(debug.Command).To(Equal("ADD"))
   205  
   206  			conf := make(map[string]interface{})
   207  			err = json.Unmarshal(debug.CmdArgs.StdinData, &conf)
   208  			Expect(err).NotTo(HaveOccurred())
   209  
   210  			// No intersection of plugin capabilities and CapabilityArgs,
   211  			// so plugin should not receive a "runtimeConfig" key
   212  			_, ok := conf["runtimeConfig"]
   213  			Expect(ok).Should(BeFalse())
   214  		})
   215  	})
   216  
   217  	Describe("Invoking a single plugin", func() {
   218  		var (
   219  			debugFilePath string
   220  			debug         *noop_debug.Debug
   221  			cniBinPath    string
   222  			pluginConfig  string
   223  			cniConfig     libcni.CNIConfig
   224  			netConfig     *libcni.NetworkConfig
   225  			runtimeConfig *libcni.RuntimeConf
   226  
   227  			expectedCmdArgs skel.CmdArgs
   228  		)
   229  
   230  		BeforeEach(func() {
   231  			debugFile, err := ioutil.TempFile("", "cni_debug")
   232  			Expect(err).NotTo(HaveOccurred())
   233  			Expect(debugFile.Close()).To(Succeed())
   234  			debugFilePath = debugFile.Name()
   235  
   236  			debug = &noop_debug.Debug{
   237  				ReportResult: `{ "ips": [{ "version": "4", "address": "10.1.2.3/24" }], "dns": {} }`,
   238  			}
   239  			Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
   240  
   241  			portMappings := []portMapping{
   242  				{HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
   243  			}
   244  
   245  			cniBinPath = filepath.Dir(pluginPaths["noop"])
   246  			pluginConfig = `{ "type": "noop", "some-key": "some-value", "cniVersion": "0.3.1", "capabilities": { "portMappings": true } }`
   247  			cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}}
   248  			netConfig = &libcni.NetworkConfig{
   249  				Network: &types.NetConf{
   250  					Type: "noop",
   251  					Capabilities: map[string]bool{
   252  						"portMappings": true,
   253  					},
   254  				},
   255  				Bytes: []byte(pluginConfig),
   256  			}
   257  			runtimeConfig = &libcni.RuntimeConf{
   258  				ContainerID: "some-container-id",
   259  				NetNS:       "/some/netns/path",
   260  				IfName:      "some-eth0",
   261  				Args:        [][2]string{{"DEBUG", debugFilePath}},
   262  				CapabilityArgs: map[string]interface{}{
   263  					"portMappings": portMappings,
   264  				},
   265  			}
   266  
   267  			// inject runtime args into the expected plugin config
   268  			conf := make(map[string]interface{})
   269  			err = json.Unmarshal([]byte(pluginConfig), &conf)
   270  			Expect(err).NotTo(HaveOccurred())
   271  			conf["runtimeConfig"] = map[string]interface{}{
   272  				"portMappings": portMappings,
   273  			}
   274  			newBytes, err := json.Marshal(conf)
   275  			Expect(err).NotTo(HaveOccurred())
   276  
   277  			expectedCmdArgs = skel.CmdArgs{
   278  				ContainerID: "some-container-id",
   279  				Netns:       "/some/netns/path",
   280  				IfName:      "some-eth0",
   281  				Args:        "DEBUG=" + debugFilePath,
   282  				Path:        cniBinPath,
   283  				StdinData:   newBytes,
   284  			}
   285  		})
   286  
   287  		AfterEach(func() {
   288  			Expect(os.RemoveAll(debugFilePath)).To(Succeed())
   289  		})
   290  
   291  		Describe("AddNetwork", func() {
   292  			It("executes the plugin with command ADD", func() {
   293  				r, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
   294  				Expect(err).NotTo(HaveOccurred())
   295  
   296  				result, err := current.GetResult(r)
   297  				Expect(err).NotTo(HaveOccurred())
   298  
   299  				Expect(result).To(Equal(&current.Result{
   300  					CNIVersion: current.ImplementedSpecVersion,
   301  					IPs: []*current.IPConfig{
   302  						{
   303  							Version: "4",
   304  							Address: net.IPNet{
   305  								IP:   net.ParseIP("10.1.2.3"),
   306  								Mask: net.IPv4Mask(255, 255, 255, 0),
   307  							},
   308  						},
   309  					},
   310  				}))
   311  
   312  				debug, err := noop_debug.ReadDebug(debugFilePath)
   313  				Expect(err).NotTo(HaveOccurred())
   314  				Expect(debug.Command).To(Equal("ADD"))
   315  				Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
   316  				Expect(string(debug.CmdArgs.StdinData)).To(ContainSubstring("\"portMappings\":"))
   317  			})
   318  
   319  			Context("when finding the plugin fails", func() {
   320  				BeforeEach(func() {
   321  					netConfig.Network.Type = "does-not-exist"
   322  				})
   323  
   324  				It("returns the error", func() {
   325  					_, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
   326  					Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`)))
   327  				})
   328  			})
   329  
   330  			Context("when the plugin errors", func() {
   331  				BeforeEach(func() {
   332  					debug.ReportError = "plugin error: banana"
   333  					Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
   334  				})
   335  				It("unmarshals and returns the error", func() {
   336  					result, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
   337  					Expect(result).To(BeNil())
   338  					Expect(err).To(MatchError("plugin error: banana"))
   339  				})
   340  			})
   341  		})
   342  
   343  		Describe("DelNetwork", func() {
   344  			It("executes the plugin with command DEL", func() {
   345  				err := cniConfig.DelNetwork(netConfig, runtimeConfig)
   346  				Expect(err).NotTo(HaveOccurred())
   347  
   348  				debug, err := noop_debug.ReadDebug(debugFilePath)
   349  				Expect(err).NotTo(HaveOccurred())
   350  				Expect(debug.Command).To(Equal("DEL"))
   351  				Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
   352  				Expect(string(debug.CmdArgs.StdinData)).To(ContainSubstring("\"portMappings\":"))
   353  			})
   354  
   355  			Context("when finding the plugin fails", func() {
   356  				BeforeEach(func() {
   357  					netConfig.Network.Type = "does-not-exist"
   358  				})
   359  
   360  				It("returns the error", func() {
   361  					err := cniConfig.DelNetwork(netConfig, runtimeConfig)
   362  					Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`)))
   363  				})
   364  			})
   365  
   366  			Context("when the plugin errors", func() {
   367  				BeforeEach(func() {
   368  					debug.ReportError = "plugin error: banana"
   369  					Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
   370  				})
   371  				It("unmarshals and returns the error", func() {
   372  					err := cniConfig.DelNetwork(netConfig, runtimeConfig)
   373  					Expect(err).To(MatchError("plugin error: banana"))
   374  				})
   375  			})
   376  		})
   377  
   378  		Describe("GetVersionInfo", func() {
   379  			It("executes the plugin with the command VERSION", func() {
   380  				versionInfo, err := cniConfig.GetVersionInfo("noop")
   381  				Expect(err).NotTo(HaveOccurred())
   382  
   383  				Expect(versionInfo).NotTo(BeNil())
   384  				Expect(versionInfo.SupportedVersions()).To(Equal([]string{
   385  					"0.-42.0", "0.1.0", "0.2.0", "0.3.0", "0.3.1",
   386  				}))
   387  			})
   388  
   389  			Context("when finding the plugin fails", func() {
   390  				It("returns the error", func() {
   391  					_, err := cniConfig.GetVersionInfo("does-not-exist")
   392  					Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`)))
   393  				})
   394  			})
   395  		})
   396  	})
   397  
   398  	Describe("Invoking a plugin list", func() {
   399  		var (
   400  			plugins       []pluginInfo
   401  			cniBinPath    string
   402  			cniConfig     libcni.CNIConfig
   403  			netConfigList *libcni.NetworkConfigList
   404  			runtimeConfig *libcni.RuntimeConf
   405  
   406  			expectedCmdArgs skel.CmdArgs
   407  		)
   408  
   409  		BeforeEach(func() {
   410  			var err error
   411  
   412  			capabilityArgs := map[string]interface{}{
   413  				"portMappings": []portMapping{
   414  					{HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
   415  				},
   416  				"otherCapability": 33,
   417  			}
   418  
   419  			cniBinPath = filepath.Dir(pluginPaths["noop"])
   420  			cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}}
   421  			runtimeConfig = &libcni.RuntimeConf{
   422  				ContainerID:    "some-container-id",
   423  				NetNS:          "/some/netns/path",
   424  				IfName:         "some-eth0",
   425  				Args:           [][2]string{{"FOO", "BAR"}},
   426  				CapabilityArgs: capabilityArgs,
   427  			}
   428  
   429  			expectedCmdArgs = skel.CmdArgs{
   430  				ContainerID: runtimeConfig.ContainerID,
   431  				Netns:       runtimeConfig.NetNS,
   432  				IfName:      runtimeConfig.IfName,
   433  				Args:        "FOO=BAR",
   434  				Path:        cniBinPath,
   435  			}
   436  
   437  			rc := map[string]interface{}{
   438  				"containerId": runtimeConfig.ContainerID,
   439  				"netNs":       runtimeConfig.NetNS,
   440  				"ifName":      runtimeConfig.IfName,
   441  				"args": map[string]string{
   442  					"FOO": "BAR",
   443  				},
   444  				"portMappings":    capabilityArgs["portMappings"],
   445  				"otherCapability": capabilityArgs["otherCapability"],
   446  			}
   447  
   448  			ipResult := `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`
   449  			plugins = make([]pluginInfo, 3, 3)
   450  			plugins[0] = newPluginInfo("some-value", "", true, ipResult, rc, []string{"portMappings", "otherCapability"})
   451  			plugins[1] = newPluginInfo("some-other-value", ipResult, true, "PASSTHROUGH", rc, []string{"otherCapability"})
   452  			plugins[2] = newPluginInfo("yet-another-value", ipResult, true, "INJECT-DNS", rc, []string{})
   453  
   454  			configList := []byte(fmt.Sprintf(`{
   455    "name": "some-list",
   456    "cniVersion": "0.3.1",
   457    "plugins": [
   458      %s,
   459      %s,
   460      %s
   461    ]
   462  }`, plugins[0].config, plugins[1].config, plugins[2].config))
   463  
   464  			netConfigList, err = libcni.ConfListFromBytes(configList)
   465  			Expect(err).NotTo(HaveOccurred())
   466  		})
   467  
   468  		AfterEach(func() {
   469  			for _, p := range plugins {
   470  				Expect(os.RemoveAll(p.debugFilePath)).To(Succeed())
   471  			}
   472  		})
   473  
   474  		Describe("AddNetworkList", func() {
   475  			It("executes all plugins with command ADD and returns an intermediate result", func() {
   476  				r, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig)
   477  				Expect(err).NotTo(HaveOccurred())
   478  
   479  				result, err := current.GetResult(r)
   480  				Expect(err).NotTo(HaveOccurred())
   481  
   482  				Expect(result).To(Equal(&current.Result{
   483  					CNIVersion: current.ImplementedSpecVersion,
   484  					// IP4 added by first plugin
   485  					IPs: []*current.IPConfig{
   486  						{
   487  							Version: "4",
   488  							Address: net.IPNet{
   489  								IP:   net.ParseIP("10.1.2.3"),
   490  								Mask: net.IPv4Mask(255, 255, 255, 0),
   491  							},
   492  						},
   493  					},
   494  					// DNS injected by last plugin
   495  					DNS: types.DNS{
   496  						Nameservers: []string{"1.2.3.4"},
   497  					},
   498  				}))
   499  
   500  				for i := 0; i < len(plugins); i++ {
   501  					debug, err := noop_debug.ReadDebug(plugins[i].debugFilePath)
   502  					Expect(err).NotTo(HaveOccurred())
   503  					Expect(debug.Command).To(Equal("ADD"))
   504  
   505  					// Must explicitly match JSON due to dict element ordering
   506  					Expect(debug.CmdArgs.StdinData).To(MatchJSON(plugins[i].stdinData))
   507  					debug.CmdArgs.StdinData = nil
   508  					Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
   509  				}
   510  			})
   511  
   512  			Context("when finding the plugin fails", func() {
   513  				BeforeEach(func() {
   514  					netConfigList.Plugins[1].Network.Type = "does-not-exist"
   515  				})
   516  
   517  				It("returns the error", func() {
   518  					_, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig)
   519  					Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`)))
   520  				})
   521  			})
   522  
   523  			Context("when the second plugin errors", func() {
   524  				BeforeEach(func() {
   525  					plugins[1].debug.ReportError = "plugin error: banana"
   526  					Expect(plugins[1].debug.WriteDebug(plugins[1].debugFilePath)).To(Succeed())
   527  				})
   528  				It("unmarshals and returns the error", func() {
   529  					result, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig)
   530  					Expect(result).To(BeNil())
   531  					Expect(err).To(MatchError("plugin error: banana"))
   532  				})
   533  			})
   534  		})
   535  
   536  		Describe("DelNetworkList", func() {
   537  			It("executes all the plugins in reverse order with command DEL", func() {
   538  				err := cniConfig.DelNetworkList(netConfigList, runtimeConfig)
   539  				Expect(err).NotTo(HaveOccurred())
   540  
   541  				for i := 0; i < len(plugins); i++ {
   542  					debug, err := noop_debug.ReadDebug(plugins[i].debugFilePath)
   543  					Expect(err).NotTo(HaveOccurred())
   544  					Expect(debug.Command).To(Equal("DEL"))
   545  
   546  					// Must explicitly match JSON due to dict element ordering
   547  					Expect(debug.CmdArgs.StdinData).To(MatchJSON(plugins[i].stdinData))
   548  					debug.CmdArgs.StdinData = nil
   549  					Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
   550  				}
   551  			})
   552  
   553  			Context("when finding the plugin fails", func() {
   554  				BeforeEach(func() {
   555  					netConfigList.Plugins[1].Network.Type = "does-not-exist"
   556  				})
   557  
   558  				It("returns the error", func() {
   559  					err := cniConfig.DelNetworkList(netConfigList, runtimeConfig)
   560  					Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`)))
   561  				})
   562  			})
   563  
   564  			Context("when the plugin errors", func() {
   565  				BeforeEach(func() {
   566  					plugins[1].debug.ReportError = "plugin error: banana"
   567  					Expect(plugins[1].debug.WriteDebug(plugins[1].debugFilePath)).To(Succeed())
   568  				})
   569  				It("unmarshals and returns the error", func() {
   570  					err := cniConfig.DelNetworkList(netConfigList, runtimeConfig)
   571  					Expect(err).To(MatchError("plugin error: banana"))
   572  				})
   573  			})
   574  		})
   575  
   576  	})
   577  })