• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tensorchord / envd / 13754521523

10 Mar 2025 01:00AM UTC coverage: 42.542% (+0.4%) from 42.15%
13754521523

Pull #1992

github

kemingy
fix lint

Signed-off-by: Keming <kemingyang@tensorchord.ai>
Pull Request #1992: chore: bump dep version, check dep monthly

26 of 32 new or added lines in 5 files covered. (81.25%)

2 existing lines in 1 file now uncovered.

5157 of 12122 relevant lines covered (42.54%)

158.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.39
/pkg/ssh/config/ssh_config.go
1
// Copyright 2023 The envd Authors
2
// Copyright 2023 The Okteto Authors
3
// based on https://github.com/havoc-io/sshconfig
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
//      http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16

17
package config
18

19
import (
20
        "bufio"
21
        "bytes"
22
        "fmt"
23
        "io"
24
        "os"
25
        "path/filepath"
26
        "strconv"
27
        "strings"
28

29
        "github.com/cockroachdb/errors"
30
        "github.com/sirupsen/logrus"
31

32
        "github.com/tensorchord/envd/pkg/util/osutil"
33
)
34

35
type (
36
        sshConfig struct {
37
                source  []byte
38
                globals []*param
39
                hosts   []*host
40
        }
41
        host struct {
42
                comments  []string
43
                hostnames []string
44
                params    []*param
45
        }
46
        param struct {
47
                comments []string
48
                keyword  string
49
                args     []string
50
        }
51
)
52

53
// will use auth fields in the future
54
const (
55
        forwardAgentKeyword           = "ForwardAgent"
56
        pubkeyAcceptedKeyTypesKeyword = "PubkeyAcceptedKeyTypes"
57
        hostKeyword                   = "Host"
58
        hostNameKeyword               = "HostName"
59
        portKeyword                   = "Port"
60
        strictHostKeyCheckingKeyword  = "StrictHostKeyChecking"
61
        hostKeyAlgorithms             = "HostKeyAlgorithms"
62
        userKnownHostsFileKeyword     = "UserKnownHostsFile"
63
        identityFile                  = "IdentityFile"
64
        userKeyword                   = "User"
65
)
66

67
func newHost(hostnames, comments []string) *host {
12✔
68
        return &host{
12✔
69
                comments:  comments,
12✔
70
                hostnames: hostnames,
12✔
71
        }
12✔
72
}
12✔
73

74
func (h *host) String() string {
12✔
75

12✔
76
        buf := &bytes.Buffer{}
12✔
77

12✔
78
        if len(h.comments) > 0 {
24✔
79
                for _, comment := range h.comments {
24✔
80
                        if !strings.HasPrefix(comment, "#") {
24✔
81
                                comment = "# " + comment
12✔
82
                        }
12✔
83
                        fmt.Fprintln(buf, comment)
12✔
84
                }
85
        }
86

87
        fmt.Fprintf(buf, "%s %s\n", hostKeyword, strings.Join(h.hostnames, " "))
12✔
88
        for _, param := range h.params {
108✔
89
                fmt.Fprint(buf, "  ", param.String())
96✔
90
        }
96✔
91

92
        return buf.String()
12✔
93

94
}
95

96
// nolint:unparam
97
func newParam(keyword string, args, comments []string) *param {
96✔
98
        return &param{
96✔
99
                comments: comments,
96✔
100
                keyword:  keyword,
96✔
101
                args:     args,
96✔
102
        }
96✔
103
}
96✔
104

105
func (p *param) String() string {
105✔
106

105✔
107
        buf := &bytes.Buffer{}
105✔
108

105✔
109
        if len(p.comments) > 0 {
105✔
110
                fmt.Fprintln(buf)
×
111
                for _, comment := range p.comments {
×
112
                        if !strings.HasPrefix(comment, "#") {
×
113
                                comment = "# " + comment
×
114
                        }
×
115
                        fmt.Fprintln(buf, comment)
×
116
                }
117
        }
118

119
        fmt.Fprintf(buf, "%s %s\n", p.keyword, strings.Join(p.args, " "))
105✔
120

105✔
121
        return buf.String()
105✔
122

123
}
124

