Merge branch '21227-keep-web-panic'
[arvados.git] / lib / config / cmd.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package config
6
7 import (
8         "bytes"
9         "errors"
10         "flag"
11         "fmt"
12         "io"
13         "os"
14         "os/exec"
15
16         "git.arvados.org/arvados.git/lib/cmd"
17         "git.arvados.org/arvados.git/sdk/go/arvados"
18         "git.arvados.org/arvados.git/sdk/go/ctxlog"
19         "github.com/ghodss/yaml"
20         "github.com/sirupsen/logrus"
21 )
22
23 var DumpCommand dumpCommand
24
25 type dumpCommand struct{}
26
27 func (dumpCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
28         var err error
29         defer func() {
30                 if err != nil {
31                         fmt.Fprintf(stderr, "%s\n", err)
32                 }
33         }()
34
35         loader := &Loader{
36                 Stdin:  stdin,
37                 Logger: ctxlog.New(stderr, "text", "info"),
38         }
39
40         flags := flag.NewFlagSet("", flag.ContinueOnError)
41         loader.SetupFlags(flags)
42
43         if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
44                 return code
45         }
46         cfg, err := loader.Load()
47         if err != nil {
48                 return 1
49         }
50         out, err := yaml.Marshal(cfg)
51         if err != nil {
52                 return 1
53         }
54         _, err = stdout.Write(out)
55         if err != nil {
56                 return 1
57         }
58         return 0
59 }
60
61 var CheckCommand checkCommand
62
63 type checkCommand struct{}
64
65 func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
66         var err error
67         var logbuf = &bytes.Buffer{}
68         defer func() {
69                 io.Copy(stderr, logbuf)
70                 if err != nil {
71                         fmt.Fprintf(stderr, "%s\n", err)
72                 }
73         }()
74
75         logger := logrus.New()
76         logger.Out = logbuf
77         loader := &Loader{
78                 Stdin:  stdin,
79                 Logger: logger,
80         }
81
82         flags := flag.NewFlagSet(prog, flag.ContinueOnError)
83         loader.SetupFlags(flags)
84         strict := flags.Bool("strict", true, "Strict validation of configuration file (warnings result in non-zero exit code)")
85         if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
86                 return code
87         }
88
89         // Load the config twice -- once without loading deprecated
90         // keys/files, once with -- and then compare the two resulting
91         // configs. This reveals whether the deprecated keys/files
92         // have any effect on the final configuration.
93         //
94         // If they do, show the operator how to update their config
95         // such that the deprecated keys/files are superfluous and can
96         // be deleted.
97         loader.SkipDeprecated = true
98         loader.SkipLegacy = true
99         withoutDepr, err := loader.Load()
100         if err != nil {
101                 return 1
102         }
103         // Reset() to avoid printing the same warnings twice when they
104         // are logged by both without-legacy and with-legacy loads.
105         logbuf.Reset()
106         loader.SkipDeprecated = false
107         loader.SkipLegacy = false
108         withDepr, err := loader.Load()
109         if err != nil {
110                 return 1
111         }
112
113         // Check for configured vocabulary validity.
114         for id, cc := range withDepr.Clusters {
115                 if cc.API.VocabularyPath == "" {
116                         continue
117                 }
118                 vd, err := os.ReadFile(cc.API.VocabularyPath)
119                 if err != nil {
120                         if errors.Is(err, os.ErrNotExist) {
121                                 // If the vocabulary path doesn't exist, it might mean that
122                                 // the current node isn't the controller; so it's not an
123                                 // error.
124                                 continue
125                         }
126                         logger.Errorf("Error reading vocabulary file %q for cluster %s: %s\n", cc.API.VocabularyPath, id, err)
127                         continue
128                 }
129                 mk := make([]string, 0, len(cc.Collections.ManagedProperties))
130                 for k := range cc.Collections.ManagedProperties {
131                         mk = append(mk, k)
132                 }
133                 _, err = arvados.NewVocabulary(vd, mk)
134                 if err != nil {
135                         logger.Errorf("Error loading vocabulary file %q for cluster %s:\n%s\n", cc.API.VocabularyPath, id, err)
136                         continue
137                 }
138         }
139
140         cmd := exec.Command("diff", "-u", "--label", "without-deprecated-configs", "--label", "relying-on-deprecated-configs", "/dev/fd/3", "/dev/fd/4")
141         for _, obj := range []interface{}{withoutDepr, withDepr} {
142                 y, _ := yaml.Marshal(obj)
143                 pr, pw, err := os.Pipe()
144                 if err != nil {
145                         return 1
146                 }
147                 defer pr.Close()
148                 go func() {
149                         io.Copy(pw, bytes.NewBuffer(y))
150                         pw.Close()
151                 }()
152                 cmd.ExtraFiles = append(cmd.ExtraFiles, pr)
153         }
154         diff, err := cmd.CombinedOutput()
155         if bytes.HasPrefix(diff, []byte("--- ")) {
156                 fmt.Fprintln(stdout, "Your configuration is relying on deprecated entries. Suggest making the following changes.")
157                 stdout.Write(diff)
158                 err = nil
159                 if *strict {
160                         return 1
161                 }
162         } else if len(diff) > 0 {
163                 fmt.Fprintf(stderr, "Unexpected diff output:\n%s", diff)
164                 if *strict {
165                         return 1
166                 }
167         } else if err != nil {
168                 return 1
169         }
170         if logbuf.Len() > 0 {
171                 if *strict {
172                         return 1
173                 }
174         }
175         return 0
176 }
177
178 var DumpDefaultsCommand defaultsCommand
179
180 type defaultsCommand struct{}
181
182 func (defaultsCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
183         _, err := stdout.Write(DefaultYAML)
184         if err != nil {
185                 fmt.Fprintln(stderr, err)
186                 return 1
187         }
188         return 0
189 }