diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..f05340dd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +### Definition of Ready + +- [ ] Short description of the feature/issue is added in the pr description +- [ ] PR is linked to the corresponding user story +- [ ] Acceptance criteria are met +- [ ] All open todos and follow ups are defined in a new ticket and justified +- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented. +- [ ] No debug or dead code +- [ ] My code has no repetitions +- [ ] All non-functional requirements are met +- [ ] The generic lifecycle acceptance test passes for affected resources. +- [ ] Examples are up-to-date and meaningful. The provider version is incremented. +- [ ] Docs are generated. +- [ ] Code is generated where possible. diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml new file mode 100644 index 00000000..a75cb44d --- /dev/null +++ b/.github/workflows/pull_request.yaml @@ -0,0 +1,55 @@ +name: Test Provider + +on: pull_request + +jobs: + test: + + runs-on: ubuntu-20.04 + + permissions: + contents: read + + steps: + + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Make Machinekey Directory Writable + working-directory: acceptance + run: "chmod -R 777 machinekey" + + - name: Set up ZITADEL + working-directory: acceptance + run: docker compose up -d zitadel + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Download Go Modules + run: go mod download + + - name: Await ZITADEL + working-directory: acceptance + run: docker compose run wait_for_zitadel + + - name: Run Acceptance Tests + run: TF_ACC=1 TF_ACC_ZITADEL_TOKEN=$(pwd)/acceptance/machinekey/zitadel-admin-sa.json go test ./... + + - name: Save ZITADEL Logs + working-directory: acceptance + if: always() + run: docker compose logs zitadel > .zitadel.log + + - name: Archive ZITADEL Logs + if: always() + uses: actions/upload-artifact@v3 + with: + name: pull-request-tests + path: | + acceptance/.zitadel.log + retention-days: 30 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 903e346d..219ec4dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,10 +6,10 @@ ```bash # export the printed environment variable from the go run ./... -debug command above. E.g. export TF_REATTACH_PROVIDERS='{"registry.terraform.io/zitadel/zitadel":{"Protocol":"grpc","ProtocolVersion":6,"Pid":8123,"Test":true,"Addr":{"Network":"unix","String":"/tmp/plugin275634719"}}}' - + # go to a directory containing .tf files. cd /my-zitadel-terraform-files - + # apply them terraform apply ``` @@ -17,15 +17,24 @@ # Run Acceptance Tests -Ensure ZITADEL listens at http://localhost:8080 and you have a service account key in your local filesystem. -The easiest way to achieve that is [to follow this guide](https://zitadel.com/docs/self-hosting/deploy/compose#docker-compose-with-service-account). +Run a local ZITADEL instance using docker compose. ```bash -TF_ACC=1 TF_ACC_ZITADEL_TOKEN=/my-token.json go test ./... +# To have the machine key written with the correct ownership, set your current users ID. +export ZITADEL_DEV_UID="$(id -u)" + +# Pull Images +docker compose --file ./acceptance/docker-compose.yaml pull + +# Setup ZITADEL +docker compose --file ./acceptance/docker-compose.yaml run wait_for_zitadel ``` -The tests are flaky when resources should be cleaned up. -This results in dangling resources. +Run the accepance tests using the machine key generated by ZITADEL. + +```bash +TF_ACC=1 TF_ACC_ZITADEL_TOKEN=$(pwd)/acceptance/machinekey/zitadel-admin-sa.json go test ./... +``` # Generate Docs diff --git a/acceptance/docker-compose.yaml b/acceptance/docker-compose.yaml new file mode 100644 index 00000000..30be8c2d --- /dev/null +++ b/acceptance/docker-compose.yaml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + zitadel: + user: '${ZITADEL_DEV_UID}' + image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}' + command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml' + ports: + - "8080:8080" + volumes: + - ./machinekey:/machinekey + - ./zitadel.yaml:/zitadel.yaml + depends_on: + db: + condition: 'service_healthy' + + db: + image: 'cockroachdb/cockroach:v22.2.2' + command: 'start-single-node --insecure --http-addr :9090' + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9090/health?ready=1'] + interval: '10s' + timeout: '30s' + retries: 5 + start_period: '20s' + ports: + - "26257:26257" + - "9090:9090" + + wait_for_zitadel: + image: curlimages/curl:8.00.1 + command: [ "/bin/sh", "-c", "i=0; while ! curl http://zitadel:8080/debug/ready && [ $$i -lt 30 ]; do sleep 1; i=$$((i+1)); done; [ $$i -eq 30 ] && exit 1 || exit 0" ] + depends_on: + - zitadel diff --git a/acceptance/machinekey/.gitignore b/acceptance/machinekey/.gitignore new file mode 100644 index 00000000..7c9f54d0 --- /dev/null +++ b/acceptance/machinekey/.gitignore @@ -0,0 +1 @@ +zitadel-admin-sa.json diff --git a/acceptance/machinekey/.gitkeep b/acceptance/machinekey/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/acceptance/zitadel.yaml b/acceptance/zitadel.yaml new file mode 100644 index 00000000..231d1311 --- /dev/null +++ b/acceptance/zitadel.yaml @@ -0,0 +1,18 @@ +FirstInstance: + MachineKeyPath: /machinekey/zitadel-admin-sa.json + Org: + Machine: + Machine: + Username: zitadel-admin-sa + Name: Admin + MachineKey: + Type: 1 + +Database: + Cockroach: + Host: db + +Logstore: + Access: + Stdout: + Enabled: true diff --git a/zitadel/v2/helper/test_utils/checks.go b/zitadel/v2/helper/test_utils/checks.go index ddce8f5b..50e7e800 100644 --- a/zitadel/v2/helper/test_utils/checks.go +++ b/zitadel/v2/helper/test_utils/checks.go @@ -1,7 +1,9 @@ package test_utils import ( + "fmt" "regexp" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -14,3 +16,25 @@ func CheckStateHasIDSet(frame BaseTestFrame) resource.TestCheckFunc { return resource.TestMatchResourceAttr(frame.TerraformName, "id", idPattern)(state) } } + +func CheckAMinute(check resource.TestCheckFunc) resource.TestCheckFunc { + return func(state *terraform.State) error { + return retryAMinute(func() error { + return check(state) + }) + } +} + +func retryAMinute(try func() error) error { + start := time.Now() + for { + err := try() + if err == nil { + return nil + } + if time.Since(start) > time.Minute { + return fmt.Errorf("function failed after retrying for a minute: %w", err) + } + time.Sleep(time.Second) + } +} diff --git a/zitadel/v2/helper/test_utils/lifecyletest.go b/zitadel/v2/helper/test_utils/lifecyletest.go index 4c350770..c456aa0b 100644 --- a/zitadel/v2/helper/test_utils/lifecyletest.go +++ b/zitadel/v2/helper/test_utils/lifecyletest.go @@ -33,7 +33,7 @@ func RunLifecyleTest( }, { // Check resource is created Config: initialConfig, Check: resource.ComposeAggregateTestCheckFunc( - checkRemoteProperty(initialProperty), + CheckAMinute(checkRemoteProperty(initialProperty)), CheckStateHasIDSet(frame), ), }, { // Check updating name has a diff @@ -43,7 +43,7 @@ func RunLifecyleTest( PlanOnly: true, }, { // Check remote state can be updated Config: updatedNameConfig, - Check: checkRemoteProperty(updatedProperty), + Check: CheckAMinute(checkRemoteProperty(updatedProperty)), }, } if secretAttribute != "" { @@ -77,7 +77,7 @@ func RunLifecyleTest( } resource.Test(t, resource.TestCase{ ProviderFactories: ZitadelProviderFactories(frame.ConfiguredProvider), - CheckDestroy: checkDestroy, + CheckDestroy: CheckAMinute(checkDestroy), Steps: steps, }) } diff --git a/zitadel/v2/helper/test_utils/org_frame.go b/zitadel/v2/helper/test_utils/org_frame.go index 27b9d61f..84c404e9 100644 --- a/zitadel/v2/helper/test_utils/org_frame.go +++ b/zitadel/v2/helper/test_utils/org_frame.go @@ -29,16 +29,25 @@ func NewOrgTestFrame(resourceType string) (*OrgTestFrame, error) { if err != nil { return nil, err } - org, err := mgmtClient.GetOrgByDomainGlobal(baseFrame, &management.GetOrgByDomainGlobalRequest{Domain: fmt.Sprintf("%s.%s", orgName, domain)}) - orgID := org.GetOrg().GetId() - if status.Code(err) == codes.NotFound { - var newOrg *management.AddOrgResponse - newOrg, err = mgmtClient.AddOrg(baseFrame, &management.AddOrgRequest{Name: orgName}) - orgID = newOrg.GetId() - } - if err != nil { + org, err := mgmtClient.AddOrg(baseFrame, &management.AddOrgRequest{Name: orgName}) + alreadyExists := status.Code(err) == codes.AlreadyExists + if err != nil && !alreadyExists { return nil, err } + orgID := org.GetId() + if alreadyExists { + err := retryAMinute(func() error { + getOrgResp, getOrgErr := mgmtClient.GetOrgByDomainGlobal(baseFrame, &management.GetOrgByDomainGlobalRequest{Domain: fmt.Sprintf("%s.%s", orgName, domain)}) + if getOrgErr != nil { + return getOrgErr + } + orgID = getOrgResp.GetOrg().GetId() + return nil + }) + if err != nil { + return nil, err + } + } mgmtClient, err = helper.GetManagementClient(baseFrame.ClientInfo, orgID) return &OrgTestFrame{ BaseTestFrame: *baseFrame,