Skip to content
Open
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
5 changes: 3 additions & 2 deletions docs/howto/script_testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ a stack, starting agents and services and validating results.
- `get_policy [-profile <profile>] [-timeout <duration>] <policy_name>`: print the details for a policy

- agent commands:
- `install_agent [-profile <profile>] [-timeout <duration>] [<network_name_label>]`: install an Elastic Agent policy, setting the environment variable named in the positional argument
- `install_agent [-profile <profile>] [-timeout <duration>] [-container_name <container_name_label>] [-network_name <network_name_label>]`: install an Elastic Agent policy, setting the environment variables named in the container_name and network_name arguments
- `uninstall_agent [-profile <profile>] [-timeout <duration>]`: remove an installed Elastic Agent policy
- `compile_registry_state [-start <first_id_to_use>] <path_to_registry_log>`: compile a Filebeat registry log.json file into a registry state and print it to stdout

- package commands:
- `add_package [-profile <profile>] [-timeout <duration>]`: add the current package's assets
Expand Down Expand Up @@ -110,7 +111,7 @@ As an example, a basic system test could be expressed as follows.
use_stack -profile ${CONFIG_PROFILES}/${PROFILE}

# Install an agent.
install_agent -profile ${CONFIG_PROFILES}/${PROFILE} NETWORK_NAME
install_agent -profile ${CONFIG_PROFILES}/${PROFILE} -network_name NETWORK_NAME

# Bring up a docker container.
#
Expand Down
7 changes: 6 additions & 1 deletion internal/agentdeployer/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (d *DockerComposeAgentDeployer) SetUp(ctx context.Context, agentInfo AgentI
return nil, fmt.Errorf("could not create resources for custom agent: %w", err)
}

composeProjectName := fmt.Sprintf("elastic-package-agent-%s-%s", d.agentName(), agentInfo.Test.RunID)
composeProjectName := d.ProjectName(agentInfo.Test.RunID)

