1package main
2
3import (
4	"fmt"
5	"os"
6	"strings"
7
8	"github.com/roboll/helmfile/pkg/app/version"
9
10	"github.com/roboll/helmfile/pkg/app"
11	"github.com/roboll/helmfile/pkg/helmexec"
12	"github.com/roboll/helmfile/pkg/maputil"
13	"github.com/roboll/helmfile/pkg/state"
14	"github.com/urfave/cli"
15	"go.uber.org/zap"
16)
17
18var logger *zap.SugaredLogger
19
20func configureLogging(c *cli.Context) error {
21	// Valid levels:
22	// https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126
23	logLevel := c.GlobalString("log-level")
24	if c.GlobalBool("debug") {
25		logLevel = "debug"
26	} else if c.GlobalBool("quiet") {
27		logLevel = "warn"
28	}
29	logger = helmexec.NewLogger(os.Stderr, logLevel)
30	if c.App.Metadata == nil {
31		// Auto-initialised in 1.19.0
32		// https://github.com/urfave/cli/blob/master/CHANGELOG.md#1190---2016-11-19
33		c.App.Metadata = make(map[string]interface{})
34	}
35	c.App.Metadata["logger"] = logger
36	return nil
37}
38
39func main() {
40
41	cliApp := cli.NewApp()
42	cliApp.Name = "helmfile"
43	cliApp.Usage = ""
44	cliApp.Version = version.Version
45	cliApp.EnableBashCompletion = true
46	cliApp.Flags = []cli.Flag{
47		cli.StringFlag{
48			Name:  "helm-binary, b",
49			Usage: "path to helm binary",
50			Value: app.DefaultHelmBinary,
51		},
52		cli.StringFlag{
53			Name:  "file, f",
54			Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference",
55		},
56		cli.StringFlag{
57			Name:  "environment, e",
58			Usage: `specify the environment name. defaults to "default"`,
59		},
60		cli.StringSliceFlag{
61			Name:  "state-values-set",
62			Usage: "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)",
63		},
64		cli.StringSliceFlag{
65			Name:  "state-values-file",
66			Usage: "specify state values in a YAML file",
67		},
68		cli.BoolFlag{
69			Name:  "quiet, q",
70			Usage: "Silence output. Equivalent to log-level warn",
71		},
72		cli.StringFlag{
73			Name:  "kube-context",
74			Usage: "Set kubectl context. Uses current context by default",
75		},
76		cli.BoolFlag{
77			Name:  "debug",
78			Usage: "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect",
79		},
80		cli.BoolFlag{
81			Name:  "no-color",
82			Usage: "Output without color",
83		},
84		cli.StringFlag{
85			Name:  "log-level",
86			Usage: "Set log level, default info",
87		},
88		cli.StringFlag{
89			Name:  "namespace, n",
90			Usage: "Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}",
91		},
92		cli.StringSliceFlag{
93			Name: "selector, l",
94			Usage: `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
95	A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
96	--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
97	The name of a release can be used as a label. --selector name=myrelease`,
98		},
99		cli.BoolFlag{
100			Name:  "allow-no-matching-release",
101			Usage: `Do not exit with an error code if the provided selector has no matching releases.`,
102		},
103		cli.BoolFlag{
104			Name:  "interactive, i",
105			Usage: "Request confirmation before attempting to modify clusters",
106		},
107	}
108
109	cliApp.Before = configureLogging
110	cliApp.Commands = []cli.Command{
111		{
112			Name:  "deps",
113			Usage: "update charts based on their requirements",
114			Flags: []cli.Flag{
115				cli.StringFlag{
116					Name:  "args",
117					Value: "",
118					Usage: "pass args to helm exec",
119				},
120				cli.BoolFlag{
121					Name:  "skip-repos",
122					Usage: `skip running "helm repo update" before running "helm dependency build"`,
123				},
124			},
125			Action: action(func(run *app.App, c configImpl) error {
126				return run.Deps(c)
127			}),
128		},
129		{
130			Name:  "repos",
131			Usage: "sync repositories from state file (helm repo add && helm repo update)",
132			Flags: []cli.Flag{
133				cli.StringFlag{
134					Name:  "args",
135					Value: "",
136					Usage: "pass args to helm exec",
137				},
138			},
139			Action: action(func(run *app.App, c configImpl) error {
140				return run.Repos(c)
141			}),
142		},
143		{
144			Name:  "charts",
145			Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)",
146			Flags: []cli.Flag{
147				cli.StringFlag{
148					Name:  "args",
149					Value: "",
150					Usage: "pass args to helm exec",
151				},
152				cli.StringSliceFlag{
153					Name:  "set",
154					Usage: "additional values to be merged into the command",
155				},
156				cli.StringSliceFlag{
157					Name:  "values",
158					Usage: "additional value files to be merged into the command",
159				},
160				cli.IntFlag{
161					Name:  "concurrency",
162					Value: 0,
163					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
164				},
165			},
166			Action: action(func(run *app.App, c configImpl) error {
167				return run.DeprecatedSyncCharts(c)
168			}),
169		},
170		{
171			Name:  "diff",
172			Usage: "diff releases from state file against env (helm diff)",
173			Flags: []cli.Flag{
174				cli.StringFlag{
175					Name:  "args",
176					Value: "",
177					Usage: "pass args to helm exec",
178				},
179				cli.StringSliceFlag{
180					Name:  "set",
181					Usage: "additional values to be merged into the command",
182				},
183				cli.StringSliceFlag{
184					Name:  "values",
185					Usage: "additional value files to be merged into the command",
186				},
187				cli.BoolFlag{
188					Name:  "skip-deps",
189					Usage: `skip running "helm repo update" and "helm dependency build"`,
190				},
191				cli.BoolFlag{
192					Name:  "detailed-exitcode",
193					Usage: "return a non-zero exit code when there are changes",
194				},
195				cli.BoolFlag{
196					Name:  "include-tests",
197					Usage: "enable the diffing of the helm test hooks",
198				},
199				cli.BoolFlag{
200					Name:  "suppress-secrets",
201					Usage: "suppress secrets in the output. highly recommended to specify on CI/CD use-cases",
202				},
203				cli.IntFlag{
204					Name:  "concurrency",
205					Value: 0,
206					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
207				},
208				cli.IntFlag{
209					Name:  "context",
210					Value: 0,
211					Usage: "output NUM lines of context around changes",
212				},
213			},
214			Action: action(func(run *app.App, c configImpl) error {
215				return run.Diff(c)
216			}),
217		},
218		{
219			Name:  "template",
220			Usage: "template releases from state file against env (helm template)",
221			Flags: []cli.Flag{
222				cli.StringFlag{
223					Name:  "args",
224					Value: "",
225					Usage: "pass args to helm template",
226				},
227				cli.StringSliceFlag{
228					Name:  "set",
229					Usage: "additional values to be merged into the command",
230				},
231				cli.StringSliceFlag{
232					Name:  "values",
233					Usage: "additional value files to be merged into the command",
234				},
235				cli.StringFlag{
236					Name:  "output-dir",
237					Usage: "output directory to pass to helm template (helm template --output-dir)",
238				},
239				cli.StringFlag{
240					Name:  "output-dir-template",
241					Usage: "go text template for generating the output directory. Default: {{ .OutputDir }}/{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}",
242				},
243				cli.IntFlag{
244					Name:  "concurrency",
245					Value: 0,
246					Usage: "maximum number of concurrent downloads of release charts",
247				},
248				cli.BoolFlag{
249					Name:  "validate",
250					Usage: "validate your manifests against the Kubernetes cluster you are currently pointing at",
251				},
252				cli.BoolFlag{
253					Name:  "include-crds",
254					Usage: "include CRDs in the templated output",
255				},
256				cli.BoolFlag{
257					Name:  "skip-deps",
258					Usage: `skip running "helm repo update" and "helm dependency build"`,
259				},
260				cli.BoolFlag{
261					Name:  "skip-cleanup",
262					Usage: "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security",
263				},
264			},
265			Action: action(func(run *app.App, c configImpl) error {
266				return run.Template(c)
267			}),
268		},
269		{
270			Name:  "write-values",
271			Usage: "write values files for releases. Similar to `helmfile template`, write values files instead of manifests.",
272			Flags: []cli.Flag{
273				cli.StringSliceFlag{
274					Name:  "set",
275					Usage: "additional values to be merged into the command",
276				},
277				cli.StringSliceFlag{
278					Name:  "values",
279					Usage: "additional value files to be merged into the command",
280				},
281				cli.StringFlag{
282					Name:  "output-file-template",
283					Usage: "go text template for generating the output file. Default: {{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}/{{ .Release.Name}}.yaml",
284				},
285				cli.IntFlag{
286					Name:  "concurrency",
287					Value: 0,
288					Usage: "maximum number of concurrent downloads of release charts",
289				},
290				cli.BoolFlag{
291					Name:  "skip-deps",
292					Usage: `skip running "helm repo update" and "helm dependency build"`,
293				},
294			},
295			Action: action(func(run *app.App, c configImpl) error {
296				return run.WriteValues(c)
297			}),
298		},
299		{
300			Name:  "lint",
301			Usage: "lint charts from state file (helm lint)",
302			Flags: []cli.Flag{
303				cli.StringFlag{
304					Name:  "args",
305					Value: "",
306					Usage: "pass args to helm exec",
307				},
308				cli.StringSliceFlag{
309					Name:  "set",
310					Usage: "additional values to be merged into the command",
311				},
312				cli.StringSliceFlag{
313					Name:  "values",
314					Usage: "additional value files to be merged into the command",
315				},
316				cli.IntFlag{
317					Name:  "concurrency",
318					Value: 0,
319					Usage: "maximum number of concurrent downloads of release charts",
320				},
321				cli.BoolFlag{
322					Name:  "skip-deps",
323					Usage: `skip running "helm repo update" and "helm dependency build"`,
324				},
325			},
326			Action: action(func(run *app.App, c configImpl) error {
327				return run.Lint(c)
328			}),
329		},
330		{
331			Name:  "sync",
332			Usage: "sync all resources from state file (repos, releases and chart deps)",
333			Flags: []cli.Flag{
334				cli.StringSliceFlag{
335					Name:  "set",
336					Usage: "additional values to be merged into the command",
337				},
338				cli.StringSliceFlag{
339					Name:  "values",
340					Usage: "additional value files to be merged into the command",
341				},
342				cli.IntFlag{
343					Name:  "concurrency",
344					Value: 0,
345					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
346				},
347				cli.StringFlag{
348					Name:  "args",
349					Value: "",
350					Usage: "pass args to helm exec",
351				},
352				cli.BoolFlag{
353					Name:  "skip-deps",
354					Usage: `skip running "helm repo update" and "helm dependency build"`,
355				},
356				cli.BoolFlag{
357					Name:  "wait",
358					Usage: `Override helmDefaults.wait setting "helm upgrade --install --wait"`,
359				},
360			},
361			Action: action(func(run *app.App, c configImpl) error {
362				return run.Sync(c)
363			}),
364		},
365		{
366			Name:  "apply",
367			Usage: "apply all resources from state file only when there are changes",
368			Flags: []cli.Flag{
369				cli.StringSliceFlag{
370					Name:  "set",
371					Usage: "additional values to be merged into the command",
372				},
373				cli.StringSliceFlag{
374					Name:  "values",
375					Usage: "additional value files to be merged into the command",
376				},
377				cli.IntFlag{
378					Name:  "concurrency",
379					Value: 0,
380					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
381				},
382				cli.IntFlag{
383					Name:  "context",
384					Value: 0,
385					Usage: "output NUM lines of context around changes",
386				},
387				cli.BoolFlag{
388					Name:  "detailed-exitcode",
389					Usage: "return a non-zero exit code 2 instead of 0 when there were changes detected AND the changes are synced successfully",
390				},
391				cli.StringFlag{
392					Name:  "args",
393					Value: "",
394					Usage: "pass args to helm exec",
395				},
396				cli.BoolFlag{
397					Name:  "retain-values-files",
398					Usage: "DEPRECATED: Use skip-cleanup instead",
399				},
400				cli.BoolFlag{
401					Name:  "skip-cleanup",
402					Usage: "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security",
403				},
404				cli.BoolFlag{
405					Name:  "skip-diff-on-install",
406					Usage: "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all",
407				},
408				cli.BoolFlag{
409					Name:  "include-tests",
410					Usage: "enable the diffing of the helm test hooks",
411				},
412				cli.BoolFlag{
413					Name:  "suppress-secrets",
414					Usage: "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases",
415				},
416				cli.BoolFlag{
417					Name:  "suppress-diff",
418					Usage: "suppress diff in the output. Usable in new installs",
419				},
420				cli.BoolFlag{
421					Name:  "skip-deps",
422					Usage: `skip running "helm repo update" and "helm dependency build"`,
423				},
424				cli.BoolFlag{
425					Name:  "wait",
426					Usage: `Override helmDefaults.wait setting "helm upgrade --install --wait"`,
427				},
428			},
429			Action: action(func(run *app.App, c configImpl) error {
430				return run.Apply(c)
431			}),
432		},
433		{
434			Name:  "status",
435			Usage: "retrieve status of releases in state file",
436			Flags: []cli.Flag{
437				cli.IntFlag{
438					Name:  "concurrency",
439					Value: 0,
440					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
441				},
442				cli.StringFlag{
443					Name:  "args",
444					Value: "",
445					Usage: "pass args to helm exec",
446				},
447			},
448			Action: action(func(run *app.App, c configImpl) error {
449				return run.Status(c)
450			}),
451		},
452		{
453			Name:  "delete",
454			Usage: "DEPRECATED: delete releases from state file (helm delete)",
455			Flags: []cli.Flag{
456				cli.IntFlag{
457					Name:  "concurrency",
458					Value: 0,
459					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
460				},
461				cli.StringFlag{
462					Name:  "args",
463					Value: "",
464					Usage: "pass args to helm exec",
465				},
466				cli.BoolFlag{
467					Name:  "purge",
468					Usage: "purge releases i.e. free release names and histories",
469				},
470			},
471			Action: action(func(run *app.App, c configImpl) error {
472				return run.Delete(c)
473			}),
474		},
475		{
476			Name:  "destroy",
477			Usage: "deletes and then purges releases",
478			Flags: []cli.Flag{
479				cli.IntFlag{
480					Name:  "concurrency",
481					Value: 0,
482					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
483				},
484				cli.StringFlag{
485					Name:  "args",
486					Value: "",
487					Usage: "pass args to helm exec",
488				},
489			},
490			Action: action(func(run *app.App, c configImpl) error {
491				return run.Destroy(c)
492			}),
493		},
494		{
495			Name:  "test",
496			Usage: "test releases from state file (helm test)",
497			Flags: []cli.Flag{
498				cli.BoolFlag{
499					Name:  "cleanup",
500					Usage: "delete test pods upon completion",
501				},
502				cli.BoolFlag{
503					Name:  "logs",
504					Usage: "Dump the logs from test pods (this runs after all tests are complete, but before any cleanup)",
505				},
506				cli.StringFlag{
507					Name:  "args",
508					Value: "",
509					Usage: "pass additional args to helm exec",
510				},
511				cli.IntFlag{
512					Name:  "timeout",
513					Value: 300,
514					Usage: "maximum time for tests to run before being considered failed",
515				},
516				cli.IntFlag{
517					Name:  "concurrency",
518					Value: 0,
519					Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
520				},
521			},
522			Action: action(func(run *app.App, c configImpl) error {
523				return run.Test(c)
524			}),
525		},
526		{
527			Name:  "build",
528			Usage: "output compiled helmfile state(s) as YAML",
529			Flags: []cli.Flag{
530				cli.BoolFlag{
531					Name:  "embed-values",
532					Usage: "Read all the values files for every release and embed into the output helmfile.yaml",
533				},
534			},
535			Action: action(func(run *app.App, c configImpl) error {
536				return run.PrintState(c)
537			}),
538		},
539		{
540			Name:  "list",
541			Usage: "list releases defined in state file",
542			Flags: []cli.Flag{
543				cli.StringFlag{
544					Name:  "output",
545					Value: "",
546					Usage: "output releases list as a json string",
547				},
548			},
549			Action: action(func(run *app.App, c configImpl) error {
550				return run.ListReleases(c)
551			}),
552		},
553		{
554			Name:      "version",
555			Usage:     "Show the version for Helmfile.",
556			ArgsUsage: "[command]",
557			Action: func(c *cli.Context) error {
558				cli.ShowVersion(c)
559				return nil
560			},
561		},
562	}
563
564	err := cliApp.Run(os.Args)
565	if err != nil {
566		fmt.Fprintf(os.Stderr, "%v\n", err)
567		os.Exit(3)
568	}
569}
570
571type configImpl struct {
572	c *cli.Context
573
574	set map[string]interface{}
575}
576
577func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
578	if c.NArg() > 0 {
579		cli.ShowAppHelp(c)
580		return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
581	}
582
583	conf := configImpl{
584		c: c,
585	}
586
587	optsSet := c.GlobalStringSlice("state-values-set")
588	if len(optsSet) > 0 {
589		set := map[string]interface{}{}
590		for i := range optsSet {
591			ops := strings.Split(optsSet[i], ",")
592			for j := range ops {
593				op := strings.SplitN(ops[j], "=", 2)
594				k := maputil.ParseKey(op[0])
595				v := op[1]
596
597				maputil.Set(set, k, v)
598			}
599		}
600		conf.set = set
601	}
602
603	return conf, nil
604}
605
606func (c configImpl) Set() []string {
607	return c.c.StringSlice("set")
608}
609
610func (c configImpl) SkipRepos() bool {
611	return c.c.Bool("skip-repos")
612}
613
614func (c configImpl) Wait() bool {
615	return c.c.Bool("wait")
616}
617
618func (c configImpl) Values() []string {
619	return c.c.StringSlice("values")
620}
621
622func (c configImpl) Args() string {
623	args := c.c.String("args")
624	enableHelmDebug := c.c.GlobalBool("debug")
625
626	if enableHelmDebug {
627		args = fmt.Sprintf("%s %s", args, "--debug")
628	}
629	return args
630}
631
632func (c configImpl) OutputDir() string {
633	return c.c.String("output-dir")
634}
635
636func (c configImpl) OutputDirTemplate() string {
637	return c.c.String("output-dir-template")
638}
639
640func (c configImpl) OutputFileTemplate() string {
641	return c.c.String("output-file-template")
642}
643
644func (c configImpl) Validate() bool {
645	return c.c.Bool("validate")
646}
647
648func (c configImpl) Concurrency() int {
649	return c.c.Int("concurrency")
650}
651
652func (c configImpl) HasCommandName(name string) bool {
653	return c.c.Command.HasName(name)
654}
655
656// DiffConfig
657
658func (c configImpl) SkipDeps() bool {
659	return c.c.Bool("skip-deps")
660}
661
662func (c configImpl) DetailedExitcode() bool {
663	return c.c.Bool("detailed-exitcode")
664}
665
666func (c configImpl) RetainValuesFiles() bool {
667	return c.c.Bool("retain-values-files")
668}
669
670func (c configImpl) IncludeTests() bool {
671	return c.c.Bool("include-tests")
672}
673
674func (c configImpl) SuppressSecrets() bool {
675	return c.c.Bool("suppress-secrets")
676}
677
678func (c configImpl) SuppressDiff() bool {
679	return c.c.Bool("suppress-diff")
680}
681
682// DeleteConfig
683
684func (c configImpl) Purge() bool {
685	return c.c.Bool("purge")
686}
687
688// TestConfig
689
690func (c configImpl) Cleanup() bool {
691	return c.c.Bool("cleanup")
692}
693
694func (c configImpl) Logs() bool {
695	return c.c.Bool("logs")
696}
697
698func (c configImpl) Timeout() int {
699	if !c.c.IsSet("timeout") {
700		return state.EmptyTimeout
701	}
702	return c.c.Int("timeout")
703}
704
705// ListConfig
706
707func (c configImpl) Output() string {
708	return c.c.String("output")
709}
710
711// GlobalConfig
712
713func (c configImpl) HelmBinary() string {
714	return c.c.GlobalString("helm-binary")
715}
716
717func (c configImpl) KubeContext() string {
718	return c.c.GlobalString("kube-context")
719}
720
721func (c configImpl) Namespace() string {
722	return c.c.GlobalString("namespace")
723}
724
725func (c configImpl) FileOrDir() string {
726	return c.c.GlobalString("file")
727}
728
729func (c configImpl) Selectors() []string {
730	return c.c.GlobalStringSlice("selector")
731}
732
733func (c configImpl) StateValuesSet() map[string]interface{} {
734	return c.set
735}
736
737func (c configImpl) StateValuesFiles() []string {
738	return c.c.GlobalStringSlice("state-values-file")
739}
740
741func (c configImpl) Interactive() bool {
742	return c.c.GlobalBool("interactive")
743}
744
745func (c configImpl) NoColor() bool {
746	return c.c.GlobalBool("no-color")
747}
748
749func (c configImpl) Context() int {
750	return c.c.Int("context")
751}
752
753func (c configImpl) SkipCleanup() bool {
754	return c.c.Bool("skip-cleanup")
755}
756
757func (c configImpl) SkipDiffOnInstall() bool {
758	return c.c.Bool("skip-diff-on-install")
759}
760
761func (c configImpl) EmbedValues() bool {
762	return c.c.Bool("embed-values")
763}
764
765func (c configImpl) IncludeCRDs() bool {
766	return c.c.Bool("include-crds")
767}
768
769func (c configImpl) Logger() *zap.SugaredLogger {
770	return c.c.App.Metadata["logger"].(*zap.SugaredLogger)
771}
772
773func (c configImpl) Env() string {
774	env := c.c.GlobalString("environment")
775	if env == "" {
776		env = os.Getenv("HELMFILE_ENVIRONMENT")
777		if env == "" {
778			env = state.DefaultEnv
779		}
780	}
781	return env
782}
783
784func action(do func(*app.App, configImpl) error) func(*cli.Context) error {
785	return func(implCtx *cli.Context) error {
786		conf, err := NewUrfaveCliConfigImpl(implCtx)
787		if err != nil {
788			return err
789		}
790
791		a := app.New(conf)
792
793		if err := do(a, conf); err != nil {
794			return toCliError(implCtx, err)
795		}
796
797		return nil
798	}
799}
800
801func toCliError(c *cli.Context, err error) error {
802	if err != nil {
803		switch e := err.(type) {
804		case *app.NoMatchingHelmfileError:
805			noMatchingExitCode := 3
806			if c.GlobalBool("allow-no-matching-release") {
807				noMatchingExitCode = 0
808			}
809			return cli.NewExitError(e.Error(), noMatchingExitCode)
810		case *app.Error:
811			return cli.NewExitError(e.Error(), e.Code())
812		default:
813			panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e))
814		}
815	}
816	return err
817}
818