diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..ed42608 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,7 @@ +name: build +on: + push: +jobs: + maven: + uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@main + secrets: inherit diff --git a/.gitea/workflows/qrun-release.yaml b/.gitea/workflows/qrun-release.yaml deleted file mode 100644 index 5ebc530..0000000 --- a/.gitea/workflows/qrun-release.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository. -name: qrun-release -on: - push: {} - workflow_dispatch: {} -permissions: - contents: read - actions: read -jobs: - release: - uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-docker.yaml@qrun-release-v1 - with: - runner: linux-small - docker-runner: container-build - browsers: true - browser-runner: browser-build - postpublish: - if: ${{ needs.release.outputs.capture == 'true' }} - needs: release - permissions: - contents: write - actions: write - uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1 - with: - runner: linux-small diff --git a/.gitea/workflows/release-rc.yaml b/.gitea/workflows/release-rc.yaml deleted file mode 100644 index 12ec9fc..0000000 --- a/.gitea/workflows/release-rc.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Release candidate -'on': - workflow_dispatch: - inputs: - dry_run: - description: Plan a cut without changing release state - type: boolean - default: false - finalize: - description: Retry finalization of an existing cut; do not cut another RC - type: boolean - default: false - recover: - description: Recover the same published RC after a deployment timeout; never - reupload - type: boolean - default: false - circle_build: - description: Original release run id (recovery only) - type: string - default: '' - merge_sha: - description: Existing cut merge_sha (finalize retry only) - type: string - default: '' - bump_sha: - description: Existing cut bump_sha (finalize retry only) - type: string - default: '' - branch: - description: Existing cut branch (finalize retry only) - type: string - default: '' - version: - description: Existing cut version (finalize retry only) - type: string - default: '' - pull_request: - types: - - closed - branches: - - develop -permissions: - contents: read -jobs: - release-rc: - # The old qbit-rc-published repository_dispatch is replaced by the already-proven workflow_dispatch - # completion: the publisher's own ActionsWRITE job dispatches this workflow with finalize=true plus - # merge_sha/bump_sha/branch/version (same inputs as the manual finalize retry). - permissions: - contents: write - actions: read - pull-requests: write - if: github.event_name == 'workflow_dispatch' - || (github.event_name == 'pull_request' && github.event.pull_request.merged - == true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name, - 'release:rc')) - uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qbit-release-rc.yaml@qrun-release-v1 - with: - dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false - }} - finalize: ${{ github.event_name == 'workflow_dispatch' && inputs.finalize }} - recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false - }} - circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build - || '' }} - merge_sha: ${{ inputs.merge_sha || '' }} - bump_sha: ${{ inputs.bump_sha || '' }} - branch: ${{ inputs.branch || '' }} - version: ${{ inputs.version || '' }} - secrets: - QRUN_TAG_SIGNING_KEY: ${{ secrets.QRUN_TAG_SIGNING_KEY }} diff --git a/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java b/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java index 016c3ef..383f1d6 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java +++ b/src/test/java/com/kingsrook/qbits/authentik/SchemaDriftTest.java @@ -161,7 +161,7 @@ public class SchemaDriftTest { return (surfacePath); } - String regex = surfacePath.replace("{key}", "\\{[^/]+\\}"); + String regex = java.util.regex.Pattern.quote(surfacePath).replace("{key}", "\\E\\{[^/]+\\}\\Q"); Iterator> fields = paths.fields(); while(fields.hasNext()) { diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java index ea0028c..4ea1659 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/AuthentikTestStack.java @@ -255,9 +255,36 @@ public final class AuthentikTestStack TestApi admin = admin(); try { + long[] nextRetry = { System.currentTimeMillis() + 60_000 }; waitFor(Duration.ofMinutes(8), "all three kof22 blueprints applied", () -> - isSuccessful(admin, INFRA_BLUEPRINT) && isSuccessful(admin, BOOTSTRAP_BLUEPRINT) && isSuccessful(admin, TOKEN_BLUEPRINT) + { + //////////////////////////////////////////////////////////////////// + // built-in blueprints may race our custom dependencies on first // + // boot, leaving one of ours in "error" or never applied // + // ("unknown"); re-apply it (throttled). // + //////////////////////////////////////////////////////////////////// + if(System.currentTimeMillis() >= nextRetry[0]) + { + nextRetry[0] = System.currentTimeMillis() + 30_000; + for(String name : List.of(INFRA_BLUEPRINT, BOOTSTRAP_BLUEPRINT, TOKEN_BLUEPRINT)) + { + try + { + JSONObject current = blueprint(name); + if(current != null && List.of("error", "unknown").contains(current.optString("status"))) + { + admin.post("/managed/blueprints/" + current.getString("pk") + "/apply/", new JSONObject()); + } + } + catch(Exception e) + { + // Bootstrap authentication and built-in dependencies settle after HTTP readiness. + } + } + } + return (isSuccessful(admin, INFRA_BLUEPRINT) && isSuccessful(admin, BOOTSTRAP_BLUEPRINT) && isSuccessful(admin, TOKEN_BLUEPRINT) && admin.first("/core/users/?username=" + SERVICE_ACCOUNT) != null); + }); } catch(IllegalStateException e) { @@ -278,8 +305,16 @@ public final class AuthentikTestStack } try { - worker.getLogs().lines().filter(line -> line.contains("blueprint") && (line.contains("error") || line.contains("warning") || line.contains("fail"))) + worker.getLogs().lines().filter(line -> (line.contains("blueprint") && (line.contains("error") || line.contains("warning") || line.contains("fail"))) + || line.contains("\"level\": \"error\"") || line.contains("Traceback")) + .filter(line -> !line.contains("clear_failed_blueprints")) .limit(60).forEach(line -> System.err.println("worker: " + line)); + //////////////////////////////////////////////////////////////// + // blueprints re-applied through the API run in the server // + //////////////////////////////////////////////////////////////// + server.getLogs().lines().filter(line -> line.contains("\"level\": \"error\"") || line.contains("\"level\": \"warning\"") || line.contains("Traceback")) + .filter(line -> !line.contains("Base URL is not configured")) + .limit(60).forEach(line -> System.err.println("server: " + line)); } catch(RuntimeException unavailable) { diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java index 5035cc6..51be1d8 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/InvitationEnrollmentContractIT.java @@ -174,7 +174,7 @@ class InvitationEnrollmentContractIT } assertThat(used).isNotNull(); assertThat(used.getJSONObject("user").optInt("pk", -1)).isNotEqualTo(userPk); - var config = assignment == null ? new com.kingsrook.qbits.authentik.AuthentikQBitConfig().withBaseUrl(stack.baseUrl()) + var config = assignment == null ? new com.kingsrook.qbits.authentik.AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin").withListCacheTtlSeconds(0) : assignment.config; var instance = assignment == null ? com.kingsrook.qbits.authentik.AuthentikTestSupport.produce(config) : assignment.instance; com.kingsrook.qqq.backend.core.context.QContext.init(instance, @@ -265,7 +265,7 @@ class InvitationEnrollmentContractIT String actor = create(admin, "/core/users/", new JSONObject().put("username", owner + "-operator").put("name", "Invitation operator") .put("email", owner + "-operator@example.test").put("type", "internal").put("path", "users/invitation-contract"), cleanup); assertThat(admin.post("/core/groups/" + admins + "/add_user/", new JSONObject().put("pk", Integer.parseInt(actor))).status()).isEqualTo(204); - config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") + config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") .withListCacheTtlSeconds(0).withGroupCacheTtlSeconds(0).withInvitationFlow(owner).withLocalBackendName("memory") .withRecoveryEmailStage("kof22-recovery-email") .withFeatures(EnumSet.of(AuthentikFeature.READ, AuthentikFeature.USERS, AuthentikFeature.GROUPS, AuthentikFeature.MEMBERSHIP)); diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java index ffae214..6095ddd 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/Phase1ReadContractIT.java @@ -96,7 +96,7 @@ class Phase1ReadContractIT void beforeEach() throws QException { config = new AuthentikQBitConfig() - .withBaseUrl(stack.baseUrl()) + .withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()) .withAppKey("kof22-admin") .withApplicationsInScope(List.of(AuthentikTestStack.APP_SLUG)) diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java index 0b4b564..2f663a0 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitReadContractIT.java @@ -126,7 +126,7 @@ class QbitReadContractIT void beforeEach() throws QException { config = new AuthentikQBitConfig() - .withBaseUrl(stack.baseUrl()) + .withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()) .withAppKey("kof22-admin") .withListCacheTtlSeconds(0); diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java index e0006a2..08bd6be 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/QbitWriteContractIT.java @@ -155,7 +155,7 @@ class QbitWriteContractIT { MemoryRecordStore.getInstance().reset(); config = new AuthentikQBitConfig() - .withBaseUrl(stack.baseUrl()) + .withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true) .withApiToken(stack.serviceAccountToken()) .withAppKey("kof22-admin") .withListCacheTtlSeconds(0) diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java index 380511b..ed80929 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/SchemaDriftIT.java @@ -89,7 +89,7 @@ class SchemaDriftIT for(java.util.Iterator it = paths.fieldNames(); it.hasNext(); ) { String candidate = it.next(); - if(candidate.equals(path) || candidate.matches(path.replace("{key}", "\\{[^/]+\\}"))) + if(candidate.equals(path) || candidate.matches(java.util.regex.Pattern.quote(path).replace("{key}", "\\E\\{[^/]+\\}\\Q"))) { return (SchemaDriftTest.parameterNames(schema, paths.get(candidate).get(operation.method()))); } diff --git a/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java b/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java index d01ad3a..89377ee 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/contract/TokenContractIT.java @@ -579,7 +579,7 @@ class TokenContractIT private void configureQbit(int owner, String subject) throws Exception { MemoryRecordStore.getInstance().reset(); - config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") + config = new AuthentikQBitConfig().withBaseUrl(stack.baseUrl()).withAllowInsecureHttp(true).withApiToken(stack.serviceAccountToken()).withAppKey("kof22-admin") .withListCacheTtlSeconds(0).withGroupCacheTtlSeconds(0).withBulkPacingMillis(0).withLocalBackendName("memory") .withRecoveryEmailStage("kof22-recovery-email") .withFeatures(EnumSet.of(AuthentikFeature.READ, AuthentikFeature.USERS, AuthentikFeature.TOKENS)) diff --git a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java index ca05fbf..ca90f32 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikMirrorPostgresIT.java @@ -16,6 +16,7 @@ package com.kingsrook.qbits.authentik.sync; +import java.util.Set; import java.util.UUID; import javax.sql.DataSource; import com.kingsrook.qbits.authentik.RequiresDockerCondition; @@ -27,6 +28,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.ExtendWith; import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.PostgreSQLContainer; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; @@ -39,6 +41,7 @@ import com.github.dockerjava.api.model.Ports; @ExtendWith(RequiresDockerCondition.class) class AuthentikMirrorPostgresIT extends AuthentikMirrorSqlTest { + private static final Set LOCAL_DOCKER_HOSTS = Set.of("localhost", "127.0.0.1"); private static PostgreSQLContainer postgres; private String schema; @@ -55,7 +58,11 @@ class AuthentikMirrorPostgresIT extends AuthentikMirrorSqlTest .withCreateContainerCmdModifier(command -> { command.withName(owner); - command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + // Loopback-only only when the daemon is local; a remote daemon (CI) is reached via its host address. + if(LOCAL_DOCKER_HOSTS.contains(DockerClientFactory.instance().dockerHostIpAddress())) + { + command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + } }); try { diff --git a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java index dec6cc9..8de82cc 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java +++ b/src/test/java/com/kingsrook/qbits/authentik/sync/AuthentikWebhookInboxPostgresIT.java @@ -17,6 +17,7 @@ package com.kingsrook.qbits.authentik.sync; import java.sql.SQLException; +import java.util.Set; import java.util.UUID; import javax.sql.DataSource; import com.kingsrook.qbits.authentik.RequiresDockerCondition; @@ -24,6 +25,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.ExtendWith; import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.PostgreSQLContainer; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.PortBinding; @@ -36,6 +38,7 @@ import com.github.dockerjava.api.model.Ports; @ExtendWith(RequiresDockerCondition.class) class AuthentikWebhookInboxPostgresIT extends AuthentikWebhookInboxTest { + private static final Set LOCAL_DOCKER_HOSTS = Set.of("localhost", "127.0.0.1"); private static PostgreSQLContainer postgres; private String schema; @@ -53,7 +56,11 @@ class AuthentikWebhookInboxPostgresIT extends AuthentikWebhookInboxTest .withCreateContainerCmdModifier(command -> { command.withName(owner); - command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + // Loopback-only only when the daemon is local; a remote daemon (CI) is reached via its host address. + if(LOCAL_DOCKER_HOSTS.contains(DockerClientFactory.instance().dockerHostIpAddress())) + { + command.getHostConfig().withPortBindings(new PortBinding(Ports.Binding.bindIp("127.0.0.1"), new ExposedPort(5432))); + } }); try {