Skip to content

Commit

Permalink
refactor: implement sub-commands and validate container image
Browse files Browse the repository at this point in the history
Signed-off-by: Adrien Mannocci <[email protected]>
  • Loading branch information
amannocci committed Aug 4, 2023
1 parent 29920c0 commit 7ab4235
Show file tree
Hide file tree
Showing 29 changed files with 336 additions and 140 deletions.
30 changes: 13 additions & 17 deletions .github/workflows/publish-docker-image.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Builds and publishes a container image to docker.elastic.co/observability-ci/apm-perf:latest
# This workflow is triggered on push to main branch when cmd, loadgen, soaktest, Dockerfile or this file are changed
# This workflow is triggered on push to main branch when cmd, loadgen, soaktest, Containerfile or this file are changed
name: publish-docker-images

on:
pull_request:
branches:
- main
push:
branches:
- main
Expand All @@ -11,33 +14,26 @@ on:
- "cmd/**"
- "loadgen/**"
- "soaktest/**"
- "Dockerfile"

env:
REGISTRY: docker.elastic.co
PREFIX: observability-ci
NAME: apm-perf
TAG: latest
- "Containerfile"

permissions:
contents: read

jobs:
publish-docker-image:
publish-container-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: elastic/apm-pipeline-library/.github/actions/docker-login@main
with:
registry: ${{ env.REGISTRY }}
registry: docker.elastic.co
secret: secret/observability-team/ci/docker-registry/prod
url: ${{ secrets.VAULT_ADDR }}
roleId: ${{ secrets.VAULT_ROLE_ID }}
secretId: ${{ secrets.VAULT_SECRET_ID }}
- name: Generate Image Name
id: generate-image-name
run: echo "image_name=${{ env.REGISTRY }}/${{ env.PREFIX }}/${{ env.NAME }}:${{ env.TAG }}" >> $GITHUB_OUTPUT
- name: build image
run: docker build -t ${{ steps.generate-image-name.outputs.image_name }} .
- name: push image
run: docker push ${{ steps.generate-image-name.outputs.image_name }}
- name: Build container image
run: make package
- name: Test container image
run: make sanitize
- name: Push container image
run: make publish
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
cmd/apmsoak/apmsoak
.vscode
.vscode
.states
dist/
1 change: 1 addition & 0 deletions .go-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.20.7
45 changes: 45 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Base image for build
ARG base_image_version=1.20.7
FROM golang:${base_image_version} as builder

# Switch workdir
WORKDIR /opt/apm-perf

# Copy files
COPY . .

# Build
RUN \
go mod download \
&& make build

# Base image for build
FROM debian:bookworm

# Arguments
ARG commit_sha
ARG current_time
ARG image_id
ARG project_url

# Labels
LABEL \
org.opencontainers.image.created="${current_time}" \
org.opencontainers.image.authors="amannocci <[email protected]>" \
org.opencontainers.image.url="${project_url}" \
org.opencontainers.image.documentation="${project_url}" \
org.opencontainers.image.source="${project_url}" \
org.opencontainers.image.version="${image_id}" \
org.opencontainers.image.revision="${commit_sha}" \
org.opencontainers.image.vendor="Elastic" \
org.opencontainers.image.licenses="Elastic License 2.0"

# Switch workdir
WORKDIR /opt/apm-perf

# Copy files
COPY --from=builder /opt/apm-perf/dist/apmsoak /usr/bin/apmsoak
COPY ./internal/loadgen/events ./events
COPY ./cmd/apmsoak/scenarios.yml /opt/apm-perf/scenarios.yml

CMD [ "/usr/bin/apmsoak" ]
12 changes: 0 additions & 12 deletions Dockerfile

This file was deleted.

43 changes: 43 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
.DEFAULT_GOAL := all

STATES_DIR=$(CURDIR)/.states
DIST_DIR=$(CURDIR)/dist

all: test

fmt: tools/go.mod
Expand All @@ -15,10 +19,49 @@ lint: tools/go.mod
clean:
rm -fr bin

