Skip to content
This repository has been archived by the owner on Jul 10, 2024. It is now read-only.

[OBS-405]Add setup command for EC2 installation #242

Merged
merged 13 commits into from
Oct 19, 2023
35 changes: 35 additions & 0 deletions cmd/internal/ec2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
### Amazon EC2/ Linux Server

### Introduction

- The Postman Live Collection Agent (LCA) runs as a systemd service on your server
- The Postman collection is populated with endpoints observed from the traffic arriving at your service.

### Prerequisites[WIP]

- Your server's OS supports `systemd`
- `root` user

### Usage

```
POSTMAN_API_KEY=<postman-api-key> postman-lc-agent setup --collection <postman-collectionID>
```

To check the status or logs please use
```
journald <add-comand-here>
```

#### Why is root required ?[WIP]

- To edit systemd files
- To Enable capabilities


### Uninstall

- You can disable the systemd service using `systemctl disable <add-command-here>`

### Systemd Configuration
- We write files at `<add-location-here>`
174 changes: 174 additions & 0 deletions cmd/internal/ec2/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package ec2

import (
"embed"
"os"
"os/exec"
"os/user"
"strings"
"text/template"

"github.com/akitasoftware/akita-cli/printer"
"github.com/akitasoftware/akita-cli/telemetry"
"github.com/pkg/errors"
)

const (
envFileName = "postman-lc-agent"
envFileTemplateName = "postman-lc-agent.tmpl"
envFileBasePath = "/etc/default/"
envFilePath = envFileBasePath + envFileName

serviceFileName = "postman-lc-agent.service"
serviceFileBasePath = "/usr/lib/systemd/system/"
serviceFilePath = serviceFileBasePath + serviceFileName
)

// Embed files inside the binary. Requires Go >=1.16

//go:embed postman-lc-agent.service
var serviceFile string

// FS is used for easier template parsing

//go:embed postman-lc-agent.tmpl
var envFileFS embed.FS

// Helper function for reporting telemetry
func reportStep(stepName string) {
telemetry.WorkflowStep("Starting systemd conguration", stepName)
}

func setupAgentForServer(collectionId string) error {

err := checkUserPermissions()
if err != nil {
return err
}
err = checkSystemdExists()
if err != nil {
return err
}

err = configureSystemdFiles(collectionId)
if err != nil {
return err
}
return nil
}

func checkUserPermissions() error {
// TODO: Make this work without root

// Exact permissions required are
// read/write permissions on /etc/default/postman-lc-agent
// read/write permission on /usr/lib/system/systemd
// enable, daemon-reload, start, stop permission for systemctl

printer.Infof("Checking user permissions \n")
cu, err := user.Current()
if err != nil {
return errors.Errorf("could not get current user. OS returned error: %q \n", err)
}
if strings.EqualFold(cu.Name, "root") {
return errors.Errorf("Please run the command again with user: root. \n")
}
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

func checkSystemdExists() error {
message := "Checking if systemd exists\n"
printer.Infof(message)
reportStep(message)
gmann42 marked this conversation as resolved.
Show resolved Hide resolved

// _, serr := exec.LookPath("systemd")
// if serr != nil {
// return errors.Errorf("Could not find systemd binary in your OS.\n We don't have support for non-systemd OS as of now; For more information please contact [email protected].\n")
// }
return nil
}

func configureSystemdFiles(collectionId string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages here are all minimally informational to the end user. What are they supposed to do next?

Probably, contact the support email for most of them, but are there any that can be corrected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't think user can do anything. because lack of systemd is quite unfixable.

message := "Configuring systemd files\n"
printer.Infof(message)
reportStep(message)

// Write collectionId and postman-api-key to go template file

tmpl, err := template.ParseFS(envFileFS, envFileTemplateName)
if err != nil {
return errors.Errorf("failed to parse systemd env template file with error :%q\n Please contact [email protected] with this log for further assistance.\n", err)
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
}

data := struct {
PostmanAPIKey string
CollectionId string
}{
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
PostmanAPIKey: os.Getenv("POSTMAN_API_KEY"),
CollectionId: collectionId,
}
envFile, err := os.Create("env-postman-lc-agent")
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return errors.Errorf("Failed to write values to env file with error %q", err)
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
}

err = tmpl.Execute(envFile, data)
if err != nil {
return errors.Errorf("Failed to write values to env file with error %q", err)
}

// Ensure /etc/default exists
cmd := exec.Command("mkdir", []string{"-p", envFileBasePath}...)
_, err = cmd.CombinedOutput()
if err != nil {
return errors.Errorf("failed to find %s directory with err %q \n", envFileBasePath, err)
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
}

// move the file to /etc/default
cmd = exec.Command("mv", []string{"env-postman-lc-agent", envFilePath}...)
_, err = cmd.CombinedOutput()
if err != nil {
return errors.Errorf("failed to create postman-lc-agent env file in /etc/default directory with err %q \n", err)
}

// Ensure /usr/lib/systemd/system exists
cmd = exec.Command("mkdir", []string{"-p", serviceFileBasePath}...)
_, err = cmd.CombinedOutput()
if err != nil {
return errors.Errorf("failed to find %s directory with err %q \n", serviceFileBasePath, err)
}

err = os.WriteFile(serviceFilePath, []byte(serviceFile), 0600)
if err != nil {
return errors.Errorf("failed to create %s file in %s directory with err %q \n", serviceFileName, serviceFilePath, err)
}

return enablePostmanAgent()
}

