Skip to content

Commit

Permalink
tests(e2e): collect and report PAM module coverage in e2e tests (#1032)
Browse files Browse the repository at this point in the history
Add two new scripts, one that recompiles the PAM module to add coverage
support, and one that collects the coverage at the end of the suite,
before deprovisioning. For simplicity we only do these additional steps
on a single runner, namely the Noble one. It's not worth it attempting
to pool the coverage from all runners since it is most likely identical.

The collected coverage is uploaded as an artifact on the workflow, with
a deterministic name, so it can be easily downloaded and reused in the
TICS check workflow.

-------------------------

Another way of doing this, with less duplication in the workflows, would
have been to download the E2E coverage in the QA workflow and combine it
there. This would have given us the updated coverage in Codecov as well,
with the side effect of adding more indirection because the coverage
"journey" would become:

```
e2e workflow -> QA workflow -> TICS workflow
```
as opposed to

```
e2e workflow | -> TICS workflow
QA workflow  |
```

While it may make sense from a code duplication point of view (i.e. only
use `reportgenerator` in the QA workflow and keep downloading a single
coverage report in the TICS check), there's no functional argument for
coupling the QA and e2e workflows like this, so I went with the more
obvious approach.


Fixes UDENG-3049
  • Loading branch information
GabrielNagy authored Jun 20, 2024
2 parents e591bf9 + 14d40f4 commit 70c5aea
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 9 deletions.
18 changes: 15 additions & 3 deletions .github/workflows/e2e-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,24 @@ jobs:
run: go run ./e2e/cmd/run_tests/01_provision_client
- name: Provision AD server
run: go run ./e2e/cmd/run_tests/02_provision_ad
- name: Recompile PAM module with coverage support
run: go run ./e2e/cmd/run_tests/03_pam_coverage_support
if: ${{ matrix.codename == 'noble' }}
- name: 'Test: non-Pro managers'
run: go run ./e2e/cmd/run_tests/03_test_non_pro_managers
run: go run ./e2e/cmd/run_tests/10_test_non_pro_managers
- name: 'Test: Pro managers'
run: go run ./e2e/cmd/run_tests/04_test_pro_managers
run: go run ./e2e/cmd/run_tests/11_test_pro_managers
- name: 'Test: PAM and Kerberos ticket cache'
run: go run ./e2e/cmd/run_tests/05_test_pam_krb5cc
run: go run ./e2e/cmd/run_tests/12_test_pam_krb5cc
- name: Collect PAM module coverage
run: go run ./e2e/cmd/run_tests/98_collect_pam_coverage
if: ${{ matrix.codename == 'noble' }}
- name: Upload PAM coverage as artifact
uses: actions/upload-artifact@v4
with:
name: pam-coverage.zip
path: ./output/pam-cobertura.xml
if: ${{ matrix.codename == 'noble' }}
- name: Collect logs on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/tics-report-daily.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,21 @@ jobs:
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ${{ env.apt_dependencies }}
go install honnef.co/go/tools/cmd/staticcheck@latest
dotnet tool install -g dotnet-reportgenerator-globaltool
- name: TiCS scan
env:
TICSAUTHTOKEN: ${{ secrets.TICSAUTHTOKEN }}
GH_TOKEN: ${{ github.token }}
run: |
set -e
# Download and move coverage to the right place so TiCS can parse it
# Download, combine and move coverage to the right place so TiCS can parse it
RUN_ID=$(gh run list --workflow 'QA & sanity checks' --limit 1 --status success --json databaseId -b main | jq '.[].databaseId')
gh run download $RUN_ID -n coverage.zip
mkdir .coverage
mv Cobertura.xml .coverage/coverage.xml
gh run download $RUN_ID -n coverage.zip -D /tmp/coverage
E2E_RUN_ID=$(gh run list --workflow 'E2E - Run tests' --limit 1 --status success --json databaseId -b main | jq '.[].databaseId')
gh run download $E2E_RUN_ID -n pam-coverage.zip -D /tmp/coverage
reportgenerator "-reports:/tmp/coverage/*.xml" "-targetdir:.coverage" -reporttypes:Cobertura
mv .coverage/Cobertura.xml .coverage/coverage.xml
# Install TiCS
. <(curl --silent --show-error 'https://canonical.tiobe.com/tiobeweb/TICS/api/public/v1/fapi/installtics/Script?cfg=default&platform=linux&url=https://canonical.tiobe.com/tiobeweb/TICS/')
Expand Down
72 changes: 72 additions & 0 deletions e2e/cmd/run_tests/03_pam_coverage_support/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Package main provides a script to recompile the adsys PAM module with coverage support.
package main

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/ubuntu/adsys/e2e/internal/command"
"github.com/ubuntu/adsys/e2e/internal/inventory"
"github.com/ubuntu/adsys/e2e/internal/remote"
"github.com/ubuntu/adsys/e2e/scripts"
)

