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
36 changes: 36 additions & 0 deletions cmd/internal/ec2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
### 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

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

### Usage

- Log in as root user, or use `sudo su` to enable root before running the below command
```
POSTMAN_API_KEY=<postman-api-key> postman-lc-agent setup --collection <postman-collectionID>
```

To check the status or logs please use

```
journalctl -fu postman-lc-agent
```

#### Why is root required?

- To enable and configure the agent as a systemd services
- Env Configuration file location `/etc/default/postman-lc-agent`
- Systemd service file location `/usr/lib/systemd/system/postman-lc-agent.service`

### Uninstall

- You can disable the systemd service using

`sudo systemctl disable --now postman-lc-agent`
183 changes: 183 additions & 0 deletions cmd/internal/ec2/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
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
}

err = enablePostmanAgent()
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.Wrapf(err, "could not get current user\n")
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
}
if !strings.EqualFold(cu.Name, "root") {
printer.Errorf("root user is required to setup systemd service and edit related files")
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
return errors.Errorf("Please run the command again with root user")
}
return nil
}

func checkSystemdExists() error {
message := "Checking if systemd exists"
printer.Infof(message + "\n")
reportStep(message)

_, serr := exec.LookPath("systemctl")
if serr != nil {
printer.Errorf("We don't have support for non-systemd OS as of now.\n For more information please contact [email protected].\n")
return errors.Errorf("Could not find systemd binary in your OS.")
}
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"
printer.Infof(message + "\n")
reportStep(message)

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

tmpl, err := template.ParseFS(envFileFS, envFileTemplateName)
if err != nil {
return errors.Wrapf(err, "systemd env file parsing failed")
}

data := struct {
PostmanAPIKey string
CollectionId string
}{
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
PostmanAPIKey: os.Getenv("POSTMAN_API_KEY"),
CollectionId: collectionId,
}

// Ensure /etc/default exists
cmd := exec.Command("mkdir", []string{"-p", envFileBasePath}...)
_, err = cmd.CombinedOutput()
if err != nil {
return errors.Wrapf(err, "failed to create %s directory\n", envFileBasePath)
}

envFile, err := os.Create(envFilePath)
if err != nil {
printer.Errorf("Failed to create systemd env file")
return err
}

err = tmpl.Execute(envFile, data)
if err != nil {
printer.Errorf("Failed to write values to systemd env file")
return err
}

// Ensure /usr/lib/systemd/system exists
cmd = exec.Command("mkdir", []string{"-p", serviceFileBasePath}...)
_, err = cmd.CombinedOutput()
if err != nil {
return errors.Wrapf(err, "failed to create %s directory\n", serviceFileBasePath)
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
}

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

return nil
}

// Starts the postman LCA agent as a systemd service
func enablePostmanAgent() error {
message := "Enabling postman-lc-agent as a service"
reportStep(message)
printer.Infof(message + "\n")

cmd := exec.Command("systemctl", []string{"daemon-reload"}...)
_, err := cmd.CombinedOutput()
if err != nil {
return err
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
}
// systemctl start postman-lc-service
cmd = exec.Command("systemctl", []string{"enable", "--now", serviceFileName}...)
_, err = cmd.CombinedOutput()
if err != nil {
return err
gmann42 marked this conversation as resolved.
Show resolved Hide resolved
}
printer.Infof("Postman LC Agent enabled as a systemd service. Please check logs using the below command \n")
printer.Infof("journalctl -fu postman-lc-agent \n")

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