Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions apps/cinc/cmd/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,13 @@ Bootstrap a host managed by a Policyfile policy group.
cinc node bootstrap web01.example.com --ssh-user ubuntu --policy-name base --policy-group prod`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// Resolved up front, before anything with a side effect: bootstrap
// creates a client on the server and installs software on the
// target, and a rejected --format should not cost the user either.
format, err := resolveFormat(cmd)
if err != nil {
return err
}
target := argAt(args, 0)
if err := promptNodeBootstrap(cmd, &target, &flags); err != nil {
return err
Expand Down Expand Up @@ -334,10 +341,6 @@ cinc node bootstrap web01.example.com --ssh-user ubuntu --policy-name base --pol
if result.ExitCode != 0 {
return fmt.Errorf("bootstrap failed on %s: %s; client %q was created and may need cleanup before retry", target, firstNonEmpty(result.Error, result.Stderr), flags.nodeName)
}
format, err := resolveFormat(cmd)
if err != nil {
return err
}
if format == printer.FormatJSON {
return printer.New(cmd.OutOrStdout(), format).Value(result)
}
Expand Down
44 changes: 44 additions & 0 deletions apps/cinc/cmd/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,50 @@ client_key = %q
}
}

// TestNodeBootstrapRejectsBadFormatBeforeDoingWork pins flag validation ahead
// of side effects. Bootstrap creates a client on the server and installs
// software on the target, so a rejected --format must cost the user neither.
// Every other command resolves the format first; this one used to resolve it
// after the SSH run.
func TestNodeBootstrapRejectsBadFormatBeforeDoingWork(t *testing.T) {
runner := &recordingRunner{result: remote.CommandResult{Stdout: "bootstrap ok\n"}}
prev := nodeRemoteRunner
nodeRemoteRunner = runner
t.Cleanup(func() { nodeRemoteRunner = prev })

clientCreated := false
mux := http.NewServeMux()
mux.HandleFunc("/organizations/acme/clients", func(w http.ResponseWriter, r *http.Request) {
clientCreated = true
_, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"name":"web01","validator":false}`)
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
cfgPath := writeCommandConfig(t, srv.URL)

root := newRootCmd()
root.SetOut(new(bytes.Buffer))
root.SetErr(new(bytes.Buffer))
root.SetArgs([]string{
"node", "bootstrap", "web01.example.test",
"--node-name", "web01", "--ssh-user", "ubuntu",
"--config", cfgPath, "--no-host-key-verify",
"--format", "yaml",
})

if err := root.Execute(); err == nil {
t.Fatal("expected an error for an unknown --format")
}
if clientCreated {
t.Error("a client was created on the server before --format was validated")
}
if len(runner.calls) != 0 {
t.Errorf("bootstrap ran over SSH before --format was validated: %+v", runner.calls)
}
}

func TestNodeBootstrapDryRunCommand(t *testing.T) {
cfgPath := writeCommandConfig(t, "https://cinc.example.test")
root := newRootCmd()
Expand Down