From f8ed6d178ecd290df73e826c5c3f497f240ce899 Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Thu, 28 Sep 2023 09:36:40 -0400 Subject: [PATCH 01/15] Adding some suggested edits to workflow --- .github/workflows/build-test.yml | 23 +++++++++++++++++++++++ Dockerfile | 21 +++++++++++++++------ Makefile | 11 ++++++++--- 3 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/build-test.yml diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..e01e130 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,23 @@ +name: build-test +on: [push] + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Install dependencies + run: go get . + + # Ideally we would also use golangci-lint tool + - name: Run Go fmt, vet, test + run: make test + + - name: Build + run: make build diff --git a/Dockerfile b/Dockerfile index 1d25644..0991fb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,29 @@ # Use the official Golang image to build the binary FROM golang:1.20 AS build - +ENV CGO_ENABLED 0 +ARG BINARY_NAME # Set the working directory WORKDIR /app # Copy the Go source files, Makefile, etc. COPY . . -# Install make -RUN apt-get update && apt-get install -y make +# Build the Go application passing BINARY_NAME from Makefile +RUN go build -o ${BINARY_NAME} + +# Using a multi-stage build +FROM alpine:3.18 +ARG BINARY_NAME -# Use the Makefile to build the Go application -RUN make build +# Ensure we have a valid user and group +RUN addgroup -g 1000 -S assistant && \ + adduser -u 1000 -G assistant -S assistant +COPY --from=build --chown=assistant:assistant "/app/{$BINARY_NAME}" /app/ # Expose port 8080 EXPOSE 8080 +WORKDIR /app + # Run the compiled binary -CMD ["/app/assistant"] \ No newline at end of file +CMD ["./assistant"] \ No newline at end of file diff --git a/Makefile b/Makefile index 95701a2..7722e2a 100644 --- a/Makefile +++ b/Makefile @@ -14,17 +14,22 @@ build: # Run tests test: @echo "Running tests..." - go test -v ./... + go fmt ./... + go vet ./... + go test ./... # Build the Docker container docker-build: build @echo "Building Docker container..." - docker build -t $(CONTAINER_NAME) . + docker build \ + --platform=linux/amd64 \ + --build-arg=BINARY_NAME=$(BINARY_NAME) \ + --tag=$(REGISTRY_NAME)/$(CONTAINER_NAME) \ + . # Push the Docker container docker-push: docker-build @echo "Pushing Docker container..." - docker tag $(CONTAINER_NAME) $(REGISTRY_NAME)/$(CONTAINER_NAME) docker push $(REGISTRY_NAME)/$(CONTAINER_NAME) # Clean up From 1fd62943eaa29cfc876e816bd1fbefcc1217bce6 Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Fri, 29 Sep 2023 10:01:55 -0400 Subject: [PATCH 02/15] Adding develop image build --- .github/workflows/build-push-dev-image.yml | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/build-push-dev-image.yml diff --git a/.github/workflows/build-push-dev-image.yml b/.github/workflows/build-push-dev-image.yml new file mode 100644 index 0000000..99cb950 --- /dev/null +++ b/.github/workflows/build-push-dev-image.yml @@ -0,0 +1,78 @@ +# Workflow responsible for the +# development release processes. +# + +name: Build-Push-Dev-Image +on: + push: + branches: + - develop + paths-ignore: + - README.md + - .github/* + - .github/workflows/* + - LICENSE + - .gitignore + - .dockerignore + - .githooks + # Do not build another image on a pull request. + # Any push to develop will trigger a new build however. + pull_request: + branches-ignore: + - '*' +jobs: + build-push-dev-image: + runs-on: ubuntu-latest + steps: + + - name: Checkout Code + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Set short git commit SHA + id: vars + run: | + echo "short_sha=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_OUTPUT + # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ + + - name: Confirm git commit SHA output + run: echo ${{ steps.vars.outputs.short_sha }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + logout: true + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: containers.renci.org + username: ${{ secrets.CONTAINERHUB_USERNAME }} + password: ${{ secrets.CONTAINERHUB_TOKEN }} + logout: true + + + # Notes on Cache: + # https://docs.docker.com/build/ci/github-actions/examples/#inline-cache + - name: Build Push Container + uses: docker/build-push-action@v4 + with: + context: . + push: true + # Push to renci-registry and dockerhub here. + # cache comes from dockerhub. + tags: | + ${{ github.repository }}:develop + ${{ github.repository }}:${{ steps.vars.outputs.short_sha }} + containers.renci.org/${{ github.repository }}:develop + containers.renci.org/${{ github.repository }}:${{ steps.vars.outputs.short_sha }} + cache-from: type=registry,ref=${{ github.repository }}:buildcache-dev + cache-to: type=registry,ref=${{ github.repository }}:buildcache-dev,mode=max \ No newline at end of file From e56a2eac73d2fde19e27fb1cdfdb44122094bd61 Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Fri, 29 Sep 2023 10:10:51 -0400 Subject: [PATCH 03/15] Adding release workflow --- .github/workflows/build-push-dev-image.yml | 3 +- .github/workflows/build-push-release.yml | 126 +++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-push-release.yml diff --git a/.github/workflows/build-push-dev-image.yml b/.github/workflows/build-push-dev-image.yml index 99cb950..d534645 100644 --- a/.github/workflows/build-push-dev-image.yml +++ b/.github/workflows/build-push-dev-image.yml @@ -27,6 +27,7 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 + - name: Setup Go uses: actions/setup-go@v4 with: @@ -63,7 +64,7 @@ jobs: # Notes on Cache: # https://docs.docker.com/build/ci/github-actions/examples/#inline-cache - name: Build Push Container - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/.github/workflows/build-push-release.yml b/.github/workflows/build-push-release.yml new file mode 100644 index 0000000..8cb018c --- /dev/null +++ b/.github/workflows/build-push-release.yml @@ -0,0 +1,126 @@ +# Workflow responsible for the +# major release processes. +# + +name: Build-Push-Release +on: + push: + branches: + - master + - main + paths-ignore: + - README.md + - .old_cicd/* + - .github/* + - .github/workflows/* + - LICENSE + - .gitignore + - .dockerignore + - .githooks + tags-ignore: + - 'v[0-9]+.[0-9]+.*' +jobs: + build-push-release: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Set short git commit SHA + id: vars + run: | + echo "short_sha=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_OUTPUT + # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ + + - name: Confirm git commit SHA output + run: echo ${{ steps.vars.outputs.short_sha }} + + # https://github.com/marketplace/actions/git-semantic-version + - name: Semver Check + uses: paulhatch/semantic-version@v5.0.3 + id: version + with: + # The prefix to use to identify tags + tag_prefix: "v" + # A string which, if present in a git commit, indicates that a change represents a + # major (breaking) change, supports regular expressions wrapped with '/' + major_pattern: "/breaking|major/" + # A string which indicates the flags used by the `major_pattern` regular expression. Supported flags: idgs + major_regexp_flags: "ig" + # Same as above except indicating a minor change, supports regular expressions wrapped with '/' + minor_pattern: "/feat|feature|minor/" + # A string which indicates the flags used by the `minor_pattern` regular expression. Supported flags: idgs + minor_regexp_flags: "ig" + # A string to determine the format of the version output + # version_format: "${major}.${minor}.${patch}-prerelease${increment}" + version_format: "${major}.${minor}.${patch}" + search_commit_body: false + + # Docker Buildx is important to caching in the Build And Push Container + # step + # https://github.com/marketplace/actions/build-and-push-docker-images + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + logout: true + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: containers.renci.org + username: ${{ secrets.CONTAINERHUB_USERNAME }} + password: ${{ secrets.CONTAINERHUB_TOKEN }} + logout: true + + # Notes on Cache: + # https://docs.docker.com/build/ci/github-actions/examples/#inline-cache + - name: Build Push Container + uses: docker/build-push-action@v5 + with: + push: true + # Push to renci-registry and dockerhub here. + # cache comes from dockerhub. + tags: | + containers.renci.org/${{ github.repository }}:v${{ steps.version.outputs.version }} + containers.renci.org/${{ github.repository }}:latest + containers.renci.org/${{ github.repository }}:${{ steps.vars.outputs.short_sha }} + ${{ github.repository }}:v${{ steps.version.outputs.version }} + ${{ github.repository }}:latest + ${{ github.repository }}:${{ steps.vars.outputs.short_sha }} + cache-from: type=registry,ref=${{ github.repository }}:buildcache-release + cache-to: type=registry,ref=${{ github.repository }}:buildcache-release,mode=max + +#==========================TAG & RELEASE W/ NOTES ========================= + + # Note: GITHUB_TOKEN is autogenerated feature of github app + # which is auto-enabled when using github actions. + # https://docs.github.com/en/actions/security-guides/automatic-token-authentication + # https://docs.github.com/en/rest/git/tags?apiVersion=2022-11-28#create-a-tag-object + # https://docs.github.com/en/rest/git/refs?apiVersion=2022-11-28#create-a-reference + # This creates a "lightweight" ref tag. + - name: Create Tag for Release + run: | + curl \ + -s --fail -X POST \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/git/refs \ + -d '{"ref":"refs/tags/v${{ steps.version.outputs.version }}","sha":"${{ github.sha }}"}' + +# https://cli.github.com/manual/gh_release_create + - name: Create Release + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ env.RELEASE_VERSION }} \ + -t "${{ env.RELEASE_VERSION }}" \ + --generate-notes \ + --latest \ No newline at end of file From 2663c73b73e77d5065ee7b90cecc0e4b7daefda7 Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Fri, 29 Sep 2023 10:12:52 -0400 Subject: [PATCH 04/15] Adding release workflow 2 --- .github/workflows/build-push-dev-image.yml | 5 ++++- .github/workflows/build-push-release.yml | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-push-dev-image.yml b/.github/workflows/build-push-dev-image.yml index d534645..ffa1058 100644 --- a/.github/workflows/build-push-dev-image.yml +++ b/.github/workflows/build-push-dev-image.yml @@ -27,7 +27,10 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 - + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + - name: Setup Go uses: actions/setup-go@v4 with: diff --git a/.github/workflows/build-push-release.yml b/.github/workflows/build-push-release.yml index 8cb018c..f5189e4 100644 --- a/.github/workflows/build-push-release.yml +++ b/.github/workflows/build-push-release.yml @@ -24,11 +24,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} fetch-depth: 0 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + - name: Set short git commit SHA id: vars run: | From 164b21cc2c3b12191791323a9c12bb5425ebc249 Mon Sep 17 00:00:00 2001 From: Joshua Seals <117867292+joshua-seals@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:21:08 -0400 Subject: [PATCH 05/15] Update Dockerfile --- Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0991fb7..4dded09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,11 @@ WORKDIR /app # Copy the Go source files, Makefile, etc. COPY . . -# Build the Go application passing BINARY_NAME from Makefile -RUN go build -o ${BINARY_NAME} +# Install make +RUN apt-get update && apt-get install -y make + +# Use the Makefile to build the Go application +RUN make build # Using a multi-stage build FROM alpine:3.18 @@ -26,4 +29,4 @@ EXPOSE 8080 WORKDIR /app # Run the compiled binary -CMD ["./assistant"] \ No newline at end of file +CMD ["./assistant"] From 82f0472303c590b5e010805d5d3dc3f525218156 Mon Sep 17 00:00:00 2001 From: Joshua Seals <117867292+joshua-seals@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:31:18 -0400 Subject: [PATCH 06/15] Update Dockerfile --- Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4dded09..9f75f70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,9 @@ WORKDIR /app # Copy the Go source files, Makefile, etc. COPY . . -# Install make -RUN apt-get update && apt-get install -y make - -# Use the Makefile to build the Go application -RUN make build +# Build the Go application passing BINARY_NAME from Makefile (local development) +# or Github Action Build-Arg. +RUN go build -o ${BINARY_NAME} # Using a multi-stage build FROM alpine:3.18 From a358993542d515f7ed24ad15cd602255f756400d Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Fri, 29 Sep 2023 12:44:19 -0400 Subject: [PATCH 07/15] Fix workflow and passing values down to the dockerfile build step --- .github/workflows/build-push-dev-image.yml | 6 ++++++ .github/workflows/build-push-release.yml | 6 ++++++ Makefile | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/.github/workflows/build-push-dev-image.yml b/.github/workflows/build-push-dev-image.yml index ffa1058..3052770 100644 --- a/.github/workflows/build-push-dev-image.yml +++ b/.github/workflows/build-push-dev-image.yml @@ -45,6 +45,11 @@ jobs: - name: Confirm git commit SHA output run: echo ${{ steps.vars.outputs.short_sha }} + - name: Echo BINARY_NAME set in Makefile + id: BINARY_NAME + run: | + make echo >> $GITHUB_OUTPUT + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -69,6 +74,7 @@ jobs: - name: Build Push Container uses: docker/build-push-action@v5 with: + build-args: BINARY_NAME=${{ $BINARY_NAME }} context: . push: true # Push to renci-registry and dockerhub here. diff --git a/.github/workflows/build-push-release.yml b/.github/workflows/build-push-release.yml index f5189e4..fd252b8 100644 --- a/.github/workflows/build-push-release.yml +++ b/.github/workflows/build-push-release.yml @@ -40,6 +40,11 @@ jobs: echo "short_sha=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_OUTPUT # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ + - name: Echo BINARY_NAME set in Makefile + id: BINARY_NAME + run: | + make echo >> $GITHUB_OUTPUT + - name: Confirm git commit SHA output run: echo ${{ steps.vars.outputs.short_sha }} @@ -90,6 +95,7 @@ jobs: - name: Build Push Container uses: docker/build-push-action@v5 with: + build-args: BINARY_NAME=${{ assistant }} push: true # Push to renci-registry and dockerhub here. # cache comes from dockerhub. diff --git a/Makefile b/Makefile index 7722e2a..a4e5475 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ # Variable for the binary name BINARY_NAME=assistant + +# This is done to easily pass BINARY_NAME to github-actions +echo: + @echo $(BINARY_NAME) + # Variable for the container name REGISTRY_NAME=containers.renci.org/helxplatform CONTAINER_NAME=gitea-assist:latest From a5935c16df6e08241cf81d72f6290472aa613902 Mon Sep 17 00:00:00 2001 From: Joshua Seals <117867292+joshua-seals@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:48:21 -0400 Subject: [PATCH 08/15] Update build-push-dev-image.yml Fix bug --- .github/workflows/build-push-dev-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-push-dev-image.yml b/.github/workflows/build-push-dev-image.yml index 3052770..918fd79 100644 --- a/.github/workflows/build-push-dev-image.yml +++ b/.github/workflows/build-push-dev-image.yml @@ -74,7 +74,7 @@ jobs: - name: Build Push Container uses: docker/build-push-action@v5 with: - build-args: BINARY_NAME=${{ $BINARY_NAME }} + build-args: BINARY_NAME=${{ BINARY_NAME }} context: . push: true # Push to renci-registry and dockerhub here. @@ -85,4 +85,4 @@ jobs: containers.renci.org/${{ github.repository }}:develop containers.renci.org/${{ github.repository }}:${{ steps.vars.outputs.short_sha }} cache-from: type=registry,ref=${{ github.repository }}:buildcache-dev - cache-to: type=registry,ref=${{ github.repository }}:buildcache-dev,mode=max \ No newline at end of file + cache-to: type=registry,ref=${{ github.repository }}:buildcache-dev,mode=max From c9e882f2d2f57cd23a6d495e908150029c4204db Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Fri, 29 Sep 2023 12:56:28 -0400 Subject: [PATCH 09/15] Updates to workflows --- .github/workflows/build-push-dev-image.yml | 6 +++--- .github/workflows/build-push-release.yml | 2 +- Makefile | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-push-dev-image.yml b/.github/workflows/build-push-dev-image.yml index 918fd79..b44dfcd 100644 --- a/.github/workflows/build-push-dev-image.yml +++ b/.github/workflows/build-push-dev-image.yml @@ -9,8 +9,8 @@ on: - develop paths-ignore: - README.md - - .github/* - - .github/workflows/* + # - .github/* + # - .github/workflows/* - LICENSE - .gitignore - .dockerignore @@ -74,7 +74,7 @@ jobs: - name: Build Push Container uses: docker/build-push-action@v5 with: - build-args: BINARY_NAME=${{ BINARY_NAME }} + build-args: BINARY_NAME=${{ steps.BINARY_NAME.outputs.BINARY_NAME }} context: . push: true # Push to renci-registry and dockerhub here. diff --git a/.github/workflows/build-push-release.yml b/.github/workflows/build-push-release.yml index fd252b8..8d05c02 100644 --- a/.github/workflows/build-push-release.yml +++ b/.github/workflows/build-push-release.yml @@ -95,7 +95,7 @@ jobs: - name: Build Push Container uses: docker/build-push-action@v5 with: - build-args: BINARY_NAME=${{ assistant }} + build-args: BINARY_NAME=${{ steps.BINARY_NAME.outputs.BINARY_NAME }} push: true # Push to renci-registry and dockerhub here. # cache comes from dockerhub. diff --git a/Makefile b/Makefile index a4e5475..3315fdc 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BINARY_NAME=assistant # This is done to easily pass BINARY_NAME to github-actions echo: - @echo $(BINARY_NAME) + @echo "BINARY_NAME=$(BINARY_NAME)" # Variable for the container name REGISTRY_NAME=containers.renci.org/helxplatform From ea41017885bb6a6d51175003f4ecc38ad30b0f2b Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Fri, 29 Sep 2023 12:59:36 -0400 Subject: [PATCH 10/15] Update to dockerfile removing typo --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9f75f70..c0ea387 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ ARG BINARY_NAME RUN addgroup -g 1000 -S assistant && \ adduser -u 1000 -G assistant -S assistant -COPY --from=build --chown=assistant:assistant "/app/{$BINARY_NAME}" /app/ +COPY --from=build --chown=assistant:assistant /app/${BINARY_NAME} /app/ # Expose port 8080 EXPOSE 8080 From 9d21850726b544ced6547f33a8e3fcc6b87e901c Mon Sep 17 00:00:00 2001 From: Joshua Seals Date: Fri, 29 Sep 2023 13:10:27 -0400 Subject: [PATCH 11/15] Ignore updates to workflow --- .github/workflows/build-push-dev-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-push-dev-image.yml b/.github/workflows/build-push-dev-image.yml index b44dfcd..2015c87 100644 --- a/.github/workflows/build-push-dev-image.yml +++ b/.github/workflows/build-push-dev-image.yml @@ -9,8 +9,8 @@ on: - develop paths-ignore: - README.md - # - .github/* - # - .github/workflows/* + - .github/* + - .github/workflows/* - LICENSE - .gitignore - .dockerignore From a013fed1b3aa0bf3289abcc29985bd2d80296545 Mon Sep 17 00:00:00 2001 From: frostyfan109 Date: Fri, 3 May 2024 12:53:18 -0400 Subject: [PATCH 12/15] Major changes required for JLP/JLS Add endpoint for downloading archive of repository, add endpoint for deleting user, fork and create repo return remote URLs, fix user being required to change their password upon first login, change port to 9000 --- README.md | 2 +- chart/templates/deployment.yaml | 4 +- chart/values.yaml | 2 +- main.go | 253 ++++++++++++++++++++++++--- test_scripts/test_add_member.py | 2 +- test_scripts/test_create_fork.py | 2 +- test_scripts/test_create_org.py | 2 +- test_scripts/test_create_repo.py | 2 +- test_scripts/test_create_user.py | 2 +- test_scripts/test_delete_user.py | 30 ++++ test_scripts/test_get_forks.py | 2 +- test_scripts/test_get_org.py | 2 +- test_scripts/test_get_org_members.py | 2 +- test_scripts/test_get_repo.py | 4 +- test_scripts/test_get_user.py | 2 +- 15 files changed, 269 insertions(+), 44 deletions(-) create mode 100644 test_scripts/test_delete_user.py diff --git a/README.md b/README.md index 1b1f5db..19f0cae 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ To run the application, simply execute: go run main.go ``` -This will start an HTTP server on port 8000. +This will start an HTTP server on port 9000. ## Kubernetes Secret Creator for Gitea Credentials (mk_passwd.py) diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index f590d77..e47bafe 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -40,11 +40,11 @@ spec: livenessProbe: httpGet: path: /liveness - port: 8000 + port: 9000 readinessProbe: httpGet: path: /readiness - port: 8000 + port: 9000 resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: diff --git a/chart/values.yaml b/chart/values.yaml index 8ed2469..bad236f 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -38,7 +38,7 @@ securityContext: {} service: type: ClusterIP - port: 8000 + port: 9000 ingress: enabled: false diff --git a/main.go b/main.go index b414ffe..6b68cba 100644 --- a/main.go +++ b/main.go @@ -46,12 +46,17 @@ type MergeContext struct { ForkIsEmpty bool } -type UserOptions struct { +type CreateUserOptions struct { Email string `json:"email"` Username string `json:"username"` Password string `json:"password"` } +type DeleteUserOptions struct { + Username string `json:"username"` + Purge bool `json:"purge"` +} + type RepoOptions struct { Name string `json:"name"` Description string `json:"description"` @@ -220,6 +225,40 @@ func findForks(repoURL, username, password string) ([]api.Repository, error) { return forks, nil } +func getRemoteUrlFromRepo(repo *api.Repository) string { + return repo.CloneURL +} + +func getRemoteUrl(giteaBaseURL, adminUsername, adminPassword, owner string, repo string) (string, error) { + client := &http.Client{} + repoURL := fmt.Sprintf("%s/repos/%s/%s", giteaBaseURL, owner, repo) + req, err := http.NewRequest("GET", repoURL, nil) + if err != nil { + return "", err + } + + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + + resp, err := client.Do(req) + if err != nil { + log.Printf("could not retrieve remote url %v", err) + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var responseError map[string]interface{} + + json.NewDecoder(resp.Body).Decode(&responseError) + return "", fmt.Errorf("failed to retrieve remote url; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) + } + + var repository api.Repository + json.NewDecoder(resp.Body).Decode(&repository) + + return getRemoteUrlFromRepo(&repository), nil +} + func transferRepoOwnership(giteaBaseURL, adminUsername, adminPassword, owner, repo, newOwner string) error { options := api.TransferRepoOption{ NewOwner: newOwner, @@ -361,7 +400,7 @@ func addUserToTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName if resp.StatusCode != http.StatusNoContent { var responseError map[string]interface{} - + log.Printf("%v %v", resp.StatusCode, reqURL) json.NewDecoder(resp.Body).Decode(&responseError) return fmt.Errorf("failed to add user to team; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) } @@ -369,12 +408,43 @@ func addUserToTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName return nil } +func deleteUserFromTeam(giteaBaseURL, adminUsername, adminPassword, orgName, teamName, userName string) error { + teamID, err := getTeamID(giteaBaseURL, adminUsername, adminPassword, orgName, teamName) + if err != nil { + return err + } + + reqURL := fmt.Sprintf("%s/teams/%d/members/%s", giteaBaseURL, teamID, userName) + req, err := http.NewRequest("DELETE", reqURL, bytes.NewBuffer(nil)) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + var responseError map[string]interface{} + log.Printf("%v %v", resp.StatusCode, reqURL) + json.NewDecoder(resp.Body).Decode(&responseError) + return fmt.Errorf("failed to delete user from team; HTTP status code: %d, message: %s", resp.StatusCode, responseError["message"]) + } + + return nil +} + func createWebhook(giteaBaseURL, adminUsername, adminPassword, owner, repo, fullname string) error { reqURL := fmt.Sprintf("%s/repos/%s/%s/hooks", giteaBaseURL, owner, repo) config := api.CreateHookOptionConfig{ "content_type": "json", - "url": "http://" + fullname + ":8000/onPush", + "url": "http://" + fullname + ":9000/onPush", } options := api.CreateHookOption{ @@ -849,10 +919,13 @@ func createUser(giteaBaseURL, adminUsername, adminPassword, username, password, Password string `json:"password" binding:"Required;MaxSize(255)"` } */ + mustChangePassword := false user := api.CreateUserOption{ Username: username, Email: email, Password: password, + // I have no idea why this wants a pointer to a bool... + MustChangePassword: &mustChangePassword, } jsonData, _ := json.Marshal(user) @@ -884,7 +957,7 @@ func handleCreateUser(w http.ResponseWriter, r *http.Request) { return } - var options UserOptions + var options CreateUserOptions err = json.Unmarshal(body, &options) if err != nil { http.Error(w, "Failed parsing request body", http.StatusBadRequest) @@ -911,6 +984,62 @@ func handleCreateUser(w http.ResponseWriter, r *http.Request) { } } +func deleteUser(giteaBaseURL, adminUsername, adminPassword, username string, purge bool) (bool, error) { + url := fmt.Sprintf("%s/admin/users/%s?purge=%t", giteaBaseURL, username, purge) + req, _ := http.NewRequest("DELETE", url, nil) + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(resp.Body) + log.Println("Failed to delete user:", string(body)) + return false, nil + } + return true, nil +} + +func handleDeleteUser(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, "Failed reading request body", http.StatusInternalServerError) + return + } + + var options DeleteUserOptions + err = json.Unmarshal(body, &options) + if err != nil { + http.Error(w, "Failed parsing request body", http.StatusBadRequest) + return + } + + if options.Username == "" { + http.Error(w, "Username must be provided", http.StatusBadRequest) + return + } + + log.Println("Received User Data:", options) + if success, err := deleteUser(access.URL, access.Username, access.Password, options.Username, options.Purge); success { + // Respond to the client + w.WriteHeader(http.StatusCreated) + w.Write([]byte("User deleted successfully")) + } else { + http.Error(w, "User deletion failed", http.StatusBadRequest) + if err != nil { + log.Printf("User deletion failed %v", err) + } else { + log.Printf("User deletion failed") + } + } +} + func getUser(giteaBaseURL, adminUsername, adminPassword, username string) ([]byte, error) { url := fmt.Sprintf("%s/users/%s", giteaBaseURL, username) @@ -966,12 +1095,14 @@ func handleUser(w http.ResponseWriter, r *http.Request) { handleCreateUser(w, r) case http.MethodGet: handleGetUser(w, r) + case http.MethodDelete: + handleDeleteUser(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } -func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, name, description string, private bool) error { +func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, name, description string, private bool) (*api.Repository, error) { data := api.CreateRepoOption{ Name: name, Description: description, @@ -982,21 +1113,25 @@ func createRepoForUser(giteaBaseURL, adminUsername, adminPassword, username, nam req, err := http.NewRequest("POST", giteaBaseURL+"/admin/users/"+username+"/repos", bytes.NewBuffer(jsonData)) if err != nil { - return err + return nil, err } req.Header.Add("Content-Type", "application/json") req.SetBasicAuth(string(adminUsername), string(adminPassword)) resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) } - return nil + + var repository api.Repository + json.NewDecoder(resp.Body).Decode(&repository) + + return &repository, nil } func handleCreateRepo(w http.ResponseWriter, r *http.Request) { @@ -1021,10 +1156,11 @@ func handleCreateRepo(w http.ResponseWriter, r *http.Request) { } fmt.Println("Received Repo Data:", options) - if err := createRepoForUser(access.URL, access.Username, access.Password, options.Owner, options.Name, options.Description, options.Private); err == nil { + if repository, err := createRepoForUser(access.URL, access.Username, access.Password, options.Owner, options.Name, options.Description, options.Private); err == nil { if err := createWebhook(access.URL, access.Username, access.Password, options.Owner, options.Name, fullname); err == nil { + remoteUrl := getRemoteUrlFromRepo(repository) w.WriteHeader(http.StatusCreated) - w.Write([]byte("Repo created successfully")) + w.Write([]byte(remoteUrl)) } else { http.Error(w, "Webhook creation failed", http.StatusBadRequest) log.Printf("Webhook creation failed %v", err) @@ -1033,10 +1169,6 @@ func handleCreateRepo(w http.ResponseWriter, r *http.Request) { http.Error(w, "Repo creation failed", http.StatusBadRequest) log.Printf("Repo creation failed %v", err) } - - // Respond to the client - w.WriteHeader(http.StatusCreated) - w.Write([]byte("Repo created successfully")) } func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string) ([]byte, error) { @@ -1076,6 +1208,40 @@ func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName return bodyBytes, nil } +func downloadRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string, commit string) ([]byte, error) { + // Build the Gitea API URL for downloading the repo archive + url := fmt.Sprintf("%s/repos/%s/%s/archive/%s.zip", giteaBaseURL, owner, repoName, commit) + + // Build request + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Printf("Error creating request %v", http.StatusInternalServerError) + return nil, err + } + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + + // Send request + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Error querying Gitea %v", http.StatusInternalServerError) + return nil, fmt.Errorf("HTTP Error: %v", resp.StatusCode) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Printf("Error downloading repo from Gitea %v %v", resp.StatusCode, url) + return nil, fmt.Errorf("HTTP Error: %v", resp.StatusCode) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Error reading Gitea response %v", err) + return nil, err + } + + return bodyBytes, nil +} + func handleGetRepo(w http.ResponseWriter, r *http.Request) { repoName := r.URL.Query().Get("name") owner := r.URL.Query().Get("owner") @@ -1091,6 +1257,23 @@ func handleGetRepo(w http.ResponseWriter, r *http.Request) { } } +func handleDownloadRepo(w http.ResponseWriter, r *http.Request) { + repoName := r.URL.Query().Get("name") + owner := r.URL.Query().Get("owner") + commit := r.URL.Query().Get("commit") + if repoName == "" || owner == "" { + http.Error(w, "Repo name and owner must be provided", http.StatusBadRequest) + return + } + if resp, err := downloadRepoForUser(access.URL, access.Username, access.Password, owner, repoName, commit); err == nil { + w.WriteHeader(http.StatusOK) + w.Write(resp) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + +} + func handleRepo(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: @@ -1102,7 +1285,7 @@ func handleRepo(w http.ResponseWriter, r *http.Request) { } } -func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo, user string) error { +func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo, user string) (*api.Repository, error) { /* reenable this once gitea bug #26234 is fixed @@ -1121,7 +1304,7 @@ func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, re req, err := http.NewRequest("POST", giteaBaseURL+"/repos/"+owner+"/"+repo+"/forks", bytes.NewBuffer(jsonData)) if err != nil { - return err + return nil, err } req.Header.Add("Content-Type", "application/json") @@ -1129,26 +1312,30 @@ func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, re resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusAccepted { if err := transferRepoOwnership(giteaBaseURL, adminUsername, adminPassword, adminUsername, tmpRepoName, user); err != nil { log.Printf("transfer ownership of %s to %s failed: %v", tmpRepoName, user, err) - return err + return nil, err } if err := renameRepo(giteaBaseURL, adminUsername, adminPassword, user, tmpRepoName, repo); err != nil { log.Printf("rename of repo from %s to %s failed %v", tmpRepoName, repo, err) - return err + return nil, err } if err := createWebhook(access.URL, access.Username, access.Password, user, repo, fullname); err != nil { log.Printf("create webhook for repo %s failed %v", repo, err) - return err + return nil, err } - return nil + + var repository api.Repository + json.NewDecoder(resp.Body).Decode(&repository) + + return &repository, nil } else { - return fmt.Errorf("fork failed with code %v", resp.StatusCode) + return nil, fmt.Errorf("fork failed with code %v", resp.StatusCode) } } @@ -1168,9 +1355,15 @@ func handleCreateFork(w http.ResponseWriter, r *http.Request) { } fmt.Println("Forking repo:", options.Repo, "for user:", options.NewOwner) - if err := forkRepositoryForUser(access.URL, access.Username, access.Password, options.Owner, options.Repo, options.NewOwner); err == nil { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(fmt.Sprintf("Repo %s forked successfully for user %s", options.Repo, options.NewOwner))) + if _, err := forkRepositoryForUser(access.URL, access.Username, access.Password, options.Owner, options.Repo, options.NewOwner); err == nil { + // Note: we can't use getRemoteUrlFromRepo since the returned repo remote is incorrect due to the way we handle forking w/ rename. + if remoteUrl, err := getRemoteUrl(access.URL, access.Username, access.Password, options.NewOwner, options.Repo); err == nil { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(remoteUrl)) + } else { + http.Error(w, "Fork failed", http.StatusBadRequest) + log.Printf("Repo creation failed %v", err) + } } else { http.Error(w, "Fork failed", http.StatusBadRequest) if err != nil { @@ -1403,6 +1596,7 @@ func handleAddMember(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte("User added to organization")) } else { + log.Printf("%v", err) http.Error(w, "Add user failed", http.StatusInternalServerError) } } @@ -1431,7 +1625,7 @@ func livenessHandler(w http.ResponseWriter, r *http.Request) { // main initializes an HTTP server with endpoints for processing push events, // checking service readiness, and determining service liveness. The server -// listens on port 8000. Logging is utilized to indicate the server's start +// listens on port 8900. Logging is utilized to indicate the server's start // and to capture any fatal errors. func main() { //mux := http.NewServeMux() @@ -1439,6 +1633,7 @@ func main() { r.HandleFunc("/onPush", webhookHandler) r.HandleFunc("/users", handleUser) r.HandleFunc("/repos", handleRepo) + r.HandleFunc("/repos/download", handleDownloadRepo).Methods("GET") r.HandleFunc("/forks", handleFork) r.HandleFunc("/orgs", handleOrg) r.HandleFunc("/orgs/{orgName}/members", handleGetMembers).Methods("GET") @@ -1446,6 +1641,6 @@ func main() { r.HandleFunc("/readiness", readinessHandler) r.HandleFunc("/liveness", livenessHandler) http.Handle("/", r) - log.Println("Server started on :8000") - log.Fatal(http.ListenAndServe(":8000", nil)) + log.Println("Server started on :9000") + log.Fatal(http.ListenAndServe(":9000", nil)) } diff --git a/test_scripts/test_add_member.py b/test_scripts/test_add_member.py index cffb57e..2e5b0e5 100644 --- a/test_scripts/test_add_member.py +++ b/test_scripts/test_add_member.py @@ -11,7 +11,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs/{args.org_name}/members/{args.user_name}" + url = f"http://{args.server}:9000/orgs/{args.org_name}/members/{args.user_name}" response = requests.put(url) diff --git a/test_scripts/test_create_fork.py b/test_scripts/test_create_fork.py index 07f0cdd..ca3a7a5 100644 --- a/test_scripts/test_create_fork.py +++ b/test_scripts/test_create_fork.py @@ -13,7 +13,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/forks" + url = f"http://{args.server}:9000/forks" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_create_org.py b/test_scripts/test_create_org.py index a118833..13e39b1 100644 --- a/test_scripts/test_create_org.py +++ b/test_scripts/test_create_org.py @@ -11,7 +11,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs" + url = f"http://{args.server}:9000/orgs" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_create_repo.py b/test_scripts/test_create_repo.py index 40d5c78..e4383ee 100644 --- a/test_scripts/test_create_repo.py +++ b/test_scripts/test_create_repo.py @@ -12,7 +12,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/repos" + url = f"http://{args.server}:9000/repos" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_create_user.py b/test_scripts/test_create_user.py index 661140a..393b3ea 100644 --- a/test_scripts/test_create_user.py +++ b/test_scripts/test_create_user.py @@ -13,7 +13,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/users" + url = f"http://{args.server}:9000/users" headers = { "Content-Type": "application/json" } diff --git a/test_scripts/test_delete_user.py b/test_scripts/test_delete_user.py new file mode 100644 index 0000000..b70221f --- /dev/null +++ b/test_scripts/test_delete_user.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import argparse +import requests +import json + +def main(): + parser = argparse.ArgumentParser(description="Test user deletion via API.") + parser.add_argument("username", help="Username of the user") + parser.add_argument("--purge", action="store_true", help="Completely purge the user from the system (repositories, membership, etc.)") + parser.add_argument("--server", default="localhost", help="Server hostname with port (default: localhost)") + + args = parser.parse_args() + + url = f"http://{args.server}:9000/users" + headers = { + "Content-Type": "application/json" + } + data = { + "username": args.username, + "purge": args.purge + } + + response = requests.delete(url, headers=headers, data=json.dumps(data)) + + print(response.status_code) + print(response.text) + +if __name__ == "__main__": + main() diff --git a/test_scripts/test_get_forks.py b/test_scripts/test_get_forks.py index 2838e36..ec0025b 100644 --- a/test_scripts/test_get_forks.py +++ b/test_scripts/test_get_forks.py @@ -11,7 +11,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/forks?name={args.repo_name}&owner={args.owner}" + url = f"http://{args.server}:9000/forks?name={args.repo_name}&owner={args.owner}" response = requests.get(url) diff --git a/test_scripts/test_get_org.py b/test_scripts/test_get_org.py index 27f59cc..7a25f7f 100644 --- a/test_scripts/test_get_org.py +++ b/test_scripts/test_get_org.py @@ -10,7 +10,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs?org_name={args.org_name}" + url = f"http://{args.server}:9000/orgs?org_name={args.org_name}" response = requests.get(url) diff --git a/test_scripts/test_get_org_members.py b/test_scripts/test_get_org_members.py index 52e6fa3..9e719f5 100644 --- a/test_scripts/test_get_org_members.py +++ b/test_scripts/test_get_org_members.py @@ -10,7 +10,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/orgs/{args.org_name}/members" + url = f"http://{args.server}:9000/orgs/{args.org_name}/members" response = requests.get(url) diff --git a/test_scripts/test_get_repo.py b/test_scripts/test_get_repo.py index c85b6e8..a921986 100644 --- a/test_scripts/test_get_repo.py +++ b/test_scripts/test_get_repo.py @@ -7,11 +7,11 @@ def main(): parser = argparse.ArgumentParser(description="Test getting repo details via API.") parser.add_argument("repo_name", help="Name of the repository") parser.add_argument("owner", help="Owner of the repository") - parser.add_argument("--server", default="localhost:8000", help="Server hostname with port (default: localhost:8000)") + parser.add_argument("--server", default="localhost", help="Server hostname with port (default: localhost)") args = parser.parse_args() - url = f"http://{args.server}/repos?name={args.repo_name}&owner={args.owner}" + url = f"http://{args.server}:9000/repos?name={args.repo_name}&owner={args.owner}" response = requests.get(url) diff --git a/test_scripts/test_get_user.py b/test_scripts/test_get_user.py index 7e74133..19ebf9c 100644 --- a/test_scripts/test_get_user.py +++ b/test_scripts/test_get_user.py @@ -10,7 +10,7 @@ def main(): args = parser.parse_args() - url = f"http://{args.server}:8000/users?username={args.username}" + url = f"http://{args.server}:9000/users?username={args.username}" response = requests.get(url) From ea4ba90fcee5dfd6c3992d351d225ba9154baff1 Mon Sep 17 00:00:00 2001 From: frostyfan109 Date: Sat, 4 May 2024 13:50:13 -0400 Subject: [PATCH 13/15] Add path option to download repo, add PATCH repository endpoint, fix status codes --- main.go | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 145 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 6b68cba..861b57a 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "archive/zip" "bytes" "encoding/json" "fmt" @@ -64,6 +65,12 @@ type RepoOptions struct { Private bool `json:"private"` } +type PatchRepoOptions struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Private bool `json:"private,omitempty"` +} + type ForkOptions struct { Owner string `json:"owner"` NewOwner string `json:"newOwner"` @@ -133,6 +140,60 @@ func getFullname() (string, error) { } +func downloadPathFromZip(zipBytes []byte, path string) ([]byte, error) { + reader := bytes.NewReader(zipBytes) + zipReader, err := zip.NewReader(reader, int64(len(zipBytes))) + if err != nil { + log.Printf("unable to parse zipfile from bytes for path %v", path) + return nil, err + } + buf := new(bytes.Buffer) + zipWriter := zip.NewWriter(buf) + + for _, zFile := range zipReader.File { + if strings.HasPrefix(strings.ToLower(zFile.Name), strings.ToLower(path)) { + fileReader, err := zFile.Open() + if err != nil { + log.Printf("failed to open reader for %v", zFile) + } + defer fileReader.Close() + + // We don't use TrimPrefix here because it's case-sensitive and we don't care about case + trimmedFilePath := zFile.Name[len(path):] + // Remove leading slash + trimmedFilePath = strings.TrimPrefix(trimmedFilePath, "/") + + // This may corrupt ZIP archive utilities if not skipped + // (e.g. this will break for MacOS's Archive Utility) + if trimmedFilePath == "" { + continue + } + + header := &zip.FileHeader{ + Name: trimmedFilePath, + Method: zip.Store, + Modified: zFile.Modified, + } + + fileWriter, err := zipWriter.CreateHeader(header) + if err != nil { + log.Printf("failed to create header for %v", zFile.Name) + } + + if _, err := io.Copy(fileWriter, fileReader); err != nil { + log.Printf("failed to write %v to new zip file", zFile.Name) + return nil, err + } + + } + } + + if err = zipWriter.Close(); err != nil { + log.Printf("failed to close zip writer %v", err) + } + return buf.Bytes(), nil +} + func createTokenForUser(giteaBaseURL, adminUsername, adminPassword, username, name string, scopes []string) (*api.AccessToken, error) { var token api.AccessToken @@ -1028,7 +1089,7 @@ func handleDeleteUser(w http.ResponseWriter, r *http.Request) { log.Println("Received User Data:", options) if success, err := deleteUser(access.URL, access.Username, access.Password, options.Username, options.Purge); success { // Respond to the client - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusOK) w.Write([]byte("User deleted successfully")) } else { http.Error(w, "User deletion failed", http.StatusBadRequest) @@ -1208,9 +1269,9 @@ func getRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName return bodyBytes, nil } -func downloadRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string, commit string) ([]byte, error) { +func downloadRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, treeishId, path string) ([]byte, error) { // Build the Gitea API URL for downloading the repo archive - url := fmt.Sprintf("%s/repos/%s/%s/archive/%s.zip", giteaBaseURL, owner, repoName, commit) + url := fmt.Sprintf("%s/repos/%s/%s/archive/%s.zip", giteaBaseURL, owner, repoName, treeishId) // Build request req, err := http.NewRequest("GET", url, nil) @@ -1239,7 +1300,81 @@ func downloadRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo return nil, err } - return bodyBytes, nil + // Gitea does not currently support the `path` option + // https://github.com/go-gitea/gitea/issues/4478 + archiveBytes, err := downloadPathFromZip(bodyBytes, fmt.Sprintf("%v/%v", repoName, path)) + if err != nil { + log.Printf("Error extracting path from zipfile %v", err) + return nil, err + } + return archiveBytes, err +} + +func modifyRepoForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName string, newName *string, newDescription *string, newPrivate *bool) (*api.Repository, error) { + data := api.EditRepoOption{ + Name: newName, + Description: newDescription, + Private: newPrivate, + } + + jsonData, _ := json.Marshal(data) + + url := fmt.Sprintf("%s/repos/%s/%s", giteaBaseURL, owner, repoName) + req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP Error: %d", resp.StatusCode) + } + + var repository api.Repository + json.NewDecoder(resp.Body).Decode(&repository) + + return &repository, nil +} + +func handlePatchRepo(w http.ResponseWriter, r *http.Request) { + repoName := r.URL.Query().Get("name") + owner := r.URL.Query().Get("owner") + if repoName == "" || owner == "" { + http.Error(w, "Repo name and owner must be provided", http.StatusBadRequest) + return + } + + body, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, "Failed reading request body", http.StatusInternalServerError) + return + } + + var options PatchRepoOptions + err = json.Unmarshal(body, &options) + if err != nil { + http.Error(w, "Failed parsing request body", http.StatusBadRequest) + return + } + + fmt.Println("Received Repo Data:", options) + if repository, err := modifyRepoForUser(access.URL, access.Username, access.Password, owner, repoName, &options.Name, &options.Description, &options.Private); err == nil { + remoteUrl := getRemoteUrlFromRepo(repository) + w.WriteHeader(http.StatusOK) + w.Write([]byte(remoteUrl)) + } else { + http.Error(w, "Repo modify failed", http.StatusBadRequest) + log.Printf("Repo modify failed %v", err) + } } func handleGetRepo(w http.ResponseWriter, r *http.Request) { @@ -1260,12 +1395,14 @@ func handleGetRepo(w http.ResponseWriter, r *http.Request) { func handleDownloadRepo(w http.ResponseWriter, r *http.Request) { repoName := r.URL.Query().Get("name") owner := r.URL.Query().Get("owner") - commit := r.URL.Query().Get("commit") + treeishId := r.URL.Query().Get("treeish_id") + path := r.URL.Query().Get("path") + if repoName == "" || owner == "" { http.Error(w, "Repo name and owner must be provided", http.StatusBadRequest) return } - if resp, err := downloadRepoForUser(access.URL, access.Username, access.Password, owner, repoName, commit); err == nil { + if resp, err := downloadRepoForUser(access.URL, access.Username, access.Password, owner, repoName, treeishId, path); err == nil { w.WriteHeader(http.StatusOK) w.Write(resp) } else { @@ -1280,6 +1417,8 @@ func handleRepo(w http.ResponseWriter, r *http.Request) { handleCreateRepo(w, r) case http.MethodGet: handleGetRepo(w, r) + case http.MethodPatch: + handlePatchRepo(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } From 978ea68b4de3f8aca6aa79d778f8fa93870dfb9a Mon Sep 17 00:00:00 2001 From: frostyfan109 Date: Sat, 4 May 2024 15:21:13 -0400 Subject: [PATCH 14/15] Add/remove collaborators from repo --- main.go | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/main.go b/main.go index 861b57a..5f6c7ca 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,19 @@ type PatchRepoOptions struct { Private bool `json:"private,omitempty"` } +type AddCollaboratorOptions struct { + Name string `json:"name"` + Owner string `json:"owner"` + CollaboratorName string `json:"collaborator_name"` + Permission string `json:"permission"` +} + +type RemoveCollaboratorOptions struct { + Name string `json:"name"` + Owner string `json:"owner"` + CollaboratorName string `json:"collaborator_name"` +} + type ForkOptions struct { Owner string `json:"owner"` NewOwner string `json:"newOwner"` @@ -1424,6 +1437,142 @@ func handleRepo(w http.ResponseWriter, r *http.Request) { } } +func addCollaboratorToRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, collaboratorName, permission string) error { + + // Build the Gitea API URL for fetching the repo details + url := fmt.Sprintf("%s/repos/%s/%s/collaborators/%s", giteaBaseURL, owner, repoName, collaboratorName) + + // Empty permission string is treated the same as omitting it by the Gitea API here. + option := api.AddCollaboratorOption{ + Permission: &permission, + } + jsonData, _ := json.Marshal(option) + + // Create a new request + req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData)) + if err != nil { + log.Printf("Error creating request %v", http.StatusInternalServerError) + return err + } + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + + // Send the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Error querying Gitea %v", http.StatusInternalServerError) + return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + } + defer resp.Body.Close() + + // Check if the request was successful + if resp.StatusCode != http.StatusNoContent { + log.Printf("Error adding contributor from Gitea %v", resp.StatusCode) + return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + } + + return nil +} + +func handleAddCollaborator(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, "Failed reading request body", http.StatusInternalServerError) + return + } + + var options AddCollaboratorOptions + err = json.Unmarshal(body, &options) + if err != nil { + http.Error(w, "Failed parsing request body", http.StatusBadRequest) + return + } + + // We won't enforce Permission since Gitea doesn't enforce it. + if options.Name == "" || options.Owner == "" || options.CollaboratorName == "" { + http.Error(w, "Repo name, owner, and collaborator name must be provided", http.StatusBadRequest) + return + } + if err := addCollaboratorToRepo(access.URL, access.Username, access.Password, options.Owner, options.Name, options.CollaboratorName, options.Permission); err == nil { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Contributor added successfully")) + } else { + w.WriteHeader(http.StatusInternalServerError) + } +} + +func removeCollaboratorFromRepo(giteaBaseURL, adminUsername, adminPassword, owner, repoName, collaboratorName string) error { + + // Build the Gitea API URL for fetching the repo details + url := fmt.Sprintf("%s/repos/%s/%s/collaborators/%s", giteaBaseURL, owner, repoName, collaboratorName) + + // Create a new request + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + log.Printf("Error creating request %v", http.StatusInternalServerError) + return err + } + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + + // Send the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Error querying Gitea %v", http.StatusInternalServerError) + return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + } + defer resp.Body.Close() + + // Check if the request was successful + if resp.StatusCode != http.StatusNoContent { + log.Printf("Error removing contributor from Gitea %v", resp.StatusCode) + return fmt.Errorf("HTTP Error: %d", resp.StatusCode) + } + + return nil +} + +func handleRemoveCollaborator(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, "Failed reading request body", http.StatusInternalServerError) + return + } + + var options RemoveCollaboratorOptions + err = json.Unmarshal(body, &options) + if err != nil { + http.Error(w, "Failed parsing request body", http.StatusBadRequest) + return + } + + if options.Name == "" || options.Owner == "" || options.CollaboratorName == "" { + http.Error(w, "Repo name, owner, and collaborator name must be provided", http.StatusBadRequest) + return + } + if err := removeCollaboratorFromRepo(access.URL, access.Username, access.Password, options.Owner, options.Name, options.CollaboratorName); err == nil { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Contributor removed successfully")) + } else { + w.WriteHeader(http.StatusInternalServerError) + } +} + +func handleRepoCollaborator(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPut: + handleAddCollaborator(w, r) + case http.MethodDelete: + handleRemoveCollaborator(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + func forkRepositoryForUser(giteaBaseURL, adminUsername, adminPassword, owner, repo, user string) (*api.Repository, error) { /* reenable this once gitea bug #26234 is fixed @@ -1772,6 +1921,7 @@ func main() { r.HandleFunc("/onPush", webhookHandler) r.HandleFunc("/users", handleUser) r.HandleFunc("/repos", handleRepo) + r.HandleFunc("/repos/collaborators", handleRepoCollaborator) r.HandleFunc("/repos/download", handleDownloadRepo).Methods("GET") r.HandleFunc("/forks", handleFork) r.HandleFunc("/orgs", handleOrg) From 064ce24c23d9fc387b1286e99b66ef2668db17b6 Mon Sep 17 00:00:00 2001 From: frostyfan109 Date: Fri, 17 May 2024 14:49:40 -0400 Subject: [PATCH 15/15] Add modify files endpoint --- main.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/main.go b/main.go index 5f6c7ca..2dfba37 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,14 @@ type RepoOptions struct { Private bool `json:"private"` } +type ModifyRepoOptions struct { + Name string `json:"name"` + Owner string `json:"owner"` + Branch string `json:"branch"` + Message string `json:"message"` + Files []*api.ChangeFileOperation `json:"files"` +} + type PatchRepoOptions struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` @@ -1405,6 +1413,84 @@ func handleGetRepo(w http.ResponseWriter, r *http.Request) { } } +func modifyRepoFilesForUser(giteaBaseURL, adminUsername, adminPassword, owner, repoName, branch, message string, files []*api.ChangeFileOperation) (string, error) { + // Build the Gitea API URL for downloading the repo archive + url := fmt.Sprintf("%s/repos/%s/%s/contents", giteaBaseURL, owner, repoName) + + data := api.ChangeFilesOptions{ + FileOptions: api.FileOptions{ + BranchName: branch, + Author: api.Identity{Name: adminUsername}, + Committer: api.Identity{Name: adminUsername}, + Message: message, + }, + Files: files, + } + + jsonData, _ := json.Marshal(data) + + // Build request + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + log.Printf("Error creating request %v", http.StatusInternalServerError) + return "", err + } + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(string(adminUsername), string(adminPassword)) + + // Send request + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Error querying Gitea %v", http.StatusInternalServerError) + return "", fmt.Errorf("HTTP Error: %v", resp.StatusCode) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + log.Printf("Error modifying repo from Gitea %v %v", resp.StatusCode, url) + return "", fmt.Errorf("HTTP Error: %v", resp.StatusCode) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Error reading Gitea response %v", err) + return "", err + } + var filesResponse api.FilesResponse + err = json.Unmarshal(bodyBytes, &filesResponse) + if err != nil { + log.Printf("Error reading Gitea response %v", err) + return "", err + } + + return filesResponse.Commit.SHA, nil +} + +func handleModifyRepoFiles(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + defer r.Body.Close() + + if err != nil { + http.Error(w, "Failed reading request body", http.StatusInternalServerError) + return + } + + var options ModifyRepoOptions + err = json.Unmarshal(body, &options) + if err != nil { + http.Error(w, "Failed parsing request body", http.StatusBadRequest) + return + } + + if commitHash, err := modifyRepoFilesForUser(access.URL, access.Username, access.Password, options.Owner, options.Name, options.Branch, options.Message, options.Files); err == nil { + w.WriteHeader(http.StatusOK) + w.Write([]byte(commitHash)) + } else { + http.Error(w, "Repo modify failed", http.StatusBadRequest) + log.Printf("Repo modify failed %v", err) + } +} + func handleDownloadRepo(w http.ResponseWriter, r *http.Request) { repoName := r.URL.Query().Get("name") owner := r.URL.Query().Get("owner") @@ -1922,6 +2008,7 @@ func main() { r.HandleFunc("/users", handleUser) r.HandleFunc("/repos", handleRepo) r.HandleFunc("/repos/collaborators", handleRepoCollaborator) + r.HandleFunc("/repos/modify", handleModifyRepoFiles).Methods("POST") r.HandleFunc("/repos/download", handleDownloadRepo).Methods("GET") r.HandleFunc("/forks", handleFork) r.HandleFunc("/orgs", handleOrg)