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

nats-io / nsc / 17923063186

22 Sep 2025 05:16PM UTC coverage: 73.615% (-0.6%) from 74.188%
17923063186

push

github

web-flow
Add TLS support to push, pull, and tool commands (#768)

- Introduced `--tls-first`, `--ca-cert`, `--client-cert`, and `--client-key` flags
- Updated the `push`, `pull`, and `tool` commands to support TLS configuration options.

Fixes #737

Signed-off-by: Alberto Ricart <alberto@synadia.com>

19 of 30 new or added lines in 3 files covered. (63.33%)

133 existing lines in 3 files now uncovered.

13105 of 17802 relevant lines covered (73.62%)

0.87 hits per line

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

60.64
/cmd/pull.go
1
// Copyright 2018-2021 The NATS Authors
2
// Licensed under the Apache License, Version 2.0 (the "License");
3
// you may not use this file except in compliance with the License.
4
// You may obtain a copy of the License at
5
//
6
// http://www.apache.org/licenses/LICENSE-2.0
7
//
8
// Unless required by applicable law or agreed to in writing, software
9
// distributed under the License is distributed on an "AS IS" BASIS,
10
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
// See the License for the specific language governing permissions and
12
// limitations under the License.
13

14
package cmd
15

16
import (
17
        "errors"
18
        "fmt"
19
        "strings"
20
        "sync"
21
        "time"
22

23
        cli "github.com/nats-io/cliprompts/v2"
24
        "github.com/nats-io/jwt/v2"
25
        "github.com/nats-io/nats.go"
26
        "github.com/nats-io/nsc/v2/cmd/store"
27
        "github.com/spf13/cobra"
28
)
29

30
func createPullCmd() *cobra.Command {
1✔
31
        var params PullParams
1✔
32
        cmd := &cobra.Command{
1✔
33
                Use:   "pull",
1✔
34
                Short: "Pull an operator or account jwt replacing the local jwt with the server's version",
1✔
35
                RunE: func(cmd *cobra.Command, args []string) error {
2✔
36
                        return RunAction(cmd, args, &params)
1✔
37
                },
1✔
38
        }
39

40
        cmd.Flags().BoolVarP(&params.All, "all", "A", false, "operator and all accounts under the operator")
1✔
41
        cmd.Flags().BoolVarP(&params.Overwrite, "overwrite-newer", "F", false, "overwrite local JWTs that are newer than remote")
1✔
42
        cmd.Flags().StringVarP(&params.sysAcc, "system-account", "", "", "System account for use with nats-resolver enabled nats-server. (Default is system account specified by operator)")
1✔
43
        cmd.Flags().StringVarP(&params.sysAccUser, "system-user", "", "", "System account user for use with nats-resolver enabled nats-server. (Default to temporarily generated user)")
1✔
44

1✔
45
        cmd.Flags().BoolVarP(&clienttls.tlsFirst, "tls-first", "", false, "use tls-first when connecting to the nats server")
1✔
46
        cmd.Flags().StringVarP(&clienttls.ca, "ca-cert", "", "", "ca certificate file for tls connections")
1✔
47
        cmd.Flags().StringVarP(&clienttls.ca, "client-cert", "", "", "client certificate file for tls connections")
1✔
48
        cmd.Flags().StringVarP(&clienttls.ca, "client-key", "", "", "client key file for tls connections")
1✔
49

1✔
50
        params.AccountContextParams.BindFlags(cmd)
1✔
51
        return cmd
1✔
52
}
53

54
func init() {
1✔
55
        rootCmd.AddCommand(createPullCmd())
1✔
56
}
1✔
57

58
type PullParams struct {
59
        AccountContextParams
60
        All        bool
61
        Jobs       PullJobs
62
        Overwrite  bool
63
        sysAccUser string // when present use
64
        sysAcc     string
65
}
66

67
type PullJob struct {
68
        Name       string
69
        ASU        string
70
        Err        error
UNCOV
71
        StoreErr   error
×
UNCOV
72
        LocalClaim jwt.Claims
×
UNCOV
73

×
UNCOV
74
        PullStatus *store.Report
×
UNCOV
75
}
×
UNCOV
76

×
77
func (j *PullJob) Token() (string, error) {
1✔
78
        if len(j.PullStatus.Data) == 0 {
1✔
79
                return "", errors.New("no data")
×
80
        }
×
81
        token, err := jwt.ParseDecoratedJWT(j.PullStatus.Data)
1✔
82
        if err != nil {
1✔
83
                return "", err
×
84
        }
×
85
        j.PullStatus.Data = []byte(token)
1✔
86
        gc, err := jwt.DecodeGeneric(token)
1✔
87
        if err != nil {
1✔
88
                return "", err
×
89
        }
×
90
        switch gc.ClaimType() {
1✔
91
        case jwt.AccountClaim:
1✔
92
                _, err := jwt.DecodeAccountClaims(token)
1✔
93
                if err != nil {
1✔
94
                        return "", err
×
95
                }
×
96
                return token, nil
1✔
97
        case jwt.OperatorClaim:
1✔
98
                _, err := jwt.DecodeOperatorClaims(token)
1✔
99
                if err != nil {
1✔
100
                        return "", err
×
101
                }
×
102
                return token, nil
1✔
103
        default:
×
104
                return "", fmt.Errorf("unsupported token type: %q", gc.ClaimType())
×
UNCOV
105
        }
×
UNCOV
106
}
×
UNCOV
107

×
108
func (j *PullJob) Run() {
1✔
109
        if j.PullStatus != nil {
2✔
110
                // already ran
1✔
111
                return
1✔
112
        }
1✔
113
        s, err := store.PullAccount(j.ASU)
1✔
114
        if err != nil {
2✔
115
                j.Err = err
1✔
116
                return
1✔
117
        }
1✔
118
        ps, ok := s.(*store.Report)
1✔
119
        if !ok {
1✔
120
                j.Err = errors.New("unable to convert pull status")
×
121
                return
×
122
        }
×
123
        j.PullStatus = ps
1✔
UNCOV
124
}
×
125

UNCOV
126
type PullJobs []*PullJob
×
UNCOV
127

×
128
func (p *PullParams) SetDefaults(ctx ActionCtx) error {
1✔
129
        return p.AccountContextParams.SetDefaults(ctx)
1✔
130
}
1✔
UNCOV
131

×
132
func (p *PullParams) PreInteractive(ctx ActionCtx) error {
×
133
        var err error
×
134
        tc := GetConfig()
×
135
        p.All, err = cli.Confirm(fmt.Sprintf("pull operator %q and all accounts", tc.Operator), true)
×
136
        if err != nil {
×
137
                return err
×
138
        }
×
139
        if !p.All {
×
140
                if err := p.AccountContextParams.Edit(ctx); err != nil {
×
141
                        return err
×
142
                }
×
UNCOV
143
        }
×
144
        return nil
×
UNCOV
145
}
×
UNCOV
146

×
147
func (p *PullParams) Load(ctx ActionCtx) error {
1✔
148
        return nil
1✔
149
}
1✔
UNCOV
150

×
151
func (p *PullParams) PostInteractive(ctx ActionCtx) error {
×
152
        return nil
×
153
}
×
UNCOV
154

×
155
func (p *PullParams) Validate(ctx ActionCtx) error {
1✔
156
        if !p.All && p.Name == "" {
1✔
157
                return errors.New("specify --all or --account")
×
158
        }
×
159
        oc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
1✔
160
        if err != nil {
1✔
161
                return err
×
162
        }
×
163
        if oc.AccountServerURL == "" {
2✔
164
                return fmt.Errorf("operator %q doesn't set account server url - unable to pull", ctx.StoreCtx().Operator.Name)
1✔
165
        }
1✔
166
        if IsResolverURL(oc.AccountServerURL) && !p.All {
1✔
167
                return fmt.Errorf("operator %q can only pull all jwt - unable to pull by account", ctx.StoreCtx().Operator.Name)
×
168
        }
×
169
        return nil
1✔
UNCOV
170
}
×
UNCOV
171

×
172
func (p *PullParams) setupJobs(ctx ActionCtx) error {
1✔
173
        s := ctx.StoreCtx().Store
1✔
174
        oc, err := s.ReadOperatorClaim()
1✔
175
        if err != nil {
1✔
176
                return err
×
177
        }
×
178
        if p.All {
2✔
179
                u, err := OperatorJwtURL(oc)
1✔
180
                if err != nil {
1✔
181
                        return err
×
182
                }
×
UNCOV
183
                // in ngs we are in v2 operator, so lets try /jwt/v2/operator first
×
184
                uv2 := strings.Replace(u, "/jwt/v1/operator", "/jwt/v2/operator", 1)
1✔
185
                j := PullJob{ASU: uv2, Name: oc.Name, LocalClaim: oc}
1✔
186
                j.Run()
1✔
187
                if j.Err == nil {
2✔
188
                        p.Jobs = append(p.Jobs, &j)
1✔
189
                } else {
2✔
190
                        j = PullJob{ASU: u, Name: oc.Name, LocalClaim: oc}
1✔
191
                        p.Jobs = append(p.Jobs, &j)
1✔
192
                }
1✔
193
                tc := GetConfig()
1✔
194
                accounts, err := tc.ListAccounts()
1✔
195
                if err != nil {
1✔
196
                        return err
×
197
                }
×
198
                for _, v := range accounts {
2✔
199
                        ac, err := s.ReadAccountClaim(v)
1✔
200
                        if err != nil {
1✔
201
                                return err
×
202
                        }
×
203
                        u, err := AccountJwtURL(oc, ac)
1✔
204
                        if err != nil {
1✔
205
                                return err
×
206
                        }
×
207
                        j := PullJob{ASU: u, Name: ac.Name, LocalClaim: ac}
1✔
208
                        p.Jobs = append(p.Jobs, &j)
1✔
UNCOV
209
                }
×
210
        } else {
1✔
211
                ac, err := s.ReadAccountClaim(p.Name)
1✔
212
                if err != nil {
1✔
213
                        return err
×
214
                }
×
215
                u, err := AccountJwtURL(oc, ac)
1✔
216
                if err != nil {
1✔
217
                        return err
×
218
                }
×
219
                j := PullJob{ASU: u, Name: ac.Name, LocalClaim: ac}
1✔
220
                p.Jobs = append(p.Jobs, &j)
1✔
UNCOV
221
        }
×
UNCOV
222

×
223
        return nil
1✔
UNCOV
224
}
×
UNCOV
225

×
226
func (p *PullParams) maybeStoreJWT(ctx ActionCtx, sub *store.Report, token string) {
1✔
227
        remoteClaim, err := jwt.DecodeGeneric(token)
1✔
228
        if err != nil {
1✔
229
                sub.AddError("error decoding remote token: %v %s", err, token)
×
230
                return
×
231
        }
×
232
        orig := int64(0)
1✔
233
        if localClaim, err := ctx.StoreCtx().Store.ReadAccountClaim(remoteClaim.Name); err == nil {
2✔
234
                orig = localClaim.IssuedAt
1✔
235
        }
1✔
236
        remote := remoteClaim.IssuedAt
1✔
237
        if (orig > remote) && !p.Overwrite {
2✔
238
                sub.AddError("local jwt for %q is newer than remote version - specify --overwrite-newer to overwrite", remoteClaim.Name)
1✔
239
                return
1✔
240
        }
1✔
241
        if err := ctx.StoreCtx().Store.StoreRaw([]byte(token)); err != nil {
1✔
242
                sub.AddError("error storing %q: %v", remoteClaim.Name, err)
×
243
                return
×
244
        }
×
245
        sub.AddOK("stored %s %q", remoteClaim.ClaimType(), remoteClaim.Name)
1✔
246
        if sub.OK() {
2✔
247
                sub.Label = fmt.Sprintf("pulled %q from the account server", remoteClaim.Name)
1✔
248
        }
1✔
UNCOV
249
}
×
UNCOV
250

×
251
func (p *PullParams) Run(ctx ActionCtx) (store.Status, error) {
1✔
252
        r := store.NewDetailedReport(true)
1✔
253
        if op, err := ctx.StoreCtx().Store.ReadOperatorClaim(); err != nil {
1✔
254
                r.AddError("could not read operator claim: %v", err)
×
255
                return r, err
×
256
        } else if op.AccountServerURL == "" {
1✔
257
                err := fmt.Errorf("operator has no account server url")
×
258
                r.AddError("operator %s: %v", op.Name, err)
×
259
                return r, err
×
260
        } else if url := op.AccountServerURL; IsResolverURL(url) {
2✔
261
                subR := store.NewReport(store.OK, `pull from cluster using system account`)
1✔
262
                r.Add(subR)
1✔
263
                ib := nats.NewInbox()
1✔
264
                _, opt, err := getSystemAccountUser(ctx, p.sysAcc, p.sysAccUser, ib, "$SYS.REQ.CLAIMS.PACK")
1✔
265
                if err != nil {
2✔
266
                        subR.AddError("failed to obtain system user: %v", err)
1✔
267
                        return r, nil
1✔
268
                }
1✔
269
                nc, err := nats.Connect(url, createDefaultToolOptions("nsc_pull", ctx, opt)...)
1✔
270
                if err != nil {
1✔
271
                        subR.AddError("failed to connect to %s: %v", url, err)
×
272
                        return r, nil
×
273
                }
×
274
                defer nc.Close()
1✔
275

1✔
276
                sub, err := nc.SubscribeSync(ib)
1✔
277
                if err != nil {
1✔
278
                        subR.AddError("failed to subscribe to response subject: %v", err)
×
279
                        return r, nil
×
280
                }
×
281
                if err := nc.PublishRequest("$SYS.REQ.CLAIMS.PACK", ib, nil); err != nil {
1✔
282
                        subR.AddError("failed to pull accounts: %v", err)
×
283
                        return r, nil
×
284
                }
×
285
                for {
2✔
286
                        if resp, err := sub.NextMsg(time.Second); err != nil {
1✔
287
                                subR.AddError("failed to get response to pull: %v", err)
×
288
                                break
×
289
                        } else if msg := string(resp.Data); msg == "" { // empty response means end
2✔
290
                                break
1✔
291
                        } else if tk := strings.Split(string(resp.Data), "|"); len(tk) != 2 {
1✔
292
                                subR.AddError("pull response bad")
×
293
                                break
×
294
                        } else {
1✔
295
                                p.maybeStoreJWT(ctx, subR, tk[1])
1✔
296
                        }
1✔
UNCOV
297
                }
×
298
                return r, nil
1✔
UNCOV
299
        }
×
UNCOV
300

×
301
        ctx.CurrentCmd().SilenceUsage = true
1✔
302
        if err := p.setupJobs(ctx); err != nil {
1✔
303
                return nil, err
×
304
        }
×
305
        var wg sync.WaitGroup
1✔
306
        wg.Add(len(p.Jobs))
1✔
307
        for _, j := range p.Jobs {
2✔
308
                go func(j *PullJob) {
2✔
309
                        defer wg.Done()
1✔
310
                        j.Run()
1✔
311
                }(j)
1✔
UNCOV
312
        }
×
313
        wg.Wait()
1✔
314

1✔
315
        for _, j := range p.Jobs {
2✔
316
                sub := store.NewReport(store.OK, "pull %q from the account server", j.Name)
1✔
317
                sub.Opt = store.DetailsOnErrorOrWarning
1✔
318
                r.Add(sub)
1✔
319
                if j.PullStatus != nil {
2✔
320
                        sub.Add(store.HoistChildren(j.PullStatus)...)
1✔
321
                }
1✔
322
                if j.Err != nil {
1✔
323
                        sub.AddFromError(j.Err)
×
324
                        continue
×
UNCOV
325
                }
×
326
                if j.PullStatus.OK() {
2✔
327
                        token, _ := j.Token()
1✔
328
                        p.maybeStoreJWT(ctx, sub, token)
1✔
329
                }
1✔
330
        }
331
        return r, nil
1✔
332
}
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