Skip to content

Commit

Permalink
Add templateable variable parsing to launcher command
Browse files Browse the repository at this point in the history
This adds templatable variable parsing to the launcher command.
Variables are defined as key/value pairs in the `tui.Update:` `case
loginMsg:` section.  Any variables defined there will be parsed and
replaced with their values if the variables are used in the user's
`terminal` or `cluster_login_command` config options in the srepd.yaml
file.

Eg: `cluster_login_command: ocm-container --cluster-id %%CLUSTER_ID%%
--launch-opts --env=INCIDENT_ID=%%INCIDENT_ID%%`

Supported variables are currently `%%CLUSTER_ID%%` and `%%INCIDENT_ID%%`

Signed-off-by: Chris Collins <[email protected]>
  • Loading branch information
clcollins committed Aug 8, 2024
1 parent 6cc5f13 commit 5657245
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 20 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,25 @@ cluster_login_command: ocm backplane login %%CLUSTER_ID%%
### Automatic Login Features
The first feature of Automatic Login is the ability to replace certain strings with their cluster-specific details. When you pass `%%VARIABLE%%` in your `terminal` or `cluster_login_command` configuration strings they will dynamically be replaced with the alert-specific variable. This allows you to be able to put the specific details of these variables inside the command. The first argument of the `terminal` setting MUST NOT BE a replaceable value.

Currently, only `%%CLUSTER_ID%%` is supported. It's also important to note that if `%%CLUSTER_ID%%` does NOT appear in the `cluster_login_command` config setting that the cluster ID will be appended to the end of the cluster login command. If the replacable `%%CLUSTER_ID%%` string is present in the `cluster_login_command` setting, it will NOT be appended to the end.
Supported Variables:

* `%%CLUSTER_ID%%` - used to identify the cluster to log into. (SEE NOTE BELOW)
* `%%INCIDENT_ID%%` - the PagerDuty Incident ID from which the cluster details have been taken. You can, for example, use this to pass in compliance reasons, or to set a variable.

Note regarding `%%CLUSTER_ID%%`:

It's also important to note that if `%%CLUSTER_ID%%` does NOT appear in the `cluster_login_command` config setting that the cluster ID will be appended to the end of the cluster login command. If the replaceable `%%CLUSTER_ID%%` string is present in the `cluster_login_command` setting, it will NOT be appended to the end.

Examples:
```
## Assume the cluster ID for these examples is `abcdefg`

cluster_login_command: ocm backplane login %%CLUSTER_ID%% --multi
## effectively runs "ocm backplane login abcdefg --multi"
cluster_login_command: ocm backplane login %%CLUSTER_ID%% --multi

cluster_login_command: ocm backplane login --multi
## effectively runs "ocm backplane login --multi abcdefg"
cluster_login_command: ocm backplane login --multi

## Logs into the cluster and sets the INCIDENT_ID env variable in ocm-container to the PagerDuty Incident ID
cluster_login_command: ocm-container --cluster-id %%CLUSTER_ID --launch-opts --env=INCIDENT_ID=%%INCIDENT_ID%%
```
35 changes: 28 additions & 7 deletions pkg/launcher/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package launcher

import (
"fmt"
"github.com/charmbracelet/log"
"strings"
)

Expand Down Expand Up @@ -59,23 +60,43 @@ func (l *ClusterLauncher) validate() error {
return nil
}

