From 4ecc2445f6c96b133dc295c6490c0e6e3c0f466c Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Fri, 12 Apr 2024 11:24:41 +0200 Subject: [PATCH] allow deleting (#26) --- .github/workflows/Test.yaml | 4 +- .github/workflows/build-cli.yaml | 124 ++++++++++++++++++++++++++++++ .idea/vcs.xml | 1 + Dockerfile | 56 ++++++++------ cli/Makefile | 14 ++-- cli/auth/resource.go | 2 +- cli/build-php.sh | 124 +++++++++++++++--------------- cli/build.sh | 72 ++++++++--------- cli/cli.go | 6 +- cli/glue/glue.go | 31 ++++---- cli/glue/response_writer.go | 19 +++-- cli/go.mod | 16 ++-- cli/go.sum | 25 ++++++ cli/lib/api.go | 51 +++++++++++- cli/lib/consumer.go | 10 ++- src/EntityClientInterface.php | 37 ++++----- src/Glue/glue.php | 29 ++++--- src/RemoteEntityClient.php | 60 ++++++++++----- src/RemoteOrchestrationClient.php | 64 ++++++++------- src/State/EntityHistory.php | 42 +++++++--- src/Task.php | 11 ++- 21 files changed, 548 insertions(+), 250 deletions(-) create mode 100644 .github/workflows/build-cli.yaml diff --git a/.github/workflows/Test.yaml b/.github/workflows/Test.yaml index 96884a68..0c7fa6cd 100644 --- a/.github/workflows/Test.yaml +++ b/.github/workflows/Test.yaml @@ -2,10 +2,10 @@ name: Run Unit Tests on: pull_request: branches: - - main + - v2 push: branches: - - main + - v2 jobs: unit-tests: name: Unit Tests diff --git a/.github/workflows/build-cli.yaml b/.github/workflows/build-cli.yaml new file mode 100644 index 00000000..2d1a9453 --- /dev/null +++ b/.github/workflows/build-cli.yaml @@ -0,0 +1,124 @@ +name: build-cli +on: + push: + branches: + - v2 + pull_request: + branches: + - v2 +jobs: + build-linux: + strategy: + fail-fast: false + matrix: + platform: [ 'amd64', 'arm64' ] + runs-on: self-hosted + steps: + - uses: actions/checkout@v2 + - name: Configure QEMU + uses: docker/setup-qemu-action@v3 + - name: Configure docker + uses: docker/setup-buildx-action@v3 + with: + platforms: ${{ matrix.platform }} + - name: Expose GitHub Runtime + uses: crazy-max/ghaction-github-runtime@v3 + - name: Build sources + run: | + docker buildx build --cache-to type=gha,mode=max,scope=${{ matrix.platform }} --cache-from type=gha,scope=${{ matrix.platform }} --pull --load --platform linux/${{ matrix.platform }} --target cli-base-alpine -t builder . + - name: Copy build + run: | + docker create --name builder builder + docker cp builder:/go/src/app/cli/dist/dphp bin/dphp + - name: Archive artifacts + uses: actions/upload-artifact@v4 + with: + name: dphp-${{ runner.os }}-${{ matrix.platform }} + path: bin/dphp + build-docker: + runs-on: self-hosted + steps: + - uses: actions/checkout@v2 + - name: Configure QEMU + uses: docker/setup-qemu-action@v3 + - name: Configure docker + uses: docker/setup-buildx-action@v3 + - name: Login to Docker + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin + - name: Calculate version + run: | + if [ "${GITHUB_REF_TYPE}" == "tag" ]; then + export VERSION=${GITHUB_REF_NAME:1} + else + export VERSION=${GITHUB_SHA} + fi + + echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository_owner }}/durable-php/runtime + tags: | + type=schedule,pattern=latest + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - name: Build + uses: docker/build-push-action@v3 + with: + context: ./ + file: Dockerfile + target: durable-php + build-args: | + VERSION=${{ env.VERSION }} + push: true + pull: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + builder: ${{ steps.buildx.outputs.name }} + cache-from: type=gha,scope=image + cache-to: type=gha,mode=max,scope=image + platforms: linux/amd64,linux/arm64 + build-osx: + strategy: + fail-fast: true + matrix: + platform: [ 'arm64', 'x86_64' ] + runs-on: ${{ matrix.platform == 'arm64' && 'macos-14' || 'macos-13' }} + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '>=1.22' + - name: Configure Version + run: | + if [ "${GITHUB_REF_TYPE}" == "tag" ]; then + export VERSION=${GITHUB_REF_NAME:1} + else + export VERSION=${GITHUB_SHA} + fi + + echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" + - name: Configure cache + uses: actions/cache@v4 + with: + path: dist + key: ${{ matrix.platform }}-${{ hashFiles('cli/*.mod') }} + - name: Run doctor + run: BUILD=no cli/build-php.sh + - name: Build php + run: cli/build-php.sh + - name: Build cli + run: cd cli && ./build.sh + - run: ls -lah cli/dist/ + - run: ls -lah dist/ || true + - name: Archive artifacts + uses: actions/upload-artifact@v4 + with: + name: dphp-${{ runner.os }}-${{ matrix.platform }} + path: cli/dist/dphp diff --git a/.idea/vcs.xml b/.idea/vcs.xml index c8397c94..8b615ef9 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1d313c4e..537bd01c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ RUN apk update; \ ln -sf /usr/bin/php83 /usr/bin/php ENV COMPOSER_ALLOW_SUPERUSER=1 -ENV PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,gmp,gd,iconv,igbinary,mbregex,mbstring,opcache,openssl,pcntl,phar,posix,readline,simplexml,sockets,sodium,sysvsem,tokenizer,uuid,uv,xml,xmlreader,xmlwriter,zip,zlib" +ENV PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,gmp,gd,iconv,igbinary,mbregex,mbstring,opcache,openssl,pcntl,phar,posix,readline,simplexml,sockets,sodium,sysvsem,tokenizer,uv,xml,xmlreader,xmlwriter,zip,zlib" ENV PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,libwebp,libzip" WORKDIR /go/src/app @@ -66,31 +66,43 @@ COPY cli/ ./cli/ WORKDIR /go/src/app/cli RUN ./build.sh -FROM php:8-zts AS base - -COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ - -RUN install-php-extensions ev apcu pcntl parallel @composer && \ - mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && \ - apt update && \ - apt install -y procps && \ - rm -rf /var/lib/apt/lists/* - -COPY composer.json composer.lock /app/ +FROM php:8-zts AS common WORKDIR /app -RUN composer install --no-interaction --optimize-autoloader +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ +ARG VERSION=dev + +FROM common AS builder + +COPY --from=golang:1.22 /usr/local/go /usr/local/go +ENV PATH /usr/local/go/bin:$PATH + +RUN apt-get update && \ + apt-get -y --no-install-recommends install \ + libargon2-dev \ + libbrotli-dev \ + libcurl4-openssl-dev \ + libonig-dev \ + libreadline-dev \ + libsodium-dev \ + libsqlite3-dev \ + libssl-dev \ + libxml2-dev \ + zlib1g-dev \ + && \ + apt-get clean -COPY . /app - -#RUN groupadd -g 1000 app && \ -# useradd -d /app -s /bin/bash -g 1000 -u 1000 app && \ -# chown -R app:app /app && \ -# adduser app sudo +WORKDIR /go/src/app +COPY --link cli/go.mod cli/go.sum ./ +RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get -ENTRYPOINT [ "php", "-d", "opcache.enable_cli=1", "-d", "opcache.jit_buffer_size=50M", "-d", "opcache.jit=tracing", "src/Run.php" ] +COPY --link cli/ . -FROM base as dev +ENV CGO_LDFLAGS="-lssl -lcrypto -lreadline -largon2 -lcurl -lonig -lz $PHP_LDFLAGS" CGO_CFLAGS="-DFRANKENPHP_VERSION=$VERSION $PHP_CFLAGS" CGO_CPPFLAGS=$PHP_CPPFLAGS +ENV GOBIN=/usr/local/bin +RUN go get durable_php +RUN go install -ldflags "-w -s -X 'main.version=$VERSION'" -RUN install-php-extensions xdebug +FROM common AS durable-php +COPY --from=builder /usr/local/bin/durable_php /usr/local/bin/dphp diff --git a/cli/Makefile b/cli/Makefile index dec6322a..4609f027 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -1,4 +1,4 @@ -TARGET := dphp-linux-x86_64 +TARGET := dphp-linux-* BIN_PATH := ../bin DOCKER_IMAGE := builder DOCKER_TARGET := cli-base-alpine @@ -6,8 +6,12 @@ BUILD_PATH := /go/src/app/cli/dist ${BIN_PATH}/${TARGET}: cli.go */* go.mod build.sh build-php.sh ../Dockerfile mkdir -p ${BIN_PATH} - cd .. && docker build --pull --target ${DOCKER_TARGET} -t ${DOCKER_IMAGE} . - docker create --name builder builder || ( docker rm -f builder && false ) - docker cp ${DOCKER_IMAGE}:${BUILD_PATH}/${TARGET} ${BIN_PATH}/${TARGET} || ( docker rm -f builder && false ) - docker rm -f builder + cd .. && docker buildx build --cache-to type=gha --cache-from type=gha --pull --load --platform ${PLATFORM} --target ${DOCKER_TARGET} -t ${DOCKER_IMAGE} . + docker create --name ${DOCKER_IMAGE} ${DOCKER_IMAGE} || ( docker rm -f ${DOCKER_IMAGE} && false ) + docker cp ${DOCKER_IMAGE}:${BUILD_PATH}/${TARGET} ${BIN_PATH}/ || ( docker rm -f ${DOCKER_IMAGE} && false ) + docker rm -f ${DOCKER_IMAGE} upx -9 --force-pie ../bin/dphp-* + +../dist: ${BIN_PATH}/${TARGET} + docker create --name builder builder + docker cp ${DOCKER_IMAGE}:${BUILD_PATH} ../dist diff --git a/cli/auth/resource.go b/cli/auth/resource.go index 25ca76fd..81f2ef2d 100644 --- a/cli/auth/resource.go +++ b/cli/auth/resource.go @@ -176,7 +176,7 @@ func (r *Resource) getOrCreatePermissions(id *glue.StateId, ctx context.Context, glu := glue.NewGlue(ctx.Value("bootstrap").(string), glue.GetPermissions, make([]any, 0), result.Name()) env := map[string]string{"STATE_ID": id.String()} - _, headers, _ := glu.Execute(ctx, make(http.Header), logger, env, nil, id) + _, headers, _, _ := glu.Execute(ctx, make(http.Header), logger, env, nil, id) data := headers.Get("Permissions") if err = json.Unmarshal([]byte(data), &perms); err != nil { return perms, err diff --git a/cli/build-php.sh b/cli/build-php.sh index ab2558a7..dad1242b 100755 --- a/cli/build-php.sh +++ b/cli/build-php.sh @@ -24,9 +24,9 @@ set -o errexit -if ! type "git" > /dev/null; then - echo "The \"git\" command must be installed." - exit 1 +if ! type "git" >/dev/null; then + echo "The \"git\" command must be installed." + exit 1 fi os="$(uname -s | tr '[:upper:]' '[:lower:]')" @@ -34,92 +34,92 @@ os="$(uname -s | tr '[:upper:]' '[:lower:]')" export CFLAGS="$CFLAGS -O2" CXXFLAGS="$CXXFLAGS -O2" if [ -z "${PHP_EXTENSIONS}" ]; then - #export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,gd,gmp,iconv,igbinary,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,phar,posix,readline,simplexml,soap,sockets,sodium,sysvmsg,sysvsem,tokenizer,uuid,uv,xml,xmlreader,xmlwriter,xsl,yaml,zip,zlib" - export PHP_EXTENSIONS="apcu,bz2,ctype,curl,dom,filter,igbinary,intl,mbstring,opcache,openssl,pcntl,phar,posix,readline,sockets,sodium,tokenizer,uuid,uv,zip,zlib" + export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,gmp,gd,iconv,igbinary,mbregex,mbstring,opcache,openssl,pcntl,phar,posix,readline,simplexml,sockets,sodium,sysvsem,tokenizer,uv,xml,xmlreader,xmlwriter,zip,zlib" fi if [ -z "${PHP_EXTENSION_LIBS}" ]; then - export PHP_EXTENSION_LIBS="" + export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,libwebp,libzip" fi if [ -z "${PHP_VERSION}" ]; then - export PHP_VERSION="8.3" + export PHP_VERSION="8.3" fi if [ -z "${FRANKENPHP_VERSION}" ]; then - FRANKENPHP_VERSION="dev" - export FRANKENPHP_VERSION + FRANKENPHP_VERSION="dev" + export FRANKENPHP_VERSION elif [ -d ".git/" ]; then - CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)" - export CURRENT_REF + CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)" + export CURRENT_REF - if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then - # Tag + if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then + # Tag - # Trim "v" prefix if any - FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v} - export FRANKENPHP_VERSION + # Trim "v" prefix if any + FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v} + export FRANKENPHP_VERSION - git checkout "v${FRANKENPHP_VERSION}" - else - git checkout "${FRANKENPHP_VERSION}" - fi + git checkout "v${FRANKENPHP_VERSION}" + else + git checkout "${FRANKENPHP_VERSION}" + fi fi if [ -n "${CLEAN}" ]; then - rm -Rf dist/ - go clean -cache + rm -Rf dist/ + go clean -cache fi # Build libphp if necessary if [ -f "dist/static-php-cli/buildroot/lib/libphp.a" ]; then - cd dist/static-php-cli + cd dist/static-php-cli + ./bin/spc doctor else - mkdir -p dist/ - cd dist/ - - if [ -d "static-php-cli/" ]; then - cd static-php-cli/ - git pull - else - git clone --depth 1 --branch main https://github.com/crazywhalecc/static-php-cli - cd static-php-cli/ + mkdir -p dist/ + cd dist/ + + if [ -d "static-php-cli/" ]; then + cd static-php-cli/ + git pull + else + git clone --depth 1 --branch main https://github.com/crazywhalecc/static-php-cli + cd static-php-cli/ + fi + + if type "brew" >/dev/null; then + if ! type "composer" >/dev/null; then + packages="composer" + fi + if ! type "go" >/dev/null; then + packages="${packages} go" + fi + if [ -n "${RELEASE}" ] && ! type "gh" >/dev/null; then + packages="${packages} gh" fi - if type "brew" > /dev/null; then - if ! type "composer" > /dev/null; then - packages="composer" - fi - if ! type "go" > /dev/null; then - packages="${packages} go" - fi - if [ -n "${RELEASE}" ] && ! type "gh" > /dev/null; then - packages="${packages} gh" - fi - - if [ -n "${packages}" ]; then - # shellcheck disable=SC2086 - brew install --formula --quiet ${packages} - fi + if [ -n "${packages}" ]; then + # shellcheck disable=SC2086 + brew install --formula --quiet ${packages} fi + fi - composer install --no-dev -a + composer install --no-dev -a - if [ "${os}" = "linux" ]; then - extraOpts="--disable-opcache-jit -I "memory_limit=2G" -I "opcache.enable_cli=1" -I "opcache.enable=1"" - echo "" - fi + if [ "${os}" = "linux" ]; then + extraOpts="--disable-opcache-jit -I "memory_limit=2G" -I "opcache.enable_cli=1" -I "opcache.enable=1"" + echo "" + fi - if [ -n "${DEBUG_SYMBOLS}" ]; then - extraOpts="${extraOpts} --no-strip" - fi + if [ -n "${DEBUG_SYMBOLS}" ]; then + extraOpts="${extraOpts} --no-strip" + fi - ./bin/spc doctor - ./bin/spc fetch --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" - # the Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli - # shellcheck disable=SC2086 + ./bin/spc doctor + ./bin/spc fetch --with-php="${PHP_VERSION}" --for-extensions="${PHP_EXTENSIONS}" + # the Brotli library must always be built as it is required by http://github.com/dunglas/caddy-cbrotli + # shellcheck disable=SC2086 - if [ -z $BUILD ]; then - ./bin/spc build --enable-zts --build-embed ${extraOpts} "${PHP_EXTENSIONS}" --with-libs="brotli,${PHP_EXTENSION_LIBS}" - fi + if [ -z $BUILD ]; then + ./bin/spc build --debug --enable-zts --build-embed ${extraOpts} "${PHP_EXTENSIONS}" --with-libs="brotli,${PHP_EXTENSION_LIBS}" + fi fi diff --git a/cli/build.sh b/cli/build.sh index 9607b0ce..d745c407 100755 --- a/cli/build.sh +++ b/cli/build.sh @@ -24,9 +24,9 @@ set -o errexit -if ! type "git" > /dev/null; then - echo "The \"git\" command must be installed." - exit 1 +if ! type "git" >/dev/null; then + echo "The \"git\" command must be installed." + exit 1 fi cd ../dist/static-php-cli @@ -35,57 +35,57 @@ arch="$(uname -m)" os="$(uname -s | tr '[:upper:]' '[:lower:]')" md5binary="md5sum" if [ "${os}" = "darwin" ]; then - os="mac" - md5binary="md5 -q" + os="mac" + md5binary="md5 -q" fi if [ -z "${PHP_EXTENSIONS}" ]; then - export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,gmp,gd,iconv,igbinary,mbregex,mbstring,opcache,openssl,pcntl,phar,posix,readline,simplexml,sockets,sodium,sysvsem,tokenizer,uuid,uv,xml,xmlreader,xmlwriter,zip,zlib" + export PHP_EXTENSIONS="apcu,bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,gmp,gd,iconv,igbinary,mbregex,mbstring,opcache,openssl,pcntl,phar,posix,readline,simplexml,sockets,sodium,sysvsem,tokenizer,uuid,uv,xml,xmlreader,xmlwriter,zip,zlib" fi if [ -z "${PHP_EXTENSION_LIBS}" ]; then - export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,libwebp,libzip" + export PHP_EXTENSION_LIBS="bzip2,freetype,libavif,libjpeg,libwebp,libzip" fi if [ -z "${PHP_VERSION}" ]; then - export PHP_VERSION="8.3" + export PHP_VERSION="8.3" fi if [ -z "${FRANKENPHP_VERSION}" ]; then - FRANKENPHP_VERSION="dev" - export FRANKENPHP_VERSION + FRANKENPHP_VERSION="dev" + export FRANKENPHP_VERSION elif [ -d ".git/" ]; then - CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)" - export CURRENT_REF + CURRENT_REF="$(git rev-parse --abbrev-ref HEAD)" + export CURRENT_REF - if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then - # Tag + if echo "${FRANKENPHP_VERSION}" | grep -F -q "."; then + # Tag - # Trim "v" prefix if any - FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v} - export FRANKENPHP_VERSION + # Trim "v" prefix if any + FRANKENPHP_VERSION=${FRANKENPHP_VERSION#v} + export FRANKENPHP_VERSION - git checkout "v${FRANKENPHP_VERSION}" - else - git checkout "${FRANKENPHP_VERSION}" - fi + git checkout "v${FRANKENPHP_VERSION}" + else + git checkout "${FRANKENPHP_VERSION}" + fi fi -bin="dphp-${os}-${arch}" +bin="dphp" if [ -n "${CLEAN}" ]; then - rm -Rf dist/ - go clean -cache + rm -Rf dist/ + go clean -cache fi CGO_CFLAGS="-O2 -DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} -I${PWD}/buildroot/include/ $(./buildroot/bin/php-config --includes | sed s\#-I/\#-I"${PWD}"/buildroot/\#g)" if [ -n "${DEBUG_SYMBOLS}" ]; then - CGO_CFLAGS="-g ${CGO_CFLAGS}" + CGO_CFLAGS="-g ${CGO_CFLAGS}" fi export CGO_CFLAGS if [ "${os}" = "mac" ]; then - export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration" + export CGO_LDFLAGS="-framework CoreFoundation -framework SystemConfiguration" fi CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags) $(./buildroot/bin/php-config --libs) -lstdc++ -lbrotlidec -lssl -lcrypto -lbrotlienc -lbrotlicommon" @@ -97,22 +97,22 @@ export LIBPHP_VERSION cd ../../cli VERSION="$(git describe --tags --always)" -if git status --porcelain | grep -q "cli"; then +if git status --porcelain | grep -q "cli"; then VERSION="$VERSION-dirty" fi # Embed PHP app, if any if [ -n "${EMBED}" ] && [ -d "${EMBED}" ]; then - tar -cf app.tar -C "${EMBED}" . - ${md5binary} app.tar > app_checksum.txt + tar -cf app.tar -C "${EMBED}" . + ${md5binary} app.tar >app_checksum.txt fi if [ "${os}" = "linux" ]; then - extraExtldflags="-Wl,-z,stack-size=0x80000" + extraExtldflags="-Wl,-z,stack-size=0x80000" fi if [ -z "${DEBUG_SYMBOLS}" ]; then - extraLdflags="-w -s -race" + extraLdflags="-w -s -race" fi env @@ -121,14 +121,14 @@ go get durable_php go build -buildmode=pie -tags "cgo netgo nats osusergo static_build" -ldflags "-linkmode=external -extldflags '-static-pie ${extraExtldflags}' ${extraLdflags} -X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ${FRANKENPHP_VERSION} PHP ${LIBPHP_VERSION} go_durable_php' -X 'main.version=$VERSION'" -o "dist/${bin}" durable_php if [ -d "${EMBED}" ]; then - truncate -s 0 app.tar - truncate -s 0 app_checksum.txt + truncate -s 0 app.tar + truncate -s 0 app_checksum.txt fi if [ -z "${NO_COMPRESS}" ]; then - if type "upx" > /dev/null; then - #upx --best "dist/${bin}" - echo "would compress" + if type "upx" >/dev/null; then + #upx --best "dist/${bin}" + echo "would compress" fi fi diff --git a/cli/cli.go b/cli/cli.go index b8777cfb..6cf472d8 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -231,12 +231,14 @@ func execute(args []string, options map[string]string) int { case "entities": err := lib.IndexerListen(ctx, cfg, glue.Entity, js, logger) if err != nil { - panic(err) + cfg.Extensions.Search.Collections = []string{} + logger.Warn("Disabling search extension due to failing to connect to typesense") } case "orchestrations": err := lib.IndexerListen(ctx, cfg, glue.Orchestration, js, logger) if err != nil { - panic(err) + cfg.Extensions.Search.Collections = []string{} + logger.Warn("Disabling search extension due to failing to connect to typesense") } } } diff --git a/cli/glue/glue.go b/cli/glue/glue.go index 2f31f152..3b7781be 100644 --- a/cli/glue/glue.go +++ b/cli/glue/glue.go @@ -71,10 +71,10 @@ func NewGlue(bootstrap string, function Method, input []any, payload string) *Gl } } -func FromApiRequest(ctx context.Context, r *http.Request, function Method, logger *zap.Logger, stream jetstream.JetStream, id *StateId, headers http.Header) ([]*nats.Msg, string, error, *http.Header) { +func FromApiRequest(ctx context.Context, r *http.Request, function Method, logger *zap.Logger, stream jetstream.JetStream, id *StateId, headers http.Header) ([]*nats.Msg, string, error, *http.Header, bool) { temp, err := os.CreateTemp("", "reqbody") if err != nil { - return nil, "", err, nil + return nil, "", err, nil, false } go func() { <-ctx.Done() @@ -84,7 +84,7 @@ func FromApiRequest(ctx context.Context, r *http.Request, function Method, logge _, err = io.Copy(temp, r.Body) if err != nil { - return nil, "", err, nil + return nil, "", err, nil, false } temp.Close() @@ -99,12 +99,12 @@ func FromApiRequest(ctx context.Context, r *http.Request, function Method, logge env["FROM_REQUEST"] = "1" env["STATE_ID"] = id.String() - msgs, responseHeaders, _ := glu.Execute(ctx, headers, logger, env, stream, id) + msgs, responseHeaders, _, deleteAfter := glu.Execute(ctx, headers, logger, env, stream, id) - return msgs, temp.Name(), nil, &responseHeaders + return msgs, temp.Name(), nil, &responseHeaders, deleteAfter } -func (g *Glue) Execute(ctx context.Context, headers http.Header, logger *zap.Logger, env map[string]string, stream jetstream.JetStream, id *StateId) ([]*nats.Msg, http.Header, int) { +func (g *Glue) Execute(ctx context.Context, headers http.Header, logger *zap.Logger, env map[string]string, stream jetstream.JetStream, id *StateId) ([]*nats.Msg, http.Header, int, bool) { var dir string var ok bool if dir, ok = GetLibraryDir("glue.php"); !ok { @@ -167,14 +167,15 @@ func (g *Glue) Execute(ctx context.Context, headers http.Header, logger *zap.Log } writer := &InternalLoggingResponseWriter{ - logger: logger, - isError: false, - status: 0, - events: make([]*nats.Msg, 0), - query: make(chan []string), - headers: make(http.Header), - CurrentId: id, - Context: ctx, + logger: logger, + isError: false, + status: 0, + events: make([]*nats.Msg, 0), + query: make(chan []string), + headers: make(http.Header), + CurrentId: id, + Context: ctx, + DeleteAfter: false, } var wg sync.WaitGroup @@ -208,7 +209,7 @@ func (g *Glue) Execute(ctx context.Context, headers http.Header, logger *zap.Log cancelCtx() wg.Wait() - return writer.events, writer.Header(), writer.status + return writer.events, writer.Header(), writer.status, writer.DeleteAfter } func DeleteState(ctx context.Context, stream jetstream.JetStream, logger *zap.Logger, id *StateId) error { diff --git a/cli/glue/response_writer.go b/cli/glue/response_writer.go index 4438890e..0ea2c401 100644 --- a/cli/glue/response_writer.go +++ b/cli/glue/response_writer.go @@ -47,14 +47,15 @@ func (c *ConsumingResponseWriter) WriteHeader(statusCode int) { } type InternalLoggingResponseWriter struct { - logger *zap.Logger - isError bool - status int - events []*nats.Msg - query chan []string - headers http.Header - CurrentId *StateId - Context context.Context + logger *zap.Logger + isError bool + status int + events []*nats.Msg + query chan []string + headers http.Header + CurrentId *StateId + Context context.Context + DeleteAfter bool } func (w *InternalLoggingResponseWriter) Header() http.Header { @@ -118,6 +119,8 @@ func (w *InternalLoggingResponseWriter) Write(b []byte) (int, error) { } else if after, found := strings.CutPrefix(line, "QUERY~!~"); found { w.logger.Debug("Performing query", zap.String("line", after)) w.query <- strings.Split(after, "~!~") + } else if _, found := strings.CutPrefix(line, "DELETE~!~"); found { + w.DeleteAfter = true } else if w.isError { w.logger.Error(line) } else { diff --git a/cli/go.mod b/cli/go.mod index 5625b6e2..6004e80e 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -2,9 +2,9 @@ module durable_php go 1.22 -require github.com/dunglas/frankenphp v1.1.1 +require github.com/dunglas/frankenphp v1.1.3-0.20240411151147-a910e39b0637 -require github.com/nats-io/nats.go v1.33.1 +require github.com/nats-io/nats.go v1.34.1 require github.com/nats-io/nats-server/v2 v2.10.12 @@ -50,7 +50,7 @@ require ( github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 // indirect + github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/iris-contrib/schema v0.0.6 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -77,7 +77,7 @@ require ( github.com/nats-io/jwt/v2 v2.5.5 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/schollz/closestmatch v2.1.0+incompatible // indirect @@ -94,10 +94,10 @@ require ( github.com/yosssi/ace v0.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 72497071..22677e96 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -40,6 +40,10 @@ github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/dunglas/frankenphp v1.1.1 h1:aB+UV5rXvJgYVJfUA22/3d7aXtCWCThJxi9gvWSC3HA= github.com/dunglas/frankenphp v1.1.1/go.mod h1:u4UasfhFvcNwFoLR2InkcXWeWA2oK71USghSo8F4d0g= +github.com/dunglas/frankenphp v1.1.2 h1:+tgsdsjNeiJ99epJgv+GtcnEPG2vU5CfrIroshcA9CQ= +github.com/dunglas/frankenphp v1.1.2/go.mod h1:u4UasfhFvcNwFoLR2InkcXWeWA2oK71USghSo8F4d0g= +github.com/dunglas/frankenphp v1.1.3-0.20240411151147-a910e39b0637 h1:RmWXF4VgZY79W5qqAxDgjqlzMyoOlu+AzkPgsPizA38= +github.com/dunglas/frankenphp v1.1.3-0.20240411151147-a910e39b0637/go.mod h1:u4UasfhFvcNwFoLR2InkcXWeWA2oK71USghSo8F4d0g= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -74,6 +78,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k= github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= +github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -158,13 +164,18 @@ github.com/nats-io/nats-server/v2 v2.10.12 h1:G6u+RDrHkw4bkwn7I911O5jqys7jJVRY6M github.com/nats-io/nats-server/v2 v2.10.12/go.mod h1:H1n6zXtYLFCgXcf/SF8QNTSIFuS8tyZQMN9NguUHdEs= github.com/nats-io/nats.go v1.33.1 h1:8TxLZZ/seeEfR97qV0/Bl939tpDnt2Z2fK3HkPypj70= github.com/nats-io/nats.go v1.33.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nats.go v1.34.1 h1:syWey5xaNHZgicYBemv0nohUPPmaLteiBEUT6Q5+F/4= +github.com/nats-io/nats.go v1.34.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/newrelic/go-agent/v3 v3.31.0/go.mod h1:MnbPbcIAmtKH80ZRXovE9kQLDs0Nc32IQa7bWydDKsk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -186,6 +197,7 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -246,8 +258,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw= golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= +golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -255,6 +274,10 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -270,6 +293,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/cli/lib/api.go b/cli/lib/api.go index 46c9d902..5dc3938f 100644 --- a/cli/lib/api.go +++ b/cli/lib/api.go @@ -260,7 +260,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po ctx, cancel := context.WithCancel(context.WithValue(ctx, "bootstrap", bootstrap)) defer cancel() - msgs, stateFile, err, responseHeaders := glue.FromApiRequest(ctx, request, function, logger, js, id, headers) + msgs, stateFile, err, responseHeaders, deleteAfter := glue.FromApiRequest(ctx, request, function, logger, js, id, headers) if err != nil { http.Error(writer, "Internal Server Error", http.StatusInternalServerError) logger.Error("Failed to glue", zap.Error(err)) @@ -298,12 +298,24 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po logger.Error("Failed to glue", zap.Error(err)) return } + + if deleteAfter { + resource, err := rm.DiscoverResource(ctx, id, logger, false) + if err != nil { + logger.Error("Unable to delete resource", zap.Error(err)) + http.Error(writer, "Internal Server Error", http.StatusInternalServerError) + return + } + rm.Delete(ctx, resource) + } } // GET /entity/{name}/{id} // get an entity state and status // PUT /entity/{name}/{id} // signal an entity + // DELETE /entity/{name}/{id} + // delete an entity r.HandleFunc("/entity/{name}/{id}", func(writer http.ResponseWriter, request *http.Request) { if stop := handleCors(writer, request); stop { return @@ -348,6 +360,24 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } + if request.Method == "DELETE" { + ctx, done := authorize(writer, request, config, ctx, rm, id.ToStateId(), logger, true, auth.Signal) + if done { + return + } + + logger.Debug("Delete entity", zap.String("id", id.String())) + rs, err := rm.DiscoverResource(ctx, id.ToStateId(), logger, true) + if err != nil { + logger.Error("Failed to discover resource", zap.Error(err)) + http.Error(writer, "Not Found", http.StatusNotFound) + return + } + rm.Delete(ctx, rs) + http.Error(writer, "Deleted", http.StatusNoContent) + return + } + http.Error(writer, "Method Not Allowed", http.StatusMethodNotAllowed) }) @@ -420,6 +450,7 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po // start a new orchestration // GET /orchestration/{name}/{id}?wait=?? // get an orchestration status and optionally wait for it's completion + // DELETE /orchestration/{name}/{id} r.HandleFunc("/orchestration/{name}/{id}", func(writer http.ResponseWriter, request *http.Request) { if stop := handleCors(writer, request); stop { return @@ -445,6 +476,24 @@ func Startup(ctx context.Context, js jetstream.JetStream, logger *zap.Logger, po return } + if request.Method == "DELETE" { + ctx, done := authorize(writer, request, config, ctx, rm, id.ToStateId(), logger, true, auth.Signal) + if done { + return + } + + rs, err := rm.DiscoverResource(ctx, id.ToStateId(), logger, true) + if err != nil { + logger.Error("Failed to discover a resource for deletion", zap.Error(err)) + http.Error(writer, "Not Found", http.StatusNotFound) + return + } + + rm.Delete(ctx, rs) + http.Error(writer, "Deleted", http.StatusNoContent) + return + } + if request.Method != "GET" { http.Error(writer, "Method Not Allowed", http.StatusMethodNotAllowed) return diff --git a/cli/lib/consumer.go b/cli/lib/consumer.go index 47615a66..0aed272a 100644 --- a/cli/lib/consumer.go +++ b/cli/lib/consumer.go @@ -263,7 +263,7 @@ func processMsg(ctx context.Context, logger *zap.Logger, msg jetstream.Msg, js j env["EVENT"] = string(msg.Data()) env["STATE_ID"] = msg.Headers().Get(string(glue.HeaderStateId)) - msgs, headers, _ := glu.Execute(ctx, headers, logger, env, js, id) + msgs, headers, _, deleteAfter := glu.Execute(ctx, headers, logger, env, js, id) // now update the stored state, if this fails due to optimistic concurrency, we immediately nak and fail err = update() @@ -292,5 +292,13 @@ func processMsg(ctx context.Context, logger *zap.Logger, msg jetstream.Msg, js j return err } + if deleteAfter { + resource, err := rm.DiscoverResource(ctx, id, logger, false) + if err != nil { + return err + } + rm.Delete(ctx, resource) + } + return nil } diff --git a/src/EntityClientInterface.php b/src/EntityClientInterface.php index 89158971..251aa714 100644 --- a/src/EntityClientInterface.php +++ b/src/EntityClientInterface.php @@ -27,6 +27,9 @@ use Bottledcode\DurablePhp\Search\EntityFilter; use Bottledcode\DurablePhp\State\EntityId; use Bottledcode\DurablePhp\State\EntityState; +use Closure; +use DateTimeImmutable; +use Generator; interface EntityClientInterface { @@ -34,49 +37,47 @@ public function withAuth(string $token): void; /** * Removes empty entities and releases orphaned locks - * - * @return void */ public function cleanEntityStorage(): void; /** * Get a list of entities * - * @return \Generator + * @return Generator */ - public function listEntities(EntityFilter $filter, int $page): \Generator; + public function listEntities(EntityFilter $filter, int $page): Generator; /** * Signal an entity either now, or at some point in the future - * - * @param EntityId $entityId - * @param string $operationName - * @param array $input - * @param \DateTimeImmutable|null $scheduledTime - * @return void */ public function signalEntity( EntityId $entityId, string $operationName, array $input = [], - \DateTimeImmutable $scheduledTime = null + ?DateTimeImmutable $scheduledTime = null ): void; /** * Signals an entity using a closure * * @template T - * @param EntityId|class-string $entityId The id of the entity to signal - * @param \Closure $signal - * @return void + * + * @param EntityId|class-string $entityId The id of the entity to signal + * @param Closure $signal */ - public function signal(EntityId|string $entityId, \Closure $signal): void; + public function signal(EntityId|string $entityId, Closure $signal): void; /** * @template T of EntityState - * @param EntityId $entityId - * @param class-string $type + * + * @param EntityId $entityId + * @param class-string $type * @return T|null */ - public function getEntitySnapshot(EntityId $entityId, string $type): EntityState|null; + public function getEntitySnapshot(EntityId $entityId, string $type): ?EntityState; + + /** + * Deletes an entity + */ + public function deleteEntity(EntityId $entityId): void; } diff --git a/src/Glue/glue.php b/src/Glue/glue.php index 16013dcf..439102de 100644 --- a/src/Glue/glue.php +++ b/src/Glue/glue.php @@ -44,6 +44,7 @@ use Bottledcode\DurablePhp\State\StateInterface; use Bottledcode\DurablePhp\Task; use DI\Container; +use DI\ContainerBuilder; use DI\Definition\AutowireDefinition; use DI\Definition\Definition; use DI\Definition\FactoryDefinition; @@ -52,7 +53,10 @@ use DI\Definition\InstanceDefinition; use DI\Definition\ObjectDefinition; use JsonException; +use LogicException; use Ramsey\Uuid\Uuid; +use ReflectionClass; +use ReflectionFunction; require_once __DIR__ . '/autoload.php'; @@ -93,7 +97,7 @@ public function __construct(private DurableLogger $logger) } if (! file_exists($_SERVER['HTTP_DPHP_PAYLOAD'])) { - throw new \LogicException('Unable to load payload'); + throw new LogicException('Unable to load payload'); } $payload = stream_get_contents($this->payloadHandle = fopen($_SERVER['HTTP_DPHP_PAYLOAD'], 'r+b')); @@ -152,7 +156,7 @@ public function processMsg(): void public function bootstrap(): Container { - $builder = new \DI\ContainerBuilder(); + $builder = new ContainerBuilder(); if ($this->bootstrap) { $builder->addDefinitions(include $this->bootstrap); } @@ -160,6 +164,11 @@ public function bootstrap(): Container return $builder->build(); } + public function outputDelete(): void + { + echo 'DELETE~!~'; + } + private function entitySignal(): void { $input = SerializedArray::import($this->payload['input'])->toArray(); @@ -252,14 +261,14 @@ private function getPermissions(): void } else { $definitions = []; } - switch($this->target->getStateType()) { + switch ($this->target->getStateType()) { case ActivityHistory::class: $permissions['mode'] = 'anon'; break; case EntityHistory::class: $entity = $this->target->toEntityId(); $class = $definitions[$entity->name] ?? $entity->name; - if($class instanceof AutowireDefinitionHelper) { + if ($class instanceof AutowireDefinitionHelper) { $class = $class->getDefinition('none')->getClassName(); } elseif ($class instanceof CreateDefinitionHelper) { $class = $class->getDefinition('none')->getClassName(); @@ -268,7 +277,7 @@ private function getPermissions(): void case OrchestrationHistory::class: $instance = $this->target->toOrchestrationInstance(); $class = $definitions[$instance->instanceId] ?? $instance->instanceId; - if($class instanceof AutowireDefinitionHelper) { + if ($class instanceof AutowireDefinitionHelper) { $class = $class->getDefinition('none')->getClassName(); } elseif ($class instanceof CreateDefinitionHelper) { $class = $class->getDefinition('none')->getClassName(); @@ -276,7 +285,7 @@ private function getPermissions(): void } if ($class !== null) { - $class = new \ReflectionClass($class); + $class = new ReflectionClass($class); foreach ($class->getAttributes() as $attribute) { switch (true) { @@ -314,14 +323,14 @@ private function getPermissions(): void header("Permissions: $permissions"); } - private function getFromDefinition(Definition $definition): \ReflectionClass|\ReflectionFunction|null + private function getFromDefinition(Definition $definition): ReflectionClass|ReflectionFunction|null { if ($definition instanceof AutowireDefinition || $definition instanceof ObjectDefinition) { - return new \ReflectionClass($definition->getClassName()); + return new ReflectionClass($definition->getClassName()); } elseif ($definition instanceof InstanceDefinition) { - return new \ReflectionClass($definition->getInstance()); + return new ReflectionClass($definition->getInstance()); } elseif ($definition instanceof FactoryDefinition) { - return new \ReflectionFunction($definition->getCallable()); + return new ReflectionFunction($definition->getCallable()); } return null; diff --git a/src/RemoteEntityClient.php b/src/RemoteEntityClient.php index 31e4934c..5ecbde8f 100644 --- a/src/RemoteEntityClient.php +++ b/src/RemoteEntityClient.php @@ -30,10 +30,17 @@ use Bottledcode\DurablePhp\State\EntityId; use Bottledcode\DurablePhp\State\EntityState; use Bottledcode\DurablePhp\State\Serializer; +use Closure; +use DateTimeImmutable; +use Exception; +use Generator; +use Override; +use ReflectionFunction; +use Throwable; class RemoteEntityClient implements EntityClientInterface { - private string $userToken = ""; + private string $userToken = ''; public function __construct( private string $apiHost = 'http://localhost:8080', @@ -43,11 +50,11 @@ public function __construct( $this->apiHost = rtrim($this->apiHost, '/'); } - #[\Override] + #[Override] public function cleanEntityStorage(): void {} - #[\Override] - public function listEntities(EntityFilter $filter, int $page): \Generator + #[Override] + public function listEntities(EntityFilter $filter, int $page): Generator { $req = new Request($this->apiHost . '/entities/filter/' . $page, 'POST', json_encode($filter, JSON_THROW_ON_ERROR)); if ($this->userToken) { @@ -58,13 +65,13 @@ public function listEntities(EntityFilter $filter, int $page): \Generator yield from $result; } - #[\Override] - public function signal(EntityId|string $entityId, \Closure $signal): void + #[Override] + public function signal(EntityId|string $entityId, Closure $signal): void { - $interfaceReflector = new \ReflectionFunction($signal); + $interfaceReflector = new ReflectionFunction($signal); $interfaceName = $interfaceReflector->getParameters()[0]->getType()?->getName(); - if(interface_exists($interfaceName) === false) { - throw new \Exception("Interface $interfaceName does not exist"); + if (interface_exists($interfaceName) === false) { + throw new Exception("Interface $interfaceName does not exist"); } $spy = $this->spyProxy->define($interfaceName); $operationName = ''; @@ -72,41 +79,41 @@ public function signal(EntityId|string $entityId, \Closure $signal): void try { $class = new $spy($operationName, $arguments); $signal($class); - } catch(\Throwable) { + } catch (Throwable) { // spies always throw } $this->signalEntity(is_string($entityId) ? new EntityId($interfaceName, $entityId) : $entityId, $operationName, $arguments); } - #[\Override] + #[Override] public function signalEntity( EntityId $entityId, string $operationName, array $input = [], - ?\DateTimeImmutable $scheduledTime = null + ?DateTimeImmutable $scheduledTime = null ): void { $name = rawurlencode($entityId->name); $id = rawurlencode($entityId->id); $input = [ 'signal' => $operationName, - 'input' => SerializedArray::fromArray($input) + 'input' => SerializedArray::fromArray($input), ]; $req = new Request("{$this->apiHost}/entity/{$name}/{$id} ", 'PUT', json_encode($input, JSON_THROW_ON_ERROR)); - if($scheduledTime) { - $req->setHeader("At", $scheduledTime->format(DATE_ATOM)); + if ($scheduledTime) { + $req->setHeader('At', $scheduledTime->format(DATE_ATOM)); } if ($this->userToken) { $req->setHeader('Authorization', 'Bearer ' . $this->userToken); } $result = $this->client->request($req); - if($result->getStatus() >= 300) { - throw new \Exception("error calling " . $req->getUri()->getPath() . "\n" . $result->getBody()->read()); + if ($result->getStatus() >= 300) { + throw new Exception('error calling ' . $req->getUri()->getPath() . "\n" . $result->getBody()->read()); } } - #[\Override] + #[Override] public function getEntitySnapshot(EntityId $entityId, string $type): ?EntityState { $req = new Request($this->apiHost . '/entity/' . $entityId->name . '/' . $entityId->id); @@ -123,8 +130,23 @@ public function getEntitySnapshot(EntityId $entityId, string $type): ?EntityStat return Serializer::deserialize($result, EntityState::class); } - #[\Override] public function withAuth(string $token): void + #[Override] + public function withAuth(string $token): void { $this->userToken = $token; } + + #[Override] + public function deleteEntity(EntityId $entityId): void + { + $req = new Request("$this->apiHost/entity/{$entityId->name}/{$entityId->id}", 'DELETE'); + if ($this->userToken) { + $req->setHeader('Authorization', 'Bearer ' . $this->userToken); + } + $result = $this->client->request($req); + + if ($result->getStatus() !== 204) { + throw new Exception('Failed to delete entity'); + } + } } diff --git a/src/RemoteOrchestrationClient.php b/src/RemoteOrchestrationClient.php index fa91a477..2d7a613e 100644 --- a/src/RemoteOrchestrationClient.php +++ b/src/RemoteOrchestrationClient.php @@ -30,13 +30,16 @@ use Bottledcode\DurablePhp\State\OrchestrationInstance; use Bottledcode\DurablePhp\State\Serializer; use Bottledcode\DurablePhp\State\Status; +use Exception; +use Generator; +use Override; use function Withinboredom\Time\Hours; use function Withinboredom\Time\Seconds; final class RemoteOrchestrationClient implements OrchestrationClientInterface { - private string $userToken = ""; + private string $userToken = ''; public function __construct( private string $apiHost = 'http://localhost:8080', @@ -46,8 +49,8 @@ public function __construct( $this->apiHost = rtrim($this->apiHost, '/'); } - #[\Override] - public function listInstances(): \Generator + #[Override] + public function listInstances(): Generator { $req = new Request("$this->apiHost/orchestrations"); if ($this->userToken) { @@ -58,30 +61,22 @@ public function listInstances(): \Generator yield from $result; } - #[\Override] + #[Override] public function purge(OrchestrationInstance $instance): void - { - throw new \LogicException('not implemented yet'); - } - - #[\Override] - public function raiseEvent(OrchestrationInstance $instance, string $eventName, array $eventData): void { $name = rawurlencode($instance->instanceId); $id = rawurlencode($instance->executionId); - $signal = rawurlencode($eventName); - $eventData = SerializedArray::fromArray($eventData); - $req = new Request("$this->apiHost/orchestration/$name/$id/$signal", 'PUT', json_encode($eventData, JSON_THROW_ON_ERROR)); + $req = new Request("$this->apiHost/orchestrations/$name/$id", 'DELETE'); if ($this->userToken) { $req->setHeader('Authorization', 'Bearer ' . $this->userToken); } $result = $this->client->request($req); - if ($result->getStatus() >= 300) { - throw new \Exception($result->getBody()->read()); + if ($result->getStatus() !== 204) { + throw new Exception('Cannot purge Orchestration'); } } - #[\Override] + #[Override] public function getStatus(OrchestrationInstance $instance): Status { $name = rawurlencode($instance->instanceId); @@ -96,50 +91,67 @@ public function getStatus(OrchestrationInstance $instance): Status return Serializer::deserialize($result, Status::class); } - #[\Override] + #[Override] + public function raiseEvent(OrchestrationInstance $instance, string $eventName, array $eventData): void + { + $name = rawurlencode($instance->instanceId); + $id = rawurlencode($instance->executionId); + $signal = rawurlencode($eventName); + $eventData = SerializedArray::fromArray($eventData); + $req = new Request("$this->apiHost/orchestration/$name/$id/$signal", 'PUT', json_encode($eventData, JSON_THROW_ON_ERROR)); + if ($this->userToken) { + $req->setHeader('Authorization', 'Bearer ' . $this->userToken); + } + $result = $this->client->request($req); + if ($result->getStatus() >= 300) { + throw new Exception($result->getBody()->read()); + } + } + + #[Override] public function restart(OrchestrationInstance $instance): void { // TODO: Implement restart() method. } - #[\Override] + #[Override] public function resume(OrchestrationInstance $instance, string $reason): void { // TODO: Implement resume() method. } - #[\Override] + #[Override] public function startNew(string $name, array $args = [], ?string $id = null): OrchestrationInstance { $data = ['input' => SerializedArray::fromArray($args)]; $data = json_encode($data, JSON_THROW_ON_ERROR); $name = rawurlencode($name); - $id = $id ? "/" . rawurlencode($id) : ''; + $id = $id ? '/' . rawurlencode($id) : ''; $req = new Request("$this->apiHost/orchestration/$name$id", 'PUT', $data); if ($this->userToken) { $req->setHeader('Authorization', 'Bearer ' . $this->userToken); } $result = $this->client->request($req); if ($result->getStatus() >= 300) { - throw new \Exception($result->getBody()->read()); + throw new Exception($result->getBody()->read()); } return (new StateId($result->getHeader('X-Id')))->toOrchestrationInstance(); } - #[\Override] + #[Override] public function suspend(OrchestrationInstance $instance, string $reason): void { // TODO: Implement suspend() method. } - #[\Override] + #[Override] public function terminate(OrchestrationInstance $instance, string $reason): void { // TODO: Implement terminate() method. } - #[\Override] + #[Override] public function waitForCompletion(OrchestrationInstance $instance): void { $name = rawurlencode($instance->instanceId); @@ -154,10 +166,10 @@ public function waitForCompletion(OrchestrationInstance $instance): void $result = $this->client->request($req); $result = $result->getBody()->read(); - } - #[\Override] public function withAuth(string $token): void + #[Override] + public function withAuth(string $token): void { $this->userToken = $token; } diff --git a/src/State/EntityHistory.php b/src/State/EntityHistory.php index 6ae72f05..89bd6b4f 100644 --- a/src/State/EntityHistory.php +++ b/src/State/EntityHistory.php @@ -44,7 +44,9 @@ use Bottledcode\DurablePhp\State\Ids\StateId; use Crell\Serde\Attributes\Field; use Generator; +use Override; use ReflectionClass; +use ReflectionException; use ReflectionNamedType; class EntityHistory extends AbstractHistory @@ -54,13 +56,18 @@ class EntityHistory extends AbstractHistory public EntityId $entityId; public string $name; + public array $history = []; - public string|null $lock; + + public ?string $lock; + private bool $debugHistory = false; - private EntityState|null $state = null; + + private ?EntityState $state = null; + private LockStateMachine $lockQueue; - public function __construct(public StateId $id, #[Field(exclude: true)] public DurableLogger|null $logger = null) + public function __construct(public StateId $id, #[Field(exclude: true)] public ?DurableLogger $logger = null) { $this->entityId = $id->toEntityId(); } @@ -77,7 +84,7 @@ public function ackedEvent(Event $event): void unset($this->history[$event->eventId]); } - public function getState(): EntityState|null + public function getState(): ?EntityState { return $this->state; } @@ -189,20 +196,21 @@ private function execute(Event $original, string $operation, array $input): Gene } try { $operationReflection = $reflector->getMethod($operation); - } catch(\ReflectionException) { + } catch (ReflectionException) { // search attributes for matching operation - foreach($reflector->getMethods() as $method) { - foreach($method->getAttributes(Operation::class) as $attribute) { + foreach ($reflector->getMethods() as $method) { + foreach ($method->getAttributes(Operation::class) as $attribute) { /** @var Operation $attributeClass */ $attributeClass = $attribute->newInstance(); - if($attributeClass->name === $operation) { + if ($attributeClass->name === $operation) { $operationReflection = $reflector->getMethod($attributeClass->name); goto done; } } } $this->logger->critical('Unknown operation', ['operation' => $operation]); + return; } done: @@ -210,13 +218,21 @@ private function execute(Event $original, string $operation, array $input): Gene try { $result = $operationReflection->getClosure($this->state); $result = $result(...$input); - } catch (Unwind) { + } catch (Unwind $e) { + if ($e->getMessage() === 'delete') { + yield 'delete'; + } + return; } } elseif (is_callable($this->name)) { try { $result = ($this->name)($context); - } catch (Unwind) { + } catch (Unwind $e) { + if ($e->getMessage() === 'delete') { + yield 'delete'; + } + return; } } @@ -263,8 +279,10 @@ private function queueIfLocked(Event $original): bool if ($this->isLocked($original)) { // queue the event $this->lockQueue['_']['events'][] = $original; + return true; } + return false; } @@ -279,6 +297,7 @@ private function isLocked(Event $original): bool } $original = $original->getInnerEvent(); } + return true; } @@ -300,7 +319,8 @@ public function applyAwaitResult(AwaitResult $event, Event $original): Generator yield from $this->finalize($event); } - #[\Override] public function setLogger(DurableLogger $logger): void + #[Override] + public function setLogger(DurableLogger $logger): void { $this->logger = $logger; } diff --git a/src/Task.php b/src/Task.php index f0d10970..3ca78a77 100644 --- a/src/Task.php +++ b/src/Task.php @@ -36,7 +36,10 @@ use Bottledcode\DurablePhp\State\Serializer; use Bottledcode\DurablePhp\State\StateInterface; use Bottledcode\DurablePhp\Transmutation\Router; +use Closure; +use JsonException; use Psr\Container\ContainerInterface; +use Throwable; require_once __DIR__ . '/Glue/autoload.php'; @@ -97,11 +100,13 @@ public function run(): void break; } $this->fire($eventOrCallable); - } elseif ($eventOrCallable instanceof \Closure) { + } elseif ($eventOrCallable instanceof Closure) { $eventOrCallable($this); + } elseif ($eventOrCallable === 'delete') { + $this->glue->outputDelete(); } } - } catch (\Throwable $exception) { + } catch (Throwable $exception) { $this->emitError( 500, 'Failed to process', @@ -113,7 +118,7 @@ public function run(): void $state->ackedEvent($event->event); try { $this->commit($state); - } catch (\JsonException $e) { + } catch (JsonException $e) { $this->emitError(500, 'json encoding error when committing state', ['exception' => $e]); }