125
func (p *param) value() string {
6✔
126
        if len(p.args) > 0 {
12✔
127
                return p.args[0]
6✔
128
        }
6✔
129
        return ""
×
130
}
131

132
func parse(r io.Reader) (*sshConfig, error) {
48✔
133

48✔
134
        // dat state
48✔
135
        var (
48✔
136
                global = true
48✔
137

48✔
138
                p = &param{}
48✔
139
                h *host
48✔
140
        )
48✔
141

48✔
142
        data, err := io.ReadAll(r)
48✔
143
        if err != nil {
48✔
144
                return nil, err
×
145
        }
×
146

147
        config := &sshConfig{
48✔
148
                source: data,
48✔
149
        }
48✔
150

48✔
151
        sc := bufio.NewScanner(bytes.NewReader(data))
48✔
152
        for sc.Scan() {
380✔
153

332✔
154
                line := strings.TrimSpace(sc.Text())
332✔
155
                if line == "" {
374✔
156
                        continue
42✔
157
                }
158

159
                if line[0] == '#' {
319✔
160
                        p.comments = append(p.comments, line)
29✔
161
                        continue
29✔
162
                }
163

164
                psc := bufio.NewScanner(strings.NewReader(line))
261✔
165
                psc.Split(bufio.ScanWords)
261✔
166
                if !psc.Scan() {
261✔
167
                        continue
×
168
                }
169

170
                p.keyword = psc.Text()
261✔
171

261✔
172
                for psc.Scan() {
522✔
173
                        p.args = append(p.args, psc.Text())
261✔
174
                }
261✔
175

176
                if p.keyword == hostKeyword {
290✔
177
                        global = false
29✔
178
                        if h != nil {
29✔
UNCOV
179
                                config.hosts = append(config.hosts, h)
×
UNCOV
180
                        }
×
181
                        h = &host{
29✔
182
                                comments:  p.comments,
29✔
183
                                hostnames: p.args,
29✔
184
                        }
29✔
185
                        p = &param{}
29✔
186
                        continue
29✔
187
                } else if global {
232✔
188
                        config.globals = append(config.globals, p)
×
189
                        p = &param{}
×
190
                        continue
×
191
                }
192

193
                h.params = append(h.params, p)
232✔
194
                p = &param{}
232✔
195

196
        }
197

198
        if global {
67✔
199
                config.globals = append(config.globals, p)
19✔
200
        } else if h != nil {
77✔
201
                config.hosts = append(config.hosts, h)
29✔
202
        }
29✔
203

204
        return config, nil
48✔
205

206
}
207

208
func (config *sshConfig) writeTo(w io.Writer) error {
22✔
209
        buf := bytes.NewBufferString("")
22✔
210
        for _, param := range config.globals {
31✔
211
                if _, err := fmt.Fprint(buf, param.String()); err != nil {
9✔
212
                        return err
×
213
                }
×
214
        }
215

216
        if len(config.globals) > 0 {
31✔
217
                if _, err := fmt.Fprintln(buf); err != nil {
9✔
218
                        return err
×
219
                }
×
220
        }
221

222
        for _, host := range config.hosts {
34✔
223
                if _, err := fmt.Fprint(buf, host.String()); err != nil {
12✔
224
                        return err
×
225
                }
×
226
        }
227

228
        _, err := fmt.Fprint(w, buf.String())
22✔
229
        return err
22✔
230
}
231

