diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 58c3bb65f..d415f8616 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -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 diff --git a/.github/workflows/tics-report-daily.yaml b/.github/workflows/tics-report-daily.yaml index ba6230e7f..5fc69782f 100644 --- a/.github/workflows/tics-report-daily.yaml +++ b/.github/workflows/tics-report-daily.yaml @@ -25,6 +25,7 @@ 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 }} @@ -32,11 +33,13 @@ jobs: 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/') diff --git a/e2e/cmd/run_tests/03_pam_coverage_support/main.go b/e2e/cmd/run_tests/03_pam_coverage_support/main.go new file mode 100644 index 000000000..26033febf --- /dev/null +++ b/e2e/cmd/run_tests/03_pam_coverage_support/main.go @@ -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 +} diff --git a/e2e/cmd/run_tests/03_test_non_pro_managers/main.go b/e2e/cmd/run_tests/10_test_non_pro_managers/main.go similarity index 100% rename from e2e/cmd/run_tests/03_test_non_pro_managers/main.go rename to e2e/cmd/run_tests/10_test_non_pro_managers/main.go diff --git a/e2e/cmd/run_tests/04_test_pro_managers/main.go b/e2e/cmd/run_tests/11_test_pro_managers/main.go similarity index 100% rename from e2e/cmd/run_tests/04_test_pro_managers/main.go rename to e2e/cmd/run_tests/11_test_pro_managers/main.go diff --git a/e2e/cmd/run_tests/05_test_pam_krb5cc/main.go b/e2e/cmd/run_tests/12_test_pam_krb5cc/main.go similarity index 100% rename from e2e/cmd/run_tests/05_test_pam_krb5cc/main.go rename to e2e/cmd/run_tests/12_test_pam_krb5cc/main.go diff --git a/e2e/cmd/run_tests/98_collect_pam_coverage/main.go b/e2e/cmd/run_tests/98_collect_pam_coverage/main.go new file mode 100644 index 000000000..6bacec9a3 --- /dev/null +++ b/e2e/cmd/run_tests/98_collect_pam_coverage/main.go @@ -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 +} diff --git a/e2e/internal/remote/remote.go b/e2e/internal/remote/remote.go index ef142fa81..1efdc4182 100644 --- a/e2e/internal/remote/remote.go +++ b/e2e/internal/remote/remote.go @@ -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 {