agent := dockerComposeDeployedAgent{
ymlPaths: []string{filepath.Join(configDir, dockerTestAgentDockerCompose)},
Expand Down Expand Up @@ -228,6 +228,11 @@ func (d *DockerComposeAgentDeployer) SetUp(ctx context.Context, agentInfo AgentI
return &agent, nil
}

// ProjectName returns the Docker Compose project name for the agent.
func (d *DockerComposeAgentDeployer) ProjectName(runID string) string {
return fmt.Sprintf("elastic-package-agent-%s-%s", d.agentName(), runID)
}
Comment on lines +231 to +234
Copy link
Contributor

Choose a reason for hiding this comment

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

Just re-reviewing this PR, I realised that DockerComposeAgentDeployer implements the interface AgentDeployer.

type AgentDeployer interface {

And this ProjectName method is not there.

I think it would be better to add this method to the interface too.

That would enforce to add this method also to KubernetesAgentDeployer, but I'm not sure that makes sense there, because in that case the agent runs directly in k8s and not in a docker-compose scenario.

func (ksd *KubernetesAgentDeployer) SetUp(ctx context.Context, agentInfo AgentInfo) (DeployedAgent, error) {

@efd6 @elastic/ecosystem WDYT about this ?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be better to add this method to the interface too.

I guess it is not an issue to keep that method as a public one but not being part of the AgentDeployer interface. Specially, because this new method does not make sense in KubernetesAgentDeployer as mentioned in:

That would enforce to add this method also to KubernetesAgentDeployer, but I'm not sure that makes sense there, because in that case the agent runs directly in k8s and not in a docker-compose scenario.


func (d *DockerComposeAgentDeployer) agentHostname() string {
return fmt.Sprintf("%s-%s", dockerTestAgentServiceName, d.agentRunID)
}
Expand Down
102 changes: 93 additions & 9 deletions internal/testrunner/script/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package script

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"time"

"github.com/rogpeppe/go-internal/testscript"
Expand Down Expand Up @@ -47,14 +50,11 @@ func installAgent(ts *testscript.TestScript, neg bool, args []string) {
flg := flag.NewFlagSet("install", flag.ContinueOnError)
profName := flg.String("profile", "default", "profile name")
timeout := flg.Duration("timeout", 0, "timeout (zero or lower indicates no timeout)")
containerNameLabel := flg.String("container_name", "", "environment variable name to place container name in")
networkNameLabel := flg.String("network_name", "", "environment variable name to place network name in")
ts.Check(flg.Parse(args))
if flg.NArg() != 0 && flg.NArg() != 1 {
ts.Fatalf("usage: install_agent [-profile <profile>] [-timeout <duration>] [<network_name_label>]")
}

var networkNameLabel string
if flg.NArg() == 1 {
networkNameLabel = flg.Arg(0)
if flg.NArg() != 0 {
ts.Fatalf("usage: install_agent [-profile <profile>] [-timeout <duration>] [-container_name <container_name_label>] [-network_name <network_name_label>]")
}

stk, ok := stacks[*profName]
Expand Down Expand Up @@ -120,8 +120,12 @@ func installAgent(ts *testscript.TestScript, neg bool, args []string) {
// ELASTIC_PACKAGE_CA_CERT is set. ¯\_(ツ)_/¯
installed.deployed, err = dep.SetUp(ctx, info)
ts.Check(decoratedWith("setting up agent", err))
if networkNameLabel != "" {
ts.Setenv(networkNameLabel, installed.deployed.Info().NetworkName)
depInfo := installed.deployed.Info()
if *networkNameLabel != "" {
ts.Setenv(*networkNameLabel, depInfo.NetworkName)
}
if *containerNameLabel != "" {
ts.Setenv(*containerNameLabel, fmt.Sprintf("%s-%s-1", dep.ProjectName(info.Test.RunID), depInfo.Name))
}
polID := installed.deployed.Info().Policy.ID
ts.Check(decoratedWith("getting kibana agent", doKibanaAgent(ctx, stk.kibana, func(a kibana.Agent) (bool, error) {
Expand Down Expand Up @@ -232,3 +236,83 @@ func deletePolicies(ctx context.Context, cli *kibana.Client, a *installedAgent)
}
return errors.Join(errs...)
}

func compileRegistryState(ts *testscript.TestScript, neg bool, args []string) {
if neg {
ts.Fatalf("unsupported: ! compile_registry_state")
}
clearStdStreams(ts)
flg := flag.NewFlagSet("uninstall", flag.ContinueOnError)
first := flg.Int64("start", 0, "first registry log operation ID to use")
pretty := flg.Bool("pretty", false, "pretty print the registry")
ts.Check(flg.Parse(args))
if flg.NArg() != 1 {
ts.Fatalf("usage: compile_registry_state [-start <first_id_to_use>] <path_to_registry_log>")
}
var choose func(int64) bool
if *first != 0 {
choose = func(i int64) bool {
return i >= *first
}
}
f, err := os.Open(ts.MkAbs(flg.Arg(0)))
ts.Check(decoratedWith("opening registry log", err))
defer f.Close()

s := make(map[string]any)
ts.Check(decoratedWith("compiling state", compileStateInto(s, f, choose)))
var msg []byte
if *pretty {
msg, err = json.MarshalIndent(s, "", " ")
} else {
msg, err = json.Marshal(s)
}
ts.Check(decoratedWith("marshaling registry", err))
fmt.Fprintf(ts.Stdout(), "%s\n", msg)
}

func compileStateInto(dst map[string]any, r io.Reader, choose func(int64) bool) error {
type action struct {
Operation string `json:"op"`
ID int64 `json:"id"`
}

type delta struct {
Key string `json:"K"`
Val map[string]any `json:"V"`
}

dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
for {
var (
a action
d delta
)
err := dec.Decode(&a)
if err != nil {
if err == io.EOF {
return nil
}
return err
}
err = dec.Decode(&d)
if err != nil {
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
}
if choose != nil && !choose(a.ID) {
continue
}
switch a.Operation {
case "set":
dst[d.Key] = d.Val
case "remove":
delete(dst, d.Key)
default:
return fmt.Errorf("unknown operation: %q", a.Operation)
}
}
}
1 change: 1 addition & 0 deletions internal/testrunner/script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func Run(dst io.Writer, cmd *cobra.Command, args []string) error {
"dump_logs": dumpLogs,
"match_file": match,
"get_policy": getPolicyCommand,
"compile_registry_state": compileRegistryState,
},
Setup: func(e *testscript.Env) error {
e.Setenv("PROFILE", config.CurrentProfile())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ use_stack -profile ${CONFIG_PROFILES}/${PROFILE}
cmpenv stdout want_use.json

# Install an agent.
install_agent -profile ${CONFIG_PROFILES}/${PROFILE} NETWORK_NAME
install_agent -profile ${CONFIG_PROFILES}/${PROFILE} -container_name CONTAINER_NAME -network_name NETWORK_NAME
! stderr .
cmp stdout want_agent_up.text
# Check the container name is valid.
exec echo ${CONTAINER_NAME}
stdout 'elastic-package-agent-with_script-first-[0-9]+-elastic-agent-1'
# Check the network name is valid.
exec echo ${NETWORK_NAME}
stdout 'elastic-package-agent-with_script-first-[0-9]+_default'

# Demonstrate we can interact with the running agent container.
exec docker exec ${CONTAINER_NAME} ls
stdout elastic-agent

# Bring up a docker container and check that we get the expected output.
docker_up -profile ${CONFIG_PROFILES}/${PROFILE} -network ${NETWORK_NAME} test-hits
! stderr .
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
compile_registry_state -pretty keys_log.json
cmp stdout want_keys_log_0.json

compile_registry_state -pretty -start=3 keys_log.json
cmp stdout want_keys_log_3.json

compile_registry_state -pretty key_log.json
cmp stdout want_key_log.json

compile_registry_state -pretty keys_removed_log.json
cmp stdout want_keys_removed_log.json

-- keys_log.json --
{"op":"set", "id": 1}
{"K":"key0","V":{"a":0}}
{"op":"set", "id": 2}
{"K":"key1","V":{"a":1}}
{"op":"set", "id": 3}
{"K":"key2","V":{"a":2}}
{"op":"set", "id": 4}
{"K":"key3","V":{"a":3}}
{"op":"set", "id": 5}
{"K":"key4","V":{"a":4}}
{"op":"set", "id": 6}
{"K":"key5","V":{"a":5}}
-- want_keys_log_0.json --
{
"key0": {
"a": 0
},
"key1": {
"a": 1
},
"key2": {
"a": 2
},
"key3": {
"a": 3
},
"key4": {
"a": 4
},
"key5": {
"a": 5
}
}
-- want_keys_log_3.json --
{
"key2": {
"a": 2
},
"key3": {
"a": 3
},
"key4": {
"a": 4
},
"key5": {
"a": 5
}
}
-- key_log.json --
{"op":"set", "id": 1}
{"K":"key0","V":{"a":0}}
{"op":"set", "id": 2}
{"K":"key0","V":{"a":1}}
{"op":"set", "id": 3}
{"K":"key0","V":{"a":2}}
{"op":"set", "id": 4}
{"K":"key0","V":{"a":3}}
{"op":"set", "id": 5}
{"K":"key0","V":{"a":4}}
{"op":"set", "id": 6}
{"K":"key0","V":{"a":5}}
-- want_key_log.json --
{
"key0": {
"a": 5
}
}
-- keys_removed_log.json --
{"op":"set", "id": 1}
{"K":"key0","V":{"a":0}}
{"op":"set", "id": 2}
{"K":"key1","V":{"a":1}}
{"op":"set", "id": 3}
{"K":"key2","V":{"a":2}}
{"op":"set", "id": 4}
{"K":"key3","V":{"a":3}}
{"op":"set", "id": 5}
{"K":"key4","V":{"a":4}}
{"op":"set", "id": 6}
{"K":"key5","V":{"a":5}}
{"op":"remove", "id": 7}
{"K":"key3"}
-- want_keys_removed_log.json --
{
"key0": {
"a": 0
},
"key1": {
"a": 1
},
"key2": {
"a": 2
},
"key4": {
"a": 4
},
"key5": {
"a": 5
}
}