232
func (config *sshConfig) writeToFilepath(p string) error {
22✔
233
        sshDir := filepath.Dir(p)
22✔
234
        if err := os.MkdirAll(sshDir, 0700); err != nil {
22✔
235
                logrus.WithError(err).
×
236
                        Infof("failed to create SSH directory %s", sshDir)
×
237
        }
×
238

239
        stat, err := os.Stat(p)
22✔
240
        var mode os.FileMode
22✔
241
        if err != nil {
25✔
242
                if !os.IsNotExist(err) {
3✔
243
                        return errors.Newf("failed to get info on %s: %w", p, err)
×
244
                }
×
245

246
                // default for sshconfig
247
                mode = 0600
3✔
248
        } else {
19✔
249
                mode = stat.Mode()
19✔
250
        }
19✔
251

252
        dir := filepath.Dir(p)
22✔
253
        temp, err := os.CreateTemp(dir, "")
22✔
254
        if err != nil {
22✔
255
                return errors.Newf("failed to create temporary config file: %w", err)
×
256
        }
×
257

258
        defer os.Remove(temp.Name())
22✔
259

22✔
260
        if err := config.writeTo(temp); err != nil {
22✔
261
                return err
×
262
        }
×
263

264
        if err := temp.Close(); err != nil {
22✔
265
                return err
×
266
        }
×
267

268
        if err := os.Chmod(temp.Name(), mode); err != nil {
22✔
269
                return errors.Newf("failed to set permissions to %s: %w", temp.Name(), err)
×
270
        }
×
271

272
        if _, err := getConfig(temp.Name()); err != nil {
22✔
273
                return errors.Newf("new config is not valid: %w", err)
×
274
        }
×
275

276
        if err := os.Rename(temp.Name(), p); err != nil {
22✔
277
                return errors.Newf("failed to move %s to %s: %w", temp.Name(), p, err)
×
278
        }
×
279

280
        return nil
22✔
281

282
}
283

284
//nolint:unused
285
func (config *sshConfig) getHost(hostname string) *host {
×
286
        for _, host := range config.hosts {
×
287
                for _, hn := range host.hostnames {
×
288
                        if hn == hostname {
×
289
                                return host
×
290
                        }
×
291
                }
292
        }
293
        return nil
×
294
}
295

296
func (h *host) getParam(keyword string) *param {
38✔
297
        for _, p := range h.params {
238✔
298
                if p.keyword == keyword {
238✔
299
                        return p
38✔
300
                }
38✔
301
        }
302
        return nil
×
303
}
304

305
func BuildHostname(name string) string {
28✔
306
        return fmt.Sprintf("%s.envd", name)
28✔
307
}
28✔
308

309
func ReplaceKeyManagedByEnvd(oldKey string, newKey string) error {
×
310
        cfg, err := getConfig(getSSHConfigPath())
×
311
        if err != nil {
×
312
                return err
×
313
        }
×
314
        logrus.Infof("Rewrite ssh keys old: %s, new: %s", oldKey, newKey)
×
315
        for ih, h := range cfg.hosts {
×
316
                for _, hn := range h.hostnames {
×
317
                        logrus.Info(h.hostnames)
×
318
                        if strings.HasSuffix(hn, ".envd") {
×
319
                                for ip, p := range h.params {
×
320
                                        if p.keyword == identityFile && strings.Trim(p.args[0], "\"") == oldKey {
×
321
                                                logrus.Debug("Change key")
×
322
                                                cfg.hosts[ih].params[ip].args[0] = newKey
×
323
                                        }
×
324
                                }
325
                        }
326
                }
327
        }
328

329
        path, err := GetPrivateKey()
×
330
        if err != nil {
×
331
                return err
×
332
        }
×
333

334
        err = os.Rename(path, newKey)
×
335
        if err != nil {
×
336
                return err
×
337
        }
×
338

339
        err = save(cfg, getSSHConfigPath())
×
340
        if err != nil {
×
341
                return err
×
342
        }
×
343

344
        if osutil.IsWsl() {
×
345
                winSshConfig, err := osutil.GetWslHostSshConfig()
×
346
                if err != nil {
×
347
                        return err
×
348
                }
×
349
                cfg, err := getConfig(winSshConfig)
×
350
                if err != nil {
×
351
                        return err
×
352
                }
×
353
                winNewKey, err := osutil.CopyToWinEnvdHome(newKey, 0600)
×
354
                if err != nil {
×
355
                        return err
×
356
                }
×
357
                winOldKey, err := osutil.CopyToWinEnvdHome(oldKey, 0600)
×
358
                if err != nil {
×
359
                        return err
×
360
                }
×
361
                logrus.Infof("Rewrite WSL ssh keys old: %s, new: %s", winOldKey, winNewKey)
×
362
                for ih, h := range cfg.hosts {
×
363
                        for _, hn := range h.hostnames {
×
364
                                logrus.Info(h.hostnames)
×
365
                                if strings.HasSuffix(hn, ".envd") {
×
366
                                        for ip, p := range h.params {
×
367
                                                if p.keyword == identityFile && strings.Trim(p.args[0], "\"") == winOldKey {
×
368
                                                        logrus.Debug("Change key")
×
369
                                                        cfg.hosts[ih].params[ip].args[0] = winNewKey
×
370
                                                }
×
371
                                        }
372
                                }
373
                        }
374
                }
375
                err = save(cfg, winSshConfig)
×
376
                if err != nil {
×
377
                        return err
×
378
                }
×
379
        }
380
        return nil
×
381
}
382

