Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new subcommand to dump Agent Policies #862

Merged
merged 28 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4b3a1c6
Rename dump installed objects action
mrodm Jun 21, 2022
e8be6c8
Add new subcommand to dump agent policies
mrodm Jun 22, 2022
ab1f310
Add parameter to get agent policies using some package
mrodm Jun 22, 2022
469fe8c
Rephrase long descriptions
mrodm Jun 22, 2022
9bb50ff
Remove debug prints
mrodm Jun 22, 2022
c33295d
Comment reviews
mrodm Jun 23, 2022
d813be9
Set required package flag just for installed-objects subcommand
mrodm Jun 23, 2022
c175000
Add pagination support to list all agent policies
mrodm Jun 23, 2022
62e51fc
Remove debug statements
mrodm Jun 23, 2022
80a61b0
Changes from review
mrodm Jun 23, 2022
898f15f
Rename dumpInstalledObject to be more generic
mrodm Jun 23, 2022
fb226d9
Reuse agent filtering method to get all policies
mrodm Jun 23, 2022
7e17639
Remove caching for agent policies
mrodm Jun 23, 2022
862883f
Rename Ingest Manager to Fleet
mrodm Jun 27, 2022
d126235
Update dump command descriptions
mrodm Jun 27, 2022
47fcf74
Update README
mrodm Jun 27, 2022
0bbcc90
Merge 'origin/main' branch into dump_agent_policies
mrodm Jun 27, 2022
c9af623
Added tests for agent-policies dump subcommand
mrodm Jun 27, 2022
5aa71f3
Use get public method in client_test
mrodm Jun 27, 2022
3fa942b
Set post,put,delete methods public as Get
mrodm Jun 27, 2022
2184cec
Rename method to list all agent policies
mrodm Jun 27, 2022
58487b5
Move functions to dump a generic json resource to its own file
mrodm Jun 28, 2022
3334135
Replace question mark char in files too
mrodm Jun 28, 2022
d872ec2
Fix default path url name
mrodm Jun 28, 2022
ac2b8ca
Use formatter package to dump JSONs - rewritten all dumped files
mrodm Jun 28, 2022
7251ad6
Set again get,post,put,delete methods as private
mrodm Jun 28, 2022
cc1a245
Fix
mrodm Jun 28, 2022
eb1fe46
Revert "Use formatter package to dump JSONs - rewritten all dumped fi…
mrodm Jun 28, 2022
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
73 changes: 70 additions & 3 deletions cmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/elastic/elastic-package/internal/cobraext"
"github.com/elastic/elastic-package/internal/dump"
"github.com/elastic/elastic-package/internal/elasticsearch"
"github.com/elastic/elastic-package/internal/kibana"
)

const dumpLongDescription = `Use this command as a exploratory tool to dump assets relevant for the package.`
Expand All @@ -19,30 +20,43 @@ const dumpInstalledObjectsLongDescription = `Use this command to dump objects in

Use this command as a exploratory tool to dump objects as they are installed by Fleet when installing a package. Dumped objects are stored in files as they are returned by APIs of the stack, without any processing.`

const dumpAgentPoliciesLongDescription = `Use this command to dump agent policies created by Fleet as part of a package installation.

Use this command as a exploratory tool to dump agent policies as they are created by Fleet when installing a package. Dumped agent policies are stored in files as they are returned by APIs of the stack, without any processing.`
Copy link
Member

Choose a reason for hiding this comment

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

Mention somewhere that if no flag is provided, all policies are dumped.


