diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml new file mode 100644 index 0000000..63bc6d6 --- /dev/null +++ b/.github/workflows/pre-merge.yaml @@ -0,0 +1,25 @@ +name: Pre Merge Checks + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + gradle: + runs-on: ubuntu-latest + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Cache + uses: gradle/gradle-build-action@v2 + - name: Validate + run: ./gradlew check validatePlugins --continue + - name: Integration Test + run: ./gradlew integrationTest --info diff --git a/.github/workflows/publish-plugin.yaml b/.github/workflows/publish-plugin.yaml new file mode 100644 index 0000000..9c286b6 --- /dev/null +++ b/.github/workflows/publish-plugin.yaml @@ -0,0 +1,24 @@ +name: Publish to Gradle Plugin Portal + +on: + push: + tags: + - '*' + +jobs: + gradle: + runs-on: ubuntu-latest + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Cache + uses: gradle/gradle-build-action@v2 + - name: Validate + run: ./gradlew check validatePlugins --continue + - name: Integration Test + run: ./gradlew integrationTest + - name: Publish + run: ./gradlew publishPlugins diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b007eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +.idea/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..988f10c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Liftric + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1333ed7..a4f19f1 100644 --- a/README.md +++ b/README.md @@ -1 +1,138 @@ -TODO +# Gradle S3 Apt Plugin + +This Gradle plugin enables you to manage an Apt Repository in an Amazon S3 bucket. It's designed to assist with the task of handling Debian packages in an S3 environment. The plugin comes with three key tasks: `uploadPackage`, `removePackage`, and `cleanPackages`. + +- The `uploadPackage` task allows for the upload of new Debian packages into the S3 Apt repository, including the necessary updating of the Release and Packages files. If no existing repository is found, a new one will be created. +- The `removePackage` task is designed for the removal of specific versions of packages from the Packages file of a repository in an S3 bucket, with automatic updating of the Release file. The actual Debian file, however, will not be removed from the pool. +- The `cleanPackages` task serves the purpose of deleting all Debian files that are no longer listed in the Packages file. + +> **Recommendation**: To create Debian packages from a Gradle project, there is this Gradle plugin https://github.com/nebula-plugins/gradle-ospackage-plugin + +> **Caution**: This plugin is still in the early stages of development and has not yet been extensively tested in a production environment. It is strongly recommended to backup your S3 bucket before using this plugin in a production setting to prevent any potential data loss. + +## Example + +```kotlin +import com.liftric.apt.extensions.* + +plugins { + id("com.liftric.s3-apt-repository-plugin") +} + +s3AptRepository { + bucket.set("s3-apt-repo") + region.set("eu-central-1") + accessKey.set(System.getenv("AWS_ACCESS_KEY")) + secretKey.set(System.getenv("AWS_SECRET_KEY")) + signingKeyPassphrase.set(System.getenv("PGP_PASSPHRASE")) + signingKeyRingFile.set(file("private.key")) + debPackage { + file.set(file("foobar_1.0.0-1_all.deb")) + packageArchitectures.set(setOf("all", "amd64")) + } +} +``` + +### uploadPackage + +The `uploadPackage` task comes equipped with a range of both required and optional attributes. + +- **bucket**: *(Required)* The name of the S3 bucket where the Apt Repository resides. +- **region**: *(Required)* The AWS region of the S3 bucket. +- **accessKey**: *(Required)* The AWS Access Key for accessing the S3 bucket. Can be overridden in the `debPackage` section. +- **secretKey**: *(Required)* The AWS Secret Key for accessing the S3 bucket. Can be overridden in the `debPackage` section. +- **signingKeyRingFile**: *(Required)* The PGP private key file used for signing the Release files. +- **signingKeyPassphrase**: *(Required)* The passphrase for the PGP private key. +- **bucketPath**: *(Optional)* The path within the bucket to store the Apt Repository. If not specified, the repository is stored at the root of the bucket. +- **endpoint**: *(Optional)* Custom S3 endpoint. Use this to override the default AWS S3 endpoint. +- **override**: *(Optional)* Boolean value indicating whether to override existing version of a Package. By default, it is `true`. + +`Release File Attributes` can be override in s3AptRepository or per debPackage + +- **origin**: *(Optional)* The value of the 'Origin' field in the Release files. By default, it is 'Debian'. +- **label**: *(Optional)* The value of the 'Label' field in the Release files. By default, it is 'Debian'. +- **suite**: *(Optional)* The value of the 'Suite' field in the Release files. By default, it is 'stable'. +- **component**: *(Optional)* The value of the 'Component' field in the Release files. By default, it is 'main'. +- **architectures**: *(Optional)* The value of the 'Architectures' field in the Release files. By default, it is set correctly by the Plugin. If you need to override it, you can do so here. +- **codename**: *(Optional)* The value of the 'Codename' field in the Release files. By default, it is not set. +- **date**: *(Optional)* The value of the 'Date' field in the Release files. By default, it is set to the current date. +- **releaseDescription**: *(Optional)* The value of the 'Description' field in the Release files. By default, it is not set. +- **releaseVersion**: *(Optional)* The value of the 'Version' field in the Release files. By default, it is not set. +- **validUntil**: *(Optional)* The value of the 'Valid-Until' field in the Release files. By default, it is not set. +- **notAutomatic**: *(Optional)* The value of the 'NotAutomatic' field in the Release files. By default, it is not set. +- **butAutomaticUpgrades**: *(Optional)* The value of the 'ButAutomaticUpgrades' field in the Release files. By default, it is not set. +- **changelogs**: *(Optional)* The value of the 'Changelogs' field in the Release files. By default, it is not set. +- **snapshots**: *(Optional)* The value of the 'Snapshots' field in the Release files. By default, it is not set. + + +- **debPackage**: *(Required)* See below for more information. + +In the `debPackage` section, you can specify package-specific attributes. These can override the top-level attributes if needed: + +- **file**: *(Required)* The Debian package file to upload. +- **packageArchitectures**: *(Required)* Set of architectures that the package supports. +- **packageName**, **packageVersion**: *(Optional)* These attributes can be used to override the default package name and version extracted form the Debian File. +- **origin**, **label**, **suite**, **component**, **architectures**, **codename**, **date**, **releaseDescription**, **releaseVersion**, **validUntil**, **notAutomatic**, **butAutomaticUpgrades**, **changelogs**, **snapshots** : *(Optional)* These attributes can be used to override the default Release file fields for the specific package. +- **accessKey**, **secretKey**, **bucket**, **bucketPath**, **region**, **endpoint**: *(Optional)* These attributes can be used to override their respective top-level attributes for the specific package. + + + + + +### removePackage + +The `removePackage` task comes equipped with a range of both required and optional attributes. + +- **bucket**: *(Required)* The name of the S3 bucket where the Apt Repository resides. +- **region**: *(Required)* The AWS region of the S3 bucket. +- **accessKey**: *(Required)* The AWS Access Key for accessing the S3 bucket. Can be overridden in the `debPackage` section. +- **secretKey**: *(Required)* The AWS Secret Key for accessing the S3 bucket. Can be overridden in the `debPackage` section. +- **signingKeyRingFile**: *(Required)* The PGP private key file used for signing the Release files. +- **signingKeyPassphrase**: *(Required)* The passphrase for the PGP private key. +- **bucketPath**: *(Optional)* The path within the bucket to store the Apt Repository. If not specified, the repository is stored at the root of the bucket. +- **endpoint**: *(Optional)* Custom S3 endpoint. Use this to override the default AWS S3 endpoint. +- **origin**: *(Optional)* The value of the 'Origin' field in the Release files. By default, it is 'Debian'. + +`Release File Attributes` can be override in s3AptRepository or per debPackage + +- **label**: *(Optional)* The value of the 'Label' field in the Release files. By default, it is 'Debian'. +- **suite**: *(Optional)* The value of the 'Suite' field in the Release files. By default, it is 'stable'. +- **component**: *(Optional)* The value of the 'Component' field in the Release files. By default, it is 'main'. +- **architectures**: *(Optional)* The value of the 'Architectures' field in the Release files. By default, it is set correctly by the Plugin. If you need to override it, you can do so here. +- **codename**: *(Optional)* The value of the 'Codename' field in the Release files. By default, it is not set. +- **date**: *(Optional)* The value of the 'Date' field in the Release files. By default, it is set to the current date. +- **releaseDescription**: *(Optional)* The value of the 'Description' field in the Release files. By default, it is not set. +- **releaseVersion**: *(Optional)* The value of the 'Version' field in the Release files. By default, it is not set. +- **validUntil**: *(Optional)* The value of the 'Valid-Until' field in the Release files. By default, it is not set. +- **notAutomatic**: *(Optional)* The value of the 'NotAutomatic' field in the Release files. By default, it is not set. +- **butAutomaticUpgrades**: *(Optional)* The value of the 'ButAutomaticUpgrades' field in the Release files. By default, it is not set. +- **changelogs**: *(Optional)* The value of the 'Changelogs' field in the Release files. By default, it is not set. +- **snapshots**: *(Optional)* The value of the 'Snapshots' field in the Release files. By default, it is not set. + +- **debPackage**: *(Required)* See below for more information. + +In the `debPackage` section, you can specify package-specific attributes. These can override the top-level attributes if needed: + +- **file**: *(Required)* The Debian package file to extract Version and Package Name. +- **packageArchitectures**: *(Required)* Set of architectures that the package supports. +- **packageName**, **packageVersion**: *(Optional)* These attributes can be used to override the default package name and version extracted form the Debian File. +- **origin**, **label**, **suite**, **component**, **architectures**, **codename**, **date**, **releaseDescription**, **releaseVersion**, **validUntil**, **notAutomatic**, **butAutomaticUpgrades**, **changelogs**, **snapshots** : *(Optional)* These attributes can be used to override the default Release file fields for the specific package. +- **accessKey**, **secretKey**, **bucket**, **bucketPath**, **region**, **endpoint**: *(Optional)* These attributes can be used to override their respective top-level attributes for the specific package. + + +### cleanPackages + +The `cleanPackages` task comes equipped with a range of both required and optional attributes. + +- **bucket**: *(Required)* The name of the S3 bucket where the Apt Repository resides. +- **region**: *(Required)* The AWS region of the S3 bucket. +- **accessKey**: *(Required)* The AWS Access Key for accessing the S3 bucket. Can be overridden in the `debPackage` section. +- **secretKey**: *(Required)* The AWS Secret Key for accessing the S3 bucket. Can be overridden in the `debPackage` section. +- **bucketPath**: *(Optional)* The path within the bucket to store the Apt Repository. If not specified, the repository is stored at the root of the bucket. +- **endpoint**: *(Optional)* Custom S3 endpoint. Use this to override the default AWS S3 endpoint. +- **suite**: *(Optional)* The value of the 'Suite' field in the Release files. By default, it is 'stable'. +- **component**: *(Optional)* The value of the 'Component' field in the Release files. By default, it is 'main'. + +## License + +This S3 Apt Repository Plugin is released under MIT License. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..2fe2d1b --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,90 @@ +@Suppress("DSL_SCOPE_VIOLATION") // IntelliJ incorrectly marks libs as not callable +plugins { + `maven-publish` + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.dockerCompose) + alias(libs.plugins.gradlePluginPublish) + alias(libs.plugins.nemerosaVersioning) +} + +group = "com.liftric" +version = with(versioning.info) { + if (branch == "HEAD" && dirty.not()) { + tag + } else { + full + } +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +sourceSets { + val main by getting + val integrationMain by creating { + compileClasspath += main.output + runtimeClasspath += main.output + } +} + +tasks { + val test by existing + withType { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } + systemProperty("org.gradle.testkit.dir", gradle.gradleUserHomeDir) + } + + register("integrationTest") { + val integrationMain by sourceSets + description = "Runs the integration tests" + group = "verification" + testClassesDirs = integrationMain.output.classesDirs + classpath = integrationMain.runtimeClasspath + mustRunAfter(test) + useJUnitPlatform() + } +} + + +gradlePlugin { + val integrationMain by sourceSets + testSourceSets(integrationMain) + plugins { + create("s3-apt-repository-plugin") { + id = "$group.s3-apt-repository-plugin" + implementationClass = "$group.apt.S3AptRepositoryPlugin" + displayName = "s3-apt-repository-plugin" + } + } +} + +pluginBundle { + website = "https://github.com/Liftric/s3-apt-repository-plugin" + vcsUrl = "https://github.com/Liftric/s3-apt-repository-plugin" + description = "A Gradle Plugin for managing an APT Repository on S3" + tags = listOf("s3", "apt", "repository", "plugin", "gradle", "debian") +} + +dependencies { + implementation(libs.kotlinStdlibJdk8) + implementation(libs.apacheCommons) + implementation(libs.xz) + implementation(libs.bouncyCastleGPG) + implementation(libs.bouncyCastleProvider) + implementation(libs.awsS3) + + testImplementation(libs.junitJupiter) + testImplementation(libs.mockk) + + "integrationMainImplementation"(gradleTestKit()) + "integrationMainImplementation"(libs.junitJupiter) + "integrationMainImplementation"(libs.testContainersJUnit5) + "integrationMainImplementation"(libs.testContainersMain) + "integrationMainImplementation"(libs.minio) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..60c76b3 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..27c417c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,35 @@ +rootProject.name = "s3-apt-repository-plugin" + +pluginManagement { + dependencyResolutionManagement { + versionCatalogs { + create("libs") { + version("kotlin", "1.8.21") + version("ktor", "2.3.0") + version("junit", "5.9.3") + version("awsSdk", "2.17.125") + version("bouncycastle", "1.70") + version("testContainers", "1.18.3") + + plugin("dockerCompose", "com.avast.gradle.docker-compose").version("0.16.12") + plugin("kotlinJvm", "org.jetbrains.kotlin.jvm").versionRef("kotlin") + plugin("kotlinSerialization", "org.jetbrains.kotlin.plugin.serialization").versionRef("kotlin") + plugin("gradlePluginPublish", "com.gradle.plugin-publish").version("1.2.0") + plugin("nemerosaVersioning", "net.nemerosa.versioning").version("3.0.0") + + library("kotlinStdlibJdk8", "org.jetbrains.kotlin", "kotlin-stdlib-jdk8").versionRef("kotlin") + library("junitBom", "org.junit", "junit-bom").versionRef("junit") + library("junitJupiter", "org.junit.jupiter", "junit-jupiter").versionRef("junit") + library("awsS3", "software.amazon.awssdk", "s3").versionRef("awsSdk") + library("bouncyCastleGPG", "org.bouncycastle", "bcpg-jdk15on").versionRef("bouncycastle") + library("bouncyCastleProvider", "org.bouncycastle", "bcprov-jdk15on").versionRef("bouncycastle") + library("xz", "org.tukaani", "xz").version("1.9") + library("apacheCommons", "org.apache.commons", "commons-compress").version("1.12") + library("testContainersJUnit5", "org.testcontainers", "junit-jupiter").versionRef("testContainers") + library("testContainersMain", "org.testcontainers", "testcontainers").versionRef("testContainers") + library("minio", "io.minio", "minio").version("8.5.3") + library("mockk", "io.mockk", "mockk").version("1.13.5") + } + } + } +} diff --git a/src/integrationMain/kotlin/com/liftric/apt/CleanPackagesTest.kt b/src/integrationMain/kotlin/com/liftric/apt/CleanPackagesTest.kt new file mode 100644 index 0000000..bb1ef4b --- /dev/null +++ b/src/integrationMain/kotlin/com/liftric/apt/CleanPackagesTest.kt @@ -0,0 +1,128 @@ +package com.liftric.apt + +import io.minio.GetObjectArgs +import io.minio.errors.ErrorResponseException +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption + +/** + * This test verifies the correct functioning of the cleanPackages Gradle task. + * cleanPackages is a Gradle task that deletes unnecessary debian files from an APT repository, i.e., debian files + * that are not listed in the Packages file. + * + * To test this task, an Ubuntu container is used to create a controlled environment + * where a dummy APT repository is set up. The repository contains a package "foobar" + * with multiple versions present in the pool directory but only one version is actually listed in the Packages file. + * + * After executing the task, the test checks whether the repository still functions correctly and + * that no extra files are removed. It further checks the removal of old versions of the Debian package + * from a minio bucket to ensure the cleanPackages task's effectiveness. + */ + + +@Testcontainers +class CleanPackagesTest : ContainerBase() { + private val cleanPackagesTestLocation = "build/cleanPackagesTest" + + @Container + val ubuntuContainer: GenericContainer<*> = + GenericContainer(DockerImageName.parse("ubuntu:22.04")) + .withNetwork(network) + .withCommand("tail", "-f", "/dev/null") + + @Test + fun testCleanPackageTask() { + uploadObjects( + CLEAN_PACKAGES_TEST_BUCKET, mapOf( + "src/integrationMain/resources/cleanPackages/Release" to "dists/stable/Release", + "src/integrationMain/resources/cleanPackages/Release.gpg" to "dists/stable/Release.gpg", + "src/integrationMain/resources/cleanPackages/Packages" to "dists/stable/main/binary-all/Packages", + "src/integrationMain/resources/cleanPackages/Packages.gz" to "dists/stable/main/binary-all/Packages.gz", + "src/integrationMain/resources/cleanPackages/foobar_1.0.0-1_all.deb" to "pool/main/f/foobar/foobar_1.0.0-1_all.deb", + "src/integrationMain/resources/cleanPackages/foobar_0.0.9-1_all.deb" to "pool/main/f/foobar/foobar_0.0.9-1_all.deb", + ) + ) + + val projectDir = File(cleanPackagesTestLocation) + projectDir.mkdirs() + Files.copy( + Paths.get("src/integrationMain/resources/$PRIVATE_KEY_FILE"), + projectDir.toPath().resolve(PRIVATE_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + Files.copy( + Paths.get("src/integrationMain/resources/$PUBLIC_KEY_FILE"), + projectDir.toPath().resolve(PUBLIC_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + projectDir.resolve("foobar").writeText(VERIFICATION_STRING) + projectDir.resolve("settings.gradle.kts").writeText("") + projectDir.resolve("build.gradle.kts").writeText( + """ +import com.liftric.apt.extensions.* +import com.netflix.gradle.plugins.deb.Deb + +plugins { + id("com.liftric.s3-apt-repository-plugin") + id("com.netflix.nebula.ospackage") version "11.3.0" +} + +group = "com.liftric.test" +version = "1.0.0" + +s3AptRepository { + bucket.set("$CLEAN_PACKAGES_TEST_BUCKET") + region.set("eu-central-1") + endpoint.set("http://localhost:${MINIO_CONTAINER.getMappedPort(MINIO_PORT)}") + accessKey.set("$MINIO_ACCESS_KEY") + secretKey.set("$MINIO_SECRET_KEY") +} + """ + ) + + val result = GradleRunner.create().withProjectDir(projectDir).withArguments("build", "cleanPackages") + .withPluginClasspath().build() + assertTrue(result.output.contains("BUILD SUCCESSFUL")) + + val packageInstall = ubuntuContainer + .withPrivilegedMode(true) + .withNetwork(network) + .execInContainer( + "bash", "-c", """ + apt-get update -y && + apt-get install -y gnupg && + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $SIGNING_KEY_ID_LONG && + echo "deb http://minio:$MINIO_PORT/$CLEAN_PACKAGES_TEST_BUCKET stable main" | tee /etc/apt/sources.list.d/s3bucket.list && + apt-get update -y && + apt-get install -y foobar && + cat /usr/bin/foobar + """ + ) + assertTrue(packageInstall.stdout.contains(VERIFICATION_STRING)) + assertTrue(packageInstall.exitCode == 0) + + try { + minioClient.getObject( + GetObjectArgs.builder() + .bucket(REMOVE_PACKAGE_TEST_BUCKET) + .`object`("pool/main/f/foobar/foobar_0.0.9-1_all.deb") + .build() + ) + } catch (e: ErrorResponseException) { + assertTrue(e.errorResponse().code() == "NoSuchKey") + assertTrue(e.message == "The specified key does not exist.") + } catch (e: Exception) { + fail("Unexpected exception: ${e.message}") + } + } +} diff --git a/src/integrationMain/kotlin/com/liftric/apt/ContainerBase.kt b/src/integrationMain/kotlin/com/liftric/apt/ContainerBase.kt new file mode 100644 index 0000000..da64b0f --- /dev/null +++ b/src/integrationMain/kotlin/com/liftric/apt/ContainerBase.kt @@ -0,0 +1,133 @@ +package com.liftric.apt + +import io.minio.* +import org.testcontainers.containers.GenericContainer +import org.testcontainers.utility.DockerImageName +import io.minio.errors.MinioException +import org.testcontainers.containers.Network +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream + +/** + * This is an abstract base class upon which all test classes are built. + * This class sets up a simulated AWS S3 environment by starting a Minio container. + * It prepares the environment for each test by creating dedicated buckets. + * This includes initializing test buckets, creating the Minio client, and + * preparing the buckets by ensuring they exist and have the correct policies. + * Additionally, it provides a method to upload objects to these buckets. + */ + +abstract class ContainerBase { + companion object { + private val testBuckets = + listOf( + UPLOAD_PACKAGE_TEST_BUCKET, + UPLOAD_PACKAGE_TEST_BUCKET_2, + REMOVE_PACKAGE_TEST_BUCKET, + CLEAN_PACKAGES_TEST_BUCKET + ) + val network: Network = Network.newNetwork() + + val MINIO_CONTAINER: GenericContainer<*> = + GenericContainer(DockerImageName.parse("quay.io/minio/minio:RELEASE.2023-06-02T23-17-26Z")) + .withPrivilegedMode(true) + .withNetwork(network) + .withNetworkAliases("minio") + .withEnv("MINIO_ROOT_USER", MINIO_ACCESS_KEY) + .withEnv("MINIO_ROOT_PASSWORD", MINIO_SECRET_KEY) + .withCommand("server", "--console-address", ":9090", "/data") + .withExposedPorts(MINIO_PORT, 9090) + .apply { start() } + + lateinit var minioClient: MinioClient + + init { + try { + minioClient = MinioClient.Builder() + .endpoint("http://localhost:${MINIO_CONTAINER.getMappedPort(MINIO_PORT)}") + .credentials(MINIO_ACCESS_KEY, MINIO_SECRET_KEY) + .build() + + testBuckets.forEach { + val found = minioClient.bucketExists( + BucketExistsArgs.builder() + .bucket(it) + .build() + ) + + if (found) { + minioClient.removeBucket( + RemoveBucketArgs.builder() + .bucket(it) + .build() + ) + } + + minioClient.makeBucket( + MakeBucketArgs.builder() + .bucket(it) + .region(MINIO_BUCKET_REGION) + .build() + ) + + minioClient.setBucketPolicy( + SetBucketPolicyArgs.builder() + .bucket(it) + .config( + """ + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AddPerm", + "Effect": "Allow", + "Principal": "*", + "Action": [ + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::$it/*" + ] + } + ] + } + """.trimIndent() + ) + .build() + ) + } + + } catch (e: MinioException) { + println("Error occurred: $e") + } + } + + fun uploadObjects(bucket: String, objects: Map) { + objects.forEach { (key, value) -> + minioClient.uploadObject( + UploadObjectArgs.builder() + .bucket(bucket) + .filename(key) + .`object`(value) + .build() + ) + } + } + + fun getFileFromBucket(path: String, bucket: String): File { + val stream: InputStream = minioClient.getObject( + GetObjectArgs.builder() + .bucket(bucket) + .`object`(path) + .build() + ) + return File.createTempFile(path.substringAfterLast("/"), null).apply { + deleteOnExit() + FileOutputStream(this).use { output -> + stream.copyTo(output) + } + } + } + } +} diff --git a/src/integrationMain/kotlin/com/liftric/apt/RemovePackageTest.kt b/src/integrationMain/kotlin/com/liftric/apt/RemovePackageTest.kt new file mode 100644 index 0000000..5c16944 --- /dev/null +++ b/src/integrationMain/kotlin/com/liftric/apt/RemovePackageTest.kt @@ -0,0 +1,148 @@ +package com.liftric.apt + +import com.liftric.apt.service.PackagesFactory +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption + +/** + * This test verifies the correct functioning of the removePackage Gradle task. + * removePackage is a Gradle task that removes a specific Version of a Package from + * the Packages file from the Apt Repository. This task does not remove the actual + * Debian File from the repository pool, so that users with an older version of the + * Repository can still install the package. For deleting removed Packages use the + * cleanPackages task. + * + * To ensure that the Apt Repository is still functioning correctly, an Ubuntu container + * is used for testing. This provides a controlled environment where potential errors or + * issues can be isolated and diagnosed more effectively. + * + * Additionally, the Packages file from the Minio client is downloaded and checked. This + * helps to confirm that the correct version of the package has been removed as intended. + */ + + +@Testcontainers +class RemovePackageTest : ContainerBase() { + private val removePackageTestLocation = "build/removePackageTest" + + @Container + val ubuntuContainer: GenericContainer<*> = + GenericContainer(DockerImageName.parse("ubuntu:22.04")) + .withNetwork(network) + .withCommand("tail", "-f", "/dev/null") + + @Test + fun testRemovePackageTask() { + uploadObjects( + REMOVE_PACKAGE_TEST_BUCKET, mapOf( + "src/integrationMain/resources/removePackage/Release" to "dists/stable/Release", + "src/integrationMain/resources/removePackage/Release.gpg" to "dists/stable/Release.gpg", + "src/integrationMain/resources/removePackage/Packages" to "dists/stable/main/binary-all/Packages", + "src/integrationMain/resources/removePackage/Packages.gz" to "dists/stable/main/binary-all/Packages.gz", + "src/integrationMain/resources/removePackage/foobar_1.0.0-1_all.deb" to "pool/main/f/foobar/foobar_1.0.0-1_all.deb", + "src/integrationMain/resources/removePackage/foobar_0.0.9-1_all.deb" to "pool/main/f/foobar/foobar_0.0.9-1_all.deb", + ) + ) + + val projectDir = File(removePackageTestLocation) + projectDir.mkdirs() + Files.copy( + Paths.get("src/integrationMain/resources/$PRIVATE_KEY_FILE"), + projectDir.toPath().resolve(PRIVATE_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + Files.copy( + Paths.get("src/integrationMain/resources/$PUBLIC_KEY_FILE"), + projectDir.toPath().resolve(PUBLIC_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + projectDir.resolve("foobar").writeText(VERIFICATION_STRING) + projectDir.resolve("settings.gradle.kts").writeText("") + projectDir.resolve("build.gradle.kts").writeText( + """ +import com.liftric.apt.extensions.* +import com.netflix.gradle.plugins.deb.Deb + +plugins { + id("com.liftric.s3-apt-repository-plugin") + id("com.netflix.nebula.ospackage") version "11.3.0" +} + +group = "com.liftric.test" +version = "1.0.0" + +val createDeb = tasks.create("createDeb", Deb::class) { + packageName = "foobar" + version = "0.0.9" + release = "1" + maintainer = "nvima " + description = "Description" + + // Long Version of Key Id throws an error https://github.com/nebula-plugins/gradle-ospackage-plugin/issues/179#issuecomment-269423747 + signingKeyId = "$SIGNING_KEY_ID_SHORT" + signingKeyPassphrase = "$MINIO_SECRET_KEY" + signingKeyRingFile = file("$PRIVATE_KEY_FILE") + + into("/usr/bin") { + from("./") { + include("foobar") + } + } +} + +s3AptRepository { + bucket.set("$REMOVE_PACKAGE_TEST_BUCKET") + region.set("eu-central-1") + endpoint.set("http://localhost:${MINIO_CONTAINER.getMappedPort(MINIO_PORT)}") + accessKey.set("$MINIO_ACCESS_KEY") + secretKey.set("$MINIO_SECRET_KEY") + signingKeyPassphrase.set("$SIGNING_KEY_PASSPHRASE") + signingKeyRingFile.set(file("$PRIVATE_KEY_FILE")) + debPackage { + file.set(createDeb.archiveFile) + packageArchitectures.set(setOf("all")) + origin.set("Liftric") + label.set("Liftric") + } +} + """ + ) + + val result = GradleRunner.create().withProjectDir(projectDir).withArguments("build", "removePackage") + .withPluginClasspath().build() + assertTrue(result.output.contains("BUILD SUCCESSFUL")) + + val packageInstall = ubuntuContainer + .withPrivilegedMode(true) + .withNetwork(network) + .execInContainer( + "bash", "-c", """ + apt-get update -y && + apt-get install -y gnupg && + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $SIGNING_KEY_ID_LONG && + echo "deb http://minio:$MINIO_PORT/$REMOVE_PACKAGE_TEST_BUCKET stable main" | tee /etc/apt/sources.list.d/s3bucket.list && + apt-get update -y && + apt-get install -y foobar && + cat /usr/bin/foobar + """ + ) + assertTrue(packageInstall.stdout.contains(VERIFICATION_STRING)) + assertTrue(packageInstall.exitCode == 0) + + val outputFile = getFileFromBucket("dists/stable/main/binary-all/Packages", REMOVE_PACKAGE_TEST_BUCKET) + val debianPackages = PackagesFactory.parsePackagesFile(outputFile) + + assertTrue(debianPackages.size == 1) + assertTrue(debianPackages[0].packageName == "foobar") + assertTrue(debianPackages[0].version == "1.0.0-1") + } +} diff --git a/src/integrationMain/kotlin/com/liftric/apt/UploadPackageTest.kt b/src/integrationMain/kotlin/com/liftric/apt/UploadPackageTest.kt new file mode 100644 index 0000000..e308ffc --- /dev/null +++ b/src/integrationMain/kotlin/com/liftric/apt/UploadPackageTest.kt @@ -0,0 +1,157 @@ +package com.liftric.apt + +import com.liftric.apt.service.PackagesFactory +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption + + +/** + * This test verifies the functionality of the uploadPackage Gradle task, which is + * responsible for creating an Apt Repository in an empty bucket or updating an existing Apt Repository. + * This Class test the updating functionality of an existing Apt Repository. + * + * To ensure that the Apt Repository is updated correctly, an Ubuntu container + * is used for testing. The newer version of the package with a different VERIFICATION_CODE + * gets installed and tested. + * + * Additionally, the Packages, Release file from the Minio client is downloaded and checked. This + * helps to confirm that the old Version is still present and the Release File is updated correctly. + */ + + +@Testcontainers +class UploadPackageTest : ContainerBase() { + private val uploadPackageTestLocation = "build/uploadPackageTest" + + @Container + val ubuntuContainer: GenericContainer<*> = + GenericContainer(DockerImageName.parse("ubuntu:22.04")) + .withNetwork(network) + .withCommand("tail", "-f", "/dev/null") + + @Test + fun testUploadPackageTask() { + uploadObjects( + UPLOAD_PACKAGE_TEST_BUCKET, mapOf( + "src/integrationMain/resources/uploadPackage/Release" to "dists/stable/Release", + "src/integrationMain/resources/uploadPackage/Release.gpg" to "dists/stable/Release.gpg", + "src/integrationMain/resources/uploadPackage/Packages" to "dists/stable/main/binary-all/Packages", + "src/integrationMain/resources/uploadPackage/Packages.gz" to "dists/stable/main/binary-all/Packages.gz", + "src/integrationMain/resources/uploadPackage/foobar_1.0.0-1_all.deb" to "pool/main/f/foobar/foobar_1.0.0-1_all.deb", + ) + ) + + val projectDir = File(uploadPackageTestLocation) + projectDir.mkdirs() + Files.copy( + Paths.get("src/integrationMain/resources/$PRIVATE_KEY_FILE"), + projectDir.toPath().resolve(PRIVATE_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + Files.copy( + Paths.get("src/integrationMain/resources/$PUBLIC_KEY_FILE"), + projectDir.toPath().resolve(PUBLIC_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + projectDir.resolve("foobar").writeText(VERIFICATION_STRING_2) + projectDir.resolve("settings.gradle.kts").writeText("") + projectDir.resolve("build.gradle.kts").writeText( + """ +import com.liftric.apt.extensions.* +import com.netflix.gradle.plugins.deb.Deb + +plugins { + id("com.liftric.s3-apt-repository-plugin") + id("com.netflix.nebula.ospackage") version "11.3.0" +} + +group = "com.liftric.test" +version = "1.0.1" + +val createDeb = tasks.create("createDeb", Deb::class) { + packageName = "foobar" + version = "1.0.1" + release = "1" + maintainer = "nvima " + description = "Description" + + // Long Version of Key Id throws an error https://github.com/nebula-plugins/gradle-ospackage-plugin/issues/179#issuecomment-269423747 + signingKeyId = "$SIGNING_KEY_ID_SHORT" + signingKeyPassphrase = "$MINIO_SECRET_KEY" + signingKeyRingFile = file("$PRIVATE_KEY_FILE") + + into("/usr/bin") { + from("./") { + include("foobar") + } + } +} + +s3AptRepository { + bucket.set("$UPLOAD_PACKAGE_TEST_BUCKET") + region.set("eu-central-1") + endpoint.set("http://localhost:${MINIO_CONTAINER.getMappedPort(MINIO_PORT)}") + accessKey.set("$MINIO_ACCESS_KEY") + secretKey.set("$MINIO_SECRET_KEY") + signingKeyPassphrase.set("$SIGNING_KEY_PASSPHRASE") + signingKeyRingFile.set(file("$PRIVATE_KEY_FILE")) + origin.set("Foobar") + label.set("Foobar") + debPackage { + file.set(createDeb.archiveFile) + packageArchitectures.set(setOf("all")) + releaseDescription.set("Foobar Repository") + releaseVersion.set("1.0.0") + } +} + """ + ) + + val result = GradleRunner + .create() + .withProjectDir(projectDir) + .withArguments("build", "uploadPackage") + .withPluginClasspath().build() + + assertTrue(result.output.contains("BUILD SUCCESSFUL")) + + val packageInstall = ubuntuContainer + .withPrivilegedMode(true) + .withNetwork(network) + .execInContainer( + "bash", "-c", """ + apt-get update -y && + apt-get install -y gnupg && + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $SIGNING_KEY_ID_LONG && + echo "deb http://minio:$MINIO_PORT/$UPLOAD_PACKAGE_TEST_BUCKET stable main" | tee /etc/apt/sources.list.d/s3bucket.list && + apt-get update -y && + apt-get install -y foobar && + cat /usr/bin/foobar + """ + ) + + assertTrue(packageInstall.stdout.contains(VERIFICATION_STRING_2)) + assertTrue(packageInstall.exitCode == 0) + + val packagesFile = getFileFromBucket("dists/stable/main/binary-all/Packages", UPLOAD_PACKAGE_TEST_BUCKET) + val debianPackages = PackagesFactory.parsePackagesFile(packagesFile) + assertTrue(debianPackages.size == 2) + assertTrue(debianPackages[0].packageName == "foobar") + assertTrue(debianPackages[0].version == "1.0.0-1") + + val releaseFileString = getFileFromBucket("dists/stable/Release", UPLOAD_PACKAGE_TEST_BUCKET).readText() + assertTrue(releaseFileString.contains("Version: 1.0.0")) + assertTrue(releaseFileString.contains("Origin: Foobar")) + assertTrue(releaseFileString.contains("Label: Foobar")) + assertTrue(releaseFileString.contains("Description: Foobar Repository")) + } +} diff --git a/src/integrationMain/kotlin/com/liftric/apt/UploadPackageTest2.kt b/src/integrationMain/kotlin/com/liftric/apt/UploadPackageTest2.kt new file mode 100644 index 0000000..c569da8 --- /dev/null +++ b/src/integrationMain/kotlin/com/liftric/apt/UploadPackageTest2.kt @@ -0,0 +1,131 @@ +package com.liftric.apt + +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.testcontainers.containers.GenericContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption + + +/** + * This test verifies the functionality of the uploadPackage Gradle task, which is + * responsible for creating an Apt Repository in an empty bucket or updating an existing Apt Repository. + * This Class test the creation of a new Apt Repository. + * + * To ensure the newly created Apt Repository is functional, we use an Ubuntu container for + * testing. This container adds the new Apt Repository and attempts to install a package from it. + * The use of an Ubuntu container provides a controlled environment in which to verify the + * functionality of the new repository. The successful installation of the package indicates + * that the repository is working. + */ + + +@Testcontainers +class UploadPackageTest2 : ContainerBase() { + private val uploadPackageTestLocation = "build/uploadPackageTest2" + + @Container + val ubuntuContainer: GenericContainer<*> = + GenericContainer(DockerImageName.parse("ubuntu:22.04")) + .withNetwork(network) + .withCommand("tail", "-f", "/dev/null") + + @Test + fun testUploadPackageTask() { + val projectDir = File(uploadPackageTestLocation) + projectDir.mkdirs() + Files.copy( + Paths.get("src/integrationMain/resources/$PRIVATE_KEY_FILE"), + projectDir.toPath().resolve(PRIVATE_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + Files.copy( + Paths.get("src/integrationMain/resources/$PUBLIC_KEY_FILE"), + projectDir.toPath().resolve(PUBLIC_KEY_FILE), + StandardCopyOption.REPLACE_EXISTING + ) + projectDir.resolve("foobar").writeText(VERIFICATION_STRING) + projectDir.resolve("settings.gradle.kts").writeText("") + projectDir.resolve("build.gradle.kts").writeText( + """ +import com.liftric.apt.extensions.* +import com.netflix.gradle.plugins.deb.Deb + +plugins { + id("com.liftric.s3-apt-repository-plugin") + id("com.netflix.nebula.ospackage") version "11.3.0" +} + +group = "com.liftric.test" +version = "1.0.0" + +val createDeb = tasks.create("createDeb", Deb::class) { + packageName = "foobar" + version = "1.0.0" + release = "1" + maintainer = "nvima " + description = "Description" + + // Long Version of Key Id throws an error https://github.com/nebula-plugins/gradle-ospackage-plugin/issues/179#issuecomment-269423747 + signingKeyId = "$SIGNING_KEY_ID_SHORT" + signingKeyPassphrase = "$MINIO_SECRET_KEY" + signingKeyRingFile = file("$PRIVATE_KEY_FILE") + + into("/usr/bin") { + from("./") { + include("foobar") + } + } +} + +s3AptRepository { + bucket.set("$UPLOAD_PACKAGE_TEST_BUCKET_2") + region.set("eu-central-1") + endpoint.set("http://localhost:${MINIO_CONTAINER.getMappedPort(MINIO_PORT)}") + accessKey.set("$MINIO_ACCESS_KEY") + secretKey.set("$MINIO_SECRET_KEY") + signingKeyPassphrase.set("$SIGNING_KEY_PASSPHRASE") + signingKeyRingFile.set(file("$PRIVATE_KEY_FILE")) + debPackage { + file.set(createDeb.archiveFile) + packageArchitectures.set(setOf("all")) + origin.set("Liftric") + label.set("Liftric") + } +} + """ + ) + + val result = GradleRunner + .create() + .withProjectDir(projectDir) + .withArguments("build", "uploadPackage") + .withPluginClasspath().build() + + assertTrue(result.output.contains("BUILD SUCCESSFUL")) + + val packageInstall = ubuntuContainer + .withPrivilegedMode(true) + .withNetwork(network) + .execInContainer( + "bash", "-c", """ + apt-get update -y && + apt-get install -y gnupg && + apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $SIGNING_KEY_ID_LONG && + echo "deb http://minio:$MINIO_PORT/$UPLOAD_PACKAGE_TEST_BUCKET_2 stable main" | tee /etc/apt/sources.list.d/s3bucket.list && + apt-get update -y && + apt-get install -y foobar && + cat /usr/bin/foobar + """ + ) + + assertTrue(packageInstall.stdout.contains(VERIFICATION_STRING)) + assertTrue(packageInstall.exitCode == 0) + } +} diff --git a/src/integrationMain/kotlin/com/liftric/apt/testConstants.kt b/src/integrationMain/kotlin/com/liftric/apt/testConstants.kt new file mode 100644 index 0000000..0ad58ef --- /dev/null +++ b/src/integrationMain/kotlin/com/liftric/apt/testConstants.kt @@ -0,0 +1,17 @@ +package com.liftric.apt + +const val MINIO_PORT = 9000 +const val MINIO_ACCESS_KEY = "admin" +const val MINIO_SECRET_KEY = "abcd1234" +const val MINIO_BUCKET_REGION = "eu-central-1" +const val SIGNING_KEY_ID_SHORT = "CAAB5A05" +const val SIGNING_KEY_ID_LONG = "D84ED0773ABB0A0AF2D9921331860335CAAB5A05" +const val SIGNING_KEY_PASSPHRASE = "abcd1234" +const val PRIVATE_KEY_FILE = "private.key" +const val PUBLIC_KEY_FILE = "public.key" +const val VERIFICATION_STRING = "BAZ_BAR" +const val VERIFICATION_STRING_2 = "BAZ_BAR2" +const val UPLOAD_PACKAGE_TEST_BUCKET = "upload-package-test-1" +const val UPLOAD_PACKAGE_TEST_BUCKET_2 = "upload-package-test-2" +const val REMOVE_PACKAGE_TEST_BUCKET = "remove-package-test-1" +const val CLEAN_PACKAGES_TEST_BUCKET = "clean-packages-test-1" diff --git a/src/integrationMain/resources/cleanPackages/Packages b/src/integrationMain/resources/cleanPackages/Packages new file mode 100644 index 0000000..e0f2354 --- /dev/null +++ b/src/integrationMain/resources/cleanPackages/Packages @@ -0,0 +1,14 @@ +Package: foobar +Version: 1.0.0-1 +Architecture: all +Maintainer: nvima +Installed-Size: 0 +Section: java +Priority: optional +Description: foobar +Filename: pool/main/f/foobar/foobar_1.0.0-1_all.deb +Size: 1018 +SHA1: abd00a88a4ff3eb30dfe4412779ca97c5aabf529 +SHA256: b9381b36cf73b10ba01a6cdfa50be13e807687cdf434925e18278d109920b1b2 +MD5sum: 53829e7ab536c57c2fddc96d0e2690b8 + diff --git a/src/integrationMain/resources/cleanPackages/Packages.gz b/src/integrationMain/resources/cleanPackages/Packages.gz new file mode 100644 index 0000000..3820053 Binary files /dev/null and b/src/integrationMain/resources/cleanPackages/Packages.gz differ diff --git a/src/integrationMain/resources/cleanPackages/Release b/src/integrationMain/resources/cleanPackages/Release new file mode 100644 index 0000000..5145325 --- /dev/null +++ b/src/integrationMain/resources/cleanPackages/Release @@ -0,0 +1,18 @@ +Origin: Liftric +Label: Liftric +Suite: stable +Components: main +Date: Fri, 09 Jun 2023 09:34:26 UTC +Architectures: all +MD5Sum: + 3a422598c40f3e4fa01260bddb315d94 388 main/binary-all/Packages + 4883de5b75465c6b9748ab23708a5797 303 main/binary-all/Packages.gz +SHA1: + c57e7599a71c534a379aa9a078de33814496f002 388 main/binary-all/Packages + ab03a75c6473e744331e4b74853518c2a662bb71 303 main/binary-all/Packages.gz +SHA256: + f8bdb77df801b557403a71f9ee2cb732521a8ceeea420e28eb3d04b781576c5d 388 main/binary-all/Packages + 207c8c3f8cb79d6a6e0e015533474c8cad782ecd9587a54350282f09cf6955a1 303 main/binary-all/Packages.gz +SHA512: + c5fbe3fedd1775c6e5580b6e0c92d308c310c8b953c1bfcdde8b41d9e5d0849c15dac5ff3a8c28406b6be48a5ebbf9e517b051c912e7b3aa6189298dcdce3811 388 main/binary-all/Packages + ef9f90a0edf4f5e576734896f09970e3e6eb4cc0f645955b461228908abf46de97cdec7f6db714d251f4bbbef5d305da9098d7e27301211bb469ec80b24de433 303 main/binary-all/Packages.gz diff --git a/src/integrationMain/resources/cleanPackages/Release.gpg b/src/integrationMain/resources/cleanPackages/Release.gpg new file mode 100644 index 0000000..ed4a1c7 --- /dev/null +++ b/src/integrationMain/resources/cleanPackages/Release.gpg @@ -0,0 +1,9 @@ +-----BEGIN PGP SIGNATURE----- +Version: BCPG v1.70 + +iJwEAAEKAAYFAmSC8iIACgkQMYYDNcqrWgWm0AP5ARNHfrg6ArjDHojy6Ivw91F+ +iOq58acSS6idwBfAl7MDFeCaLALz0DJ77j+p8ao225n+uOjbBCT4jnHuVkHC+co6 +VKEj1Onn27v3T5EijDskeAUFUA8uv+O59kzAwKX/AzFBQlJcXV3oa6Q2vk5SN2JZ +PEIz+NK6dmIKUoSJE3k= +=V51k +-----END PGP SIGNATURE----- diff --git a/src/integrationMain/resources/cleanPackages/foobar_0.0.9-1_all.deb b/src/integrationMain/resources/cleanPackages/foobar_0.0.9-1_all.deb new file mode 100644 index 0000000..03111e5 Binary files /dev/null and b/src/integrationMain/resources/cleanPackages/foobar_0.0.9-1_all.deb differ diff --git a/src/integrationMain/resources/cleanPackages/foobar_1.0.0-1_all.deb b/src/integrationMain/resources/cleanPackages/foobar_1.0.0-1_all.deb new file mode 100644 index 0000000..03111e5 Binary files /dev/null and b/src/integrationMain/resources/cleanPackages/foobar_1.0.0-1_all.deb differ diff --git a/src/integrationMain/resources/private.key b/src/integrationMain/resources/private.key new file mode 100644 index 0000000..ca09c3f --- /dev/null +++ b/src/integrationMain/resources/private.key @@ -0,0 +1,57 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: Keybase OpenPGP v1.0.0 +Comment: https://keybase.io/crypto + +xcFGBGR/HxcBBADC/hnMViMTgdcFIlfbkDIPPEPCLxi1cD4N/99D9HpdF3R426JY +Bqu7cl5YANM6e3zIK5HCp89hEaM3PQCuQcMahnxW3dKn6I1ru63cZpi3y+1ySiY/ +PvO28BKFX5wKq4mx0UZpv0SBKSSimrV+VSE/pRwxHdgiGN5fD43tVUDphwARAQAB +/gkDCBTdyeo2bWOiYOHekbZ43MJCvdj3EkULRWp/xu0csFPOlBGXeVKsUqZBY/1+ +2s/YVeag/W9jg3tnp8O73LCOXr3eCKWtBV/j0hp29uPSSvjLfT5YnzwaKZpIB+2c +4KquMTcPLfixMsulOVsrlvWsTf5R33tMMmp+VyG2jt+krwTAwSvLbKngJigVeYA5 +zfkOqwTw2DvwxlTlgvCp791JfPqqebDI6iYSQNKT0KmrNsNTfUod80GydzF0FIkW +bmTzoLlRSHiyefVmiyKaWHrB5IrsFLUiVqK28Tzdm9pdIuAOQFcNWqWKbm9zOW/W +9vgfKjYC7YfCV4+MZxTDndQdQHwCJn92vYYSrPS7OKCodgg0MGhNS5IpazTxdrWz +trPv7SsPQGwDkk9X3HSYBJh9fPugjpSCKxfuIz+A+tBiBr4H9ecahOpczDu0NLVD +JtpO34fYLlgs/SX0GQiBnO+CtgtHEg5IerNQHMEK0jfEuhchncPzXUfNJ0ludGVn +cmF0aW9uIFRlc3QgPGludGVncmF0aW9uQHRlc3QuZGV2PsKtBBMBCgAXBQJkfx8X +AhsvAwsJBwMVCggCHgECF4AACgkQMYYDNcqrWgXVsQP/e8wIHwSdfWVvsiOfIE6S +VZJc6iTQPfJ/VGkTm8yQZv3viXSFjMT5pDXeokLr6LEwTYP1x67EVMP+csekg3kN +S+WfhB2e1bC4pMaxM7wwWxPJMg4bywx8QNXCkPT9pZGFgTaYi1HwT+QsKsPxTdm6 +1QU0fbQLtaP7aPvLCRqyRafHwUYEZH8fFwEEAOOmDe0UDHy++SLYPduSX/PcSci5 +gS++nuxFE23nfeA92OvghP+QwzU1t5TtWebpnsReG1zywHw55ZYDhB9e8mRyOm1o +Y1Lu8PxSV3oMqj4O3i0jhZGkxHHANa6Nt8nBVzrtGuRNIOQ+x0793S+0Xi0adIcE +Z13KXYKcJ27+bI0TABEBAAH+CQMIZv2QK/eSWQhghSOjdoOtm7YeVJgLEwHKJ24P +eiWysetbctk1SrlvIEYK/uQJEyt5b1Q/Prseglwf/uYhie2v1xZiXNmo3TiEqfex +osAueaQpxFavPFhadAUQr1HPnHl0aV5OnsUinMv7TVDPYJLRsUs87dln0+rs00Vv +q/bJ6WaYmNhk6WyqQxkXqi1oWlwIY7AhI1mXLFS3KlsMSZ9odyY4f1KLHt1aP1nN +syZfPeYwbN/ygW7hnapqTXiFyupqZ0v9rmcM6hA/PxpqJPunf8jNi6F95j364oKs +7k5lW/21gyVNw/fPlyZIIL5QG5yubMonM5ML0lraUuc067Ou6JUVvKxrgB6d23lH +xf2/HMhh5m0LR3/xNbZ/ignnF2nXLhhIxvFkOOVAYBqeZtWr0i0gSBQW57gS0bbc +Xv3e+F/mHzotaztxujSNqmjc3h0tN6VDYfBuYvl1Wv7CfB9tYZ1zKg5t34k+mWJs +n7QkC4pQGEuELcLAgwQYAQoADwUCZH8fFwUJDwmcAAIbLgCoCRAxhgM1yqtaBZ0g +BBkBCgAGBQJkfx8XAAoJEAfczH4FuR6QHS0EAIFggMmPD3ChEB+J3aDbBeVy4fl5 +zTSHmjD3fqurI8kEafNJBV+lH/5iuBLwQrc0jwWcdziAwiF/KE9C8MJrSPwJlVEO +aoGV6D+8nBK1pr8y227xL76EyoWybNBNkp+d5ej8p/QGLIwC3ztcNgarIws4WjWc +Pc3yDD1weuvE7qZCzHQD/2klFuLra4uceFbjWp5fhO0Zo8q/W/s7gtIFpf8GXLuf +HqDVw7Z4ue7SzLSTTj0liInkf2lFF1QOkdSMFiW/l6/6bHacwavxgnG8k9JGcamh +mxap3PagMjX/Ij2NJVeHcYmCo4G2UjI2BWj5j2umxMnbMp6pPN1H5LJi5eZ0/8vh +x8FGBGR/HxcBBADrsY3VGdJrl1MtOws1SK5ejsAd5mnWZY3BZmEX+BqkJmYOo9D0 +v1ykJNKPiPlS+NWGVkI2rvOPDGTJBE36r5qqorX/N4M8Vbmtd+8+CY3VpGc5GyGO +VDpLesj3jdO/tUJ9e4r1enGnEdUKnyha9iiZXlNpUCkW3pwKAsezR/chBwARAQAB +/gkDCEDy6mvmqC3jYHEfdUVfXzS2Ql2jZ0CNr+gYL44nmpGol2ItrojbgTdYuhdD +mw3UXttLN/Ty0SOP4tbHNm6wajrX9nZU6Tgo4yV8NIWwq0cM42aL/a4JVWoqgYbi +EdeQmKnZBWQOezOTi2X67EO6jloGdCIm1k3HsM+9mgMR6vN9/pzW3ZFEF6wzilzM +G07d4xyJig4nfH6V1LLXTzuy0IXkRyZ48RZ1/QbL+N/urLFoRCWuQaD0ST9Opk5O +yN18h8Sp9/i3+Xsop2v9UG3pvYN0Ox0FfeRj4fDGHiQKIsEzwSlsD7kRmGytzp8i +6MxujEvWKjiArj6N+Ae01/V9xBKUxQVPVfIIkulGmMjf6lKRwupGjCrTNDunpSR2 +M70vzewud6GjWq92LmoyFkV+d05gKoY05rXk5JDIxKoGki0PpmAbWPKK6WORqC4Y +9oA+ApRne39StmuA8SO5Ig5k4XgGartmaXKKif2B2J5ZoCUYz23ezU7CwIMEGAEK +AA8FAmR/HxcFCQ8JnAACGy4AqAkQMYYDNcqrWgWdIAQZAQoABgUCZH8fFwAKCRBT +xi/Onv7Zv1zIBACaoNnjerhWsaBXwtIh4OBBcHxAgp56GBpjcH9iAZUndivTrQpu +pkT4ltgf4E6NcQXYqCG3s7a0hNBxPscQPs/DrzLAjPXTA3JKeRjeMKWWsDhdZ65m +v+82vUc0fVOC9i5Pv3gUW3uX+bM1uzXrVaf2fgtirkdJ37lMV1/JTmcSccK4A/9f +k/hUwSos36R7muW4VgAzBkjIZ4tOXd+Bm+hLV5fnNgqa9CG3qS/Jo1N/5SyLdP46 +uUtNk00qn5J6leLp5ZLVhi5CFyN3Ss09yBgT32t37qIAH/uaywKP1v5Lv9R9gAXA +XZ/dY9NKoFks9WPQEs48cJviVIKVe52jdLJyOC9N1Q== +=92IK +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/integrationMain/resources/public.key b/src/integrationMain/resources/public.key new file mode 100644 index 0000000..d2aa353 --- /dev/null +++ b/src/integrationMain/resources/public.key @@ -0,0 +1,34 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: Keybase OpenPGP v1.0.0 +Comment: https://keybase.io/crypto + +xo0EZH8fFwEEAML+GcxWIxOB1wUiV9uQMg88Q8IvGLVwPg3/30P0el0XdHjbolgG +q7tyXlgA0zp7fMgrkcKnz2ERozc9AK5BwxqGfFbd0qfojWu7rdxmmLfL7XJKJj8+ +87bwEoVfnAqribHRRmm/RIEpJKKatX5VIT+lHDEd2CIY3l8Pje1VQOmHABEBAAHN +J0ludGVncmF0aW9uIFRlc3QgPGludGVncmF0aW9uQHRlc3QuZGV2PsKtBBMBCgAX +BQJkfx8XAhsvAwsJBwMVCggCHgECF4AACgkQMYYDNcqrWgXVsQP/e8wIHwSdfWVv +siOfIE6SVZJc6iTQPfJ/VGkTm8yQZv3viXSFjMT5pDXeokLr6LEwTYP1x67EVMP+ +csekg3kNS+WfhB2e1bC4pMaxM7wwWxPJMg4bywx8QNXCkPT9pZGFgTaYi1HwT+Qs +KsPxTdm61QU0fbQLtaP7aPvLCRqyRafOjQRkfx8XAQQA46YN7RQMfL75Itg925Jf +89xJyLmBL76e7EUTbed94D3Y6+CE/5DDNTW3lO1Z5umexF4bXPLAfDnllgOEH17y +ZHI6bWhjUu7w/FJXegyqPg7eLSOFkaTEccA1ro23ycFXOu0a5E0g5D7HTv3dL7Re +LRp0hwRnXcpdgpwnbv5sjRMAEQEAAcLAgwQYAQoADwUCZH8fFwUJDwmcAAIbLgCo +CRAxhgM1yqtaBZ0gBBkBCgAGBQJkfx8XAAoJEAfczH4FuR6QHS0EAIFggMmPD3Ch +EB+J3aDbBeVy4fl5zTSHmjD3fqurI8kEafNJBV+lH/5iuBLwQrc0jwWcdziAwiF/ +KE9C8MJrSPwJlVEOaoGV6D+8nBK1pr8y227xL76EyoWybNBNkp+d5ej8p/QGLIwC +3ztcNgarIws4WjWcPc3yDD1weuvE7qZCzHQD/2klFuLra4uceFbjWp5fhO0Zo8q/ +W/s7gtIFpf8GXLufHqDVw7Z4ue7SzLSTTj0liInkf2lFF1QOkdSMFiW/l6/6bHac +wavxgnG8k9JGcamhmxap3PagMjX/Ij2NJVeHcYmCo4G2UjI2BWj5j2umxMnbMp6p +PN1H5LJi5eZ0/8vhzo0EZH8fFwEEAOuxjdUZ0muXUy07CzVIrl6OwB3madZljcFm +YRf4GqQmZg6j0PS/XKQk0o+I+VL41YZWQjau848MZMkETfqvmqqitf83gzxVua13 +7z4JjdWkZzkbIY5UOkt6yPeN07+1Qn17ivV6cacR1QqfKFr2KJleU2lQKRbenAoC +x7NH9yEHABEBAAHCwIMEGAEKAA8FAmR/HxcFCQ8JnAACGy4AqAkQMYYDNcqrWgWd +IAQZAQoABgUCZH8fFwAKCRBTxi/Onv7Zv1zIBACaoNnjerhWsaBXwtIh4OBBcHxA +gp56GBpjcH9iAZUndivTrQpupkT4ltgf4E6NcQXYqCG3s7a0hNBxPscQPs/DrzLA +jPXTA3JKeRjeMKWWsDhdZ65mv+82vUc0fVOC9i5Pv3gUW3uX+bM1uzXrVaf2fgti +rkdJ37lMV1/JTmcSccK4A/9fk/hUwSos36R7muW4VgAzBkjIZ4tOXd+Bm+hLV5fn +Ngqa9CG3qS/Jo1N/5SyLdP46uUtNk00qn5J6leLp5ZLVhi5CFyN3Ss09yBgT32t3 +7qIAH/uaywKP1v5Lv9R9gAXAXZ/dY9NKoFks9WPQEs48cJviVIKVe52jdLJyOC9N +1Q== +=jXXW +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/integrationMain/resources/removePackage/Packages b/src/integrationMain/resources/removePackage/Packages new file mode 100644 index 0000000..c92a661 --- /dev/null +++ b/src/integrationMain/resources/removePackage/Packages @@ -0,0 +1,28 @@ +Package: foobar +Version: 1.0.0-1 +Architecture: all +Maintainer: nvima +Installed-Size: 0 +Section: java +Priority: optional +Description: foobar +Filename: pool/main/f/foobar/foobar_1.0.0-1_all.deb +Size: 1016 +SHA1: 6b941852047a4e65a2eb4dbe3588eb99f686ce82 +SHA256: 377c8d8530aa6a09194f4b2e8b6dc9af20fab2470c3ce8a098d807c1ed379552 +MD5sum: 25ae006f26bf6e8bf5c7421424f9d508 + +Package: foobar +Version: 0.0.9-1 +Architecture: all +Maintainer: nvima +Installed-Size: 0 +Section: java +Priority: optional +Description: foobar +Filename: pool/main/f/foobar/foobar_0.0.9-1_all.deb +Size: 1016 +SHA1: 942a0cf83d5adbfcda9eea82f1371f472c388c2e +SHA256: 5a08f081d42e79491b1286f534b73c2f32d31e3d8955bfca21e38b22709ed8c0 +MD5sum: 992b56a2609ae0ae77c2f3fbb27f6e41 + diff --git a/src/integrationMain/resources/removePackage/Packages.gz b/src/integrationMain/resources/removePackage/Packages.gz new file mode 100644 index 0000000..5fb7cae Binary files /dev/null and b/src/integrationMain/resources/removePackage/Packages.gz differ diff --git a/src/integrationMain/resources/removePackage/Release b/src/integrationMain/resources/removePackage/Release new file mode 100644 index 0000000..e812052 --- /dev/null +++ b/src/integrationMain/resources/removePackage/Release @@ -0,0 +1,18 @@ +Origin: Liftric +Label: Liftric +Suite: stable +Components: main +Date: Fri, 09 Jun 2023 13:49:36 UTC +Architectures: all +MD5Sum: + aaa89313abd9c03f2081cdfc1e6ec305 775 main/binary-all/Packages + 0370184af6f72ae92312afdc05f65758 414 main/binary-all/Packages.gz +SHA1: + b74c20751e3aa9db4fba802bd4b796742b6538cf 775 main/binary-all/Packages + 90c7eb2a32dbcfea8862a4c457701d1402aa0243 414 main/binary-all/Packages.gz +SHA256: + 652af4c74358bbbf11997c9b563c1b1398f20f2a645c26726c0cdbc3aeb0bcaf 775 main/binary-all/Packages + e3fb6eb6df307252d344dcb8ed4cdb4a47ff2b58768cd99ecdb8dd4b886f3201 414 main/binary-all/Packages.gz +SHA512: + 104143ee92f1f973c7c2275b9e02550c73aa2ea396ba9b04b80101c89aa2cfeaaecc226d169c370f37ced4a9a1361c6edf971bb02b76425a1d6c1a0dbed15993 775 main/binary-all/Packages + 677ab9ca7ec4dd7830b80b55681be6128b3f9a861d8a95b925d14b3b33d182e3c8d55b1c622c039a8949d953899507b94320d201acc8e0feab372034c413fcb7 414 main/binary-all/Packages.gz diff --git a/src/integrationMain/resources/removePackage/Release.gpg b/src/integrationMain/resources/removePackage/Release.gpg new file mode 100644 index 0000000..34be6f3 --- /dev/null +++ b/src/integrationMain/resources/removePackage/Release.gpg @@ -0,0 +1,9 @@ +-----BEGIN PGP SIGNATURE----- +Version: BCPG v1.70 + +iJwEAAEKAAYFAmSDLfAACgkQMYYDNcqrWgVzmAP/QoVQdbW4NH9Jf34EdI96fCLX +nUPZXk62SaDLNqqJTswfPNiKIKX+NgHnM0fjro3oTAmCCMG9rRvMGPJOW+lqfWlU +z0r+veg5Qd5DudgdCqVDKQHoKkhxzDN1/iIHZ8kDsUlbHJkv31qPd4cgvkKOJqIO +A2F5of6IU/MCtFS+7G0= +=uKV5 +-----END PGP SIGNATURE----- diff --git a/src/integrationMain/resources/removePackage/foobar_0.0.9-1_all.deb b/src/integrationMain/resources/removePackage/foobar_0.0.9-1_all.deb new file mode 100644 index 0000000..8d2af80 Binary files /dev/null and b/src/integrationMain/resources/removePackage/foobar_0.0.9-1_all.deb differ diff --git a/src/integrationMain/resources/removePackage/foobar_1.0.0-1_all.deb b/src/integrationMain/resources/removePackage/foobar_1.0.0-1_all.deb new file mode 100644 index 0000000..ac931e8 Binary files /dev/null and b/src/integrationMain/resources/removePackage/foobar_1.0.0-1_all.deb differ diff --git a/src/integrationMain/resources/uploadPackage/Packages b/src/integrationMain/resources/uploadPackage/Packages new file mode 100644 index 0000000..00ad318 --- /dev/null +++ b/src/integrationMain/resources/uploadPackage/Packages @@ -0,0 +1,13 @@ +Package: foobar +Version: 1.0.0-1 +Architecture: all +Maintainer: nvima +Installed-Size: 0 +Section: java +Priority: optional +Description: foobar +Filename: pool/main/f/foobar/foobar_1.0.0-1_all.deb +Size: 1018 +SHA1: abd00a88a4ff3eb30dfe4412779ca97c5aabf529 +SHA256: b9381b36cf73b10ba01a6cdfa50be13e807687cdf434925e18278d109920b1b2 +MD5sum: 53829e7ab536c57c2fddc96d0e2690b8 diff --git a/src/integrationMain/resources/uploadPackage/Packages.gz b/src/integrationMain/resources/uploadPackage/Packages.gz new file mode 100644 index 0000000..3820053 Binary files /dev/null and b/src/integrationMain/resources/uploadPackage/Packages.gz differ diff --git a/src/integrationMain/resources/uploadPackage/Release b/src/integrationMain/resources/uploadPackage/Release new file mode 100644 index 0000000..5145325 --- /dev/null +++ b/src/integrationMain/resources/uploadPackage/Release @@ -0,0 +1,18 @@ +Origin: Liftric +Label: Liftric +Suite: stable +Components: main +Date: Fri, 09 Jun 2023 09:34:26 UTC +Architectures: all +MD5Sum: + 3a422598c40f3e4fa01260bddb315d94 388 main/binary-all/Packages + 4883de5b75465c6b9748ab23708a5797 303 main/binary-all/Packages.gz +SHA1: + c57e7599a71c534a379aa9a078de33814496f002 388 main/binary-all/Packages + ab03a75c6473e744331e4b74853518c2a662bb71 303 main/binary-all/Packages.gz +SHA256: + f8bdb77df801b557403a71f9ee2cb732521a8ceeea420e28eb3d04b781576c5d 388 main/binary-all/Packages + 207c8c3f8cb79d6a6e0e015533474c8cad782ecd9587a54350282f09cf6955a1 303 main/binary-all/Packages.gz +SHA512: + c5fbe3fedd1775c6e5580b6e0c92d308c310c8b953c1bfcdde8b41d9e5d0849c15dac5ff3a8c28406b6be48a5ebbf9e517b051c912e7b3aa6189298dcdce3811 388 main/binary-all/Packages + ef9f90a0edf4f5e576734896f09970e3e6eb4cc0f645955b461228908abf46de97cdec7f6db714d251f4bbbef5d305da9098d7e27301211bb469ec80b24de433 303 main/binary-all/Packages.gz diff --git a/src/integrationMain/resources/uploadPackage/Release.gpg b/src/integrationMain/resources/uploadPackage/Release.gpg new file mode 100644 index 0000000..ed4a1c7 --- /dev/null +++ b/src/integrationMain/resources/uploadPackage/Release.gpg @@ -0,0 +1,9 @@ +-----BEGIN PGP SIGNATURE----- +Version: BCPG v1.70 + +iJwEAAEKAAYFAmSC8iIACgkQMYYDNcqrWgWm0AP5ARNHfrg6ArjDHojy6Ivw91F+ +iOq58acSS6idwBfAl7MDFeCaLALz0DJ77j+p8ao225n+uOjbBCT4jnHuVkHC+co6 +VKEj1Onn27v3T5EijDskeAUFUA8uv+O59kzAwKX/AzFBQlJcXV3oa6Q2vk5SN2JZ +PEIz+NK6dmIKUoSJE3k= +=V51k +-----END PGP SIGNATURE----- diff --git a/src/integrationMain/resources/uploadPackage/foobar_1.0.0-1_all.deb b/src/integrationMain/resources/uploadPackage/foobar_1.0.0-1_all.deb new file mode 100644 index 0000000..03111e5 Binary files /dev/null and b/src/integrationMain/resources/uploadPackage/foobar_1.0.0-1_all.deb differ diff --git a/src/main/kotlin/com/liftric/apt/S3AptRepositoryPlugin.kt b/src/main/kotlin/com/liftric/apt/S3AptRepositoryPlugin.kt new file mode 100644 index 0000000..d4dabde --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/S3AptRepositoryPlugin.kt @@ -0,0 +1,103 @@ +package com.liftric.apt + +import com.liftric.apt.extensions.S3AptRepositoryPluginExtension +import com.liftric.apt.tasks.* +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * This plugin provides tasks for managing an Apt repository hosted on S3. + * The tasks include uploading a package, removing a package, and cleaning removed packages from the repository. + * It allows configuration through a DSL by creating a PluginExtension object for the project. + * These import in the build.gradle is needed to access the DSL: + * import com.liftric.apt.extensions.* + */ + +internal const val extensionName = "s3AptRepository" +internal const val taskGroup = "S3 Apt Repository Plugin" + +const val DEFAULT_ORIGIN = "Debian" +const val DEFAULT_LABEL = "Debian" +const val DEFAULT_SUITE = "stable" +const val DEFAULT_COMPONENT = "main" + +class S3AptRepositoryPlugin : Plugin { + override fun apply(project: Project) { + val extension = project.extensions.create(extensionName, S3AptRepositoryPluginExtension::class.java, project) + + project.tasks.register("uploadPackage", UploadPackage::class.java) { task -> + task.group = taskGroup + task.description = "Upload Package to S3 and create/update S3 Apt Repository" + task.accessKey.set(extension.accessKey) + task.secretKey.set(extension.secretKey) + task.bucket.set(extension.bucket) + task.bucketPath.set(extension.bucketPath.convention("")) + task.region.set(extension.region) + task.endpoint.set(extension.endpoint) + task.override.set(extension.override) + task.debianFiles.set(extension.debPackages) + task.signingKeyRingFile.set(extension.signingKeyRingFile) + task.signingKeyPassphrase.set(extension.signingKeyPassphrase) + task.origin.set(extension.origin.convention(DEFAULT_ORIGIN)) + task.label.set(extension.label.convention(DEFAULT_LABEL)) + task.suite.set(extension.suite.convention(DEFAULT_SUITE)) + task.components.set(extension.components.convention(DEFAULT_COMPONENT)) + task.architectures.set(extension.architectures) + task.codename.set(extension.codename) + task.date.set(extension.date) + task.releaseDescription.set(extension.releaseDescription) + task.releaseVersion.set(extension.releaseVersion) + task.validUntil.set(extension.validUntil) + task.notAutomatic.set(extension.notAutomatic) + task.butAutomaticUpgrades.set(extension.butAutomaticUpgrades) + task.changelogs.set(extension.changelogs) + task.snapshots.set(extension.snapshots) + } + + project.tasks.register("removePackage", RemovePackage::class.java) { task -> + task.group = taskGroup + task.description = "Remove Package from S3 Apt Repository Packages List" + task.accessKey.set(extension.accessKey) + task.secretKey.set(extension.secretKey) + task.bucket.set(extension.bucket) + task.bucketPath.set(extension.bucketPath.convention("")) + task.region.set(extension.region) + task.endpoint.set(extension.endpoint) + task.debianFiles.set(extension.debPackages) + task.signingKeyRingFile.set(extension.signingKeyRingFile) + task.signingKeyPassphrase.set(extension.signingKeyPassphrase) + task.origin.set(extension.origin.convention(DEFAULT_ORIGIN)) + task.label.set(extension.label.convention(DEFAULT_LABEL)) + task.suite.set(extension.suite.convention(DEFAULT_SUITE)) + task.components.set(extension.components.convention(DEFAULT_COMPONENT)) + task.architectures.set(extension.architectures) + task.codename.set(extension.codename) + task.date.set(extension.date) + task.releaseDescription.set(extension.releaseDescription) + task.releaseVersion.set(extension.releaseVersion) + task.validUntil.set(extension.validUntil) + task.notAutomatic.set(extension.notAutomatic) + task.butAutomaticUpgrades.set(extension.butAutomaticUpgrades) + task.changelogs.set(extension.changelogs) + task.snapshots.set(extension.snapshots) + } + + project.tasks.register("cleanPackages", CleanPackages::class.java) { task -> + task.group = taskGroup + task.description = "Delete removed Packages from S3 Apt Repository" + task.accessKey.set(extension.accessKey) + task.secretKey.set(extension.secretKey) + task.bucket.set(extension.bucket) + task.bucketPath.set(extension.bucketPath.convention("")) + task.region.set(extension.region) + task.endpoint.set(extension.endpoint) + task.suite.set(extension.suite.convention(DEFAULT_SUITE)) + task.components.set(extension.components.convention(DEFAULT_COMPONENT)) + } + } +} + +fun Project.s3AptRepository(): S3AptRepositoryPluginExtension { + return extensions.getByName(extensionName) as? S3AptRepositoryPluginExtension + ?: throw IllegalStateException("$extensionName is not of the correct type") +} diff --git a/src/main/kotlin/com/liftric/apt/extensions/DebPackage.kt b/src/main/kotlin/com/liftric/apt/extensions/DebPackage.kt new file mode 100644 index 0000000..aa645be --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/extensions/DebPackage.kt @@ -0,0 +1,132 @@ +package com.liftric.apt.extensions + +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.provider.Property + +@Suppress("MemberVisibilityCanBePrivate") +@ConfigDsl +class DebPackage(@get:Internal val project: Project) { + + @get:Input + // The Package file to upload. Currently only Debian Files are supported */ + val file: RegularFileProperty = project.objects.fileProperty() + + @get:Input + @get:Optional + // Used for override the default bucket + val bucket: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default bucket path + val bucketPath: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default region + val region: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default S3 endpoint + val endpoint: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Optional: Used for override the default accessKey + val accessKey: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default secretKey + val secretKey: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Origin + val origin: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Label + val label: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Suite + val suite: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Component + val components: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Architectures + val architectures: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Codename + val codename: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Date + val date: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Description + val releaseDescription: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Version + val releaseVersion: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Valid-Until + val validUntil: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release NotAutomatic + val notAutomatic: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release ButAutomaticUpgrades + val butAutomaticUpgrades: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Changelogs + val changelogs: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default Release Snapshots + val snapshots: Property = project.objects.property(String::class.java) + + @get:Input + // Set of supported Architectures from Package + val packageArchitectures: SetProperty = project.objects.setProperty(String::class.java) + + @get:Input + @get:Optional + // Used for override the default package name + val packageName: Property = project.objects.property(String::class.java) + + @get:Input + @get:Optional + // Used for override the default package version + val packageVersion: Property = project.objects.property(String::class.java) +} diff --git a/src/main/kotlin/com/liftric/apt/extensions/dsl.kt b/src/main/kotlin/com/liftric/apt/extensions/dsl.kt new file mode 100644 index 0000000..cdb6812 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/extensions/dsl.kt @@ -0,0 +1,5 @@ +package com.liftric.apt.extensions + +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.FILE) +@DslMarker +annotation class ConfigDsl diff --git a/src/main/kotlin/com/liftric/apt/extensions/extension.kt b/src/main/kotlin/com/liftric/apt/extensions/extension.kt new file mode 100644 index 0000000..12cceab --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/extensions/extension.kt @@ -0,0 +1,128 @@ +package com.liftric.apt.extensions + +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property + +abstract class S3AptRepositoryPluginExtension(val project: Project) { + /** + * AWS Configuration + * These properties can be individually overridden for each Debian file. + * For more information see the DebianFileBuilder + */ + abstract val accessKey: Property + abstract val secretKey: Property + abstract val bucket: Property + abstract val bucketPath: Property + abstract val region: Property + abstract val endpoint: Property + + /** Override Versions that already exist in Apt Repository, Default is true */ + abstract val override: Property + + /** You can specify multiple Debian Files for different Architectures */ + abstract val debPackages: ListProperty + + /** PGP Signing of Release File */ + abstract val signingKeyRingFile: RegularFileProperty + abstract val signingKeyPassphrase: Property + + + /***************************************************************************** + * Release File Fields: + * https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files + * If u don't know what to set, just leave it as it is. + ******************************************************************************/ + + /** Set the APT Repository Origin, Default: Debian */ + abstract val origin: Property + + /** Set the APT Repository Label, Default: Debian */ + abstract val label: Property + + /** + * The suite field may describe the suite. A suite is a single word. + * In Debian, this shall be one of oldstable, stable, testing, unstable, or experimental. + * Default: stable + */ + abstract val suite: Property + + /** + * The Components property represents the components of an APT repository. In APT repositories, components + * categorize software packages based on their source or licensing. Commonly used components are "main", + * "contrib", and "non-free". + * Default: main + */ + abstract val components: Property + + /** + * Optional: If not specified gets automated set by the plugin. It read all binary-* Folders and set + * the architectures property. Can also be overridden by the user if needed. + * Whitespace separated unique single words identifying Debian machine architectures + */ + abstract val architectures: Property + + /** + * Optional: The Codename field shall describe the codename of the release. + * A codename is a single word. Debian's releases are codenamed after Toy Story Characters, + * and the unstable suite has the codename sid, the experimental suite has the codename experimental. + */ + abstract val codename: Property + + /** + * Optional: The Date Field of the Release File. Gets automatically set by the plugin. + * Can be overridden by the user if needed. Be sure to set a valid date format. + */ + abstract val date: Property + + /** + * Optional: The Description Field of the Release File. + * Gets automatically set by the plugin, if the old Release File has this Field set, + * or if the user sets this property. Is not needed for a valid Release File. + */ + abstract val releaseDescription: Property + + /** + * Optional: The Version Field of the Release File. + * Gets automatically set by the plugin, if the old Release File has this Field set, + * or if the user sets this property. Is not needed for a valid Release File. + */ + abstract val releaseVersion: Property + + + /** + * Optional: The Valid-Until Field of the Release File. + * The Valid-Until field may specify at which time the Release file should be considered expired by the client. + * Gets automatically set by the plugin, if the old Release File has this Field set, + * or if the user sets this property. Is not needed for a valid Release File. + */ + abstract val validUntil: Property + + /** + * Optional: The NotAutomatic and ButAutomaticUpgrades fields are optional boolean fields instructing the package manager. + * They may contain the values "yes" and "no". If one the fields is not specified, this has the same meaning as a value of "no". + * If a value of "yes" is specified for the NotAutomatic field, a package manager should not install + * packages (or upgrade to newer versions) from this repository without explicit + * user consent (APT assigns priority 1 to this) If the field ButAutomaticUpgrades is specified as well and has the value "yes", + * the package manager should automatically install package upgrades from this repository, + * if the installed version of the package is higher than the version of the package in other sources (APT assigns priority 100). + * Specifying "yes" for ButAutomaticUpgrades without specifying "yes" for NotAutomatic is invalid. + */ + abstract val notAutomatic: Property + abstract val butAutomaticUpgrades: Property + + /** + * Optional: The Changelogs field tells the client where to find changelogs. + */ + abstract val changelogs: Property + + /** + * Optional: The Snapshots field tells the client where to find snapshots for this archive. + */ + abstract val snapshots: Property +} + +fun S3AptRepositoryPluginExtension.debPackage(action: DebPackage.() -> Unit) { + debPackages.add(DebPackage(project).apply(action)) +} diff --git a/src/main/kotlin/com/liftric/apt/model/debianPackage.kt b/src/main/kotlin/com/liftric/apt/model/debianPackage.kt new file mode 100644 index 0000000..5ba6a53 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/model/debianPackage.kt @@ -0,0 +1,93 @@ +package com.liftric.apt.model + +/** + * The DebianPackage data class represents the metadata of a package in an apt repository. + * This metadata typically resides in "Packages" files within the repository and provides essential + * details about each package, such as its name, version, architecture, dependencies, conflicts, + * and various other properties. + * + * Each instance of this class corresponds to one package's metadata. The properties of this class + * mirror the fields found in the repository's "Packages" file entries. + * + * In addition to the package's attributes, this class includes methods for constructing a string representation + * of the package's metadata, suitable for inclusion in a "Packages" file. + * + * The `toFileString` method generates a string representation of a DebianPackage object that follows + * the conventional formatting in "Packages" files. This makes it easy to write the package's metadata back + * to a repository. + */ + +data class DebianPackage( + val packageName: String, + val version: String, + val architecture: String, + val fileName: String, + val size: String, + val maintainer: String?, + val installedSize: String?, + val depends: String?, + val conflicts: String?, + val replaces: String?, + val provides: String?, + val preDepends: String?, + val recommends: String?, + val suggests: String?, + val enhances: String?, + val builtUsing: String?, + val section: String?, + val priority: String?, + val homepage: String?, + val description: String?, + val sha1: String?, + val sha256: String?, + val md5sum: String?, +) + +fun DebianPackage.toFileString(): String { + return buildString { + appendLine("Package: $packageName") + appendLine("Version: $version") + appendLine("Architecture: $architecture") + appendLine("Filename: $fileName") + appendLine("Size: $size") + maintainer?.let { appendLine("Maintainer: $it") } + installedSize?.let { appendLine("Installed-Size: $it") } + depends?.let { appendLine("Depends: $it") } + conflicts?.let { appendLine("Conflicts: $it") } + replaces?.let { appendLine("Replaces: $it") } + provides?.let { appendLine("Provides: $it") } + preDepends?.let { appendLine("Pre-Depends: $it") } + recommends?.let { appendLine("Recommends: $it") } + suggests?.let { appendLine("Suggests: $it") } + enhances?.let { appendLine("Enhances: $it") } + builtUsing?.let { appendLine("Built-Using: $it") } + section?.let { appendLine("Section: $it") } + priority?.let { appendLine("Priority: $it") } + homepage?.let { appendLine("Homepage: $it") } + description?.let { appendLine("Description: $it") } + md5sum?.let { appendLine("MD5sum: $it") } + sha1?.let { appendLine("SHA1: $it") } + sha256?.let { appendLine("SHA256: $it") } + appendLine() + } +} + +fun List.toFileString(): String { + return buildString { + for (packageItem in this@toFileString) { + append(packageItem.toFileString()) + } + } +} + +fun List.combineDebianPackages( + debianPackage: DebianPackage, +): List { + return this.filter { it.packageName != debianPackage.packageName || it.version != debianPackage.version } + debianPackage +} + +fun List.removeDebianPackage( + debianPackage: DebianPackage, +): List { + return this.filter { it.packageName != debianPackage.packageName || it.version != debianPackage.version } +} diff --git a/src/main/kotlin/com/liftric/apt/model/releaseInfo.kt b/src/main/kotlin/com/liftric/apt/model/releaseInfo.kt new file mode 100644 index 0000000..aff2665 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/model/releaseInfo.kt @@ -0,0 +1,116 @@ +package com.liftric.apt.model + +/** + * The ReleaseInfo data class represents the metadata of a repository in an apt system. + * This metadata typically resides in "Release" files within the repository and provides essential + * details about the repository, such as its origin, label, suite, components, architectures, and + * various other properties. + * + * In addition to the repository's attributes, this class includes methods for constructing a string representation + * of the repository's metadata, suitable for inclusion in a "Release" file. + * + * The `toFileString` method generates a string representation of a ReleaseInfo object that follows + * the conventional formatting in "Release" files. This makes it easy to write the repository's metadata back + * to the apt system. + * + * The `parseReleaseFile` method reads a "Release" file and populates a ReleaseInfo object with its contents. It + * creates and returns a new ReleaseInfo instance, representing the metadata of the repository described by the "Release" file. + * + * The MD5Sum, SHA1, SHA256, and SHA512 data classes represent different types of checksums for the package indexes in the repository. + * These checksums are typically included in "Release" files to ensure the integrity and authenticity of the package indexes. + */ + +data class ReleaseInfo( + val origin: String, + val label: String, + val suite: String, + val components: String, + val architectures: String?, + val codename: String?, + val date: String?, + val description: String?, + val version: String?, + val validUntil: String?, + val notAutomatic: String?, + val butAutomaticUpgrades: String?, + val changelogs: String?, + val snapshots: String?, + val md5Sum: List, + val sha1: List, + val sha256: List, + val sha512: List, +) + +data class MD5Sum( + val md5: String, + val size: Long, + val filename: String, +) + +data class SHA1( + val sha1: String, + val size: Long, + val filename: String, +) + +data class SHA256( + val sha256: String, + val size: Long, + val filename: String, +) + +data class SHA512( + val sha512: String, + val size: Long, + val filename: String, +) + +data class FileData( + val md5Sum: MD5Sum, + val sha1Sum: SHA1, + val sha256Sum: SHA256, + val sha512Sum: SHA512, +) + +fun ReleaseInfo.toFileString(): String { + return buildString { + appendLine("Origin: $origin") + appendLine("Label: $label") + appendLine("Suite: $suite") + appendLine("Components: $components") + codename?.let { appendLine("Codename: $it") } + date?.let { appendLine("Date: $it") } + architectures?.let { appendLine("Architectures: $it") } + description?.let { appendLine("Description: $it") } + version?.let { appendLine("Version: $it") } + validUntil?.let { appendLine("ValidUntil: $it") } + notAutomatic?.let { appendLine("NotAutomatic: $it") } + butAutomaticUpgrades?.let { appendLine("ButAutomaticUpgrades: $it") } + changelogs?.let { appendLine("Changelogs: $it") } + snapshots?.let { appendLine("Snapshots: $it") } + md5Sum.let { list -> + appendLine("MD5Sum:") + list.forEach { md5Sum -> + appendLine(" ${md5Sum.md5} ${md5Sum.size} ${md5Sum.filename}") + } + } + sha1.let { list -> + appendLine("SHA1:") + list.forEach { sha1 -> + appendLine(" ${sha1.sha1} ${sha1.size} ${sha1.filename}") + } + } + sha256.let { list -> + appendLine("SHA256:") + list.forEach { sha256 -> + appendLine(" ${sha256.sha256} ${sha256.size} ${sha256.filename}") + } + } + sha512.let { list -> + appendLine("SHA512:") + list.forEach { sha512 -> + appendLine(" ${sha512.sha512} ${sha512.size} ${sha512.filename}") + } + } + } +} diff --git a/src/main/kotlin/com/liftric/apt/service/AwsS3Client.kt b/src/main/kotlin/com/liftric/apt/service/AwsS3Client.kt new file mode 100644 index 0000000..680d8b0 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/service/AwsS3Client.kt @@ -0,0 +1,128 @@ +package com.liftric.apt.service + +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.services.s3.model.* +import java.io.File +import java.net.URI + +/** + * The AwsS3Client class is a utility for interacting with AWS S3 service. + * It provides simplified methods for common operations such as checking if an object exists, + * uploading an object, and retrieving an object from an S3 bucket. + * + * The class employs the AWS SDK's S3Client to establish a connection to AWS S3, + * which is configured at the time of the AwsS3Client instance creation. + * The AWS region, access key, and secret key are required parameters. + * + * The provided methods wrap the underlying AWS SDK calls, abstracting away the complexity + * of the raw API and offering a more streamlined and intuitive interface for the following operations: + * + * - doesObjectExist: Checks if an object exists in a specified S3 bucket. + * - uploadObject: Uploads a file to a specified S3 bucket. + * - getObject: Retrieves a specified object from an S3 bucket and writes it to a temporary local file. + */ + +class AwsS3Client( + accessKey: String, + secretKey: String, + region: String, + endpoint: String? = null, +) { + private val s3Client by lazy { + with(S3Client.builder()) { + when { + endpoint != null -> { + endpointOverride(URI.create(endpoint)) + } + } + region(convertToRegion(region)) + credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create( + accessKey, + secretKey + ) + ) + ) + build() + } + } + + private fun convertToRegion(regionStr: String): Region { + return Region.of(regionStr) + } + + fun doesObjectExist(bucket: String, key: String): Boolean { + return try { + val headObjectRequest = HeadObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + s3Client.headObject(headObjectRequest) + true + } catch (e: NoSuchKeyException) { + false + } + } + + fun uploadObject(bucket: String, key: String, file: File): PutObjectResponse { + val path = file.toPath() + val request = PutObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + + return s3Client.putObject(request, RequestBody.fromFile(path.toFile())) + } + + fun getObject(bucket: String, key: String): File { + val request = GetObjectRequest.builder() + .bucket(bucket) + .key(key) + .build() + + val response = s3Client.getObject(request) + val file = File.createTempFile(key, null).apply { + deleteOnExit() + } + response.use { input -> + file.outputStream().use { fileOut -> + input.copyTo(fileOut) + } + } + return file + } + + fun listAllObjects(bucket: String, prefix: String? = null): List { + val builder = ListObjectsV2Request.builder().bucket(bucket) + if (prefix != null) { + builder.prefix(prefix) + } + var request = builder.build() + val keys = mutableListOf() + + var response: ListObjectsV2Response + do { + response = s3Client.listObjectsV2(request) + response.contents().forEach { + keys.add(it.key()) + } + val token = response.nextContinuationToken() + request = request.toBuilder().continuationToken(token).build() + } while (response.isTruncated) + + return keys + } + + fun deleteObjects(bucket: String, keys: List): DeleteObjectsResponse { + val request = DeleteObjectsRequest.builder() + .bucket(bucket) + .delete(Delete.builder().objects(keys.map { ObjectIdentifier.builder().key(it).build() }).build()) + .build() + return s3Client.deleteObjects(request) + } +} diff --git a/src/main/kotlin/com/liftric/apt/service/PackagesFactory.kt b/src/main/kotlin/com/liftric/apt/service/PackagesFactory.kt new file mode 100644 index 0000000..f226bbc --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/service/PackagesFactory.kt @@ -0,0 +1,140 @@ +package com.liftric.apt.service + +import com.liftric.apt.utils.FileHashUtil.md5Hash +import com.liftric.apt.utils.FileHashUtil.sha1Hash +import com.liftric.apt.utils.FileHashUtil.sha256Hash +import com.liftric.apt.model.DebianPackage +import com.liftric.apt.utils.parseToMap +import org.apache.commons.compress.archivers.ar.ArArchiveEntry +import org.apache.commons.compress.archivers.ar.ArArchiveInputStream +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream +import org.apache.commons.compress.compressors.CompressorStreamFactory +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream + +/** + * This class, PackagesFactory, serves two primary purposes related to Debian packages and APT repositories. + * + * Firstly, it's used to parse Debian files, specifically focusing on control files which contain metadata. + * This metadata is utilized to create a Packages file for an APT repository. + * + * Secondly, it is used to read Packages files from an APT repository. This allows for further operations + * or manipulations to be performed on the data retrieved from the repository. + */ + +object PackagesFactory { + fun parseDebianFile( + file: File, + archs: Set, + poolBucketKey: String, + packageName: String?, + packageVersion: String?, + ): List { + val controlInfo = readDebFile(file) + return parseControlInfo(file, controlInfo, archs, poolBucketKey, packageName, packageVersion) + } + + fun parsePackagesFile(file: File): List { + val packagesText = file.readText() + val packages = packagesText.split("\n\n").filter { it.isNotBlank() } + + return packages.map { + val parsedMap = parseToMap(it) + DebianPackage( + packageName = parsedMap["Package"] ?: error("Invalid Packages file, PackageName missing"), + version = parsedMap["Version"] ?: error("Invalid Packages file, Version missing"), + architecture = parsedMap["Architecture"] ?: error("Invalid Packages file, Architecture missing"), + fileName = parsedMap["Filename"] ?: error("Invalid Packages file, Filename missing"), + size = parsedMap["Size"] ?: error("Invalid Packages file, Size missing"), + sha1 = parsedMap["SHA1"], + sha256 = parsedMap["SHA256"], + md5sum = parsedMap["MD5sum"], + maintainer = parsedMap["Maintainer"], + installedSize = parsedMap["Installed-Size"], + depends = parsedMap["Depends"], + conflicts = parsedMap["Conflicts"], + replaces = parsedMap["Replaces"], + provides = parsedMap["Provides"], + preDepends = parsedMap["Pre-Depends"], + recommends = parsedMap["Recommends"], + suggests = parsedMap["Suggests"], + enhances = parsedMap["Enhances"], + builtUsing = parsedMap["Built-Using"], + section = parsedMap["Section"], + priority = parsedMap["Priority"], + homepage = parsedMap["Homepage"], + description = parsedMap["Description"], + ) + } + } + + private fun parseControlInfo( + file: File, + controlInfo: String, + archs: Set, + fileName: String, + packageName: String?, + packageVersion: String?, + ): List { + val controlMap = parseToMap(controlInfo) + return archs.map { arch -> + DebianPackage( + packageName = packageName ?: controlMap["Package"] + ?: error("Package Name in Control Info missing but required"), + version = packageVersion ?: controlMap["Version"] + ?: error("Version in Control Info missing but required"), + architecture = arch, + fileName = fileName, + size = file.length().toString(), + sha1 = file.sha1Hash(), + sha256 = file.sha256Hash(), + md5sum = file.md5Hash(), + maintainer = controlMap["Maintainer"], + installedSize = controlMap["Installed-Size"], + depends = controlMap["Depends"], + conflicts = controlMap["Conflicts"], + replaces = controlMap["Replaces"], + provides = controlMap["Provides"], + preDepends = controlMap["Pre-Depends"], + recommends = controlMap["Recommends"], + suggests = controlMap["Suggests"], + enhances = controlMap["Enhances"], + builtUsing = controlMap["Built-Using"], + section = controlMap["Section"], + priority = controlMap["Priority"], + homepage = controlMap["Homepage"], + description = controlMap["Description"], + ) + } + } + + private fun readDebFile(file: File): String { + val arInput = ArArchiveInputStream(FileInputStream(file)) + var entry: ArArchiveEntry? = arInput.nextArEntry + while (entry != null) { + if (entry.name.startsWith("control.tar")) { + val byteArray = arInput.readBytes() + return processTarBytes(byteArray) + } + entry = arInput.nextArEntry + } + arInput.close() + throw Exception("control.tar not found") + } + + private fun processTarBytes(tarBytes: ByteArray): String { + val compressorInput = + CompressorStreamFactory().createCompressorInputStream(BufferedInputStream(tarBytes.inputStream())) + val tarInput = TarArchiveInputStream(compressorInput) + var tarEntry = tarInput.nextTarEntry + while (tarEntry != null) { + if (tarEntry.name == "./control") { + return String(tarInput.readBytes()) + } + tarEntry = tarInput.nextTarEntry + } + tarInput.close() + throw Exception("control not found") + } +} diff --git a/src/main/kotlin/com/liftric/apt/service/aptRepository.kt b/src/main/kotlin/com/liftric/apt/service/aptRepository.kt new file mode 100644 index 0000000..a8bb4da --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/service/aptRepository.kt @@ -0,0 +1,239 @@ +package com.liftric.apt.service + +import com.liftric.apt.model.* +import com.liftric.apt.utils.FileHashUtil.compressWithGzip +import com.liftric.apt.utils.FileHashUtil.md5Hash +import com.liftric.apt.utils.FileHashUtil.sha1Hash +import com.liftric.apt.utils.FileHashUtil.sha256Hash +import com.liftric.apt.utils.FileHashUtil.sha512Hash +import com.liftric.apt.utils.signReleaseFile +import org.gradle.api.logging.Logger +import java.io.File +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.* + +/** + * This file contains several utility functions for managing and interacting + * with an APT (Advanced Package Tool) repository that's hosted on an Amazon S3 bucket. + * It provides capabilities for operations like uploading, cleaning, updating, and + * maintaining packages in the repository. + */ + + +fun uploadDebianFile( + logger: Logger, + s3Client: AwsS3Client, + bucket: String, + bucketPath: String, + bucketKey: String, + file: File, + override: Boolean, +) { + val fullBucketKey = getFullBucketKey(bucketPath, bucketKey) + val exist = s3Client.doesObjectExist(bucket, bucketKey) + + if (!exist || override) { + s3Client.uploadObject(bucket, fullBucketKey, file) + logger.info("File uploaded to s3://$bucket/${fullBucketKey}") + } else { + throw RuntimeException("Version already exist in Repo") + } +} + +fun getPoolBucketKey(fileName: String, component: String): String { + val firstLetter = fileName.substring(0, 1) + val packageName = fileName.substringBefore("_") + return "pool/$component/$firstLetter/$packageName/$fileName" +} + +fun cleanPackages( + logger: Logger, + s3Client: AwsS3Client, + bucket: String, + bucketPath: String, + component: String, + usedPackages: Set, +) { + val files: List = s3Client.listAllObjects(bucket, getFullBucketKey(bucketPath, "pool/$component/")) + val filesToRemove = files.filter { !usedPackages.contains(it) } + if (filesToRemove.isEmpty()) { + logger.info("No files to remove") + return + } + val deletedObjects = s3Client.deleteObjects(bucket, filesToRemove) + logger.info("Deleted ${deletedObjects.deleted().size} objects") +} + +fun getUsedPackagesPoolKeys( + s3Client: AwsS3Client, + bucket: String, + bucketPath: String, + suite: String, + component: String, +): Set { + val fileKeys = s3Client.listAllObjects(bucket, getFullBucketKey(bucketPath, "dists/$suite/$component/binary-")) + return fileKeys.filter { key -> key.endsWith("Packages") } + .map { key -> s3Client.getObject(bucket, key) } + .flatMap { file -> + PackagesFactory.parsePackagesFile(file) + } + .map { packagesInfo -> getFullBucketKey(bucketPath, packagesInfo.fileName) } + .toSet() +} + +fun updateReleaseFile( + logger: Logger, + s3Client: AwsS3Client, + bucket: String, + bucketPath: String, + releaseInfo: ReleaseInfo, + signingKeyRingFile: File? = null, + signingKeyPassphrase: String? = null, +) { + val files: List = + s3Client.listAllObjects( + bucket, + getFullBucketKey(bucketPath, "dists/${releaseInfo.suite}/${releaseInfo.components}/binary-") + ) + + val fileDataList = files.map { filePath -> + val packageFile = s3Client.getObject(bucket, filePath) + val relativeFilePath = + "${releaseInfo.components}${filePath.substringAfter("${releaseInfo.suite}/${releaseInfo.components}")}" + + FileData( + md5Sum = MD5Sum(packageFile.md5Hash(), packageFile.length(), relativeFilePath), + sha1Sum = SHA1(packageFile.sha1Hash(), packageFile.length(), relativeFilePath), + sha256Sum = SHA256(packageFile.sha256Hash(), packageFile.length(), relativeFilePath), + sha512Sum = SHA512(packageFile.sha512Hash(), packageFile.length(), relativeFilePath) + ) + } + + val newReleaseInfo = releaseInfo.copy( + architectures = releaseInfo.architectures ?: getReleaseArchitecturesFromS3FileList(files), + date = releaseInfo.date ?: getReleaseDate(), + md5Sum = fileDataList.map { it.md5Sum }, + sha1 = fileDataList.map { it.sha1Sum }, + sha256 = fileDataList.map { it.sha256Sum }, + sha512 = fileDataList.map { it.sha512Sum }, + ) + + val releaseFile = createTemporaryFile(newReleaseInfo.toFileString(), "Release") + val releaseFileLocation = getFullBucketKey(bucketPath, "dists/${releaseInfo.suite}/") + + s3Client.uploadObject(bucket, "${releaseFileLocation}Release", releaseFile) + logger.info("Release file uploaded to s3://$bucket/${releaseFileLocation}Release") + + if (signingKeyRingFile != null && signingKeyPassphrase != null) { + val signedReleaseFile = signReleaseFile(signingKeyRingFile, signingKeyPassphrase.toCharArray(), releaseFile) + s3Client.uploadObject(bucket, "${releaseFileLocation}Release.gpg", signedReleaseFile) + logger.info("Signed Release file uploaded to s3://$bucket/${releaseFileLocation}Release.gpg") + } +} + +fun uploadPackagesFiles( + logger: Logger, + packageFiles: Map, + s3Client: AwsS3Client, + bucket: String, +) { + packageFiles.forEach { (key, value) -> + logger.info("Uploading $key") + s3Client.uploadObject(bucket, key, value) + } +} + +fun getUpdatedPackagesFiles( + logger: Logger, + suite: String, + component: String, + s3Client: AwsS3Client, + bucket: String, + bucketPath: String, + debianPackages: List, +): Map { + return debianPackages.flatMap { debianPackage -> + val relativePackagesFileLocation = "dists/$suite/$component/binary-${debianPackage.architecture}/Packages" + val packagesFileLocation = getFullBucketKey(bucketPath, relativePackagesFileLocation) + + val packagesFile = try { + val packagesFile = s3Client.getObject(bucket, packagesFileLocation) + logger.info("Parsing Packages file: s3://$bucket/${packagesFileLocation}") + val oldDebianPackages = PackagesFactory.parsePackagesFile(packagesFile) + val combinedPackages = oldDebianPackages.combineDebianPackages(debianPackage) + createTemporaryFile(combinedPackages.toFileString(), packagesFileLocation) + } catch (e: Exception) { + logger.info("Packages file for '${debianPackage.architecture}' not found, creating new Packages file") + createTemporaryFile(debianPackage.toFileString(), packagesFileLocation) + } + val gzipPackagesFile = packagesFile.compressWithGzip() + + listOf( + packagesFileLocation to packagesFile, + "$packagesFileLocation.gz" to gzipPackagesFile + ) + }.toMap() +} + +fun getCleanedPackagesFiles( + logger: Logger, + suite: String, + component: String, + s3Client: AwsS3Client, + bucket: String, + bucketPath: String, + debianPackages: List, +): Map { + return debianPackages.flatMap { debianPackage -> + val relativePackagesFileLocation = "dists/$suite/$component/binary-${debianPackage.architecture}/Packages" + val packagesFileLocation = getFullBucketKey(bucketPath, relativePackagesFileLocation) + + val packagesFile = try { + val packagesFile = s3Client.getObject(bucket, packagesFileLocation) + logger.info("Parsing Packages file: s3://$bucket/${packagesFileLocation}") + val oldDebianPackages = PackagesFactory.parsePackagesFile(packagesFile) + val cleanedDebianPackages = oldDebianPackages.removeDebianPackage(debianPackage) + createTemporaryFile(cleanedDebianPackages.toFileString(), packagesFileLocation) + } catch (e: Exception) { + logger.error("Packages file for Architecture '${debianPackage.architecture}' not found! Can't remove '${debianPackage.packageName}'") + throw e + } + + val gzipPackagesFile = packagesFile.compressWithGzip() + + listOf( + packagesFileLocation to packagesFile, + "$packagesFileLocation.gz" to gzipPackagesFile + ) + }.toMap() +} + + +private fun getReleaseDate(): String { + val now = ZonedDateTime.now(ZoneId.of("UTC")) + val formatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH) + return now.format(formatter) +} + +private fun getReleaseArchitecturesFromS3FileList(files: List): String { + return files.map { filePath -> + filePath.substringAfterLast("binary-").substringBefore("/Packages") + }.toSet().joinToString(separator = " ") +} + +internal fun getFullBucketKey(bucketPath: String, bucketKey: String): String { + return when { + bucketPath.endsWith('/') -> "$bucketPath$bucketKey" + bucketPath.isBlank() -> bucketKey + else -> "$bucketPath/$bucketKey" + } +} + +internal fun createTemporaryFile(content: String, fileName: String, prefix: String? = null): File { + return File.createTempFile(fileName, prefix).apply { + writeText(content) + deleteOnExit() + } +} diff --git a/src/main/kotlin/com/liftric/apt/tasks/CleanPackages.kt b/src/main/kotlin/com/liftric/apt/tasks/CleanPackages.kt new file mode 100644 index 0000000..382c4c0 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/tasks/CleanPackages.kt @@ -0,0 +1,55 @@ +package com.liftric.apt.tasks + +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.* +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.* +import org.gradle.api.provider.Property + +/** + * Delete all Packages from pool that are not referenced in the Packages file + */ + +abstract class CleanPackages : DefaultTask() { + @get:Input + abstract val suite: Property + + @get:Input + abstract val components: Property + + @get:Input + abstract val accessKey: Property + + @get:Input + abstract val secretKey: Property + + @get:Input + abstract val bucket: Property + + @get:Input + abstract val bucketPath: Property + + @get:Input + abstract val region: Property + + @get:Input + @get:Optional + abstract val endpoint: Property + + @TaskAction + fun main() { + val suiteValue = suite.get() + val componentValue = components.get() + val accessKeyValue = accessKey.get() + val secretKeyValue = secretKey.get() + val bucketValue = bucket.get() + val bucketPathValue = bucketPath.get() + val regionValue = region.get() + val endpointValue = endpoint.orNull + + val s3Client = AwsS3Client(accessKeyValue, secretKeyValue, regionValue, endpointValue) + + val usedPackages = getUsedPackagesPoolKeys(s3Client, bucketValue, bucketPathValue, suiteValue, componentValue) + cleanPackages(logger, s3Client, bucketValue, bucketPathValue, componentValue, usedPackages) + } +} diff --git a/src/main/kotlin/com/liftric/apt/tasks/RemovePackage.kt b/src/main/kotlin/com/liftric/apt/tasks/RemovePackage.kt new file mode 100644 index 0000000..085629a --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/tasks/RemovePackage.kt @@ -0,0 +1,193 @@ +package com.liftric.apt.tasks + +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.extensions.DebPackage +import com.liftric.apt.model.ReleaseInfo +import com.liftric.apt.service.* +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.* +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Optional + +/** + * Removes a Package from the Packages file List inside an S3 Apt Repository + */ + +abstract class RemovePackage : DefaultTask() { + @get:Nested + abstract val debianFiles: ListProperty + + @get:Input + abstract val accessKey: Property + + @get:Input + abstract val secretKey: Property + + @get:Input + abstract val bucket: Property + + @get:Input + abstract val bucketPath: Property + + @get:Input + abstract val region: Property + + @get:Input + @get:Optional + abstract val endpoint: Property + + @get:Input + @get:Optional + abstract val signingKeyRingFile: RegularFileProperty + + @get:Input + @get:Optional + abstract val signingKeyPassphrase: Property + + @get:Input + @get:Optional + abstract val origin: Property + + @get:Input + @get:Optional + abstract val label: Property + + @get:Input + @get:Optional + abstract val suite: Property + + @get:Input + @get:Optional + abstract val components: Property + + @get:Input + @get:Optional + abstract val architectures: Property + + @get:Input + @get:Optional + abstract val codename: Property + + @get:Input + @get:Optional + abstract val date: Property + + @get:Input + @get:Optional + abstract val releaseDescription: Property + + @get:Input + @get:Optional + abstract val releaseVersion: Property + + @get:Input + @get:Optional + abstract val validUntil: Property + + @get:Input + @get:Optional + abstract val notAutomatic: Property + + @get:Input + @get:Optional + abstract val butAutomaticUpgrades: Property + + @get:Input + @get:Optional + abstract val changelogs: Property + + @get:Input + @get:Optional + abstract val snapshots: Property + + @TaskAction + fun main() { + debianFiles.get().forEach { debianFile -> + /** depPackage File **/ + val inputFile = debianFile.file.get().asFile + + /** Package file values **/ + val archs = debianFile.packageArchitectures.get() + val packageName = debianFile.packageName.orNull + val packageVersion = debianFile.packageVersion.orNull + + /** GPG Signing **/ + val signingKeyRingFileValue = signingKeyRingFile.orNull?.asFile + val signingKeyPassphraseValue = signingKeyPassphrase.orNull + + /** AWS S3 values **/ + val accessKey = debianFile.accessKey.orNull ?: accessKey.get() + val secretKey = debianFile.secretKey.orNull ?: secretKey.get() + val bucket = debianFile.bucket.orNull ?: bucket.get() + val bucketPath = debianFile.bucketPath.orNull ?: bucketPath.get() + val region = debianFile.region.orNull ?: region.get() + val endpoint = debianFile.endpoint.orNull ?: endpoint.orNull + + /** Release file values **/ + val origin = debianFile.origin.orNull ?: origin.get() + val label = debianFile.label.orNull ?: label.get() + val suite = debianFile.suite.orNull ?: suite.get() + val components = debianFile.components.orNull ?: components.get() + val architectures = debianFile.architectures.orNull ?: architectures.orNull + val codename = debianFile.codename.orNull ?: codename.orNull + val date = debianFile.date.orNull ?: date.orNull + val releaseDescription = debianFile.releaseDescription.orNull ?: releaseDescription.orNull + val releaseVersion = debianFile.releaseVersion.orNull ?: releaseVersion.orNull + val validUntil = debianFile.validUntil.orNull ?: validUntil.orNull + val notAutomatic = debianFile.notAutomatic.orNull ?: notAutomatic.orNull + val butAutomaticUpgrades = debianFile.butAutomaticUpgrades.orNull ?: butAutomaticUpgrades.orNull + val changelogs = debianFile.changelogs.orNull ?: changelogs.orNull + val snapshots = debianFile.snapshots.orNull ?: snapshots.orNull + + val s3Client = AwsS3Client(accessKey, secretKey, region, endpoint) + val debianPackages = + PackagesFactory.parseDebianFile(inputFile, archs, "", packageName, packageVersion) + + val packagesFiles = + getCleanedPackagesFiles( + logger, + suite, + components, + s3Client, + bucket, + bucketPath, + debianPackages + ) + + uploadPackagesFiles(logger, packagesFiles, s3Client, bucket) + + val releaseInfo = ReleaseInfo( + origin, + label, + suite, + components, + architectures, + codename, + date, + releaseDescription, + releaseVersion, + validUntil, + notAutomatic, + butAutomaticUpgrades, + changelogs, + snapshots, + md5Sum = listOf(), + sha1 = listOf(), + sha256 = listOf(), + sha512 = listOf() + ) + + updateReleaseFile( + logger, + s3Client, + bucket, + bucketPath, + releaseInfo, + signingKeyRingFileValue, + signingKeyPassphraseValue + ) + } + } +} diff --git a/src/main/kotlin/com/liftric/apt/tasks/UploadPackage.kt b/src/main/kotlin/com/liftric/apt/tasks/UploadPackage.kt new file mode 100644 index 0000000..12608c5 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/tasks/UploadPackage.kt @@ -0,0 +1,195 @@ +package com.liftric.apt.tasks + +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.extensions.DebPackage +import com.liftric.apt.model.ReleaseInfo +import com.liftric.apt.service.* +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.* +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Optional + +/** + * Uploads a debian packages to a s3 bucket and create or update the Repository. + */ + +abstract class UploadPackage : DefaultTask() { + @get:Nested + abstract val debianFiles: ListProperty + + @get:Input + abstract val accessKey: Property + + @get:Input + abstract val secretKey: Property + + @get:Input + abstract val bucket: Property + + @get:Input + abstract val bucketPath: Property + + @get:Input + abstract val region: Property + + @get:Input + @get:Optional + abstract val endpoint: Property + + @get:Input + @get:Optional + abstract val override: Property + + @get:Input + @get:Optional + abstract val signingKeyRingFile: RegularFileProperty + + @get:Input + @get:Optional + abstract val signingKeyPassphrase: Property + + @get:Input + @get:Optional + abstract val origin: Property + + @get:Input + @get:Optional + abstract val label: Property + + @get:Input + @get:Optional + abstract val suite: Property + + @get:Input + @get:Optional + abstract val components: Property + + @get:Input + @get:Optional + abstract val architectures: Property + + @get:Input + @get:Optional + abstract val codename: Property + + @get:Input + @get:Optional + abstract val date: Property + + @get:Input + @get:Optional + abstract val releaseDescription: Property + + @get:Input + @get:Optional + abstract val releaseVersion: Property + + @get:Input + @get:Optional + abstract val validUntil: Property + + @get:Input + @get:Optional + abstract val notAutomatic: Property + + @get:Input + @get:Optional + abstract val butAutomaticUpgrades: Property + + @get:Input + @get:Optional + abstract val changelogs: Property + + @get:Input + @get:Optional + abstract val snapshots: Property + + @TaskAction + fun main() { + debianFiles.get().forEach { debianFile -> + /** depPackage File **/ + val inputFile = debianFile.file.get().asFile + + /** Package file values **/ + val packageName = debianFile.packageName.orNull + val packageVersion = debianFile.packageVersion.orNull + val packageArchitectures = debianFile.packageArchitectures.get() + + /** GPG Signing **/ + val signingKeyRingFileValue = signingKeyRingFile.orNull?.asFile + val signingKeyPassphraseValue = signingKeyPassphrase.orNull + + /** AWS S3 values **/ + val accessKey = debianFile.accessKey.orNull ?: accessKey.get() + val secretKey = debianFile.secretKey.orNull ?: secretKey.get() + val region = debianFile.region.orNull ?: region.get() + val endpoint = debianFile.endpoint.orNull ?: endpoint.orNull + val bucket = debianFile.bucket.orNull ?: bucket.get() + val bucketPath = debianFile.bucketPath.orNull ?: bucketPath.get() + + /** Release file values **/ + val suite = debianFile.suite.orNull ?: suite.get() + val components = debianFile.components.orNull ?: components.get() + val origin = debianFile.origin.orNull ?: origin.get() + val label = debianFile.label.orNull ?: label.get() + val architectures = debianFile.architectures.orNull ?: architectures.orNull + val codename = debianFile.codename.orNull ?: codename.orNull + val date = debianFile.date.orNull ?: date.orNull + val releaseDescription = debianFile.releaseDescription.orNull ?: releaseDescription.orNull + val releaseVersion = debianFile.releaseVersion.orNull ?: releaseVersion.orNull + val validUntil = debianFile.validUntil.orNull ?: validUntil.orNull + val notAutomatic = debianFile.notAutomatic.orNull ?: notAutomatic.orNull + val butAutomaticUpgrades = debianFile.butAutomaticUpgrades.orNull ?: butAutomaticUpgrades.orNull + val changelogs = debianFile.changelogs.orNull ?: changelogs.orNull + val snapshots = debianFile.snapshots.orNull ?: snapshots.orNull + + val s3Client = AwsS3Client(accessKey, secretKey, region, endpoint) + val debianPoolBucketKey = getPoolBucketKey(inputFile.name, components) + val debianPackages = PackagesFactory.parseDebianFile( + inputFile, packageArchitectures, debianPoolBucketKey, packageName, packageVersion + ) + + uploadDebianFile( + logger, s3Client, bucket, bucketPath, debianPoolBucketKey, inputFile, override.getOrElse(true) + ) + + val packagesFiles = + getUpdatedPackagesFiles(logger, suite, components, s3Client, bucket, bucketPath, debianPackages) + + uploadPackagesFiles(logger, packagesFiles, s3Client, bucket) + + val releaseInfo = ReleaseInfo( + origin, + label, + suite, + components, + architectures, + codename, + date, + releaseDescription, + releaseVersion, + validUntil, + notAutomatic, + butAutomaticUpgrades, + changelogs, + snapshots, + md5Sum = listOf(), + sha1 = listOf(), + sha256 = listOf(), + sha512 = listOf() + ) + + updateReleaseFile( + logger, + s3Client, + bucket, + bucketPath, + releaseInfo, + signingKeyRingFileValue, + signingKeyPassphraseValue + ) + } + } +} diff --git a/src/main/kotlin/com/liftric/apt/utils/FileHashUtil.kt b/src/main/kotlin/com/liftric/apt/utils/FileHashUtil.kt new file mode 100644 index 0000000..6681a42 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/utils/FileHashUtil.kt @@ -0,0 +1,61 @@ +package com.liftric.apt.utils + +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.math.BigInteger +import java.security.MessageDigest +import java.util.zip.GZIPOutputStream + +/** + * FileHashUtil is a utility object that extends the Java File class with additional functionalities + * specifically designed for managing and manipulating files within an apt repository. + * + * It provides convenient methods to compute various types of hashes (MD5, SHA-1, SHA-256) for a file, + * which are commonly used in apt repositories for data integrity and verification purposes. + * + * Besides hashing, it also provides a method to retrieve the size of a file and a function to compress + * a file using the GZIP compression algorithm, another operation frequently needed when working with + * apt repositories. + * + * These methods are implemented as extension functions to the File class, making them readily available + * on any File object within the scope of this object. + */ + +object FileHashUtil { + private fun File.hash(algo: String, padStart: Int, padChar: Char): String { + val md = MessageDigest.getInstance(algo) + val inputStream = this.inputStream() + return inputStream.use { + val buffer = ByteArray(1024 * 1024 * 8) + var bytesRead = it.read(buffer) + + while (bytesRead > 0) { + md.update(buffer, 0, bytesRead) + bytesRead = it.read(buffer) + } + + val hashedBytes = md.digest() + + BigInteger(1, hashedBytes).toString(16).padStart(padStart, padChar) + } + } + + fun File.md5Hash(): String = hash("MD5", 32, '0') + + fun File.sha1Hash(): String = hash("SHA-1", 40, '0') + + fun File.sha256Hash(): String = hash("SHA-256", 64, '0') + + fun File.sha512Hash(): String = hash("SHA-512", 128, '0') + + fun File.compressWithGzip(): File { + val gzipFile = File(this.parentFile, this.name + ".gz") + FileInputStream(this).use { fileInputStream -> + GZIPOutputStream(FileOutputStream(gzipFile)).use { gzipOutputStream -> + fileInputStream.copyTo(gzipOutputStream) + } + } + return gzipFile + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/liftric/apt/utils/parseToMap.kt b/src/main/kotlin/com/liftric/apt/utils/parseToMap.kt new file mode 100644 index 0000000..9b87fff --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/utils/parseToMap.kt @@ -0,0 +1,22 @@ +package com.liftric.apt.utils + +/** + * This utility function, `parseToMap`, is used for parsing text-based files from an APT repository + * such as Release or Packages files. + * + * Note: This function does not check if the input text is properly formatted. Malformed input may + * lead to unexpected results. + */ + +fun parseToMap(text: String): Map { + val removeMultiLines = text + .replace(Regex("\n\\s+"), " ") + + val parsedMap = removeMultiLines.split("\n").associate { + val strings = it.split(": ") + strings[0].trim() to + strings.getOrNull(1) + ?.trim() + } + return parsedMap +} diff --git a/src/main/kotlin/com/liftric/apt/utils/signGPG.kt b/src/main/kotlin/com/liftric/apt/utils/signGPG.kt new file mode 100644 index 0000000..6b41895 --- /dev/null +++ b/src/main/kotlin/com/liftric/apt/utils/signGPG.kt @@ -0,0 +1,83 @@ +package com.liftric.apt.utils + +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.security.Security + +/** + * Signs the Release file of an Apt Repository using a given private key. + * This function first ensures the Bouncy Castle security provider is added, then loads the private key, + * and sets up a signature generator with the proper signing algorithm. + * It reads the Release file into a byte array, which is then used to update the signature generator. + * After generating the signature, it writes the signature into a temporary file which will be deleted when the program ends. + * + * @param privateKeyFile File object representing the private key file + * @param passphrase CharArray object representing the passphrase to the private key file + * @param releaseFile File object representing the Release file to be signed + * @return File object representing the signed Release file + */ + + +fun signReleaseFile(privateKeyFile: File, passphrase: CharArray, releaseFile: File): File { + if (Security.getProvider("BC") == null) { + Security.addProvider(BouncyCastleProvider()); + } + + val privateKey = loadPrivateKey(privateKeyFile, passphrase) + + val sigGen = PGPSignatureGenerator(JcaPGPContentSignerBuilder(privateKey.publicKeyPacket.algorithm, PGPUtil.SHA512)) + sigGen.init(PGPSignature.BINARY_DOCUMENT, privateKey) + + val fileBytes = releaseFile.readBytes() + sigGen.update(fileBytes) + + val signedFile = File.createTempFile("Release", ".gpg").apply { + deleteOnExit() + } + + val fileOutputStream = FileOutputStream(signedFile) + val armoredOutputStream = ArmoredOutputStream(fileOutputStream) + + sigGen.generate().encode(armoredOutputStream) + + armoredOutputStream.close() + fileOutputStream.close() + + return signedFile +} + +fun loadPrivateKey(privateKeyFile: File, passphrase: CharArray): PGPPrivateKey { + val keyIn = PGPUtil.getDecoderStream(FileInputStream(privateKeyFile)) + val pgpF = JcaPGPObjectFactory(keyIn) + val pgpSec = pgpF.nextObject() as PGPSecretKeyRing + + var secretKey: PGPSecretKey? = null + val keys = pgpSec.secretKeys + while (secretKey == null && keys.hasNext()) { + val key = keys.next() + if (key.isSigningKey) { + secretKey = key + } + } + + if (secretKey == null) { + throw IllegalArgumentException("Can't find signing key in key ring.") + } + + val decryptor: PBESecretKeyDecryptor = + JcePBESecretKeyDecryptorBuilder().setProvider(BouncyCastleProvider()).build(passphrase) + return secretKey.extractPrivateKey(decryptor) +} diff --git a/src/test/kotlin/com/liftric/apt/DebianPackageTest.kt b/src/test/kotlin/com/liftric/apt/DebianPackageTest.kt new file mode 100644 index 0000000..47445d9 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/DebianPackageTest.kt @@ -0,0 +1,168 @@ +package com.liftric.apt + +import com.liftric.apt.model.DebianPackage +import com.liftric.apt.model.combineDebianPackages +import com.liftric.apt.model.removeDebianPackage +import com.liftric.apt.model.toFileString +import org.junit.jupiter.api.Test + +class DebianPackageTest { + private val package1 = DebianPackage( + packageName = "foo", + version = "1.0.0", + architecture = "all", + fileName = "foo_1.0.0_all.deb", + size = "123", + maintainer = null, + installedSize = null, + depends = null, + conflicts = null, + replaces = null, + provides = null, + preDepends = null, + recommends = null, + suggests = null, + enhances = null, + builtUsing = null, + section = null, + priority = null, + homepage = null, + description = null, + sha1 = null, + sha256 = null, + md5sum = null + ) + + private val package2 = DebianPackage( + packageName = "foo", + version = "1.0.1", + architecture = "all", + fileName = "foo_1.0.1_all.deb", + size = "123", + maintainer = null, + installedSize = null, + depends = null, + conflicts = null, + replaces = null, + provides = null, + preDepends = null, + recommends = null, + suggests = null, + enhances = null, + builtUsing = null, + section = null, + priority = null, + homepage = null, + description = null, + sha1 = null, + sha256 = null, + md5sum = null + ) + + private val package3 = DebianPackage( + packageName = "foo", + version = "1.0.2", + architecture = "all", + fileName = "foo_1.0.2_all.deb", + size = "234", + maintainer = null, + installedSize = null, + depends = null, + conflicts = null, + replaces = null, + provides = null, + preDepends = null, + recommends = null, + suggests = null, + enhances = null, + builtUsing = null, + section = null, + priority = null, + homepage = null, + description = null, + sha1 = null, + sha256 = null, + md5sum = null + ) + + @Test + fun `test combineDebianPackages with different versions`() { + val packageList = listOf(package1, package2) + val combined = packageList.combineDebianPackages(package3) + + assert(combined.contains(package1)) + assert(combined.contains(package2)) + assert(combined.contains(package3)) + assert(combined.size == 3) + } + + @Test + fun `test combineDebianPackages with duplicate version`() { + val packageList = listOf(package1, package2) + val packageDuplicate = package2.copy(size = "234") + val combined = packageList.combineDebianPackages(packageDuplicate) + + assert(combined.contains(package1)) + assert(!combined.contains(package2)) + assert(combined.contains(packageDuplicate)) + assert(combined.size == 2) + } + + @Test + fun `test removeDebianPackages`() { + val packageList = listOf(package1, package2) + val removePackage = package2.copy() + val combined = packageList.removeDebianPackage(removePackage) + + assert(combined.contains(package1)) + assert(!combined.contains(package2)) + assert(!combined.contains(removePackage)) + assert(combined.size == 1) + } + + @Test + fun `test removeDebianPackages with a Package that is not there`() { + val packageList = listOf(package1, package2) + val combined = packageList.removeDebianPackage(package3) + + assert(combined.contains(package1)) + assert(combined.contains(package2)) + assert(!combined.contains(package3)) + assert(combined.size == 2) + } + + @Test + fun `test toFileString for DebianPackage`() { + val expected = """ + Package: foo + Version: 1.0.0 + Architecture: all + Filename: foo_1.0.0_all.deb + Size: 123 + + """.trimIndent() + "\n" + + assert(expected == package1.toFileString()) + } + + @Test + fun `test toFileString for List of DebianPackage`() { + val packageList = listOf(package1, package2) + val expected = """ + Package: foo + Version: 1.0.0 + Architecture: all + Filename: foo_1.0.0_all.deb + Size: 123 + + Package: foo + Version: 1.0.1 + Architecture: all + Filename: foo_1.0.1_all.deb + Size: 123 + + """.trimIndent() + "\n" + + assert(expected == packageList.toFileString()) + } +} diff --git a/src/test/kotlin/com/liftric/apt/FileHashUtilTest.kt b/src/test/kotlin/com/liftric/apt/FileHashUtilTest.kt new file mode 100644 index 0000000..9092544 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/FileHashUtilTest.kt @@ -0,0 +1,68 @@ +package com.liftric.apt + +import com.liftric.apt.utils.FileHashUtil.compressWithGzip +import com.liftric.apt.utils.FileHashUtil.md5Hash +import com.liftric.apt.utils.FileHashUtil.sha1Hash +import com.liftric.apt.utils.FileHashUtil.sha256Hash +import com.liftric.apt.utils.FileHashUtil.sha512Hash +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.io.File +import java.io.FileInputStream +import java.util.zip.GZIPInputStream + +class FileHashUtilTest { + private val testFile = File("src/test/resources/test_hash.txt") + + @Test + fun `md5 hash test`() { + val expectedMd5Hash = "3858f62230ac3c915f300c664312c63f" + val actualMd5Hash = testFile.md5Hash() + + assertEquals(expectedMd5Hash, actualMd5Hash) + } + + @Test + fun `sha1 hash test`() { + val expectedSha1Hash = "8843d7f92416211de9ebb963ff4ce28125932878" + val actualSha1Hash = testFile.sha1Hash() + + assertEquals(expectedSha1Hash, actualSha1Hash) + } + + @Test + fun `sha256 hash test`() { + val expectedSha256Hash = "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" + val actualSha256Hash = testFile.sha256Hash() + + assertEquals(expectedSha256Hash, actualSha256Hash) + } + + @Test + fun `sha512 hash test`() { + val expectedSha512Hash = + "0a50261ebd1a390fed2bf326f2673c145582a6342d523204973d0219337f81616a8069b012587cf5635f6925f1b56c360230c19b273500ee013e030601bf2425" + val actualSha512Hash = testFile.sha512Hash() + + assertEquals(expectedSha512Hash, actualSha512Hash) + } + + @Test + fun `gzip compression test`() { + val compressedFile = testFile.compressWithGzip() + + assertTrue(compressedFile.exists(), "Compressed file does not exist") + assertTrue(compressedFile.extension == "gz", "Compressed file does not have '.gz' extension") + + val decompressedFileContent = + GZIPInputStream(FileInputStream(compressedFile)).bufferedReader().use { it.readText() } + val originalFileContent = testFile.readText() + + assertEquals( + originalFileContent, + decompressedFileContent, + "Decompressed file content does not match the original file content" + ) + compressedFile.delete() + } +} diff --git a/src/test/kotlin/com/liftric/apt/PackagesFactoryTest.kt b/src/test/kotlin/com/liftric/apt/PackagesFactoryTest.kt new file mode 100644 index 0000000..e699467 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/PackagesFactoryTest.kt @@ -0,0 +1,43 @@ +package com.liftric.apt + +import com.liftric.apt.service.PackagesFactory +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.io.File + +class PackagesFactoryTest { + private val testDebFile = File("src/test/resources/foobar_1.0.0-1_all.deb") + private val archs = setOf("all", "amd64") + + @Test + fun `test parseDebianFile`() { + val list = PackagesFactory.parseDebianFile(testDebFile, archs, "foobar_1.0.0-1_all.deb", null, null) + + assert(list.isNotEmpty()) + list.forEach { + assertEquals("foobar", it.packageName) + assertEquals("1.0.0-1", it.version) + assert(archs.contains(it.architecture)) + } + } + + @Test + fun `test parseDebianFile with custom input`() { + val list = PackagesFactory.parseDebianFile(testDebFile, archs, "foobar_1.0.0-1_all.deb", "foobaz", "1.0.0") + + assert(list.isNotEmpty()) + list.forEach { + assertEquals("foobaz", it.packageName) + assertEquals("1.0.0", it.version) + assert(archs.contains(it.architecture)) + } + } + + @Test + fun `test parsePackagesFile`() { + val testPackagesFile = File("src/integrationMain/resources/removePackage/Packages") + val debianPackages = PackagesFactory.parsePackagesFile(testPackagesFile) + + assert(debianPackages.size == 2) + } +} diff --git a/src/test/kotlin/com/liftric/apt/ParseToMapTest.kt b/src/test/kotlin/com/liftric/apt/ParseToMapTest.kt new file mode 100644 index 0000000..eb2328c --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/ParseToMapTest.kt @@ -0,0 +1,54 @@ +package com.liftric.apt + +import com.liftric.apt.utils.parseToMap +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ParseToMapTest { + @Test + fun `when input is properly formatted, it should return a correct map`() { + val input = """ + key1: value1 + key2: value2 + key3: value3 + """.trimIndent() + val expectedMap = mapOf( + "key1" to "value1", + "key2" to "value2", + "key3" to "value3" + ) + assertEquals(expectedMap, parseToMap(input)) + + } + + @Test + fun `when input has extra spaces, it should still return a correct map`() { + val input = """ + key1 : value1 + key2: value2 + key3 : value3 + """.trimIndent() + val expectedMap = mapOf( + "key1" to "value1", + "key2" to "value2", + "key3" to "value3" + ) + assertEquals(expectedMap, parseToMap(input)) + } + + @Test + fun `when input has multiline values, it should merge them into a single line`() { + val input = """ + key1: value1 + key2: value with + multiple lines + key3: value3 + """.trimIndent() + val expectedMap = mapOf( + "key1" to "value1", + "key2" to "value with multiple lines", + "key3" to "value3" + ) + assertEquals(expectedMap, parseToMap(input)) + } +} diff --git a/src/test/kotlin/com/liftric/apt/ReleaseInfoTest.kt b/src/test/kotlin/com/liftric/apt/ReleaseInfoTest.kt new file mode 100644 index 0000000..39d2428 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/ReleaseInfoTest.kt @@ -0,0 +1,61 @@ +package com.liftric.apt + +import com.liftric.apt.model.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals + +class ReleaseInfoTest { + @Test + fun `test converting ReleaseInfo to file string`() { + val releaseInfo = ReleaseInfo( + origin = "MyOrigin", + label = "MyLabel", + suite = "MySuite", + components = "MyComponent", + codename = "MyCodename", + date = "MyDate", + architectures = "MyArchitectures", + description = "MyDescription", + version = "MyVersion", + validUntil = "MyValidUntil", + notAutomatic = "MyNotAutomatic", + butAutomaticUpgrades = "MyButAutomaticUpgrades", + changelogs = "MyChangelogs", + snapshots = "MySnapshots", + md5Sum = listOf(MD5Sum(md5 = "MyMD5Sum", size = 123, filename = "md5Filename")), + sha1 = listOf(SHA1(sha1 = "MySHA1", size = 234, filename = "sha1Filename")), + sha256 = listOf(SHA256(sha256 = "MySHA256", size = 345, filename = "sha256Filename")), + sha512 = listOf(SHA512(sha512 = "MySHA512", size = 456, filename = "sha512Filename")), + ) + + val fileString = releaseInfo.toFileString().trim() + + val expectedString = """ + Origin: MyOrigin + Label: MyLabel + Suite: MySuite + Components: MyComponent + Codename: MyCodename + Date: MyDate + Architectures: MyArchitectures + Description: MyDescription + Version: MyVersion + ValidUntil: MyValidUntil + NotAutomatic: MyNotAutomatic + ButAutomaticUpgrades: MyButAutomaticUpgrades + Changelogs: MyChangelogs + Snapshots: MySnapshots + MD5Sum: + MyMD5Sum 123 md5Filename + SHA1: + MySHA1 234 sha1Filename + SHA256: + MySHA256 345 sha256Filename + SHA512: + MySHA512 456 sha512Filename + + """.trimIndent().trim() + + assertEquals(expectedString, fileString) + } +} diff --git a/src/test/kotlin/com/liftric/apt/S3AptRepositoryPluginTest.kt b/src/test/kotlin/com/liftric/apt/S3AptRepositoryPluginTest.kt new file mode 100644 index 0000000..1282674 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/S3AptRepositoryPluginTest.kt @@ -0,0 +1,30 @@ +package com.liftric.apt + +import org.junit.jupiter.api.Assertions.assertNotNull +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test + + +class S3AptRepositoryPluginTest { + private val project = ProjectBuilder.builder().build() + + @Test + fun testApply() { + project.pluginManager.apply("com.liftric.s3-apt-repository-plugin") + assertNotNull(project.plugins.getPlugin(S3AptRepositoryPlugin::class.java)) + } + + @Test + fun testExtension() { + project.pluginManager.apply("com.liftric.s3-apt-repository-plugin") + assertNotNull(project.s3AptRepository()) + } + + @Test + fun testTasks() { + project.pluginManager.apply("com.liftric.s3-apt-repository-plugin") + assertNotNull(project.tasks.findByName("uploadPackage")) + assertNotNull(project.tasks.findByName("cleanPackages")) + assertNotNull(project.tasks.findByName("removePackage")) + } +} diff --git a/src/test/kotlin/com/liftric/apt/SignGPGTest.kt b/src/test/kotlin/com/liftric/apt/SignGPGTest.kt new file mode 100644 index 0000000..7bac05f --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/SignGPGTest.kt @@ -0,0 +1,63 @@ +package com.liftric.apt + +import com.liftric.apt.utils.signReleaseFile +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory +import org.bouncycastle.openpgp.jcajce.JcaPGPPublicKeyRingCollection +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.io.File +import java.io.FileInputStream + +class SignGPGTest { + private val privateKeyFile = File("src/test/resources/private.key") + private val publicKeyFile = File("src/test/resources/public.key") + private val fakePublicKeyFile = File("src/test/resources/fake_public.key") + private val releaseFile = File("src/test/resources/Release") + private val passphrase = "abcd1234".toCharArray() + + @Test + fun `release file signing test`() { + val signedFile = signReleaseFile(privateKeyFile, passphrase, releaseFile) + + assertTrue(signedFile.exists(), "Signed file does not exist") + assertTrue(signedFile.length() > 0, "Signed file is empty") + + val isValid = verifySignedFile(signedFile, publicKeyFile, releaseFile) + assertTrue(isValid, "Signature is not valid") + } + + @Test + fun `check wrong public key`() { + val signedFile = signReleaseFile(privateKeyFile, passphrase, releaseFile) + + assertTrue(signedFile.exists(), "Signed file does not exist") + assertTrue(signedFile.length() > 0, "Signed file is empty") + + val isValid = verifySignedFile(signedFile, fakePublicKeyFile, releaseFile) + assertFalse(isValid, "Signature should not be valid") + } + + //Just for Internal Use, to verify the result of the signing process + private fun verifySignedFile(signedFile: File, publicKeyFile: File, releaseFile: File): Boolean { + val pubIn = PGPUtil.getDecoderStream(FileInputStream(publicKeyFile)) + val pgpPubRingCollection = JcaPGPPublicKeyRingCollection(pubIn) + val keyRing = pgpPubRingCollection.keyRings.next() + val publicKey = keyRing.publicKey + + val pgpFact = JcaPGPObjectFactory(PGPUtil.getDecoderStream(FileInputStream(signedFile))) + val signatureList = pgpFact.nextObject() as PGPSignatureList + val signature = signatureList[0] + + signature.init(JcaPGPContentVerifierBuilderProvider().setProvider(BouncyCastleProvider()), publicKey) + + val fileBytes = releaseFile.readBytes() + signature.update(fileBytes) + + return signature.verify() + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/CleanPackagesTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/CleanPackagesTest.kt new file mode 100644 index 0000000..652e2c5 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/CleanPackagesTest.kt @@ -0,0 +1,52 @@ +package com.liftric.apt.aptRepository + +import org.junit.jupiter.api.Test +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.cleanPackages +import io.mockk.* +import org.gradle.api.logging.Logger +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse +import software.amazon.awssdk.services.s3.model.DeletedObject + +class CleanPackagesTest { + private val mockLogger = mockk() + private val mockS3Client = mockk() + private val bucket = "mockBucket" + private val bucketPath = "" + private val component = "mockComponent" + + @Test + fun `test cleanPackages method`() { + val usedPackages = setOf("package1", "package2") + val allFiles = listOf("package1", "package2", "package3") + val filesToRemove = allFiles.filter { !usedPackages.contains(it) } + + every { mockS3Client.listAllObjects(bucket, "pool/$component/") } returns allFiles + + val mockedResponse = mockk() + every { mockedResponse.deleted() } returns filesToRemove.map { mockk() } + every { mockS3Client.deleteObjects(bucket, filesToRemove) } returns mockedResponse + every { mockLogger.info(any()) } just runs + + cleanPackages(mockLogger, mockS3Client, bucket, bucketPath, component, usedPackages) + + verify { mockS3Client.listAllObjects(bucket, "pool/$component/") } + verify { mockS3Client.deleteObjects(bucket, filesToRemove) } + verify { mockLogger.info("Deleted ${filesToRemove.size} objects") } + } + + @Test + fun `test cleanPackages method with no files to remove`() { + val usedPackages = setOf("package1", "package2", "package3") + val allFiles = listOf("package1", "package2", "package3") + + every { mockS3Client.listAllObjects(bucket, "pool/$component/") } returns allFiles + every { mockLogger.info(any()) } just runs + + cleanPackages(mockLogger, mockS3Client, bucket, bucketPath, component, usedPackages) + + verify { mockS3Client.listAllObjects(bucket, "pool/$component/") } + verify(exactly = 0) { mockS3Client.deleteObjects(any(), any()) } + verify { mockLogger.info("No files to remove") } + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/GetCleanPackagesFilesTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/GetCleanPackagesFilesTest.kt new file mode 100644 index 0000000..ad3f99b --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/GetCleanPackagesFilesTest.kt @@ -0,0 +1,79 @@ +package com.liftric.apt.aptRepository + +import com.liftric.apt.aptRepository.util.FileCompressor +import org.junit.jupiter.api.Test +import com.liftric.apt.utils.FileHashUtil.compressWithGzip +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.PackagesFactory +import com.liftric.apt.service.getCleanedPackagesFiles +import io.mockk.* +import org.gradle.api.logging.Logger +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.assertThrows +import java.io.File + +class GetCleanPackagesFilesTest { + private val logger = mockk() + private val s3Client = mockk() + private val fileCompressor = mockk() + + private val testDebFile = File("src/test/resources/foobar_1.0.0-1_all.deb") + private val suite = "mockSuite" + private val component = "mockComponent" + private val bucket = "mockBucket" + private val bucketPath = "" + + @Test + fun `test getCleanedPackagesFiles method`() { + val archs = setOf("all", "amd64") + val debianPackages = PackagesFactory.parseDebianFile(testDebFile, archs, "foobar_1.0.0-1_all.deb", null, null) + + val mockFile = File.createTempFile("Packages", null).apply { + writeText("Package: foobar\nVersion: 1.0.0-1\nArchitecture: all\nSize: 123\nFilename: pool/main/f/foobar/foobar_1.0.0-1_all.deb\n") + deleteOnExit() + } + val mockCompressedFile = mockFile.compressWithGzip() + + every { logger.info(any()) } just runs + every { s3Client.getObject(any(), any()) } returns mockFile + every { fileCompressor.compressWithGzip(mockFile) } returns mockCompressedFile + + val out = getCleanedPackagesFiles(logger, suite, component, s3Client, bucket, bucketPath, debianPackages) + verify(exactly = archs.size) { s3Client.getObject(any(), any()) } + + for (arch in archs) { + assert(out.keys.contains("dists/$suite/$component/binary-$arch/Packages")) + assert(out.keys.contains("dists/$suite/$component/binary-$arch/Packages.gz")) + } + + for (file in out.values) { + assert(file.isFile) + if (!file.name.endsWith(".gz")) { + assert(!file.readText().contains("foobar")) + } + } + + for (arch in archs) { + verify { logger.info("Parsing Packages file: s3://$bucket/dists/$suite/$component/binary-$arch/Packages") } + } + } + + @Test + fun `test getCleanedPackagesFiles method with Exception`() { + val archs = setOf("all") + val debianPackages = PackagesFactory.parseDebianFile(testDebFile, archs, "foobar_1.0.0-1_all.deb", null, null) + val exceptionMsg = "No such key" + + every { logger.error(any()) } just runs + every { s3Client.getObject(any(), any()) } throws RuntimeException(exceptionMsg) + + val exception = assertThrows { + getCleanedPackagesFiles(logger, suite, component, s3Client, bucket, bucketPath, debianPackages) + } + assertEquals(exceptionMsg, exception.message) + + for (debianPackage in debianPackages) { + verify { logger.error("Packages file for Architecture '${debianPackage.architecture}' not found! Can't remove '${debianPackage.packageName}'") } + } + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/GetPoolBucketKeyTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/GetPoolBucketKeyTest.kt new file mode 100644 index 0000000..c13dc18 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/GetPoolBucketKeyTest.kt @@ -0,0 +1,17 @@ +package com.liftric.apt.aptRepository + +import com.liftric.apt.service.getPoolBucketKey +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class GetPoolBucketKeyTest { + @Test + fun `test getPoolBucketKey method`() { + val fileName = "foobar_1.0.0-1_all.deb" + val component = "main" + val expectedKey = "pool/main/f/foobar/foobar_1.0.0-1_all.deb" + + val actualKey = getPoolBucketKey(fileName, component) + assertEquals(expectedKey, actualKey) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/GetUsedPackagesPoolKeysTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/GetUsedPackagesPoolKeysTest.kt new file mode 100644 index 0000000..2a1c6a6 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/GetUsedPackagesPoolKeysTest.kt @@ -0,0 +1,39 @@ +package com.liftric.apt.aptRepository + +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.getUsedPackagesPoolKeys +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.io.File + +class GetUsedPackagesPoolKeysTest { + private val mockS3Client = mockk() + private val bucket = "mockBucket" + private val bucketPath = "" + private val suite = "stable" + private val component = "main" + + @Test + fun `test getUsedPackagesPoolKeys method`() { + val mockFilePaths = + listOf("dists/$suite/$component/binary-all/Packages.gz", "dists/$suite/$component/binary-all/Packages") + + every { mockS3Client.listAllObjects(any(), any()) } returns mockFilePaths + + val filteredFilePaths = mockFilePaths.filter { it.endsWith("Packages") } + val tempFile = File.createTempFile("Packages", null).apply { + writeText("Package: foobar\nVersion: 1.0.0-1\nArchitecture: all\nSize: 123\nFilename: pool/main/f/foobar/foobar_1.0.0-1_all.deb\n") + deleteOnExit() + } + + every { mockS3Client.getObject(any(), any()) } returns tempFile + + val usedPackages = getUsedPackagesPoolKeys(mockS3Client, bucket, bucketPath, suite, component) + assertEquals(setOf("pool/main/f/foobar/foobar_1.0.0-1_all.deb"), usedPackages) + + verify { mockS3Client.getObject(bucket, filteredFilePaths[0]) } + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/UpdatePackagesFilesTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/UpdatePackagesFilesTest.kt new file mode 100644 index 0000000..f3977d6 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/UpdatePackagesFilesTest.kt @@ -0,0 +1,82 @@ +package com.liftric.apt.aptRepository + +import com.liftric.apt.aptRepository.util.FileCompressor +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.PackagesFactory +import com.liftric.apt.service.getUpdatedPackagesFiles +import com.liftric.apt.utils.FileHashUtil.compressWithGzip +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import org.gradle.api.logging.Logger +import org.junit.jupiter.api.Test +import java.io.File + +class UpdatePackagesFilesTest { + private val logger = mockk() + private val s3Client = mockk() + private val fileCompressor = mockk() + private val testDebFile = File("src/test/resources/foobar_1.0.0-1_all.deb") + private val archs = setOf("all", "amd64") + private val suite = "mockSuite" + private val component = "mockComponent" + private val bucket = "mockBucket" + private val bucketPath = "" + private val debianPackages = + PackagesFactory.parseDebianFile(testDebFile, archs, "foobar_1.0.0-1_all.deb", null, null) + private val mockFile = File.createTempFile("Packages", null).apply { + writeText("Package: foobar\nVersion: 1.0.0-1\nFilename: pool/main/f/foobar/foobar_1.0.0-1_all.deb\n") + deleteOnExit() + } + + @Test + fun `test getUpdatedPackagesFiles method`() { + val mockCompressedFile = mockFile.compressWithGzip() + + every { s3Client.getObject(any(), any()) } returns mockFile + every { fileCompressor.compressWithGzip(mockFile) } returns mockCompressedFile + every { logger.info(any()) } just runs + + val result = getUpdatedPackagesFiles(logger, suite, component, s3Client, bucket, bucketPath, debianPackages) + + verify(exactly = archs.size) { s3Client.getObject(any(), any()) } + + for (arch in archs) { + assert(result.keys.contains("dists/$suite/$component/binary-$arch/Packages")) + assert(result.keys.contains("dists/$suite/$component/binary-$arch/Packages.gz")) + } + + for (file in result.values) { + assert(file.isFile) + } + + for (arch in archs) { + verify { logger.info("Parsing Packages file: s3://$bucket/dists/$suite/$component/binary-$arch/Packages") } + } + } + + @Test + fun `test getUpdatedPackagesFiles method with clean s3 bucket`() { + every { s3Client.getObject(any(), any()) } throws RuntimeException("No such key") + every { fileCompressor.compressWithGzip(mockFile) } returns mockFile + every { logger.info(any()) } just runs + + val result = getUpdatedPackagesFiles(logger, suite, component, s3Client, bucket, bucketPath, debianPackages) + verify(exactly = archs.size) { s3Client.getObject(any(), any()) } + + for (arch in archs) { + assert(result.keys.contains("dists/$suite/$component/binary-$arch/Packages")) + assert(result.keys.contains("dists/$suite/$component/binary-$arch/Packages.gz")) + } + + for (file in result.values) { + assert(file.isFile) + } + + for (arch in archs) { + verify { logger.info("Packages file for '$arch' not found, creating new Packages file") } + } + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/UpdateReleaseFilesTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/UpdateReleaseFilesTest.kt new file mode 100644 index 0000000..7565bf9 --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/UpdateReleaseFilesTest.kt @@ -0,0 +1,81 @@ +package com.liftric.apt.aptRepository + +import com.liftric.apt.model.* +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.updateReleaseFile +import io.mockk.* +import org.gradle.api.logging.Logger +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import software.amazon.awssdk.services.s3.model.PutObjectResponse +import java.io.File + +class UpdateReleaseFilesTest { + private val mockLogger = mockk() + private val mockS3Client = mockk() + private val bucket = "mockBucket" + private val bucketPath = "" + private val releaseInfo = ReleaseInfo( + origin = "mockOrigin", + label = "mockLabel", + suite = "mockSuite", + components = "main", + architectures = "all amd64", + codename = "mockCodename", + date = null, + description = "mockDescription", + version = "mockVersion", + validUntil = null, + notAutomatic = null, + butAutomaticUpgrades = null, + changelogs = null, + snapshots = null, + md5Sum = listOf(), + sha1 = listOf(), + sha256 = listOf(), + sha512 = listOf() + ) + + private val mockFile = File.createTempFile("mockFile", null).apply { + deleteOnExit() + writeText( + """ + Package: foobar + Version: 1.0.0-1 + Architecture: all + Maintainer: nvima + Installed-Size: 0 + Section: java + Priority: optional + Description: foobar + Filename: pool/main/f/foobar/foobar_1.0.0-1_all.deb + Size: 1018 + SHA1: abd00a88a4ff3eb30dfe4412779ca97c5aabf529 + SHA256: b9381b36cf73b10ba01a6cdfa50be13e807687cdf434925e18278d109920b1b2 + MD5sum: 53829e7ab536c57c2fddc96d0e2690b8 + """.trimIndent().trim() + ) + } + + @Test + fun `test updateReleaseFiles method`() { + every { mockS3Client.getObject(any(), any()) } returns mockFile + every { mockS3Client.listAllObjects(any(), any()) } returns listOf("filePath") + every { mockS3Client.uploadObject(bucket, any(), any()) } returns mockk() + every { mockLogger.info(any()) } just Runs + + assertDoesNotThrow { + updateReleaseFile( + mockLogger, + mockS3Client, + bucket, + bucketPath, + releaseInfo, + null, + null + ) + } + + verify { mockLogger.info("Release file uploaded to s3://$bucket/${bucketPath}dists/${releaseInfo.suite}/Release") } + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/UploadDebianFileTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/UploadDebianFileTest.kt new file mode 100644 index 0000000..542ec4d --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/UploadDebianFileTest.kt @@ -0,0 +1,65 @@ +package com.liftric.apt.aptRepository + +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.getFullBucketKey +import com.liftric.apt.service.uploadDebianFile +import io.mockk.* +import org.gradle.api.logging.Logger +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import software.amazon.awssdk.services.s3.model.PutObjectResponse +import java.io.File + +class UploadDebianFileTest { + private val mockLogger = mockk() + private val mockS3Client = mockk() + private val mockFile = mockk() + private val bucket = "mockBucket" + private val bucketPath = "" + private val bucketKey = "mockBucketKey" + private val fullBucketKey = getFullBucketKey(bucketPath, bucketKey) + + @Test + fun `test uploadDebianFile method with override false`() { + val override = false + + every { mockS3Client.doesObjectExist(bucket, bucketKey) } returns true + + val exception = assertThrows { + uploadDebianFile(mockLogger, mockS3Client, bucket, bucketPath, bucketKey, mockFile, override) + } + assertEquals("Version already exist in Repo", exception.message) + } + + @Test + fun `test uploadDebianFile method with override true`() { + val override = true + + every { mockLogger.info(any()) } just Runs + every { mockS3Client.doesObjectExist(bucket, bucketKey) } returns true + every { mockS3Client.uploadObject(bucket, fullBucketKey, mockFile) } returns mockk() + + assertDoesNotThrow { + uploadDebianFile(mockLogger, mockS3Client, bucket, bucketPath, bucketKey, mockFile, override) + } + + verify { mockLogger.info("File uploaded to s3://$bucket/${fullBucketKey}") } + } + + + @Test + fun `test uploadDebianFile method with override false and no version of package found`() { + val override = false + + every { mockLogger.info(any()) } just Runs + every { mockS3Client.doesObjectExist(bucket, bucketKey) } returns false + every { mockS3Client.uploadObject(bucket, fullBucketKey, mockFile) } returns mockk() + + assertDoesNotThrow { + uploadDebianFile(mockLogger, mockS3Client, bucket, bucketPath, bucketKey, mockFile, override) + } + + verify { mockLogger.info("File uploaded to s3://$bucket/${fullBucketKey}") } + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/UploadPackagesFilesTest.kt b/src/test/kotlin/com/liftric/apt/aptRepository/UploadPackagesFilesTest.kt new file mode 100644 index 0000000..4b5ca0a --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/UploadPackagesFilesTest.kt @@ -0,0 +1,39 @@ +package com.liftric.apt.aptRepository + +import com.liftric.apt.service.AwsS3Client +import com.liftric.apt.service.uploadPackagesFiles +import io.mockk.* +import org.gradle.api.logging.Logger +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import software.amazon.awssdk.services.s3.model.PutObjectResponse +import java.io.File + +class UploadPackagesFilesTest { + @Test + fun `test uploadPackagesFiles method`() { + val mockLogger = mockk() + val mockS3Client = mockk() + val bucket = "mockBucket" + + val mockFile1 = mockk() + val mockFile2 = mockk() + + val packageFiles = mapOf( + "file1" to mockFile1, + "file2" to mockFile2 + ) + + every { mockLogger.info(any()) } just Runs + every { mockS3Client.uploadObject(bucket, any(), any()) } returns mockk() + + assertDoesNotThrow { + uploadPackagesFiles(mockLogger, packageFiles, mockS3Client, bucket) + } + + packageFiles.forEach { (key, file) -> + verify { mockLogger.info("Uploading $key") } + verify { mockS3Client.uploadObject(bucket, key, file) } + } + } +} diff --git a/src/test/kotlin/com/liftric/apt/aptRepository/util/FileCompressor.kt b/src/test/kotlin/com/liftric/apt/aptRepository/util/FileCompressor.kt new file mode 100644 index 0000000..e49bfcd --- /dev/null +++ b/src/test/kotlin/com/liftric/apt/aptRepository/util/FileCompressor.kt @@ -0,0 +1,11 @@ +package com.liftric.apt.aptRepository.util + +import com.liftric.apt.utils.FileHashUtil.compressWithGzip +import java.io.File + +abstract class FileCompressor { + open fun compressWithGzip(file: File): File { + return file.compressWithGzip() + } +} + diff --git a/src/test/resources/Release b/src/test/resources/Release new file mode 100644 index 0000000..5145325 --- /dev/null +++ b/src/test/resources/Release @@ -0,0 +1,18 @@ +Origin: Liftric +Label: Liftric +Suite: stable +Components: main +Date: Fri, 09 Jun 2023 09:34:26 UTC +Architectures: all +MD5Sum: + 3a422598c40f3e4fa01260bddb315d94 388 main/binary-all/Packages + 4883de5b75465c6b9748ab23708a5797 303 main/binary-all/Packages.gz +SHA1: + c57e7599a71c534a379aa9a078de33814496f002 388 main/binary-all/Packages + ab03a75c6473e744331e4b74853518c2a662bb71 303 main/binary-all/Packages.gz +SHA256: + f8bdb77df801b557403a71f9ee2cb732521a8ceeea420e28eb3d04b781576c5d 388 main/binary-all/Packages + 207c8c3f8cb79d6a6e0e015533474c8cad782ecd9587a54350282f09cf6955a1 303 main/binary-all/Packages.gz +SHA512: + c5fbe3fedd1775c6e5580b6e0c92d308c310c8b953c1bfcdde8b41d9e5d0849c15dac5ff3a8c28406b6be48a5ebbf9e517b051c912e7b3aa6189298dcdce3811 388 main/binary-all/Packages + ef9f90a0edf4f5e576734896f09970e3e6eb4cc0f645955b461228908abf46de97cdec7f6db714d251f4bbbef5d305da9098d7e27301211bb469ec80b24de433 303 main/binary-all/Packages.gz diff --git a/src/test/resources/Release.gpg b/src/test/resources/Release.gpg new file mode 100644 index 0000000..a1b01aa --- /dev/null +++ b/src/test/resources/Release.gpg @@ -0,0 +1,31 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEEAUbcbUoLKRS97TTbZIrP1iLz0TgFAmSEObIACgkQZIrP1iLz +0ThHHBAAo/81Q+rZ5WaLgxJ4jb06IZumO+hSzUrlYcQMEGbKnIeQSI9lp64RP9ZH +DWcpfx7s6LtAFdmK+LlBLaa+SdRPfmq9tvuqQ6WwCzSl0EokRmrUvJmnQkFCGF/k +UlcU1HotxAjeoeQgm2sudhbOlXGa7cmT6DnAFxK2htKVUIaY7YSSFtHzqeTQydN7 +AAalYEzkH4+whYS5njJssgm2SQ8uciGwIH1mLhB6jsNTQOH8W7iRl69xOR6pTAfJ +70FQmz8i2TJhOljaT+wPIJLl4ojbdUlb+uoPUWZ41gXwL9e5hGflYXlew/p/ojE8 +0+kLDWyYAVxDkUEnK7WHVCP3E7ewjRx9NZxJU2ox2ptJZEiHpRF8nvbiPgYaMZQy +a2anH0UCX1/xX06yFEakfZVlBDNV1kHvY0d9hnIe4tI3VQF6jcQqpSFj/LaojuHO +IFZDdUWR6XPx6UFuF+8LotrEL6DYl+9Fp3EBdok4QZ3DIxROMvvMPS98QDZ+M5cM +7Y99tJIC78Y3Rb1Hj6DSQvBtLZMprHuScUODQ84HADnpuXfgZmsMg0C6L5+Tk0mX +3dX7VMuORu+N1D4upWwC8gUThhNxQUqHxI6HyK9p+NZUPVWrpYW9GFOopzkRbMB6 +Da6LxcGchJL7JoNm1tofOZOeOXxP9tm2NJI+Cv1VFdRa1aMKNz2JAjMEAAEIAB0W +IQSnI2iG88zKrRSKJ/gOmEBNOG+h2QUCZIQ5sgAKCRAOmEBNOG+h2eE9D/9HAWJb +0SM7K5nHcoetdWDIqae8b/YznShoI3aMCW26xjiJ+Irau/WYVTj4Tnh2ddsv8Oig +2PTBjkRyAEroCySor1rKsOnc9jrQKjrmRqywEUe8xxmxzcjFP7hHP+pZD1L6suwS +Aip+bh8ZTNuzVRdRcPyfKd5kM+0ds/ZtMieIf7a5+gKbTRaWyMnrXBzFt3IyWaDt +APgPUJq84HnSTdDrU0Ej5uzO0OxPXvL5G34VA90Hd0YiTeW8k/8cf0Aj9qzedGvs ++ZNG6emENojyfSX7hNMwYC7u8Drj51bmVFK6YC48hz4WRtPGMraHHVg8vsQWvKC0 +uyGDjfq12mzvT5DI9WqtwbF8qMsV8BQ2hVuUTYNHVp0UeA6jLjB+DOJ2F9+1sQlg +wBgICPyup69Oy+LP+w2sSpkWaVQxT0K6+o/HBKVW39SY4jhnECaLf9mag+x2xjxF +AuFvO4Scg725kYwZtjUX8md4sP/nOPy2uOdPByAjmdz8RkAIcCKp4S0tvXdEv+JJ +yXkv5g8vxaJOBRqK2ySb+/PwOpv5yJ3S5qy+WVLNr7G3+FR7S2adSboPgdWSmmJA +sYS5gGNAVepzXSAM626f84x4ffpONMBRxEYJ5X2VjkFqGztZJ0kZhN9YOAwsaYDJ +uZkL2ztB2wHEakl0+ifP0ZJ35AZrEJURbz0kf4iWBAAWCAA+FiEETWT+wRnCApBn +1ueR+NJYW4eD1IEFAmSEQ2sgHGRlYmlhbi1yZWxlYXNlQGxpc3RzLmRlYmlhbi5v +cmcACgkQ+NJYW4eD1IFz7AEA94m5b409o+qUowrO5uNa1fup6So1S/NuAcEAzVHe +t5cA/0pvWaMVuxxts+eExJE+56v/6yNBYiZN9/1mkTLgz6kH +=cZgo +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/src/test/resources/curl_7.64.0-4+deb10u2_amd64.deb b/src/test/resources/curl_7.64.0-4+deb10u2_amd64.deb new file mode 100644 index 0000000..327bdd2 Binary files /dev/null and b/src/test/resources/curl_7.64.0-4+deb10u2_amd64.deb differ diff --git a/src/test/resources/fake_public.key b/src/test/resources/fake_public.key new file mode 100644 index 0000000..3d4c2a0 --- /dev/null +++ b/src/test/resources/fake_public.key @@ -0,0 +1,28 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v2.0.22 (GNU/Linux) + +mQENBE5OMmIBCAD+FPYKGriGGf7NqwKfWC83cBV01gabgVWQmZbMcFzeW+hMsgxH +W6iimD0RsfZ9oEbfJCPG0CRSZ7ppq5pKamYs2+EJ8Q2ysOFHHwpGrA2C8zyNAs4I +QxnZZIbETgcSwFtDun0XiqPwPZgyuXVm9PAbLZRbfBzm8wR/3SWygqZBBLdQk5TE +fDR+Eny/M1RVR4xClECONF9UBB2ejFdI1LD45APbP2hsN/piFByU1t7yK2gpFyRt +97WzGHn9MV5/TL7AmRPM4pcr3JacmtCnxXeCZ8nLqedoSuHFuhwyDnlAbu8I16O5 +XRrfzhrHRJFM1JnIiGmzZi6zBvH0ItfyX6ttABEBAAG0KW5naW54IHNpZ25pbmcg +a2V5IDxzaWduaW5nLWtleUBuZ2lueC5jb20+iQE+BBMBAgAoAhsDBgsJCAcDAgYV +CAIJCgsEFgIDAQIeAQIXgAUCV2K1+AUJGB4fQQAKCRCr9b2Ce9m/YloaB/9XGrol +kocm7l/tsVjaBQCteXKuwsm4XhCuAQ6YAwA1L1UheGOG/aa2xJvrXE8X32tgcTjr +KoYoXWcdxaFjlXGTt6jV85qRguUzvMOxxSEM2Dn115etN9piPl0Zz+4rkx8+2vJG +F+eMlruPXg/zd88NvyLq5gGHEsFRBMVufYmHtNfcp4okC1klWiRIRSdp4QY1wdrN +1O+/oCTl8Bzy6hcHjLIq3aoumcLxMjtBoclc/5OTioLDwSDfVx7rWyfRhcBzVbwD +oe/PD08AoAA6fxXvWjSxy+dGhEaXoTHjkCbz/l6NxrK3JFyauDgU4K4MytsZ1HDi +MgMW8hZXxszoICTTiQEcBBABAgAGBQJOTkelAAoJEKZP1bF62zmo79oH/1XDb29S +YtWp+MTJTPFEwlWRiyRuDXy3wBd/BpwBRIWfWzMs1gnCjNjk0EVBVGa2grvy9Jtx +JKMd6l/PWXVucSt+U/+GO8rBkw14SdhqxaS2l14v6gyMeUrSbY3XfToGfwHC4sa/ +Thn8X4jFaQ2XN5dAIzJGU1s5JA0tjEzUwCnmrKmyMlXZaoQVrmORGjCuH0I0aAFk +RS0UtnB9HPpxhGVbs24xXZQnZDNbUQeulFxS4uP3OLDBAeCHl+v4t/uotIad8v6J +SO93vc1evIje6lguE81HHmJn9noxPItvOvSMb2yPsE8mH4cJHRTFNSEhPW6ghmlf +Wa9ZwiVX5igxcvaIRgQQEQIABgUCTk5b0gAKCRDs8OkLLBcgg1G+AKCnacLb/+W6 +cflirUIExgZdUJqoogCeNPVwXiHEIVqithAM1pdY/gcaQZmIRgQQEQIABgUCTk5f +YQAKCRCpN2E5pSTFPnNWAJ9gUozyiS+9jf2rJvqmJSeWuCgVRwCcCUFhXRCpQO2Y +Va3l3WuB+rgKjsQ= +=EWWI +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/src/test/resources/foobar_1.0.0-1_all.deb b/src/test/resources/foobar_1.0.0-1_all.deb new file mode 100644 index 0000000..03111e5 Binary files /dev/null and b/src/test/resources/foobar_1.0.0-1_all.deb differ diff --git a/src/test/resources/private.key b/src/test/resources/private.key new file mode 100644 index 0000000..ca09c3f --- /dev/null +++ b/src/test/resources/private.key @@ -0,0 +1,57 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: Keybase OpenPGP v1.0.0 +Comment: https://keybase.io/crypto + +xcFGBGR/HxcBBADC/hnMViMTgdcFIlfbkDIPPEPCLxi1cD4N/99D9HpdF3R426JY +Bqu7cl5YANM6e3zIK5HCp89hEaM3PQCuQcMahnxW3dKn6I1ru63cZpi3y+1ySiY/ +PvO28BKFX5wKq4mx0UZpv0SBKSSimrV+VSE/pRwxHdgiGN5fD43tVUDphwARAQAB +/gkDCBTdyeo2bWOiYOHekbZ43MJCvdj3EkULRWp/xu0csFPOlBGXeVKsUqZBY/1+ +2s/YVeag/W9jg3tnp8O73LCOXr3eCKWtBV/j0hp29uPSSvjLfT5YnzwaKZpIB+2c +4KquMTcPLfixMsulOVsrlvWsTf5R33tMMmp+VyG2jt+krwTAwSvLbKngJigVeYA5 +zfkOqwTw2DvwxlTlgvCp791JfPqqebDI6iYSQNKT0KmrNsNTfUod80GydzF0FIkW +bmTzoLlRSHiyefVmiyKaWHrB5IrsFLUiVqK28Tzdm9pdIuAOQFcNWqWKbm9zOW/W +9vgfKjYC7YfCV4+MZxTDndQdQHwCJn92vYYSrPS7OKCodgg0MGhNS5IpazTxdrWz +trPv7SsPQGwDkk9X3HSYBJh9fPugjpSCKxfuIz+A+tBiBr4H9ecahOpczDu0NLVD +JtpO34fYLlgs/SX0GQiBnO+CtgtHEg5IerNQHMEK0jfEuhchncPzXUfNJ0ludGVn +cmF0aW9uIFRlc3QgPGludGVncmF0aW9uQHRlc3QuZGV2PsKtBBMBCgAXBQJkfx8X +AhsvAwsJBwMVCggCHgECF4AACgkQMYYDNcqrWgXVsQP/e8wIHwSdfWVvsiOfIE6S +VZJc6iTQPfJ/VGkTm8yQZv3viXSFjMT5pDXeokLr6LEwTYP1x67EVMP+csekg3kN +S+WfhB2e1bC4pMaxM7wwWxPJMg4bywx8QNXCkPT9pZGFgTaYi1HwT+QsKsPxTdm6 +1QU0fbQLtaP7aPvLCRqyRafHwUYEZH8fFwEEAOOmDe0UDHy++SLYPduSX/PcSci5 +gS++nuxFE23nfeA92OvghP+QwzU1t5TtWebpnsReG1zywHw55ZYDhB9e8mRyOm1o +Y1Lu8PxSV3oMqj4O3i0jhZGkxHHANa6Nt8nBVzrtGuRNIOQ+x0793S+0Xi0adIcE +Z13KXYKcJ27+bI0TABEBAAH+CQMIZv2QK/eSWQhghSOjdoOtm7YeVJgLEwHKJ24P +eiWysetbctk1SrlvIEYK/uQJEyt5b1Q/Prseglwf/uYhie2v1xZiXNmo3TiEqfex +osAueaQpxFavPFhadAUQr1HPnHl0aV5OnsUinMv7TVDPYJLRsUs87dln0+rs00Vv +q/bJ6WaYmNhk6WyqQxkXqi1oWlwIY7AhI1mXLFS3KlsMSZ9odyY4f1KLHt1aP1nN +syZfPeYwbN/ygW7hnapqTXiFyupqZ0v9rmcM6hA/PxpqJPunf8jNi6F95j364oKs +7k5lW/21gyVNw/fPlyZIIL5QG5yubMonM5ML0lraUuc067Ou6JUVvKxrgB6d23lH +xf2/HMhh5m0LR3/xNbZ/ignnF2nXLhhIxvFkOOVAYBqeZtWr0i0gSBQW57gS0bbc +Xv3e+F/mHzotaztxujSNqmjc3h0tN6VDYfBuYvl1Wv7CfB9tYZ1zKg5t34k+mWJs +n7QkC4pQGEuELcLAgwQYAQoADwUCZH8fFwUJDwmcAAIbLgCoCRAxhgM1yqtaBZ0g +BBkBCgAGBQJkfx8XAAoJEAfczH4FuR6QHS0EAIFggMmPD3ChEB+J3aDbBeVy4fl5 +zTSHmjD3fqurI8kEafNJBV+lH/5iuBLwQrc0jwWcdziAwiF/KE9C8MJrSPwJlVEO +aoGV6D+8nBK1pr8y227xL76EyoWybNBNkp+d5ej8p/QGLIwC3ztcNgarIws4WjWc +Pc3yDD1weuvE7qZCzHQD/2klFuLra4uceFbjWp5fhO0Zo8q/W/s7gtIFpf8GXLuf +HqDVw7Z4ue7SzLSTTj0liInkf2lFF1QOkdSMFiW/l6/6bHacwavxgnG8k9JGcamh +mxap3PagMjX/Ij2NJVeHcYmCo4G2UjI2BWj5j2umxMnbMp6pPN1H5LJi5eZ0/8vh +x8FGBGR/HxcBBADrsY3VGdJrl1MtOws1SK5ejsAd5mnWZY3BZmEX+BqkJmYOo9D0 +v1ykJNKPiPlS+NWGVkI2rvOPDGTJBE36r5qqorX/N4M8Vbmtd+8+CY3VpGc5GyGO +VDpLesj3jdO/tUJ9e4r1enGnEdUKnyha9iiZXlNpUCkW3pwKAsezR/chBwARAQAB +/gkDCEDy6mvmqC3jYHEfdUVfXzS2Ql2jZ0CNr+gYL44nmpGol2ItrojbgTdYuhdD +mw3UXttLN/Ty0SOP4tbHNm6wajrX9nZU6Tgo4yV8NIWwq0cM42aL/a4JVWoqgYbi +EdeQmKnZBWQOezOTi2X67EO6jloGdCIm1k3HsM+9mgMR6vN9/pzW3ZFEF6wzilzM +G07d4xyJig4nfH6V1LLXTzuy0IXkRyZ48RZ1/QbL+N/urLFoRCWuQaD0ST9Opk5O +yN18h8Sp9/i3+Xsop2v9UG3pvYN0Ox0FfeRj4fDGHiQKIsEzwSlsD7kRmGytzp8i +6MxujEvWKjiArj6N+Ae01/V9xBKUxQVPVfIIkulGmMjf6lKRwupGjCrTNDunpSR2 +M70vzewud6GjWq92LmoyFkV+d05gKoY05rXk5JDIxKoGki0PpmAbWPKK6WORqC4Y +9oA+ApRne39StmuA8SO5Ig5k4XgGartmaXKKif2B2J5ZoCUYz23ezU7CwIMEGAEK +AA8FAmR/HxcFCQ8JnAACGy4AqAkQMYYDNcqrWgWdIAQZAQoABgUCZH8fFwAKCRBT +xi/Onv7Zv1zIBACaoNnjerhWsaBXwtIh4OBBcHxAgp56GBpjcH9iAZUndivTrQpu +pkT4ltgf4E6NcQXYqCG3s7a0hNBxPscQPs/DrzLAjPXTA3JKeRjeMKWWsDhdZ65m +v+82vUc0fVOC9i5Pv3gUW3uX+bM1uzXrVaf2fgtirkdJ37lMV1/JTmcSccK4A/9f +k/hUwSos36R7muW4VgAzBkjIZ4tOXd+Bm+hLV5fnNgqa9CG3qS/Jo1N/5SyLdP46 +uUtNk00qn5J6leLp5ZLVhi5CFyN3Ss09yBgT32t37qIAH/uaywKP1v5Lv9R9gAXA +XZ/dY9NKoFks9WPQEs48cJviVIKVe52jdLJyOC9N1Q== +=92IK +-----END PGP PRIVATE KEY BLOCK----- diff --git a/src/test/resources/public.key b/src/test/resources/public.key new file mode 100644 index 0000000..d2aa353 --- /dev/null +++ b/src/test/resources/public.key @@ -0,0 +1,34 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: Keybase OpenPGP v1.0.0 +Comment: https://keybase.io/crypto + +xo0EZH8fFwEEAML+GcxWIxOB1wUiV9uQMg88Q8IvGLVwPg3/30P0el0XdHjbolgG +q7tyXlgA0zp7fMgrkcKnz2ERozc9AK5BwxqGfFbd0qfojWu7rdxmmLfL7XJKJj8+ +87bwEoVfnAqribHRRmm/RIEpJKKatX5VIT+lHDEd2CIY3l8Pje1VQOmHABEBAAHN +J0ludGVncmF0aW9uIFRlc3QgPGludGVncmF0aW9uQHRlc3QuZGV2PsKtBBMBCgAX +BQJkfx8XAhsvAwsJBwMVCggCHgECF4AACgkQMYYDNcqrWgXVsQP/e8wIHwSdfWVv +siOfIE6SVZJc6iTQPfJ/VGkTm8yQZv3viXSFjMT5pDXeokLr6LEwTYP1x67EVMP+ +csekg3kNS+WfhB2e1bC4pMaxM7wwWxPJMg4bywx8QNXCkPT9pZGFgTaYi1HwT+Qs +KsPxTdm61QU0fbQLtaP7aPvLCRqyRafOjQRkfx8XAQQA46YN7RQMfL75Itg925Jf +89xJyLmBL76e7EUTbed94D3Y6+CE/5DDNTW3lO1Z5umexF4bXPLAfDnllgOEH17y +ZHI6bWhjUu7w/FJXegyqPg7eLSOFkaTEccA1ro23ycFXOu0a5E0g5D7HTv3dL7Re +LRp0hwRnXcpdgpwnbv5sjRMAEQEAAcLAgwQYAQoADwUCZH8fFwUJDwmcAAIbLgCo +CRAxhgM1yqtaBZ0gBBkBCgAGBQJkfx8XAAoJEAfczH4FuR6QHS0EAIFggMmPD3Ch +EB+J3aDbBeVy4fl5zTSHmjD3fqurI8kEafNJBV+lH/5iuBLwQrc0jwWcdziAwiF/ +KE9C8MJrSPwJlVEOaoGV6D+8nBK1pr8y227xL76EyoWybNBNkp+d5ej8p/QGLIwC +3ztcNgarIws4WjWcPc3yDD1weuvE7qZCzHQD/2klFuLra4uceFbjWp5fhO0Zo8q/ +W/s7gtIFpf8GXLufHqDVw7Z4ue7SzLSTTj0liInkf2lFF1QOkdSMFiW/l6/6bHac +wavxgnG8k9JGcamhmxap3PagMjX/Ij2NJVeHcYmCo4G2UjI2BWj5j2umxMnbMp6p +PN1H5LJi5eZ0/8vhzo0EZH8fFwEEAOuxjdUZ0muXUy07CzVIrl6OwB3madZljcFm +YRf4GqQmZg6j0PS/XKQk0o+I+VL41YZWQjau848MZMkETfqvmqqitf83gzxVua13 +7z4JjdWkZzkbIY5UOkt6yPeN07+1Qn17ivV6cacR1QqfKFr2KJleU2lQKRbenAoC +x7NH9yEHABEBAAHCwIMEGAEKAA8FAmR/HxcFCQ8JnAACGy4AqAkQMYYDNcqrWgWd +IAQZAQoABgUCZH8fFwAKCRBTxi/Onv7Zv1zIBACaoNnjerhWsaBXwtIh4OBBcHxA +gp56GBpjcH9iAZUndivTrQpupkT4ltgf4E6NcQXYqCG3s7a0hNBxPscQPs/DrzLA +jPXTA3JKeRjeMKWWsDhdZ65mv+82vUc0fVOC9i5Pv3gUW3uX+bM1uzXrVaf2fgti +rkdJ37lMV1/JTmcSccK4A/9fk/hUwSos36R7muW4VgAzBkjIZ4tOXd+Bm+hLV5fn +Ngqa9CG3qS/Jo1N/5SyLdP46uUtNk00qn5J6leLp5ZLVhi5CFyN3Ss09yBgT32t3 +7qIAH/uaywKP1v5Lv9R9gAXAXZ/dY9NKoFks9WPQEs48cJviVIKVe52jdLJyOC9N +1Q== +=jXXW +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/test/resources/test_hash.txt b/src/test/resources/test_hash.txt new file mode 100644 index 0000000..f6ea049 --- /dev/null +++ b/src/test/resources/test_hash.txt @@ -0,0 +1 @@ +foobar \ No newline at end of file