383
// GetPort returns the corresponding SSH port for the dev env
384
func GetPort(name string) (int, error) {
6✔
385
        cfg, err := getConfig(getSSHConfigPath())
6✔
386
        if err != nil {
6✔
387
                return 0, err
×
388
        }
×
389

390
        hostname := BuildHostname(name)
6✔
391
        i, found := findHost(cfg, hostname)
6✔
392
        if !found {
6✔
393
                return 0, errors.Newf("development container not found")
×
394
        }
×
395

396
        param := cfg.hosts[i].getParam(portKeyword)
6✔
397
        if param == nil {
6✔
398
                return 0, errors.Newf("port not found")
×
399
        }
×
400

401
        port, err := strconv.Atoi(param.value())
6✔
402
        if err != nil {
6✔
403
                return 0, errors.Newf("invalid port: %s", param.value())
×
404
        }
×
405

406
        return port, nil
6✔
407
}
408

409
func remove(path, name string) error {
11✔
410
        cfg, err := getConfig(path)
11✔
411
        if err != nil {
11✔
412
                return err
×
413
        }
×
414

415
        if removeHost(cfg, name) {
21✔
416
                return save(cfg, path)
10✔
417
        }
10✔
418

419
        return nil
1✔
420
}
421

422
func removeHost(cfg *sshConfig, name string) bool {
23✔
423
        ix, ok := findHost(cfg, name)
23✔
424
        if ok {
33✔
425
                cfg.hosts = append(cfg.hosts[:ix], cfg.hosts[ix+1:]...)
10✔
426
                return true
10✔
427
        }
10✔
428

429
        return false
13✔
430
}
431

432
func findHost(cfg *sshConfig, name string) (int, bool) {
29✔
433
        for i, h := range cfg.hosts {
46✔
434
                for _, hn := range h.hostnames {
34✔
435
                        if hn == name {
33✔
436
                                p := h.getParam(portKeyword)
16✔
437
                                s := h.getParam(strictHostKeyCheckingKeyword)
16✔
438
                                if p != nil && s != nil {
32✔
439
                                        return i, true
16✔
440
                                }
16✔
441
                        }
442
                }
443
        }
444

445
        return 0, false
13✔
446
}
447

448
func getConfig(path string) (*sshConfig, error) {
51✔
449
        f, err := os.Open(path)
51✔
450
        if err != nil {
54✔
451
                if os.IsNotExist(err) {
6✔
452
                        return &sshConfig{
3✔
453
                                hosts: []*host{},
3✔
454
                        }, nil
3✔
455
                }
3✔
456

457
                return nil, errors.Newf("can't open %s: %w", path, err)
×
458
        }
459

460
        defer f.Close()
48✔
461

48✔
462
        cfg, err := parse(f)
48✔
463
        if err != nil {
48✔
464
                return nil, errors.Newf("fail to decode %s: %w", path, err)
×
465
        }
×
466

467
        return cfg, nil
48✔
468
}
469

470
func save(cfg *sshConfig, path string) error {
22✔
471
        if err := cfg.writeToFilepath(path); err != nil {
22✔
472
                return errors.Newf("fail to update SSH config file %s: %w", path, err)
×
473
        }
×
474

475
        return nil
22✔
476
}
477

478
func getSSHConfigPath() string {
29✔
479
        dirname, err := os.UserHomeDir()
29✔
480
        if err != nil {
29✔
481
                logrus.WithError(err).Fatal()
×
482
        }
×
483
        return filepath.Join(dirname, ".ssh", "config")
29✔
484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc