diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..778b09e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# build directory +build/ + +# docker dev build +docker/ +docker-compose.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..cb4b3a2 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,114 @@ +variables: + IMAGE_VERSION: $CI_COMMIT_SHORT_SHA + IMAGE_TYPE: users + LATEST_NAME: main + GOLANGCI_LINT_VERSION: 'v1.56.2' + +default: + image: debian:11 + + + services: + - name: docker:24.0.7-dind + alias: docker + + +stages: + - check + - build + - test + - release + - release-latest + +before_script: + - id + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker login -u gnomezworc -p $DOCKERHUB_TOKEN + +lint: + image: golangci/golangci-lint:$GOLANGCI_LINT_VERSION + stage: check + before_script: [] + script: + - golangci-lint run --issues-exit-code 1 --print-issued-lines=false --out-format code-climate:gl-code-quality-report.json,line-number + artifacts: + reports: + codequality: gl-code-quality-report.json + paths: + - gl-code-quality-report.json + +build: + stage: build + image: docker:23.0.3-cli + artifacts: + paths: + - $IMAGE_TYPE-$IMAGE_VERSION-release.tar.gz + variables: + DOCKER_TLS_VERIFY: 1 + DOCKER_CERT_PATH: /certs/client + DOCKER_PATH: '.' + script: + - docker build --pull -t $CI_REGISTRY_IMAGE:$IMAGE_VERSION-release --target release $DOCKER_PATH + - docker save $CI_REGISTRY_IMAGE:$IMAGE_VERSION-release | gzip > $IMAGE_TYPE-$IMAGE_VERSION-release.tar.gz + needs: + - lint + +integration_test: + stage: test + image: golang:1.21-alpine + variables: + DOCKER_TLS_VERIFY: 1 + DOCKER_CERT_PATH: /certs/client + DOCKER_HOST: tcp://docker:2376/ + DOCKER_URL: docker + before_script: + - apk update && apk add --no-cache docker-cli + - apk add bash + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker login -u gnomezworc -p $DOCKERHUB_TOKEN + script: + - bash -ex ./scripts/prepar_test.sh + - . ./config/test.src + - go test -v ./cmd/ + needs: + - build + +release-latest: + stage: release-latest + image: docker:23.0.3-cli + variables: + DOCKER_TLS_VERIFY: 1 + DOCKER_CERT_PATH: /certs/client + script: + - docker load --input $IMAGE_TYPE-$IMAGE_VERSION-release.tar.gz + - docker images + - docker tag $CI_REGISTRY_IMAGE:$IMAGE_VERSION-release $CI_REGISTRY_IMAGE:latest + - docker push $CI_REGISTRY_IMAGE:latest + dependencies: + - build + rules: + - if: $CI_COMMIT_BRANCH == $LATEST_NAME + needs: + - build + - integration_test + +release-front: + stage: release + image: docker:23.0.3-cli + variables: + IMAGE_TYPE: front + DOCKER_TLS_VERIFY: 1 + DOCKER_CERT_PATH: /certs/client + script: + - docker load --input $IMAGE_TYPE-$IMAGE_VERSION-release.tar.gz + - docker images + - docker tag $CI_REGISTRY_IMAGE:$IMAGE_VERSION-release $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG + dependencies: + - build + only: + refs: + - tags + needs: + - build + - integration_test \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a41f9ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.21-alpine AS develop + +# Changement du répertoire de travail +WORKDIR /app + +# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change +COPY go.mod go.sum ./ +RUN go mod download && go mod verify + +COPY . . + +CMD ["sh", "-c", "echo 'run application' && go run ./cmd/"] + +FROM develop AS build + +RUN CGO_ENABLED=0 go build -o ./build/app ./cmd/ + +FROM scratch AS release + +COPY --from=build /app/build/app /exec + +EXPOSE 1222 + +ENTRYPOINT ["/exec"] \ No newline at end of file diff --git a/README.md b/README.md index ba445fa..f3552f3 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,42 @@ v0.0.0: version ## Developpement - Golang -- Docker \ No newline at end of file +- Docker + +### with docker compose + +```bash +cat < docker-compose.yml +version: '3.1' + +services: + postgres: + image: postgres:15.2-alpine3.17 + restart: unless-stopped + environment: + POSTGRES_USER: 'acc' + POSTGRES_PASSWORD: 'totor' + POSTGRES_DB: 'accounts' + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 + users: + build: + context: . + target: develop + ports: + - 127.0.0.1:2300:1222 + volumes: + - .:/app:ro + - ./docker/keys:/keys + depends_on: + postgres: + condition: service_healthy +ENDFILE + +cat config/db.sql| docker compose exec -T postgres psql -U acc accounts + +curl -s http://127.0.0.1:2300/ -H "Content-Type: application/json" -d "{\"username\":\"root@root.fr\",\"password\":\"root\"}" | jq -r '.token' +``` \ No newline at end of file diff --git a/cmd/001_basic_test.go b/cmd/001_basic_test.go new file mode 100644 index 0000000..7396a90 --- /dev/null +++ b/cmd/001_basic_test.go @@ -0,0 +1,9 @@ +package main_test + +import ( + "testing" +) + +func TestBasic(t *testing.T) { + skipCI(t, "TestBasic") +} diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..d288c4f --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,21 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + logins "gitlab.g3e.fr/h6n/users/internal" +) + +func login(c echo.Context) error { + var request logins.RequestLogin + if err := c.Bind(&request); err != nil { + return c.JSON(http.StatusBadRequest, err) + } + + if response, err := logins.CreateLogin(db, request); err != nil { + return c.JSON(http.StatusBadRequest, "Not a valide user username or password") + } else { + return c.JSON(http.StatusOK, response) + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..e799c8e --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "gitlab.g3e.fr/h6n/users/lib" + + "database/sql" + + _ "github.com/lib/pq" +) + +var db *sql.DB = nil + +func logout(c echo.Context) error { + return c.NoContent(http.StatusNoContent) +} + +func isLoggedIn(c echo.Context) error { + return c.NoContent(http.StatusNoContent) +} + +func init_database() { + var err error + + connStr := "postgres://acc:totor@postgres:5432/accounts?sslmode=disable" + + db, err = sql.Open("postgres", connStr) + if err != nil { + fmt.Println(err) + } +} + +func skip_auth(c echo.Context) bool { + if c.Request().Method == "POST" && c.Path() == "/" { + return true + } + return false +} + +func main() { + init_database() + if err := lib.InitLoginBiscuit(); err != nil { + fmt.Println("error : ", err) + return + } + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.CORS()) + e.Use(lib.AuthMiddleware(skip_auth)) + + // Routes + e.GET("/", isLoggedIn) + e.POST("/", login) + e.DELETE("/", logout) + + // Start server + e.Logger.Fatal(e.Start(":1222")) +} diff --git a/cmd/skipCi_test.go b/cmd/skipCi_test.go new file mode 100644 index 0000000..3a80710 --- /dev/null +++ b/cmd/skipCi_test.go @@ -0,0 +1,12 @@ +package main_test + +import ( + "os" + "testing" +) + +func skipCI(t *testing.T, test string) { + if os.Getenv(test) != "" { + t.Skipf("Skipping testing %v\n", test) + } +} diff --git a/config/db.sql b/config/db.sql new file mode 100644 index 0000000..591d4d8 --- /dev/null +++ b/config/db.sql @@ -0,0 +1,7 @@ +CREATE TABLE users ( + username TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL +); + +INSERT INTO users(username, email, password) VALUES('0000000023', 'root@root.fr', '$2a$14$FU8FKR7pUq4akLThraD9kOu0zY0BuUFASo0xakgWEWbYF2gz20DuG'); \ No newline at end of file diff --git a/config/test.src b/config/test.src new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2eff39b --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module gitlab.g3e.fr/h6n/users + +go 1.21.0 + +require ( + github.com/alecthomas/participle/v2 v2.0.0 // indirect + github.com/biscuit-auth/biscuit-go/v2 v2.2.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/labstack/echo/v4 v4.11.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b19315 --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= +github.com/alecthomas/participle/v2 v2.0.0/go.mod h1:rAKZdJldHu8084ojcWevWAL8KmEU+AT+Olodb+WoN2Y= +github.com/biscuit-auth/biscuit-go/v2 v2.2.0 h1:1zBfZ0ZCbxJbhtAhou6Fa07lBlJ+wcphBEPV/sENBvY= +github.com/biscuit-auth/biscuit-go/v2 v2.2.0/go.mod h1:c7AsMdr816vPd/4Psb3z6Xv2EKciKxSEayI8ueBaJio= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/internal/createLogin.go b/internal/createLogin.go new file mode 100644 index 0000000..140b350 --- /dev/null +++ b/internal/createLogin.go @@ -0,0 +1,72 @@ +package logins + +import ( + "database/sql" + "encoding/base64" + "encoding/hex" + "errors" + "time" + + "github.com/biscuit-auth/biscuit-go/v2" + "github.com/biscuit-auth/biscuit-go/v2/parser" + "gitlab.g3e.fr/h6n/users/lib" + "golang.org/x/crypto/bcrypt" +) + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func CreateLogin(db *sql.DB, request RequestLogin) (ResponseLogin, error) { + type ( + User struct { + Username string + EncryptedPassword string + } + ) + var user User + var response ResponseLogin + + if err := db.QueryRow("SELECT username, password FROM users WHERE email = $1", request.Username).Scan(&user.Username, &user.EncryptedPassword); err != nil { + return response, err + } + if !CheckPasswordHash(request.Password, user.EncryptedPassword) { + return response, errors.ErrUnsupported + } + now := time.Now() + ValidateTime := 14 + expire := now.AddDate(0, 0, ValidateTime) + + user_id := string(user.Username) + + auth := `user("` + user_id + `"); + check if time($time), $time <= ` + expire.Format("2006-01-02T15:04:05Z") + `; + ` + + authority, err := parser.FromStringBlockWithParams(auth, map[string]biscuit.Term{"read": biscuit.String("read"), "write": biscuit.String("write")}) + + if err != nil { + return response, err + } + + builder := biscuit.NewBuilder(lib.PrivateKey) + if err = builder.AddBlock(authority); err != nil { + return response, err + } + + b, err := builder.Build() + if err != nil { + return response, err + } + + token, err := b.Serialize() + if err != nil { + return response, err + } + + response.Token = base64.URLEncoding.EncodeToString(token) + response.PublicKey = hex.EncodeToString(lib.PublicKey) + + return response, nil +} diff --git a/internal/struct.go b/internal/struct.go new file mode 100644 index 0000000..751ee07 --- /dev/null +++ b/internal/struct.go @@ -0,0 +1,13 @@ +package logins + +type ( + RequestLogin struct { + Username string `json:"username"` + Password string `json:"password"` + } + + ResponseLogin struct { + Token string `json:"token"` + PublicKey string `json:"publickey"` + } +) diff --git a/lib/authMiddleware.go b/lib/authMiddleware.go new file mode 100644 index 0000000..fd8e92b --- /dev/null +++ b/lib/authMiddleware.go @@ -0,0 +1,73 @@ +package lib + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + "time" + + "github.com/biscuit-auth/biscuit-go/v2" + "github.com/biscuit-auth/biscuit-go/v2/parser" + "github.com/labstack/echo/v4" +) + +func queryUser(authorizer biscuit.Authorizer) (biscuit.FactSet, error) { + rule, err := parser.FromStringRule(`data($name) <- user($name)`) + if err != nil { + return nil, fmt.Errorf("failed to parse check: %v", err) + } + + return authorizer.Query(rule) +} + +func AuthMiddleware(skipper_auth func(echo.Context) bool) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if skipper_auth(c) { + return next(c) + } + tokens := strings.Split(c.Request().Header.Get("Authorization"), " ") + + if len(tokens) != 2 { + return c.String(http.StatusUnauthorized, "Header d'authentification manquant") + } + + c.Set("token", tokens[1]) + byteToken, _ := base64.URLEncoding.DecodeString(tokens[1]) + b, err := biscuit.Unmarshal(byteToken) + + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + + authorizer, err := b.Authorizer(PublicKey) + + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + + now := time.Now() + authorizerContents, err := parser.FromStringAuthorizerWithParams(` + time(`+now.Format("2006-01-02T15:04:05Z")+`); + allow if time($time), $time <= `+now.Format("2006-01-02T15:04:05Z")+`; + `, map[string]biscuit.Term{}) + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + authorizer.AddAuthorizer(authorizerContents) + + if err := authorizer.Authorize(); err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + + fact, err := queryUser(authorizer) + if err != nil { + return c.JSON(http.StatusUnauthorized, err) + } + c.Set("username", strings.Split(fact[0].IDs[0].String(), "\"")[1]) + + return next(c) + } + } +} diff --git a/lib/initBiscuit.go b/lib/initBiscuit.go new file mode 100644 index 0000000..0853db2 --- /dev/null +++ b/lib/initBiscuit.go @@ -0,0 +1,108 @@ +package lib + +import ( + "crypto/ed25519" + "crypto/rand" + "io" + "os" + "time" +) + +var PrivateKey ed25519.PrivateKey +var PublicKey ed25519.PublicKey +var filePrivateKey string = "/keys/privatekey.pem" +var filePublicKey string = "/keys/publickey.pem" + +func InitLoginBiscuit() error { + + if _, err := os.Stat(filePrivateKey); os.IsNotExist(err) { + rng := rand.Reader + PublicKey, PrivateKey, _ = ed25519.GenerateKey(rng) + + if file, err := os.Create(filePrivateKey); err != nil { + return err + } else { + defer file.Close() + + if _, err = io.WriteString(file, string(PrivateKey)); err != nil { + return err + } + if err := file.Chmod(0600); err != nil { + return err + } + } + if file, err := os.Create(filePublicKey); err != nil { + return err + } else { + defer file.Close() + + if _, err = io.WriteString(file, string(PublicKey)); err != nil { + return err + } + if err := file.Chmod(0600); err != nil { + return err + } + } + } else { + if file, err := os.Open(filePrivateKey); err != nil { + return err + } else { + defer file.Close() + + PrivateKey, err = io.ReadAll(file) + if err != nil { + return err + } + } + if file, err := os.Open(filePublicKey); err != nil { + return err + } else { + defer file.Close() + + PublicKey, err = io.ReadAll(file) + if err != nil { + return err + } + } + } + return nil +} + +func InitGlobalBiscuit() error { + for { + time.Sleep(1 * time.Second) + _, err := os.Stat(filePublicKey) + if err == nil { + break + } + } + for { + time.Sleep(1 * time.Second) + _, err := os.Stat(filePrivateKey) + if err == nil { + break + } + } + if file, err := os.Open(filePrivateKey); err != nil { + return err + } else { + defer file.Close() + + PrivateKey, err = io.ReadAll(file) + if err != nil { + return err + } + } + if file, err := os.Open(filePublicKey); err != nil { + return err + } else { + defer file.Close() + + PublicKey, err = io.ReadAll(file) + if err != nil { + return err + } + } + + return nil +} diff --git a/scripts/prepar_test.sh b/scripts/prepar_test.sh new file mode 100644 index 0000000..2db967f --- /dev/null +++ b/scripts/prepar_test.sh @@ -0,0 +1,42 @@ +#!/bin/bash + + +cat <> /etc/apk/repositories +http://ftp.halifax.rwth-aachen.de/alpine/v3.16/main +http://ftp.halifax.rwth-aachen.de/alpine/v3.16/community +ENDFILE +apk update +apk add docker docker-cli-compose +docker version +docker compose version +docker load --input "$IMAGE_TYPE-$IMAGE_VERSION-release.tar.gz" +docker tag $CI_REGISTRY_IMAGE:$IMAGE_VERSION-release $IMAGE_TYPE:latest + +cat < docker-compose.yml +version: '3.1' + +services: + postgres: + image: postgres:15.2-alpine3.17 + restart: unless-stopped + environment: + POSTGRES_USER: 'acc' + POSTGRES_PASSWORD: 'totor' + POSTGRES_DB: 'accounts' + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 + users: + image: $IMAGE_TYPE + volumes: + - ./docker/keys:/keys + depends_on: + postgres: + condition: service_healthy +ENDFILE + +docker compose pull --quiet --ignore-pull-failures +docker images +docker compose up -d \ No newline at end of file