// Starts the postman LCA agent as a systemd service
func enablePostmanAgent() error {
reportStep("Enabling postman-lc-agent as a service")

cmd := exec.Command("systemctl", []string{"daemon-reload"}...)
_, err := cmd.CombinedOutput()
if err != nil {
return errors.Errorf("failed to run systemctl daeomon-reload %q \n", err)
}
// systemctl start postman-lc-service
cmd = exec.Command("systemctl", []string{"start", serviceFileName}...)
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
_, err = cmd.CombinedOutput()
if err != nil {
return errors.Errorf("failed to run systemctl daeomon-reload %q \n", err)
}

return nil
}

// Run post-checks
func postChecks() error {
reportStep("EC2:Running post checks")

// TODO: How to Verify if traffic is being captured ?
return nil
}
76 changes: 76 additions & 0 deletions cmd/internal/ec2/ec2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package ec2

import (
"fmt"

"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
"github.com/akitasoftware/akita-cli/rest"
"github.com/akitasoftware/akita-cli/telemetry"
"github.com/akitasoftware/akita-cli/util"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

var (
// Mandatory flag: Postman collection id
collectionId string

// Print out the steps that would be taken, but do not do them
dryRunFlag bool
)

var Cmd = &cobra.Command{
Use: "setup",
Short: "Add the Postman Live Collections Agent to the current server.",
Long: "The CLI will add the Postman Live Collections Agent as a systemd service to your current server.",
SilenceUsage: true,
RunE: addAgentToEC2,
}

var RemoveFromEC2Cmd = &cobra.Command{
Use: "remove",
Short: "Remove the Postman Live Collections Agent from EC2.",
Long: "Remove a previously installed Postman agent from an EC2 server.",
SilenceUsage: true,
RunE: removeAgentFromEC2,

// Temporarily hide from users until complete
Hidden: true,
}

func init() {
Cmd.PersistentFlags().StringVar(&collectionId, "collection", "", "Your Postman collection ID")
Cmd.MarkPersistentFlagRequired("collection")
Cmd.PersistentFlags().BoolVar(
&dryRunFlag,
"dry-run",
false,
"Perform a dry run: show what will be done, but do not modify systemd services.",
)
gmann42 marked this conversation as resolved.
Show resolved Hide resolved

Cmd.AddCommand(RemoveFromEC2Cmd)
}

func addAgentToEC2(cmd *cobra.Command, args []string) error {
// Check for API key
_, err := cmderr.RequirePostmanAPICredentials("The Postman Live Collections Agent must have an API key in order to capture traces.")
if err != nil {
return err
}

// Check collecton Id's existence
if collectionId == "" {
return errors.New("Must specify the ID of your collection with the --collection flag.")
}
frontClient := rest.NewFrontClient(rest.Domain, telemetry.GetClientID())
_, err = util.GetOrCreateServiceIDByPostmanCollectionID(frontClient, collectionId)
if err != nil {
return err
}

return setupAgentForServer(collectionId )
}

func removeAgentFromEC2(cmd *cobra.Command, args []string) error {
return fmt.Errorf("this command is not yet implemented")
}
11 changes: 11 additions & 0 deletions cmd/internal/ec2/postman-lc-agent.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Unit]
Description=Postman Live Collections Agent
Wants=network-online.target
After=network-online.target NetworkManager.service systemd-resolved.service

[Service]
EnvironmentFile=/etc/default/postman-lc-agent
ExecStart=/usr/bin/postman-lc-agent apidump --collection "${COLLECTION_ID}" --interfaces "${INTERFACES}" --filter "${FILTER}"

[Install]
WantedBy=multi-user.target
31 changes: 31 additions & 0 deletions cmd/internal/ec2/postman-lc-agent.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Add your Postman API key below. For example:
#
# POSTMAN_API_KEY=PMAC-XXXXXXX
#
# This is required.

POSTMAN_API_KEY={{.PostmanAPIKey}}


# Add your your Postman Live Collection ID.
#This is required.

COLLECTION_ID={{.CollectionId}}

# For example,
# COLLECTION_ID=1234567-890abcde-f123-4567-890a-bcdef1234567


# INTERFACES is optional. If left blank, the agent will listen on all available
# network interfaces.
#
# FILTER is optional. If left blank, no packet-capture filter will be applied.

INTERFACES=
FILTER=

# For example
# INTERFACES=lo,eth0,eth1
# FILTER="port 80 or port 8080"
#
#
19 changes: 19 additions & 0 deletions cmd/internal/ec2/usage_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ec2
gmann42 marked this conversation as resolved.
Show resolved Hide resolved

import "fmt"

type UsageError struct {
err error
}

func (ue UsageError) Error() string {
return ue.err.Error()
}

func NewUsageError(err error) error {
return UsageError{err}
}

func UsageErrorf(f string, args ...interface{}) error {
return NewUsageError(fmt.Errorf(f, args...))
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/akitasoftware/akita-cli/cmd/internal/ci_guard"
"github.com/akitasoftware/akita-cli/cmd/internal/cmderr"
"github.com/akitasoftware/akita-cli/cmd/internal/daemon"
"github.com/akitasoftware/akita-cli/cmd/internal/ec2"
"github.com/akitasoftware/akita-cli/cmd/internal/ecs"
"github.com/akitasoftware/akita-cli/cmd/internal/get"
"github.com/akitasoftware/akita-cli/cmd/internal/kube"
Expand Down Expand Up @@ -288,6 +289,7 @@ func init() {
rootCmd.AddCommand(ecs.Cmd)
rootCmd.AddCommand(nginx.Cmd)
rootCmd.AddCommand(kube.Cmd)
rootCmd.AddCommand(ec2.Cmd)

// Legacy commands, included for backward compatibility but are hidden.
legacy.SessionsCmd.Hidden = true
Expand Down