func (l *ClusterLauncher) BuildLoginCommand(cluster string) []string {
func (l *ClusterLauncher) BuildLoginCommand(vars map[string]string) []string {
///func (l *ClusterLauncher) BuildLoginCommand() []string {
command := []string{}

// Handle the Terminal command
// The first arg should not be something replaceable, as checked in the
// validate function
log.Debug("launcher.ClusterLauncher():", "Building command from terminal", "terminal", l.terminal[0])
command = append(command, l.terminal[0])
command = append(command, replaceVars(l.terminal[1:], cluster)...)
command = append(command, replaceVars(l.clusterLoginCommand, cluster)...)

// If there are more than one terminal arguments, replace the vars
// If there's not more than one terminal argument, the "replacement"
// nil []string{} ends up being appended as a whitespace, so don't append
if len(l.terminal) > 1 {
command = append(command, replaceVars(l.terminal[1:], vars)...)
}
command = append(command, replaceVars(l.clusterLoginCommand, vars)...)
log.Debug("launcher.ClusterLauncher():", "Built command", command)
for x, i := range command {
log.Debug("launcher.ClusterLauncher():", fmt.Sprintf("Built command argument [%d]", x), i)
}

return command
}

func replaceVars(args []string, cluster string) []string {
transformedArgs := []string{}
for _, str := range args {
transformedArgs = append(transformedArgs, strings.Replace(str, "%%CLUSTER_ID%%", cluster, -1))
func replaceVars(args []string, vars map[string]string) []string {
if args == nil || vars == nil {
return []string{}
}

str := strings.Join(args, " ")

for k, v := range vars {
log.Debug("ClusterLauncher():", "Replacing vars in string", str, k, v)
str = strings.Replace(str, k, v, -1)
}

transformedArgs := strings.Split(str, " ")
return transformedArgs
}
23 changes: 21 additions & 2 deletions pkg/launcher/launcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestClusterLauncherValidation(t *testing.T) {
expectErr: true,
},
{
name: "Tests terminal is nill but login args are not",
name: "Tests terminal is nil but login args are not",
terminalArg: "",
loginCommandArg: "ocm backplane session",
expectErr: true,
Expand Down Expand Up @@ -92,6 +92,20 @@ func TestLoginCommandBuild(t *testing.T) {
}
},
},
{
name: "validate multiple templated replacements",
launcher: ClusterLauncher{
terminal: []string{"gnome-terminal", "--"},
clusterLoginCommand: []string{"ocm-container", "--cluster-id", "%%CLUSTER_ID%%", "--launch-opts", "\"-e INCIDENT_ID=%%INCIDENT_ID%%\""},
},
expectErr: true,
comparisonFN: func(t *testing.T, cmd []string) {
expected := "gnome-terminal -- ocm-container --cluster-id abcdefg --launch-opts \"-e INCIDENT_ID=PD123456\""
if strings.Join(cmd, " ") != expected {
t.Fatalf("Expected command to be %s, got: %s", expected, strings.Join(cmd, " "))
}
},
},
{
name: "validate that the cluster login command can be collapsed to a single string",
launcher: ClusterLauncher{
Expand Down Expand Up @@ -134,7 +148,12 @@ func TestLoginCommandBuild(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd := test.launcher.BuildLoginCommand("abcdefg")
vars := map[string]string{
"%%CLUSTER_ID%%": "abcdefg",
"%%INCIDENT_ID%%": "PD123456",
"%%UNHANDLED_VAR%%": "I SHOULD NOT SHOW UP",
}
cmd := test.launcher.BuildLoginCommand(vars)
test.comparisonFN(t, cmd)
})
}
Expand Down
24 changes: 17 additions & 7 deletions pkg/tui/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,41 +359,51 @@ type loginFinishedMsg struct {
err error
}

func login(cluster string, launcher launcher.ClusterLauncher) tea.Cmd {
func login(vars map[string]string, launcher launcher.ClusterLauncher) tea.Cmd {
// The first element of Terminal is the command to be executed, followed by args, in order
// This handles if folks use, eg: flatpak run <some package> as a terminal.
command := launcher.BuildLoginCommand(cluster)
command := launcher.BuildLoginCommand(vars)
c := exec.Command(command[0], command[1:]...)

log.Debug(fmt.Sprintf("tui.login(): %v", c.String()))
stderr, pipeErr := c.StderrPipe()
if pipeErr != nil {
log.Debug(fmt.Sprintf("tui.login(): %v", pipeErr.Error()))
log.Debug("tui.login():", "pipeErr", pipeErr.Error())
return func() tea.Msg {
return loginFinishedMsg{err: pipeErr}
}
}

err := c.Start()
if err != nil {
log.Debug(fmt.Sprintf("tui.login(): %v", err.Error()))
log.Debug("tui.login():", "startErr", err.Error())
return func() tea.Msg {
return loginFinishedMsg{err}
}
}

out, err := io.ReadAll(stderr)
if err != nil {
log.Debug(fmt.Sprintf("tui.login(): %v", err.Error()))
log.Debug("tui.login():", "loginStdErr", err.Error())
return func() tea.Msg {
return loginFinishedMsg{err}
}
}

// c.Wait() must be run after reading any output from stderrPipe
err = c.Wait()
if err != nil {
log.Debug("tui.login():", "waitErr", err.Error())
return func() tea.Msg {
return loginFinishedMsg{err}
}
}

if len(out) > 0 {
log.Debug(fmt.Sprintf("tui.login(): error: %s", out))
outErr := fmt.Errorf("%s", out)
log.Debug("tui.login():", "outErr", outErr.Error())
return func() tea.Msg {
return loginFinishedMsg{fmt.Errorf("%s", out)}
return loginFinishedMsg{outErr}
}
}

Expand Down
9 changes: 8 additions & 1 deletion pkg/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.setStatus(fmt.Sprintf("multiple alerts found - logging into cluster %s from first alert %s", cluster, m.selectedIncidentAlerts[0].ID))
}

cmds = append(cmds, login(cluster, m.launcher))
// NOTE: It's important that **ALL** of these variables' values are NOT NIL.
// They can be empty strings, but the must not be nil.
var vars map[string]string = map[string]string{
"%%CLUSTER_ID%%": cluster,
"%%INCIDENT_ID%%": m.selectedIncident.ID,
}

cmds = append(cmds, login(vars, m.launcher))

case loginFinishedMsg:
if msg.err != nil {
Expand Down

0 comments on commit 5657245

Please sign in to comment.