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

tensorchord / envd / 18149755418

01 Oct 2025 02:45AM UTC coverage: 42.475% (-0.3%) from 42.787%
18149755418

push

github

web-flow
chore(deps): bump github.com/go-viper/mapstructure/v2 from 2.3.0 to 2.4.0 (#2034)

chore(deps): bump github.com/go-viper/mapstructure/v2

Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

5182 of 12200 relevant lines covered (42.48%)

165.16 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 {
13✔
68
        return &host{
13✔
69
                comments:  comments,
13✔
70
                hostnames: hostnames,
13✔
71
        }
13✔
72
}
13✔
73

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

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

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

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

92
        return buf.String()
13✔
93

94
}
95

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

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

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

114✔
109
        if len(p.comments) > 0 {
114✔
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, " "))
114✔
120

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

123
}
124

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

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

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

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

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

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

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

368✔
154
                line := strings.TrimSpace(sc.Text())
368✔
155
                if line == "" {
416✔
156
                        continue
48✔
157
                }
158

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

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

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

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

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

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

196
        }
197

198
        if global {
74✔
199
                config.globals = append(config.globals, p)
21✔
200
        } else if h != nil {
85✔
201
                config.hosts = append(config.hosts, h)
32✔
202
        }
32✔
203

204
        return config, nil
53✔
205

206
}
207

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

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

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

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

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

239
        stat, err := os.Stat(p)
24✔
240
        var mode os.FileMode
24✔
241
        if err != nil {
27✔
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 {
21✔
249
                mode = stat.Mode()
21✔
250
        }
21✔
251

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

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

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

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

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

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

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

280
        return nil
24✔
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 {
43✔
297
        for _, p := range h.params {
269✔
298
                if p.keyword == keyword {
269✔
299
                        return p
43✔
300
                }
43✔
301
        }
302
        return nil
×
303
}
304

305
func BuildHostname(name string) string {
31✔
306
        return fmt.Sprintf("%s.envd", name)
31✔
307
}
31✔
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) {
7✔
385
        cfg, err := getConfig(getSSHConfigPath())
7✔
386
        if err != nil {
7✔
387
                return 0, err
×
388
        }
×
389

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

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

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

406
        return port, nil
7✔
407
}
408

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

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

419
        return nil
1✔
420
}
421

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

429
        return false
14✔
430
}
431

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

445
        return 0, false
14✔
446
}
447

448
func getConfig(path string) (*sshConfig, error) {
56✔
449
        f, err := os.Open(path)
56✔
450
        if err != nil {
59✔
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()
53✔
461

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

467
        return cfg, nil
53✔
468
}
469

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

475
        return nil
24✔
476
}
477

478
func getSSHConfigPath() string {
32✔
479
        dirname, err := os.UserHomeDir()
32✔
480
        if err != nil {
32✔
481
                logrus.WithError(err).Fatal()
×
482
        }
×
483
        return filepath.Join(dirname, ".ssh", "config")
32✔
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