.PHONY: build
build: COMMIT_SHA=$$(git rev-parse HEAD)
build: CURRENT_TIME_ISO=$$(date -u +"%Y-%m-%dT%H:%M:%SZ")
build: LDFLAGS=-X 'github.com/elastic/apm-perf/internal/version.commitSha=$(COMMIT_SHA)'
build: LDFLAGS+=-X 'github.com/elastic/apm-perf/internal/version.buildTime=$(CURRENT_TIME_ISO)'
build:
mkdir -p $(DIST_DIR)
go build -ldflags "$(LDFLAGS)" -o $(DIST_DIR)/apmsoak cmd/apmsoak/*.go

.PHONY: test
test: go.mod
go test -v ./...

.PHONY: package
package: BASE_IMAGE_VERSION=$$(cat .go-version)
package: COMMIT_SHA_SHORT=$$(git rev-parse --short HEAD)
package: COMMIT_SHA=$$(git rev-parse HEAD)
package: CURRENT_TIME_ISO=$$(date -u +"%Y-%m-%dT%H:%M:%SZ")
package: CURRENT_TIME=$$(date +%s)
package: IMAGE_ID=$${IMAGE_VERSION:-latest}-$(CURRENT_TIME)-$(COMMIT_SHA_SHORT)
package: IMAGE_REF=docker.elastic.co/observability-ci/apm-perf:$(IMAGE_ID)
package: PROJECT_URL=$$(go list -m all | head -1)
package:
mkdir -p $(STATES_DIR)
echo "$(IMAGE_REF)" > "$(STATES_DIR)/image_ref"
docker buildx build \
--build-arg base_image_version=$(BASE_IMAGE_VERSION) \
--build-arg commit_sha=$(COMMIT_SHA) \
--build-arg current_time=$(CURRENT_TIME_ISO) \
--build-arg image_id=$(IMAGE_ID) \
--build-arg project_url=$(PROJECT_URL) \
-t $$(cat "$(STATES_DIR)/image_ref") \
.

.PHONY: sanitize
sanitize: IMAGE_REF=$$(cat "$(STATES_DIR)/image_ref")
sanitize:
docker run -it --rm $(IMAGE_REF) apmsoak version

.PHONY: publish`
publish: IMAGE_REF=$$(cat "$(STATES_DIR)/image_ref")
publish:
docker push $(IMAGE_REF)

MODULE_DEPS=$(sort $(shell go list -deps -tags=darwin,linux,windows -f "{{with .Module}}{{if not .Main}}{{.Path}}{{end}}{{end}}" ./...))

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ Usage of /var/folders/y8/c7ft78m54v78z8yy16bznmkw0000gn/T/go-build1835306963/b00
All the flags are optional, but it's expected to provide a scenario flag to start a specific scenario from [pre-defined scenarios file](https://github.com/elastic/apm-perf/blob/main/cmd/apmsoak/scenarios.yml#L2), it is `steady` by default.
```sh
# start soaktest that generates 2000 events per second
./apmsoak -scenario=steady
./apmsoak run -scenario=steady
```

The configs in `scenarios.yml` file inherits [loadgen](./loadgen/config/config.go) configs, with extra fields such as project_id and api_key that can be used for the managed APM service.

Note that the managed service will use API key based auth and one soaktest can target multiple projects, so we provide key-value pairs(projectID and api key respectively). the api key can also be provided as an [ENV variable](https://github.com/elastic/apm-perf/blob/main/soaktest/config.go#L54).

```sh
./apmsoak -scenario=fairness -api-keys="project_1:key-to-project_1,project_2:key-to-project_2"
./apmsoak run -scenario=fairness -api-keys="project_1:key-to-project_1,project_2:key-to-project_2"
```
45 changes: 30 additions & 15 deletions cmd/apmsoak/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,49 @@ package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"strings"
"syscall"

"go.elastic.co/ecszap"
"go.uber.org/zap"

"github.com/elastic/apm-perf/soaktest"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

func main() {
encoderConfig := ecszap.NewDefaultEncoderConfig()
core := ecszap.NewCore(encoderConfig, os.Stdout, zap.InfoLevel)
logger := zap.New(core, zap.AddCaller())
const envVarPrefix = "ELASTIC_APM_"

flag.Parse()
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

runner, err := soaktest.NewRunner(soaktest.SoakConfig.ScenariosPath, soaktest.SoakConfig.Scenario, logger)
if err != nil {
logger.Fatal("Fail to initialize runner", zap.Error(err))
// Register root command in cobra
var rootCmd = &cobra.Command{
Use: "apmsoak",
TraverseChildren: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var err error
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
optionName := strings.ToUpper(flag.Name)
optionName = strings.ReplaceAll(optionName, "-", "_")
envVar := envVarPrefix + optionName
if val, ok := os.LookupEnv(envVar); !flag.Changed && ok {
flagErr := flag.Value.Set(val)
if flagErr != nil {
err = fmt.Errorf("invalid environment variable %s: %w", envVar, flagErr)
}
}
})
return err
},
}
rootCmd.AddCommand(NewCmdVersion())
rootCmd.AddCommand(NewCmdRun())

if err := runner.Run(ctx); err != nil {
// Execute commands
if err := rootCmd.ExecuteContext(ctx); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Fatal("runner exited with error", zap.Error(err))
fmt.Println(err)
}
}
}
84 changes: 84 additions & 0 deletions cmd/apmsoak/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package main

import (
"context"
"errors"
"os"
"strings"

"github.com/spf13/cobra"
"go.elastic.co/ecszap"
"go.uber.org/zap"

"github.com/elastic/apm-perf/internal/soaktest"
)

type RunOptions struct {
Scenario string
ScenariosPath string
ServerURL string
SecretToken string
ApiKeys string
BypassProxy bool
}

func (opts *RunOptions) toRunnerConfig() (*soaktest.RunnerConfig, error) {
apiKeys := make(map[string]string)
pairs := strings.Split(opts.ApiKeys, ",")
for _, pair := range pairs {
kv := strings.Split(pair, ":")
if len(kv) != 2 {
return nil, errors.New("invalid api keys provided. example: project_id:my_api_key")
}
apiKeys[kv[0]] = kv[1]
}
return &soaktest.RunnerConfig{
Scenario: opts.Scenario,
ScenariosPath: opts.ScenariosPath,
ServerURL: opts.ServerURL,
SecretToken: opts.SecretToken,
ApiKeys: apiKeys,
BypassProxy: opts.BypassProxy,
}, nil
}

func NewCmdRun() *cobra.Command {
options := &RunOptions{}
cmd := &cobra.Command{
Use: "run",
Short: "Run apmsoak",
RunE: func(cmd *cobra.Command, args []string) error {
encoderConfig := ecszap.NewDefaultEncoderConfig()
core := ecszap.NewCore(encoderConfig, os.Stdout, zap.InfoLevel)
logger := zap.New(core, zap.AddCaller())

config, err := options.toRunnerConfig()
if err != nil {
logger.Fatal("Fail to parse flags", zap.Error(err))
}

runner, err := soaktest.NewRunner(config, logger)
if err != nil {
logger.Fatal("Fail to initialize runner", zap.Error(err))
}

if err := runner.Run(cmd.Context()); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Fatal("runner exited with error", zap.Error(err))
}
}
return nil
},
}
cmd.Flags().StringVar(&options.ServerURL, "server-url", "", "Server URL (default http://127.0.0.1:8200), if specified <project_id>, it will be replaced with the project_id provided by the config, (example: https://<project_id>.apm.elastic.cloud)")
cmd.Flags().StringVar(&options.Scenario, "scenario", "steady", "Specify which scenario to use. the value should match one of the scenario key defined in given scenarios YAML file")
cmd.Flags().StringVarP(&options.ScenariosPath, "file", "f", "./scenarios.yml", "Path to scenarios file")
cmd.Flags().StringVar(&options.SecretToken, "secret-token", "", "Secret token for APM Server. Managed intake service doesn't support secret token")
cmd.Flags().StringVar(&options.ApiKeys, "api-keys", "", "API keys for managed service. Specify key value pairs as `project_id_1:my_api_key,project_id_2:my_key`")
cmd.Flags().BoolVar(&options.BypassProxy, "bypass-proxy", false, "Detach from proxy dependency and provide projectID via header. Useful when testing locally")
return cmd
}
2 changes: 1 addition & 1 deletion cmd/apmsoak/scenarios_test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
scenarios:
steady:
- project_id: f0d3b9322a51464aa782f46eef0bb157
- project_id: af862b688c2841f789f65dba30a5e005
event_rate: 10/1s
apm-server:
- event_rate: 10/1s
Expand Down
32 changes: 32 additions & 0 deletions cmd/apmsoak/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package main

import (
"bytes"
"fmt"
"runtime"

"github.com/spf13/cobra"

"github.com/elastic/apm-perf/internal/version"
)

func NewCmdVersion() *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Show current version info",
Run: func(cmd *cobra.Command, args []string) {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s %s", version.CommitSha(), version.BuildTime())
fmt.Fprintf(cmd.OutOrStdout(),
"apmsoak version %s (%s/%s) [%s]\n",
version.Version, runtime.GOOS, runtime.GOARCH,
buf.String(),
)
},
}
return cmd
}
Loading

0 comments on commit 7ab4235

Please sign in to comment.