func setupDumpCommand() *cobraext.Command {
dumpInstalledObjectsCmd := &cobra.Command{
Use: "installed-objects",
Short: "Dump objects installed in the stack",
Long: dumpInstalledObjectsLongDescription,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Maybe we can add more guidance to the dump description. agent-policies would be another supported feature :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I've updated the dump description as:

const dumpLongDescription = `Use this command as an exploratory tool to dump resources from Elastic Stack (objects installed as part of package and agent policies).`

Would you add anything else here?

RunE: dumpInstalledObjectsCmd,
RunE: dumpInstalledObjectsCmdAction,
}
dumpInstalledObjectsCmd.Flags().Bool(cobraext.TLSSkipVerifyFlagName, false, cobraext.TLSSkipVerifyFlagDescription)

dumpAgentPoliciesCmd := &cobra.Command{
Use: "agent-policies",
Short: "Dump agent policies defined in the stack",
Long: dumpAgentPoliciesLongDescription,
RunE: dumpAgentPoliciesCmdAction,
}
dumpAgentPoliciesCmd.Flags().StringP(cobraext.AgentPolicyFlagName, "", "", cobraext.AgentPolicyDescription)

cmd := &cobra.Command{
Use: "dump",
Short: "Dump package assets",
Long: dumpLongDescription,
}
cmd.PersistentFlags().StringP(cobraext.PackageFlagName, cobraext.PackageFlagShorthand, "", cobraext.PackageFlagDescription)
cmd.MarkFlagRequired(cobraext.PackageFlagName)
cmd.MarkFlagRequired(cobraext.PackageFlagName) // TODO: required for dumping agent policies?
cmd.PersistentFlags().StringP(cobraext.DumpOutputFlagName, "o", "package-dump", cobraext.DumpOutputFlagDescription)

cmd.AddCommand(dumpInstalledObjectsCmd)
cmd.AddCommand(dumpAgentPoliciesCmd)

return cobraext.NewCommand(cmd, cobraext.ContextGlobal)
}