var sshKey string

func main() {
os.Exit(run())
}

func run() int {
cmd := command.New(action,
command.WithValidateFunc(validate),
command.WithStateTransition(inventory.ADProvisioned, inventory.ADProvisioned),
)
cmd.Usage = fmt.Sprintf(`go run ./%s [options]
Rebuild PAM module with coverage support with the goal of collecting coverage data at the end of the suite.`, filepath.Base(os.Args[0]))

return cmd.Execute(context.Background())
}

func validate(_ context.Context, cmd *command.Command) (err error) {
sshKey, err = command.ValidateAndExpandPath(cmd.Inventory.SSHKeyPath, command.DefaultSSHKeyPath)
if err != nil {
return err
}

return nil
}

func action(ctx context.Context, cmd *command.Command) error {
adsysRootDir, err := scripts.RootDir()
if err != nil {
return err
}

// Establish remote connection
client, err := remote.NewClient(cmd.Inventory.IP, "root", sshKey)
if err != nil {
return fmt.Errorf("failed to connect to VM: %w", err)
}
defer client.Close()

// Install required dependencies to process coverage data
if _, err := client.Run(ctx, "DEBIAN_FRONTEND=noninteractive apt-get install -y gcc gcovr libpam0g-dev"); err != nil {
return fmt.Errorf("failed to install coverage dependencies: %w", err)
}

// Upload PAM module source code to a persistent location
if err := client.Upload(filepath.Join(adsysRootDir, "pam", "pam_adsys.c"), "/root/pam/pam_adsys.c"); err != nil {
return fmt.Errorf("failed to upload PAM module source code: %w", err)
}

// Rebuild PAM module in-place with coverage support
if _, err := client.Run(ctx, fmt.Sprintf("gcc --coverage -shared -Wl,-soname,libpam_adsys.so -o %s/pam_adsys.so pam/pam_adsys.c -lpam", remote.PAMModuleDirectory)); err != nil {
return fmt.Errorf("failed to compile PAM module: %w", err)
}

return nil
}
66 changes: 66 additions & 0 deletions e2e/cmd/run_tests/98_collect_pam_coverage/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Package main provides a script to collect PAM module coverage from the remote Ubuntu VM.
package main

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/ubuntu/adsys/e2e/internal/command"
"github.com/ubuntu/adsys/e2e/internal/inventory"
"github.com/ubuntu/adsys/e2e/internal/remote"
"github.com/ubuntu/adsys/e2e/scripts"
)

var sshKey string

func main() {
os.Exit(run())
}

func run() int {
cmd := command.New(action,
command.WithValidateFunc(validate),
command.WithStateTransition(inventory.ADProvisioned, inventory.ADProvisioned),
)
cmd.Usage = fmt.Sprintf(`go run ./%s [options]
Collect PAM module coverage and save it locally to output/pam-cobertura.xml`, filepath.Base(os.Args[0]))

return cmd.Execute(context.Background())
}

func validate(_ context.Context, cmd *command.Command) (err error) {
sshKey, err = command.ValidateAndExpandPath(cmd.Inventory.SSHKeyPath, command.DefaultSSHKeyPath)
if err != nil {
return err
}

return nil
}

func action(ctx context.Context, cmd *command.Command) error {
adsysRootDir, err := scripts.RootDir()
if err != nil {
return err
}

// Establish remote connection
client, err := remote.NewClient(cmd.Inventory.IP, "root", sshKey)
if err != nil {
return fmt.Errorf("failed to connect to VM: %w", err)
}
defer client.Close()

// Collect PAM module coverage if present
if _, err := client.Run(ctx, fmt.Sprintf("gcovr --cobertura --output=/tmp/pam-cobertura.xml %s", remote.PAMModuleDirectory)); err != nil {
return fmt.Errorf("failed to collect PAM module coverage: %w", err)
}

if err := client.Download("/tmp/pam-cobertura.xml", filepath.Join(adsysRootDir, "output", "pam-cobertura.xml")); err != nil {
return fmt.Errorf("failed to download PAM module coverage: %w", err)
}

return nil
}
9 changes: 7 additions & 2 deletions e2e/internal/remote/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ import (
// commandTimeout is the maximum time a command can run before being cancelled.
const commandTimeout = 90 * time.Minute

// DomainUserPassword is the password to login as domain users.
const DomainUserPassword = "supersecretpassword"
const (
// DomainUserPassword is the password to login as domain users.
DomainUserPassword = "supersecretpassword"

// PAMModuleDirectory is the default directory for PAM modules on an amd64 system.
PAMModuleDirectory = "/usr/lib/x86_64-linux-gnu/security"
)

// Client represents a remote SSH client.
type Client struct {
Expand Down

0 comments on commit 70c5aea

Please sign in to comment.