From de86b9bcee4204f3a06451f1a06f5d691523d768 Mon Sep 17 00:00:00 2001 From: David Wooldridge Date: Sat, 7 Sep 2024 00:10:26 +0100 Subject: [PATCH] Add support for role dependencies (#11) * Add support for role local and galaxy dependencies --- .envrc | 1 - .github/workflows/test.yml | 42 +- .gitignore | 2 + .mise.toml | 6 + README.md | 10 +- cmd/converge.go | 11 +- cmd/create.go | 3 - cmd/destroy.go | 3 - cmd/init.go | 3 - cmd/list.go | 3 - cmd/root.go | 7 +- cmd/shell.go | 3 - cmd/test.go | 3 - cmd/verify.go | 3 - go.mod | 2 +- go.sum | 6 + justfile | 32 +- main.go | 3 - pkg/config/config.go | 74 ++- pkg/config/config_test.go | 39 ++ pkg/container/engine.go | 17 +- pkg/container/engine_test.go | 51 ++ .../utils.go => filesystem/filesystem.go} | 2 +- pkg/instance/instance.go | 35 +- pkg/provisioner/ansible.go | 61 ++- pkg/provisioner/ansible_metadata.go | 68 +++ pkg/provisioner/ansible_test.go | 514 +++++++++++++++++- pkg/provisioner/provisioner.go | 6 + testing/ansible/roles/simple/meta/main.yml | 0 testing/ansible/roles/simple/rolecule.yml | 4 +- testing/ansible/roles/simple/tests/goss.yaml | 4 +- .../ansible/roles/simple/tests/playbook.yml | 2 +- testing/ansible/roles/sshd/tests/goss.yaml | 2 +- testing/ansible/roles/sshd/tests/playbook.yml | 2 +- .../roles/sshd/{ => tests}/rolecule.yml | 13 +- .../roles/sshd/tests/ubuntu/playbook.yml | 11 +- .../ansible/roles/sysctl/defaults/main.yml | 2 + testing/ansible/roles/sysctl/meta/main.yml | 2 + testing/ansible/roles/sysctl/tasks/main.yml | 12 + testing/ansible/roles/sysctl/tests/goss.yaml | 3 + .../ansible/roles/sysctl/tests/playbook.yml | 5 + .../ansible/roles/sysctl/tests/rolecule.yml | 21 + .../sysctl/tests/scenarios/build/test_all.py | 0 .../tests/scenarios/provision/test_all.py | 0 .../ansible/roles/website/defaults/main.yml | 0 .../ansible/roles/website/files/index.html | 9 + testing/ansible/roles/website/meta/main.yml | 4 + testing/ansible/roles/website/tasks/main.yml | 8 + testing/ansible/roles/website/tests/goss.yaml | 17 + .../ansible/roles/website/tests/playbook.yml | 5 + .../ansible/roles/website/tests/rolecule.yml | 18 + .../ansible/ubuntu-22.04-systemd.Dockerfile | 11 +- .../ansible/ubuntu-24.04-systemd.Dockerfile | 41 ++ 53 files changed, 1063 insertions(+), 143 deletions(-) delete mode 100644 .envrc create mode 100644 .mise.toml create mode 100644 pkg/config/config_test.go create mode 100644 pkg/container/engine_test.go rename pkg/{utils/utils.go => filesystem/filesystem.go} (94%) create mode 100644 pkg/provisioner/ansible_metadata.go create mode 100644 testing/ansible/roles/simple/meta/main.yml rename testing/ansible/roles/sshd/{ => tests}/rolecule.yml (76%) create mode 100644 testing/ansible/roles/sysctl/defaults/main.yml create mode 100644 testing/ansible/roles/sysctl/meta/main.yml create mode 100644 testing/ansible/roles/sysctl/tasks/main.yml create mode 100644 testing/ansible/roles/sysctl/tests/goss.yaml create mode 100644 testing/ansible/roles/sysctl/tests/playbook.yml create mode 100644 testing/ansible/roles/sysctl/tests/rolecule.yml create mode 100644 testing/ansible/roles/sysctl/tests/scenarios/build/test_all.py create mode 100644 testing/ansible/roles/sysctl/tests/scenarios/provision/test_all.py create mode 100644 testing/ansible/roles/website/defaults/main.yml create mode 100644 testing/ansible/roles/website/files/index.html create mode 100644 testing/ansible/roles/website/meta/main.yml create mode 100644 testing/ansible/roles/website/tasks/main.yml create mode 100644 testing/ansible/roles/website/tests/goss.yaml create mode 100644 testing/ansible/roles/website/tests/playbook.yml create mode 100644 testing/ansible/roles/website/tests/rolecule.yml create mode 100644 testing/ansible/ubuntu-24.04-systemd.Dockerfile diff --git a/.envrc b/.envrc deleted file mode 100644 index 234188d..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -PATH_add bin diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6c968f..d8e52d8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,41 +1,35 @@ -name: Build and test +name: Test +run-name: Test ${{ github.ref_name }} triggered by @${{ github.actor }} on: push: - branches: [ "main" ] + branches: + - main pull_request: - branches: [ "main" ] jobs: - - build-linux: + test-linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v3 + - name: Set up go + uses: actions/setup-go@v5 with: - go-version: 1.19 - - - name: Build - run: go build -v ./... + go-version-file: go.mod - name: Test - run: go test -v ./... + run: go test -v ./... -coverprofile=cover.out - build-windows: + test-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.19 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod - - name: Build - run: go build -v ./... - - - name: Test - run: go test -v ./... + - name: Test + run: go test -v ./... -coverprofile=cover.out diff --git a/.gitignore b/.gitignore index 54c6470..91f1f46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ dist/ +.idea/ +.vscode/ rolecule rolecule.exe diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..db2eead --- /dev/null +++ b/.mise.toml @@ -0,0 +1,6 @@ +[env] +mise.path = ["./bin"] +GO_VERSION = "{{exec(command='grep \"^go 1\\.[0-9]\\+\\.[0-9]\\+$\" go.mod | cut -f2 -d\" \"')}}" + +[tools] +golang = "{{exec(command='grep \"^go 1\\.[0-9]\\+\\.[0-9]\\+$\" go.mod | cut -f2 -d\" \"')}}" diff --git a/README.md b/README.md index 60060a5..e6f17a7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ instances: ``` -Then, from the root of your role (e.g. [sshd](testing/ansible/roles/sshd/rolecule.yml)), run `rolecule test`, e.g.: +Then, from the root of your role (e.g. [sshd](testing/ansible/roles/sshd/tests/rolecule.yml)), run `rolecule test`, e.g.: ```text » rolecule test @@ -177,6 +177,14 @@ provisioner: - --verbose ``` +## Role dependencies + +If you have role dependencies in your `meta/main.yml` file using local roles in the same location +as the current role, that directory will be mounted at `/etc/ansible/roles` in the container so +ansible can find them. + +Support for using roles from a galaxy server is not yet implemented. + ## Instances These are instances of each test scenario, allowing you can test different ansible tags with specific test files. diff --git a/cmd/converge.go b/cmd/converge.go index ca37580..4ce3f0b 100644 --- a/cmd/converge.go +++ b/cmd/converge.go @@ -1,6 +1,3 @@ -/* -Copyright © 2022 David Wooldridge -*/ package cmd import ( @@ -33,7 +30,6 @@ var convergeCmd = &cobra.Command{ func converge(cfg *config.Config) error { for _, instance := range cfg.Instances { if !instance.Engine.Exists(instance.Name) { - log.Errorf("container does not exist, creating...") err := create(cfg) if err != nil { log.Error(err.Error()) @@ -41,6 +37,13 @@ func converge(cfg *config.Config) error { } } + if len(instance.Provisioner.GetDependencies().GalaxyRoles) > 0 { + log.Infof("preparing container %s", instance.Name) + if err := instance.Prepare(); err != nil { + log.Error(err.Error()) + } + } + log.Infof("converging container %s with %s", instance.Name, instance.Provisioner) if err := instance.Converge(); err != nil { log.Error(err.Error()) diff --git a/cmd/create.go b/cmd/create.go index 4f793c2..3a72532 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( diff --git a/cmd/destroy.go b/cmd/destroy.go index 77addd6..f24f232 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( diff --git a/cmd/init.go b/cmd/init.go index c895376..3034ac0 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( diff --git a/cmd/list.go b/cmd/list.go index 9c2ca38..7da7a5d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( diff --git a/cmd/root.go b/cmd/root.go index 3b4349c..1c786b2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( @@ -21,8 +18,8 @@ var rootCmd = &cobra.Command{ Use: "rolecule", Short: "rolecule helps you test your ansible roles", Long: `rolecule uses docker or podman to test your -configuration management roles/recipes/modules in a systemd enabled container, -then tests them with a verifier (goss/testinfra).`, +ansible roles in a systemd enabled container, +then tests them with a verifier (goss).`, PersistentPreRun: func(cmd *cobra.Command, args []string) { log.SetHandler(cli.New(os.Stderr)) diff --git a/cmd/shell.go b/cmd/shell.go index 7337aeb..8cbc95a 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( diff --git a/cmd/test.go b/cmd/test.go index 941dcad..8733c7a 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( diff --git a/cmd/verify.go b/cmd/verify.go index 38a9d75..658970d 100644 --- a/cmd/verify.go +++ b/cmd/verify.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package cmd import ( diff --git a/go.mod b/go.mod index 2056c46..a7cb575 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/z0mbix/rolecule -go 1.19 +go 1.23.0 require ( github.com/apex/log v1.9.0 diff --git a/go.sum b/go.sum index b519013..f96dbec 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -111,6 +112,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -149,9 +151,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -183,6 +187,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= @@ -211,6 +216,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= diff --git a/justfile b/justfile index dcee294..1144de1 100644 --- a/justfile +++ b/justfile @@ -2,50 +2,54 @@ set shell := ["bash", "-uc"] # Show available targets/recipes default: - @just --choose + @just --choose # Clean up old files clean: - rm -rf ./dist/* - rm ./rolecule + rm -rf ./dist/* + rm ./rolecule # Build the binary for the current os/arch build: - go build -o bin/rolecule + go build -o bin/rolecule # Configure your host to use this repo setup: - direnv allow + mise trust + mise install + mise ls -c # Show git tags tags: - @git tag | sort -V + @git tag | sort -V # Run unit tests test: - go test ./... + go test ./... -v -coverprofile=/dev/null # Build docker images with ansible support build-docker-ansible-images: - docker build -t rockylinux-systemd:9.1 -f testing/ansible/rockylinux-9.1-systemd.Dockerfile . - docker build -t ubuntu-systemd:22.04 -f testing/ansible/ubuntu-22.04-systemd.Dockerfile . + docker build -t rockylinux-systemd:9.1 -f testing/ansible/rockylinux-9.1-systemd.Dockerfile . + docker build -t ubuntu-systemd:22.04 -f testing/ansible/ubuntu-22.04-systemd.Dockerfile . + docker build -t ubuntu-systemd:24.04 -f testing/ansible/ubuntu-24.04-systemd.Dockerfile . # Build podman images with ansible support build-podman-ansible-images: - podman build -t rockylinux-systemd:9.1 -f testing/ansible/rockylinux-9.1-systemd.Dockerfile . - podman build -t ubuntu-systemd:22.04 -f testing/ansible/ubuntu-22.04-systemd.Dockerfile . + podman build -t rockylinux-systemd:9.1 -f testing/ansible/rockylinux-9.1-systemd.Dockerfile . + podman build -t ubuntu-systemd:22.04 -f testing/ansible/ubuntu-22.04-systemd.Dockerfile . + podman build -t ubuntu-systemd:24.04 -f testing/ansible/ubuntu-24.04-systemd.Dockerfile . # Build all images with ansible support build-ansible-images: build-docker-ansible-images build-podman-ansible-images # Build a local only, snapshot release snapshot: - goreleaser --snapshot --skip-publish --rm-dist --debug + goreleaser --snapshot --skip-publish --rm-dist --debug # Create and publish a new release release: - goreleaser --rm-dist + goreleaser --rm-dist # Show help menu help: - @just --list --list-prefix ' ❯ ' + @just --list --list-prefix ' ❯ ' diff --git a/main.go b/main.go index 0b17839..44d664e 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,3 @@ -/* -Copyright © 2023 David Wooldridge -*/ package main import "github.com/z0mbix/rolecule/cmd" diff --git a/pkg/config/config.go b/pkg/config/config.go index cc80cc6..11f373a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,11 +1,14 @@ package config import ( + "errors" "fmt" "os" "path/filepath" "strings" + "github.com/z0mbix/rolecule/pkg/filesystem" + "github.com/apex/log" "github.com/spf13/viper" "github.com/z0mbix/rolecule/pkg/container" @@ -14,7 +17,10 @@ import ( "github.com/z0mbix/rolecule/pkg/verifier" ) -var AppName = "rolecule" +var ( + AppName = "rolecule" + defaultEngine = "docker" +) type configFile struct { Engine container.EngineConfig `mapstructure:"engine"` @@ -35,12 +41,12 @@ func Get() (*Config, error) { viper.SetConfigName(AppName) viper.SetConfigType("yaml") viper.AddConfigPath(".") + viper.AddConfigPath("tests") if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { + var configFileNotFoundError viper.ConfigFileNotFoundError + if errors.As(err, &configFileNotFoundError) { log.Fatalf("config file not found: %s.yml", AppName) - } else { - log.Fatalf("config file not valid: %s", err) } } @@ -51,24 +57,35 @@ func Get() (*Config, error) { } log.Debugf("config file: %+v", configValues) - log.Debugf("config file instances: %+v", configValues.Instances) + if configValues.Engine.Name == "" { + log.Debugf("enginer not specified, using default engine: %s", defaultEngine) + configValues.Engine.Name = defaultEngine + } engine, err := container.NewEngine(configValues.Engine.Name) if err != nil { return nil, err } + if !filesystem.CommandExists(configValues.Engine.Name) { + return nil, fmt.Errorf("container engine '%s' not found in PATH", configValues.Engine.Name) + } + cwd, err := os.Getwd() if err != nil { return nil, err } + // resolve any symlinks in the current working directory cwdNoSymlinks, err := filepath.EvalSymlinks(cwd) if err != nil { return nil, err } roleName := filepath.Base(cwd) + roleDir := filepath.Dir(cwd) + log.Debugf("role name: %s", roleName) + log.Debugf("role dir: %s", roleDir) prov, err := provisioner.NewProvisioner(configValues.Provisioner) if err != nil { @@ -80,6 +97,33 @@ func Get() (*Config, error) { return nil, err } + // Check if the role has a meta/main.yml file to determine if it has dependencies + roleMounts := make(map[string]string) + if filesystem.FileExists("meta/main.yml") { + roleMetadata, err := provisioner.GetRoleMetadata() + if err != nil { + return nil, err + } + + for _, dep := range roleMetadata.LocalDependencies() { + log.Debugf("found local role dependency: %s", dep) + roleMounts[filepath.Join(roleDir, dep)] = filepath.Join("/etc/ansible/roles", dep) + } + + for _, dep := range roleMetadata.GalaxyDependencies() { + log.Debugf("found galaxy role dependency: %s", dep) + } + + prov = prov.WithLocalDependencies(roleMetadata.LocalDependencies()) + prov = prov.WithGalaxyDependencies(roleMetadata.GalaxyDependencies()) + } + + var localRoleDependencies []string + for _, v := range roleMounts { + log.Debugf("adding local dependency: %s", v) + localRoleDependencies = append(localRoleDependencies, v) + } + var instances instance.Instances for _, i := range configValues.Instances { iProvisioner := prov.WithTags(i.Tags).WithSkipTags(i.SkipTags) @@ -100,9 +144,12 @@ func Get() (*Config, error) { Args: i.Args, Playbook: i.Playbook, WorkDir: cwdNoSymlinks, + RoleName: roleName, + RoleDir: roleDir, Engine: engine, Provisioner: iProvisioner, Verifier: iVerifier, + RoleMounts: roleMounts, } instances = append(instances, instanceConfig) @@ -119,12 +166,27 @@ func Get() (*Config, error) { func generateContainerName(name, roleName string) string { replacer := strings.NewReplacer("_", "-", " ", "-", ":", "-") - return fmt.Sprintf("%s-%s-%s", AppName, roleName, replacer.Replace(name)) + return fmt.Sprintf("%s-%s-%s", AppName, replacer.Replace(roleName), replacer.Replace(name)) } // Create creates a rolecule.yml file in the current directory func Create(engine, provisioner, verifier string) error { // TODO: yeah, actually implement this log.Debugf("creating config with: %s/%s/%s", engine, provisioner, verifier) + return nil } + +var roleculeFileTemplate = `engine: + name: {{.Engine}} + +provisioner: + name: ansible + +verifier: + name: goss + +instances: + - name: ubuntu-24.04 + image: ubuntu-systemd:24.04 +` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..9d4f73c --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,39 @@ +package config + +import "testing" + +func Test_generateContainerName(t *testing.T) { + type args struct { + name string + roleName string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "ubuntu-22.04", + args: args{ + name: "ubuntu-22.04", + roleName: "foobar", + }, + want: "rolecule-foobar-ubuntu-22.04", + }, + { + name: "arch", + args: args{ + name: "arch", + roleName: "i_use_arch_btw", + }, + want: "rolecule-i-use-arch-btw-arch", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := generateContainerName(tt.args.name, tt.args.roleName); got != tt.want { + t.Errorf("generateContainerName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/container/engine.go b/pkg/container/engine.go index 3c9d3ba..527f227 100644 --- a/pkg/container/engine.go +++ b/pkg/container/engine.go @@ -2,8 +2,6 @@ package container import ( "fmt" - - "github.com/z0mbix/rolecule/pkg/utils" ) type Engine interface { @@ -21,23 +19,18 @@ type EngineConfig struct { } func NewEngine(name string) (Engine, error) { - if !utils.CommandExists(name) { - return nil, fmt.Errorf("container engine '%s' not found in PATH", name) - } - - if name == "docker" { + switch name { + case "docker": return &DockerEngine{ Name: "docker", Socket: "docker://", }, nil - } - - if name == "podman" { + case "podman": return &PodmanEngine{ Name: "podman", Socket: "podman://", }, nil + default: + return nil, fmt.Errorf("container engine '%s' not recognised (docker and podman currently supported)", name) } - - return nil, fmt.Errorf("container engine '%s' not recognised (docker and podman currently supported)", name) } diff --git a/pkg/container/engine_test.go b/pkg/container/engine_test.go new file mode 100644 index 0000000..dbb1c13 --- /dev/null +++ b/pkg/container/engine_test.go @@ -0,0 +1,51 @@ +package container + +import ( + "reflect" + "testing" +) + +func TestNewEngine(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want Engine + wantErr bool + }{ + { + name: "docker", + args: args{ + name: "docker", + }, + want: &DockerEngine{ + Name: "docker", + Socket: "docker://", + }, + }, + { + name: "podman", + args: args{ + name: "podman", + }, + want: &PodmanEngine{ + Name: "podman", + Socket: "podman://", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewEngine(tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("NewEngine() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewEngine() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/utils/utils.go b/pkg/filesystem/filesystem.go similarity index 94% rename from pkg/utils/utils.go rename to pkg/filesystem/filesystem.go index 8d404f4..87f29c5 100644 --- a/pkg/utils/utils.go +++ b/pkg/filesystem/filesystem.go @@ -1,4 +1,4 @@ -package utils +package filesystem import ( "os" diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index 356a3a2..7dcab0f 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -2,6 +2,7 @@ package instance import ( "fmt" + "path/filepath" "github.com/apex/log" "github.com/z0mbix/rolecule/pkg/container" @@ -23,22 +24,25 @@ type Config struct { } type Instance struct { - Name string - Image string - Arch string - Args []string - Playbook string - TestFile string - SkipTags []string - Tags []string - WorkDir string + Name string + Image string + Arch string + Args []string + Playbook string + TestFile string + SkipTags []string + Tags []string + WorkDir string + RoleName string + RoleDir string + RoleMounts map[string]string container.Engine Provisioner provisioner.Provisioner Verifier verifier.Verifier } func (i *Instance) Create() (string, error) { - workDir := "/src" + workDir := filepath.Join("/etc/ansible/roles", i.RoleName) instanceArgs := []string{ "run", "--privileged", @@ -49,9 +53,13 @@ func (i *Instance) Create() (string, error) { "--tmpfs", "/run/lock", "--tmpfs", "/var/lib/docker", "--cgroupns", "host", + "--workdir", workDir, "--volume", "/sys/fs/cgroup:/sys/fs/cgroup:rw", "--volume", fmt.Sprintf("%s:%s", i.WorkDir, workDir), - "--workdir", workDir, + } + + for src, dst := range i.RoleMounts { + instanceArgs = append(instanceArgs, "--volume", fmt.Sprintf("%s:%s", src, dst)) } if i.Arch != "" { @@ -70,6 +78,11 @@ func (i *Instance) Create() (string, error) { return output, nil } +func (i *Instance) Prepare() error { + env, cmd, args := i.Provisioner.GetInstallDependenciesCommand() + return i.Exec(i.Name, env, cmd, args) +} + func (i *Instance) Converge() error { env, cmd, args := i.Provisioner.GetCommand() return i.Exec(i.Name, env, cmd, args) diff --git a/pkg/provisioner/ansible.go b/pkg/provisioner/ansible.go index c806c16..0661027 100644 --- a/pkg/provisioner/ansible.go +++ b/pkg/provisioner/ansible.go @@ -1,28 +1,33 @@ package provisioner import ( - "fmt" + "path/filepath" "strings" "github.com/apex/log" "golang.org/x/exp/maps" ) -type AnsibleLocalProvisioner struct { - Name string - Command string - Args []string - ExtraArgs []string - SkipTags []string - Tags []string - EnvVars map[string]string - Playbook string +type Dependencies struct { + Collections []string + LocalRoles []string + GalaxyRoles []string } -func (a AnsibleLocalProvisioner) String() string { - return a.Name +type AnsibleLocalProvisioner struct { + Name string + Command string + Args []string + ExtraArgs []string + SkipTags []string + Tags []string + EnvVars map[string]string + Playbook string + Dependencies Dependencies } +var ansibleRoleDir = "/etc/ansible/roles" + var defaultAnsibleConfig = AnsibleLocalProvisioner{ Name: "ansible", Command: "ansible-playbook", @@ -33,7 +38,7 @@ var defaultAnsibleConfig = AnsibleLocalProvisioner{ "localhost,", }, EnvVars: map[string]string{ - "ANSIBLE_ROLES_PATH": ".", + "ANSIBLE_ROLES_PATH": ansibleRoleDir, "ANSIBLE_NOCOWS": "True", }, Playbook: "playbook.yml", @@ -75,7 +80,7 @@ func getAnsibleConfig(config Config) AnsibleLocalProvisioner { } func (a AnsibleLocalProvisioner) WithExtraArgs(args []string) Provisioner { - a.Tags = append(a.ExtraArgs, args...) + a.ExtraArgs = append(a.ExtraArgs, args...) return a } @@ -94,8 +99,30 @@ func (a AnsibleLocalProvisioner) WithPlaybook(playbook string) Provisioner { return a } +func (a AnsibleLocalProvisioner) WithLocalDependencies(dependencies []string) Provisioner { + a.Dependencies.LocalRoles = dependencies + return a +} + +func (a AnsibleLocalProvisioner) WithGalaxyDependencies(dependencies []string) Provisioner { + a.Dependencies.GalaxyRoles = dependencies + return a +} + +func (a AnsibleLocalProvisioner) GetDependencies() Dependencies { + return a.Dependencies +} + +func (a AnsibleLocalProvisioner) GetInstallDependenciesCommand() (map[string]string, string, []string) { + log.Debugf("installing galaxy role(s):") + args := []string{"install", "--roles-path", ansibleRoleDir} + args = append(args, a.Dependencies.GalaxyRoles...) + + return a.EnvVars, "ansible-galaxy", args +} + func (a AnsibleLocalProvisioner) GetCommand() (map[string]string, string, []string) { - playbookPath := fmt.Sprintf("tests/%s", a.Playbook) + playbookPath := filepath.Join(testDirectory, a.Playbook) args := a.Args for _, tag := range a.Tags { @@ -111,3 +138,7 @@ func (a AnsibleLocalProvisioner) GetCommand() (map[string]string, string, []stri args = append(args, playbookPath) return a.EnvVars, a.Command, args } + +func (a AnsibleLocalProvisioner) String() string { + return a.Name +} diff --git a/pkg/provisioner/ansible_metadata.go b/pkg/provisioner/ansible_metadata.go new file mode 100644 index 0000000..d851eb4 --- /dev/null +++ b/pkg/provisioner/ansible_metadata.go @@ -0,0 +1,68 @@ +package provisioner + +import ( + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +type RoleMetadata struct { + Collections []string `yaml:"collections"` + Dependencies []struct { + Role string `yaml:"role"` + } `yaml:"dependencies"` +} + +func (md *RoleMetadata) LocalDependencies() []string { + var roles []string + + for _, dep := range md.Dependencies { + // if role name does not contain a dot it is a local role + if !strings.Contains(dep.Role, ".") { + roles = append(roles, dep.Role) + } + } + + return roles +} + +func (md *RoleMetadata) GalaxyDependencies() []string { + var roles []string + + for _, dep := range md.Dependencies { + // if role name contains a dot it is a galaxy role + if strings.Contains(dep.Role, ".") { + roles = append(roles, dep.Role) + } + } + + return roles +} + +// GetRoleMetadata parses the role meta/main.yml file +// and returns a RoleMetadata struct +func GetRoleMetadata() (*RoleMetadata, error) { + metaFile := "meta/main.yml" + if _, err := os.Stat(metaFile); os.IsNotExist(err) { + return &RoleMetadata{}, nil + } + + // Open the meta/main.yml file + file, err := os.Open(metaFile) + if err != nil { + return &RoleMetadata{}, err + } + defer file.Close() + + // Create a RoleMetadata struct to hold the parsed data + var metadata RoleMetadata + + // Decode the YAML file into the RoleMetadata struct + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(&metadata); err != nil { + return &RoleMetadata{}, err + } + + return &metadata, nil +} diff --git a/pkg/provisioner/ansible_test.go b/pkg/provisioner/ansible_test.go index dd01335..5bb2da1 100644 --- a/pkg/provisioner/ansible_test.go +++ b/pkg/provisioner/ansible_test.go @@ -1,6 +1,7 @@ package provisioner import ( + "path/filepath" "reflect" "testing" ) @@ -38,7 +39,7 @@ func TestAnsibleProvisioner_GetCommand(t *testing.T) { name: "command", a: defaultAnsibleConfig, want: map[string]string{ - "ANSIBLE_ROLES_PATH": ".", + "ANSIBLE_ROLES_PATH": "/etc/ansible/roles", "ANSIBLE_NOCOWS": "True", }, want1: "ansible-playbook", @@ -47,7 +48,7 @@ func TestAnsibleProvisioner_GetCommand(t *testing.T) { "local", "--inventory", "localhost,", - "tests/playbook.yml", + filepath.Join("tests", "playbook.yml"), }, }, } @@ -66,3 +67,512 @@ func TestAnsibleProvisioner_GetCommand(t *testing.T) { }) } } + +func TestAnsibleLocalProvisioner_WithExtraArgs(t *testing.T) { + type fields struct { + Name string + } + type args struct { + args []string + } + tests := []struct { + name string + fields fields + args args + want Provisioner + }{ + { + name: "MultipleExtraArgs", + fields: fields{ + Name: "ansible", + }, + args: args{ + args: []string{ + "--diff", + "--verbose", + }, + }, + want: AnsibleLocalProvisioner{ + Name: "ansible", + ExtraArgs: []string{"--diff", "--verbose"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + } + if got := a.WithExtraArgs(tt.args.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithExtraArgs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAnsibleLocalProvisioner_WithTags(t *testing.T) { + type fields struct { + Name string + } + type args struct { + args []string + } + tests := []struct { + name string + fields fields + args args + want Provisioner + }{ + { + name: "MultipleTags", + fields: fields{ + Name: "ansible", + }, + args: args{ + args: []string{ + "build", + "configure", + }, + }, + want: AnsibleLocalProvisioner{ + Name: "ansible", + Tags: []string{"build", "configure"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + } + if got := a.WithTags(tt.args.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithExtraArgs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAnsibleLocalProvisioner_WithSkipTags(t *testing.T) { + type fields struct { + Name string + } + type args struct { + args []string + } + tests := []struct { + name string + fields fields + args args + want Provisioner + }{ + { + name: "SkipTags", + fields: fields{ + Name: "ansible", + }, + args: args{ + args: []string{ + "ignore", + }, + }, + want: AnsibleLocalProvisioner{ + Name: "ansible", + SkipTags: []string{"ignore"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + } + if got := a.WithSkipTags(tt.args.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithExtraArgs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAnsibleLocalProvisioner_WithPlaybook(t *testing.T) { + type fields struct { + Name string + Command string + Args []string + ExtraArgs []string + SkipTags []string + Tags []string + EnvVars map[string]string + Playbook string + Dependencies Dependencies + } + type args struct { + playbook string + } + tests := []struct { + name string + fields fields + args args + want Provisioner + }{ + { + name: "Playbook", + fields: fields{ + Name: "ansible", + }, + args: args{ + playbook: "playbook.yaml", + }, + want: AnsibleLocalProvisioner{ + Name: "ansible", + Playbook: "playbook.yaml", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + Playbook: tt.fields.Playbook, + } + if got := a.WithPlaybook(tt.args.playbook); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithPlaybook() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAnsibleLocalProvisioner_WithLocalDependencies(t *testing.T) { + type fields struct { + Name string + Command string + Args []string + ExtraArgs []string + SkipTags []string + Tags []string + EnvVars map[string]string + Playbook string + Dependencies Dependencies + } + type args struct { + dependencies []string + } + tests := []struct { + name string + fields fields + args args + want Provisioner + }{ + { + name: "local", + fields: fields{ + Name: "ansible", + }, + args: args{ + dependencies: []string{ + "depone", + "deptwo", + }, + }, + want: AnsibleLocalProvisioner{ + Name: "ansible", + Dependencies: Dependencies{ + LocalRoles: []string{ + "depone", + "deptwo", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + Dependencies: tt.fields.Dependencies, + } + if got := a.WithLocalDependencies(tt.args.dependencies); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithLocalDependencies() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAnsibleLocalProvisioner_WithGalaxyDependencies(t *testing.T) { + type fields struct { + Name string + Dependencies Dependencies + } + type args struct { + dependencies []string + } + tests := []struct { + name string + fields fields + args args + want Provisioner + }{ + { + name: "galaxy", + fields: fields{ + Name: "ansible", + }, + args: args{ + dependencies: []string{ + "z0mbix.depone", + "z0mbix.deptwo", + }, + }, + want: AnsibleLocalProvisioner{ + Name: "ansible", + Dependencies: Dependencies{ + GalaxyRoles: []string{ + "z0mbix.depone", + "z0mbix.deptwo", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + Dependencies: tt.fields.Dependencies, + } + if got := a.WithGalaxyDependencies(tt.args.dependencies); !reflect.DeepEqual(got, tt.want) { + t.Errorf("WithGalaxyDependencies() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestAnsibleLocalProvisioner_GetInstallDependenciesCommand(t *testing.T) { + type fields struct { + Name string + Command string + Args []string + ExtraArgs []string + SkipTags []string + Tags []string + EnvVars map[string]string + Playbook string + Dependencies Dependencies + } + tests := []struct { + name string + fields fields + want map[string]string + want1 string + want2 []string + }{ + { + name: "basic", + fields: fields{ + Name: "ansible", + Command: "ansible-playbook", + Dependencies: Dependencies{ + Collections: nil, + LocalRoles: nil, + GalaxyRoles: []string{ + "z0mbix.depone", + "z0mbix.deptwo", + }, + }, + }, + want: nil, + //want: map[string]string{ + // "ANSIBLE_ROLES_PATH": "/etc/ansible/roles", + // "ANSIBLE_NOCOWS": "True", + //}, + want1: "ansible-galaxy", + want2: []string{ + "install", + "--roles-path", + "/etc/ansible/roles", + "z0mbix.depone", + "z0mbix.deptwo", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + Command: tt.fields.Command, + Args: tt.fields.Args, + ExtraArgs: tt.fields.ExtraArgs, + SkipTags: tt.fields.SkipTags, + Tags: tt.fields.Tags, + EnvVars: tt.fields.EnvVars, + Playbook: tt.fields.Playbook, + Dependencies: tt.fields.Dependencies, + } + got, got1, got2 := a.GetInstallDependenciesCommand() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetInstallDependenciesCommand() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("GetInstallDependenciesCommand() got1 = %v, want %v", got1, tt.want1) + } + if !reflect.DeepEqual(got2, tt.want2) { + t.Errorf("GetInstallDependenciesCommand() got2 = %v, want %v", got2, tt.want2) + } + }) + } +} + +func TestAnsibleLocalProvisioner_GetCommand(t *testing.T) { + type fields struct { + Name string + Command string + Args []string + ExtraArgs []string + SkipTags []string + Tags []string + EnvVars map[string]string + Playbook string + Dependencies Dependencies + } + tests := []struct { + name string + fields fields + want map[string]string + want1 string + want2 []string + }{ + { + name: "basic", + fields: fields{ + Name: "ansible", + Command: "ansible-playbook", + EnvVars: map[string]string{ + "ANSIBLE_ROLES_PATH": "/etc/ansible/roles", + "ANSIBLE_NOCOWS": "True", + }, + Args: []string{ + "--connection", + "local", + "--inventory", + "localhost,", + }, + Playbook: "playbook.yml", + }, + want: map[string]string{ + "ANSIBLE_ROLES_PATH": "/etc/ansible/roles", + "ANSIBLE_NOCOWS": "True", + }, + want1: "ansible-playbook", + want2: []string{ + "--connection", + "local", + "--inventory", + "localhost,", + filepath.Join("tests", "playbook.yml"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + Command: tt.fields.Command, + Args: tt.fields.Args, + ExtraArgs: tt.fields.ExtraArgs, + SkipTags: tt.fields.SkipTags, + Tags: tt.fields.Tags, + EnvVars: tt.fields.EnvVars, + Playbook: tt.fields.Playbook, + Dependencies: tt.fields.Dependencies, + } + got, got1, got2 := a.GetCommand() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetCommand() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("GetCommand() got1 = %v, want %v", got1, tt.want1) + } + if !reflect.DeepEqual(got2, tt.want2) { + t.Errorf("GetCommand() got2 = %v, want %v", got2, tt.want2) + } + }) + } +} + +func Test_getAnsibleConfig(t *testing.T) { + type args struct { + config Config + } + tests := []struct { + name string + args args + want AnsibleLocalProvisioner + }{ + { + name: "foobar", + args: args{ + config: Config{ + Name: "foo", + }, + }, + want: AnsibleLocalProvisioner{ + Name: "ansible", + Command: "ansible-playbook", + Args: []string{ + "--connection", + "local", + "--inventory", + "localhost,", + }, + EnvVars: map[string]string{ + "ANSIBLE_ROLES_PATH": ansibleRoleDir, + "ANSIBLE_NOCOWS": "True", + }, + Playbook: "playbook.yml", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getAnsibleConfig(tt.args.config); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getAnsibleConfig() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAnsibleLocalProvisioner_String(t *testing.T) { + type fields struct { + Name string + Command string + Args []string + ExtraArgs []string + SkipTags []string + Tags []string + EnvVars map[string]string + Playbook string + Dependencies Dependencies + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "ansible", + fields: fields{ + Name: "ansible", + }, + want: "ansible", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := AnsibleLocalProvisioner{ + Name: tt.fields.Name, + } + if got := a.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index 3ab1dda..bfe2b95 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -5,10 +5,14 @@ import ( ) type Provisioner interface { + GetInstallDependenciesCommand() (map[string]string, string, []string) GetCommand() (map[string]string, string, []string) WithSkipTags([]string) Provisioner WithTags([]string) Provisioner WithPlaybook(string) Provisioner + GetDependencies() Dependencies + WithLocalDependencies([]string) Provisioner + WithGalaxyDependencies([]string) Provisioner String() string } @@ -28,3 +32,5 @@ func NewProvisioner(config Config) (Provisioner, error) { return nil, fmt.Errorf("provisioner '%s' not recognised", config.Name) } + +var testDirectory = "tests" diff --git a/testing/ansible/roles/simple/meta/main.yml b/testing/ansible/roles/simple/meta/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/testing/ansible/roles/simple/rolecule.yml b/testing/ansible/roles/simple/rolecule.yml index 20f27df..b23e1a4 100644 --- a/testing/ansible/roles/simple/rolecule.yml +++ b/testing/ansible/roles/simple/rolecule.yml @@ -16,5 +16,5 @@ verifier: - tap instances: - - name: rockylinux-9.1 - image: rockylinux-systemd:9.1 + - name: ubuntu-24.04 + image: ubuntu-systemd:24.04 diff --git a/testing/ansible/roles/simple/tests/goss.yaml b/testing/ansible/roles/simple/tests/goss.yaml index 03b485e..a14397d 100644 --- a/testing/ansible/roles/simple/tests/goss.yaml +++ b/testing/ansible/roles/simple/tests/goss.yaml @@ -11,7 +11,7 @@ file: owner: root group: root filetype: file - contains: + contents: - "test1" /tmp/test2.txt: exists: true @@ -19,7 +19,7 @@ file: owner: root group: root filetype: file - contains: + contents: - "test2" package: git: diff --git a/testing/ansible/roles/simple/tests/playbook.yml b/testing/ansible/roles/simple/tests/playbook.yml index c8e1509..306fecb 100644 --- a/testing/ansible/roles/simple/tests/playbook.yml +++ b/testing/ansible/roles/simple/tests/playbook.yml @@ -2,4 +2,4 @@ - name: test hosts: localhost roles: - - . + - simple diff --git a/testing/ansible/roles/sshd/tests/goss.yaml b/testing/ansible/roles/sshd/tests/goss.yaml index 2d356eb..22d97da 100644 --- a/testing/ansible/roles/sshd/tests/goss.yaml +++ b/testing/ansible/roles/sshd/tests/goss.yaml @@ -11,7 +11,7 @@ file: owner: root group: root filetype: file - contains: + contents: - "Port 22" - "AddressFamily any" - "ListenAddress 0.0.0.0" diff --git a/testing/ansible/roles/sshd/tests/playbook.yml b/testing/ansible/roles/sshd/tests/playbook.yml index c8e1509..fe733e4 100644 --- a/testing/ansible/roles/sshd/tests/playbook.yml +++ b/testing/ansible/roles/sshd/tests/playbook.yml @@ -2,4 +2,4 @@ - name: test hosts: localhost roles: - - . + - sshd diff --git a/testing/ansible/roles/sshd/rolecule.yml b/testing/ansible/roles/sshd/tests/rolecule.yml similarity index 76% rename from testing/ansible/roles/sshd/rolecule.yml rename to testing/ansible/roles/sshd/tests/rolecule.yml index 2fb4371..431107a 100644 --- a/testing/ansible/roles/sshd/rolecule.yml +++ b/testing/ansible/roles/sshd/tests/rolecule.yml @@ -1,6 +1,6 @@ --- engine: - name: podman + name: docker provisioner: name: ansible @@ -19,24 +19,19 @@ instances: - name: ubuntu-22.04 image: ubuntu-systemd:22.04 playbook: ubuntu/playbook.yml + - name: ubuntu-22.04-build image: ubuntu-systemd:22.04 - arch: amd64 playbook: ubuntu/playbook.yml testfile: goss-build.yaml tags: - build + - name: rockylinux-9.1 image: rockylinux-systemd:9.1 + - name: rockylinux-9.1-build image: rockylinux-systemd:9.1 testfile: goss-build.yaml tags: - build - - name: rockylinux-9.1-build - image: rockylinux-systemd:9.1 - testfile: goss-build.yaml - tags: - - build - skip_tags: - - provision diff --git a/testing/ansible/roles/sshd/tests/ubuntu/playbook.yml b/testing/ansible/roles/sshd/tests/ubuntu/playbook.yml index c8e1509..6c98c22 100644 --- a/testing/ansible/roles/sshd/tests/ubuntu/playbook.yml +++ b/testing/ansible/roles/sshd/tests/ubuntu/playbook.yml @@ -1,5 +1,14 @@ --- +- name: update apt cache + hosts: localhost + tasks: + - name: update apt cache + ansible.builtin.apt: + update_cache: yes + tags: + - always + - name: test hosts: localhost roles: - - . + - sshd diff --git a/testing/ansible/roles/sysctl/defaults/main.yml b/testing/ansible/roles/sysctl/defaults/main.yml new file mode 100644 index 0000000..089998c --- /dev/null +++ b/testing/ansible/roles/sysctl/defaults/main.yml @@ -0,0 +1,2 @@ +--- +ansible_swappiness: 15 diff --git a/testing/ansible/roles/sysctl/meta/main.yml b/testing/ansible/roles/sysctl/meta/main.yml new file mode 100644 index 0000000..794757e --- /dev/null +++ b/testing/ansible/roles/sysctl/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - role: simple diff --git a/testing/ansible/roles/sysctl/tasks/main.yml b/testing/ansible/roles/sysctl/tasks/main.yml new file mode 100644 index 0000000..7ce13f5 --- /dev/null +++ b/testing/ansible/roles/sysctl/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: manage swappiness + ansible.builtin.sysctl: + name: vm.swappiness + value: "{{ ansible_swappiness }}" + state: present + tags: + - provision + +- name: output swappiness + ansible.builtin.debug: + msg: "vm.swappiness is {{ ansible_swappiness }}" diff --git a/testing/ansible/roles/sysctl/tests/goss.yaml b/testing/ansible/roles/sysctl/tests/goss.yaml new file mode 100644 index 0000000..fa649fc --- /dev/null +++ b/testing/ansible/roles/sysctl/tests/goss.yaml @@ -0,0 +1,3 @@ +kernel-param: + vm.swappiness: + value: '15' diff --git a/testing/ansible/roles/sysctl/tests/playbook.yml b/testing/ansible/roles/sysctl/tests/playbook.yml new file mode 100644 index 0000000..fcdda76 --- /dev/null +++ b/testing/ansible/roles/sysctl/tests/playbook.yml @@ -0,0 +1,5 @@ +--- +- name: test + hosts: localhost + roles: + - sysctl diff --git a/testing/ansible/roles/sysctl/tests/rolecule.yml b/testing/ansible/roles/sysctl/tests/rolecule.yml new file mode 100644 index 0000000..86a4b7e --- /dev/null +++ b/testing/ansible/roles/sysctl/tests/rolecule.yml @@ -0,0 +1,21 @@ +--- +engine: + name: docker + +provisioner: + name: ansible + extra_args: + - --diff + - --verbose + env: + ANSIBLE_NOCOLOR: False + +verifier: + name: goss + extra_args: + - --format + - tap + +instances: + - name: ubuntu-24.04 + image: ubuntu-systemd:24.04 diff --git a/testing/ansible/roles/sysctl/tests/scenarios/build/test_all.py b/testing/ansible/roles/sysctl/tests/scenarios/build/test_all.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/ansible/roles/sysctl/tests/scenarios/provision/test_all.py b/testing/ansible/roles/sysctl/tests/scenarios/provision/test_all.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/ansible/roles/website/defaults/main.yml b/testing/ansible/roles/website/defaults/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/testing/ansible/roles/website/files/index.html b/testing/ansible/roles/website/files/index.html new file mode 100644 index 0000000..a2e1c73 --- /dev/null +++ b/testing/ansible/roles/website/files/index.html @@ -0,0 +1,9 @@ + + + Home page + + +

Welcome!

+

Hello visitor

+ + diff --git a/testing/ansible/roles/website/meta/main.yml b/testing/ansible/roles/website/meta/main.yml new file mode 100644 index 0000000..7cdd9ee --- /dev/null +++ b/testing/ansible/roles/website/meta/main.yml @@ -0,0 +1,4 @@ +dependencies: + - role: geerlingguy.nginx + - role: geerlingguy.redis + - role: simple diff --git a/testing/ansible/roles/website/tasks/main.yml b/testing/ansible/roles/website/tasks/main.yml new file mode 100644 index 0000000..d2350b4 --- /dev/null +++ b/testing/ansible/roles/website/tasks/main.yml @@ -0,0 +1,8 @@ +- name: output the hostname + ansible.builtin.debug: + msg: "hostname is {{ ansible_hostname }}" + +- name: install index.html + ansible.builtin.copy: + src: index.html + dest: /var/www/html/index.html diff --git a/testing/ansible/roles/website/tests/goss.yaml b/testing/ansible/roles/website/tests/goss.yaml new file mode 100644 index 0000000..d5a554b --- /dev/null +++ b/testing/ansible/roles/website/tests/goss.yaml @@ -0,0 +1,17 @@ +file: + /var/www/html/index.html: + exists: true + mode: "0644" + owner: root + group: root + filetype: file + contents: | + + + Home page + + +

Welcome!

+

Hello visitor

+ + diff --git a/testing/ansible/roles/website/tests/playbook.yml b/testing/ansible/roles/website/tests/playbook.yml new file mode 100644 index 0000000..59fa1da --- /dev/null +++ b/testing/ansible/roles/website/tests/playbook.yml @@ -0,0 +1,5 @@ +--- +- name: test + hosts: localhost + roles: + - website diff --git a/testing/ansible/roles/website/tests/rolecule.yml b/testing/ansible/roles/website/tests/rolecule.yml new file mode 100644 index 0000000..bed82ea --- /dev/null +++ b/testing/ansible/roles/website/tests/rolecule.yml @@ -0,0 +1,18 @@ +--- +provisioner: + name: ansible + extra_args: + - --diff + - --verbose + env: + ANSIBLE_NOCOLOR: False + +verifier: + name: goss + extra_args: + - --format + - tap + +instances: + - name: ubuntu-24.04 + image: ubuntu-systemd:24.04 diff --git a/testing/ansible/ubuntu-22.04-systemd.Dockerfile b/testing/ansible/ubuntu-22.04-systemd.Dockerfile index bc3a892..5f110cd 100644 --- a/testing/ansible/ubuntu-22.04-systemd.Dockerfile +++ b/testing/ansible/ubuntu-22.04-systemd.Dockerfile @@ -1,14 +1,15 @@ FROM ubuntu:22.04 -ENV container docker -ENV DEBIAN_FRONTEND noninteractive +ENV container=docker +ENV DEBIAN_FRONTEND=noninteractive RUN sed -i 's/# deb/deb/g' /etc/apt/sources.list # hadolint ignore=DL3008 RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates systemd curl cron python3 python3-pip sudo bash iproute2 net-tools vim \ - && python3 -m pip install ansible ansible-core \ + && apt-get install -y --no-install-recommends ca-certificates software-properties-common systemd curl cron gpg-agent less sudo bash iproute2 net-tools python3-apt vim \ + && apt-add-repository -y ppa:ansible/ansible 1>/dev/null \ + && apt-get install -y --no-install-recommends ansible ansible-lint \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* @@ -34,7 +35,7 @@ RUN curl -sSL https://github.com/goss-org/goss/releases/latest/download/goss-lin WORKDIR / RUN systemctl set-default multi-user.target -ENV init /lib/systemd/systemd +ENV init=/lib/systemd/systemd VOLUME [ "/sys/fs/cgroup" ] ENTRYPOINT ["/lib/systemd/systemd"] diff --git a/testing/ansible/ubuntu-24.04-systemd.Dockerfile b/testing/ansible/ubuntu-24.04-systemd.Dockerfile new file mode 100644 index 0000000..da61619 --- /dev/null +++ b/testing/ansible/ubuntu-24.04-systemd.Dockerfile @@ -0,0 +1,41 @@ +FROM ubuntu:24.04 + +ENV container=docker +ENV DEBIAN_FRONTEND=noninteractive + +RUN sed -i 's/# deb/deb/g' /etc/apt/sources.list + +# hadolint ignore=DL3008 +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates software-properties-common systemd curl cron gpg-agent less sudo bash iproute2 net-tools python3-apt vim \ + && apt-add-repository -y ppa:ansible/ansible 1>/dev/null \ + && apt-get install -y --no-install-recommends ansible ansible-lint \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +RUN dpkg-reconfigure ca-certificates + +WORKDIR /lib/systemd/system/sysinit.target.wants/ +# hadolint ignore=SC2010,SC2086 +RUN ls | grep -v systemd-tmpfiles-setup | xargs rm -f $1 + +RUN rm -f /lib/systemd/system/multi-user.target.wants/* \ + /etc/systemd/system/*.wants/* \ + /lib/systemd/system/local-fs.target.wants/* \ + /lib/systemd/system/sockets.target.wants/*udev* \ + /lib/systemd/system/sockets.target.wants/*initctl* \ + /lib/systemd/system/basic.target.wants/* \ + /lib/systemd/system/anaconda.target.wants/* \ + /lib/systemd/system/plymouth* \ + /lib/systemd/system/systemd-update-utmp* + +# Install goss (https://github.com/goss-org/goss) +RUN curl -sSL https://github.com/goss-org/goss/releases/latest/download/goss-linux-amd64 -o /usr/local/bin/goss && \ + chmod +rx /usr/local/bin/goss + +WORKDIR / +RUN systemctl set-default multi-user.target +ENV init=/lib/systemd/systemd +VOLUME [ "/sys/fs/cgroup" ] + +ENTRYPOINT ["/lib/systemd/systemd"]