func dumpInstalledObjectsCmd(cmd *cobra.Command, args []string) error {
func dumpInstalledObjectsCmdAction(cmd *cobra.Command, args []string) error {
packageName, err := cmd.Flags().GetString(cobraext.PackageFlagName)
if err != nil {
return cobraext.FlagParsingError(err, cobraext.PackageFlagName)
Expand Down Expand Up @@ -76,3 +90,56 @@ func dumpInstalledObjectsCmd(cmd *cobra.Command, args []string) error {
cmd.Printf("Dumped %d installed objects for package %s to %s\n", n, packageName, outputPath)
return nil
}

func dumpAgentPoliciesCmdAction(cmd *cobra.Command, args []string) error {
packageName, err := cmd.Flags().GetString(cobraext.PackageFlagName)
if err != nil {
return cobraext.FlagParsingError(err, cobraext.PackageFlagName)
}

agentPolicy, err := cmd.Flags().GetString(cobraext.AgentPolicyFlagName)
if err != nil {
return cobraext.FlagParsingError(err, cobraext.AgentPolicyFlagName)
}

outputPath, err := cmd.Flags().GetString(cobraext.DumpOutputFlagName)
if err != nil {
return cobraext.FlagParsingError(err, cobraext.DumpOutputFlagName)
}

tlsSkipVerify, _ := cmd.Flags().GetBool(cobraext.TLSSkipVerifyFlagName)
Copy link
Contributor

Choose a reason for hiding this comment

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

side question to @jsoriano:

With your change enforcing SSL mode, will such options (like tlsSkipVerify) work? I'm thinking about the use case when a developer wants to dump agent policies from some random Kibana, not booted with elastic-package.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this flag should continue working after the changes in #847.


var clientOptions []kibana.ClientOption
if tlsSkipVerify {
clientOptions = append(clientOptions, kibana.TLSSkipVerify())
}
kibanaClient, err := kibana.NewClient(clientOptions...)
if err != nil {
return errors.Wrap(err, "failed to initialize Kibana client")
}

switch {
case agentPolicy != "":
jsoriano marked this conversation as resolved.
Show resolved Hide resolved
dumper := dump.NewAgentPolicyDumper(kibanaClient, agentPolicy)
err = dumper.DumpAgentPolicy(cmd.Context(), outputPath)
if err != nil {
return errors.Wrap(err, "dump failed")
}
cmd.Printf("Dumped agent policy %s to %s\n", agentPolicy, outputPath)
case packageName != "":
dumper := dump.NewAgentPoliciesDumper(kibanaClient)
count, err := dumper.DumpAgentPoliciesFileteredByPackage(cmd.Context(), packageName, outputPath)
if err != nil {
return errors.Wrap(err, "dump failed")
}
cmd.Printf("Dumped %d agent policies filtering by package name %s to %s\n", count, packageName, outputPath)
Copy link
Member

Choose a reason for hiding this comment

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

Nit. Log a different message if no policies are dumped. It prints now:

Dumped 0 agent policies filtering by package name apache to package-dump

default:
dumper := dump.NewAgentPoliciesDumper(kibanaClient)
count, err := dumper.DumpAll(cmd.Context(), outputPath)
if err != nil {
return errors.Wrap(err, "dump failed")
}
cmd.Printf("Dumped %d agent policies to %s\n", count, outputPath)
}
return nil
}
3 changes: 3 additions & 0 deletions internal/cobraext/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const (

// Flag names and descriptions used by CLI commands
const (
AgentPolicyFlagName = "agent-policy"
AgentPolicyDescription = "name of the agent policy"

BuildZipFlagName = "zip"
BuildZipFlagDescription = "archive the built package"

Expand Down
187 changes: 187 additions & 0 deletions internal/dump/agentpolicies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package dump

import (
"context"
"encoding/json"
"fmt"
"path/filepath"

"github.com/elastic/elastic-package/internal/common"
"github.com/elastic/elastic-package/internal/kibana"
)

const AgentPoliciesDumpDir = "agent_policies"

type AgentPolicyDumper struct {
name string
client *kibana.Client

policy *AgentPolicy
}

type AgentPoliciesDumper struct {
jsoriano marked this conversation as resolved.
Show resolved Hide resolved
client *kibana.Client

policies []AgentPolicy
jsoriano marked this conversation as resolved.
Show resolved Hide resolved
}

type AgentPolicy struct {
name string
raw json.RawMessage
}

func (p AgentPolicy) Name() string {
return p.name
}

func (p AgentPolicy) JSON() []byte {
return p.raw
}

func NewAgentPolicyDumper(client *kibana.Client, agentPolicy string) *AgentPolicyDumper {
return &AgentPolicyDumper{
name: agentPolicy,
client: client,
}
}

func NewAgentPoliciesDumper(client *kibana.Client) *AgentPoliciesDumper {
return &AgentPoliciesDumper{
client: client,
}
}

func (d *AgentPolicyDumper) getAgentPolicy(ctx context.Context) (*AgentPolicy, error) {
if d.policy == nil {
policy, err := d.client.GetRawPolicy(d.name)

if err != nil {
return nil, err
}
agentPolicy := AgentPolicy{name: d.name, raw: policy}
d.policy = &agentPolicy
}
return d.policy, nil
}

func (d *AgentPolicyDumper) DumpAgentPolicy(ctx context.Context, dir string) error {
agentPolicy, err := d.getAgentPolicy(ctx)
if err != nil {
return fmt.Errorf("failed to get agent policy: %w", err)
}

dir = filepath.Join(dir, AgentPoliciesDumpDir)
err = dumpInstalledObject(dir, agentPolicy)
if err != nil {
return fmt.Errorf("failed to dump agent policy %s: %w", agentPolicy.Name(), err)
}
return nil
}

func (d *AgentPoliciesDumper) getAllAgentPolicies(ctx context.Context) ([]AgentPolicy, error) {
if len(d.policies) == 0 {
policies, err := d.client.ListRawPolicy()

if err != nil {
return nil, err
}

var policyName struct {
ID string `json:"id"`
}

for _, policy := range policies {
err = json.Unmarshal(policy, &policyName)
if err != nil {
return nil, fmt.Errorf("failed to get Agent Policy ID: %w", err)
}
agentPolicy := AgentPolicy{name: policyName.ID, raw: policy}
d.policies = append(d.policies, agentPolicy)
}
}
return d.policies, nil
}

type packagePolicy struct {
ID string `json:"id"`
Name string `json:"name"`
Package struct {
Name string `json:"name"`
Title string `json:"title"`
Version string `json:"version"`
} `json:"package"`
}

func getPackagesUsingAgentPolicy(packagePolicies []packagePolicy) []string {
var packageNames []string
for _, packagePolicy := range packagePolicies {
packageNames = append(packageNames, packagePolicy.Package.Name)
}
return packageNames
}

func (d *AgentPoliciesDumper) getAgentPoliciesFilteredByPackage(ctx context.Context, packageName string) ([]AgentPolicy, error) {
if len(d.policies) == 0 {
policies, err := d.client.ListRawPolicy()

if err != nil {
return nil, err
}

var policyPackages struct {
ID string `json:"id"`
PackagePolicies []packagePolicy `json:"package_policies"`
}

for _, policy := range policies {
err = json.Unmarshal(policy, &policyPackages)
if err != nil {
return nil, fmt.Errorf("failed to get Agent Policy ID: %w", err)
}
packageNames := getPackagesUsingAgentPolicy(policyPackages.PackagePolicies)
if !common.StringSliceContains(packageNames, packageName) {
continue
}
jsoriano marked this conversation as resolved.
Show resolved Hide resolved

agentPolicy := AgentPolicy{name: policyPackages.ID, raw: policy}
d.policies = append(d.policies, agentPolicy)
}
}
return d.policies, nil
}

func (d *AgentPoliciesDumper) DumpAll(ctx context.Context, dir string) (count int, err error) {
agentPolicies, err := d.getAllAgentPolicies(ctx)
if err != nil {
return 0, fmt.Errorf("failed to get agent policy: %w", err)
}

dir = filepath.Join(dir, AgentPoliciesDumpDir)
for _, agentPolicy := range agentPolicies {
err := dumpInstalledObject(dir, agentPolicy)
Copy link
Member

Choose a reason for hiding this comment

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

Nit. Maybe we should rename dumpInstalledObject now 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've just renamed it to dumpJSONResource.
Do you think it is worthy to move it to different file in the same package dump? @jsoriano

Something like internal/dump/utils.go ? I would move that function and formatJSON

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this could be moved to some common file, maybe called json.go, as you prefer.

if err != nil {
return 0, fmt.Errorf("failed to dump agent policy %s: %w", agentPolicy.Name(), err)
}
}
return len(agentPolicies), nil
}

func (d *AgentPoliciesDumper) DumpAgentPoliciesFileteredByPackage(ctx context.Context, packageName, dir string) (count int, err error) {
agentPolicies, err := d.getAgentPoliciesFilteredByPackage(ctx, packageName)
if err != nil {
return 0, fmt.Errorf("failed to get agent policy: %w", err)
}

dir = filepath.Join(dir, AgentPoliciesDumpDir)
for _, agentPolicy := range agentPolicies {
err := dumpInstalledObject(dir, agentPolicy)
if err != nil {
return 0, fmt.Errorf("failed to dump agent policy %s: %w", agentPolicy.Name(), err)
}
}
return len(agentPolicies), nil
}
44 changes: 44 additions & 0 deletions internal/kibana/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,50 @@ func (c *Client) GetPolicy(policyID string) (*Policy, error) {
return &resp.Item, nil
}

// GetRawPolicy fetches the given Policy with all the fields in the Ingest Manager.
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, now the Ingest Manager is called the Fleet :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updating comments here. I was not totally sure about whether or not they were referring to the same.
Thanks!

func (c *Client) GetRawPolicy(policyID string) (json.RawMessage, error) {
statusCode, respBody, err := c.get(fmt.Sprintf("%s/agent_policies/%s", FleetAPI, policyID))
if err != nil {
return nil, errors.Wrap(err, "could not get policy")
}

if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get policy; API status code = %d; response body = %s", statusCode, respBody)
}

var resp struct {
Item json.RawMessage `json:"item"`
}

if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, errors.Wrap(err, "could not convert policy (response) to JSON")
}

return resp.Item, nil
}

// ListRawPolicy fetches all the Policies in the Ingest Manager.
func (c *Client) ListRawPolicy() ([]json.RawMessage, error) {
statusCode, respBody, err := c.get(fmt.Sprintf("%s/agent_policies?full=true", FleetAPI))
if err != nil {
return nil, errors.Wrap(err, "could not get policy")
}
mrodm marked this conversation as resolved.
Show resolved Hide resolved

if statusCode != http.StatusOK {
return nil, fmt.Errorf("could not get policy; API status code = %d; response body = %s", statusCode, respBody)
}

var resp struct {
Items []json.RawMessage `json:"items"`
}

if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, errors.Wrap(err, "could not convert policy (response) to JSON")
}

return resp.Items, nil
}

// DeletePolicy removes the given Policy from the Ingest Manager.
func (c *Client) DeletePolicy(p Policy) error {
reqBody := `{ "agentPolicyId": "` + p.ID + `" }`
Expand Down