Published: Feb 27, 2024 by Isaac Johnson
OpenProject is an Open-Source project management offering that also has a paid SaaS offering as well (similar to Plane.so). I’ll start by saying there are some things I really like about OpenProject and a few big annoyances. There is a lot here and it’s one of the few Open Source PjM Suites I think holds a torch to the heavyweight, JIRA in terms of deep custom configuration.
We’re going to dive in with Kubernetes and Docker. We’re going to look at setup and usage, APIs and REST, and more.
Note: they use the term “work package” for what many might use the work “work item”, “issue”, or “ticket”. So if I fall back to calling things “work items” or “tickets”, it’s only because for me, a “package” is generally a built thing and I find the terminology a bit strange
Background
OpenProject actually forked from ChiliProject in 2015 which had forked from Redmine in 2011. (while Wikipedia claims it forked in 2015, the first company blog post is from 2014 and the Git releases go back to 2012)
While there is an “OpenProject Foundation” that was established in 2012 and registered in the Amtsgericht of Charlottenburg-Wilmersdorf (a district of Berlin, Germany), there is a commercial Enterprise offering developed by the OpenProject company which has about 11-50 employees (29 on LI)
I’ve always loved this model of a totally free and OS version one could just download from Github but also an easy to use and scale SaaS offering.
Getting started
Let’s start with the OS self-hosted version first.
Let’s just keep it simple to start with; a simple helm install
$ helm repo add openproject https://charts.openproject.org
"openproject" has been added to your repositories
$ helm upgrade --create-namespace --namespace openproject --install my-openproject openproject/openproject
Release "my-openproject" does not exist. Installing it now.
coalesce.go:223: warning: destination for memcached.service.sessionAffinity is a table. Ignoring non-table value ()
NAME: my-openproject
LAST DEPLOYED: Fri Feb 9 13:08:16 2024
NAMESPACE: openproject
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing OpenProject 🎉
You can access it via https://openproject.example.com/
Summary:
--------
OpenProject: 13-slim
PostgreSQL: 15.4.0-debian-11-r45
Memcached: 1.6.23-debian-11-r2
That immediately created 6 pods
$ kubectl get pods -n openproject
NAME READY STATUS RESTARTS AGE
my-openproject-seeder-20240209130816-hrjwp 0/1 Pending 0 33s
my-openproject-web-8d8df9c9b-nz6kh 0/1 Pending 0 34s
my-openproject-worker-67bd58494c-xflzs 0/1 Pending 0 34s
my-openproject-memcached-5cf96d79fc-v85w5 1/1 Running 0 34s
my-openproject-postgresql-0 0/1 ContainerCreating 0 33s
I initially had some issues with my default storage class. Once corrected, I started to see the pods come up
NAME READY STATUS RESTARTS AGE
my-openproject-web-8d8df9c9b-chd8x 0/1 Init:0/1 0 2m18s
my-openproject-worker-67bd58494c-p4t5v 0/1 Init:0/1 0 2m18s
my-openproject-memcached-5cf96d79fc-tbm2j 1/1 Running 0 2m18s
my-openproject-postgresql-0 1/1 Running 0 2m17s
my-openproject-seeder-20240209132207-flgft 1/1 Running 0 2m17s
$ kubectl get pods -n openproject
NAME READY STATUS RESTARTS AGE
my-openproject-memcached-5cf96d79fc-tbm2j 1/1 Running 0 134m
my-openproject-postgresql-0 1/1 Running 0 134m
my-openproject-worker-67bd58494c-p4t5v 1/1 Running 0 134m
my-openproject-web-8d8df9c9b-chd8x 1/1 Running 0 134m
However, this did not work. Neither port-forwarding to the service nor pod seemed to connect
Trying with a disabled SSL
$ helm upgrade --create-namespace --namespace openproject --install my-openproject openproject/openproject --set openproject.https=false --set openproject.admin_user.password="Testing1234" --set openproject.admin_user.name="builder" --set openproject.admin_user.mail="isaac.johnson@gmail.com" --set environment.OPENPROJECT_APP__TITLE='My Freshbrewed OpenProject'
coalesce.go:223: warning: destination for memcached.service.sessionAffinity is a table. Ignoring non-table value ()
Release "my-openproject" has been upgraded. Happy Helming!
NAME: my-openproject
LAST DEPLOYED: Fri Feb 9 16:07:23 2024
NAMESPACE: openproject
STATUS: deployed
REVISION: 2
NOTES:
Thank you for installing OpenProject 🎉
You can access it via https://openproject.example.com/
Summary:
--------
OpenProject: 13-slim
PostgreSQL: 15.4.0-debian-11-r45
Memcached: 1.6.23-debian-11-r2
This time I was able to port-forward to the pod
$ kubectl port-forward my-openproject-web-58845ddbbb-728sz -n openproject 8888:8080
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080
Handling connection for 8888
Even though I gave it a password, it did not take. It still used ‘admin/admin’ for the login.
After figuring that out, I was prompted to change passwords
I took a break and came back to a crashing web pod
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl get pods -n openproject
NAME READY STATUS RESTARTS AGE
my-openproject-memcached-5cf96d79fc-tbm2j 1/1 Running 0 18h
my-openproject-worker-7dd5975d97-fzxhn 1/1 Running 0 15h
my-openproject-postgresql-0 1/1 Running 0 18h
my-openproject-web-58845ddbbb-728sz 0/1 CrashLoopBackOff 147 (3m21s ago) 15h
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ kubectl logs my-openproject-web-58845ddbbb-728sz -n openproject
Defaulted container "openproject" out of: openproject, wait-for-db (init)
=> Booting Puma
=> Rails 7.0.8 application starting in production
=> Run `bin/rails server --help` for more startup options
A server is already running. Check /app/tmp/pids/server.pid.
Exiting
I bounced the pod, but it only stayed up for a short bit before dying again
$ kubectl port-forward my-openproject-web-58845ddbbb-nmj7s -n openproject 8889:8080
Forwarding from 127.0.0.1:8889 -> 8080
Forwarding from [::1]:8889 -> 8080
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
Handling connection for 8889
E0210 07:57:35.599805 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
E0210 07:59:35.608438 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
E0210 08:01:35.601000 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
E0210 08:03:35.613126 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
E0210 08:05:35.600319 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
E0210 08:07:35.600567 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
E0210 08:09:35.603765 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
E0210 08:11:35.604255 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
Handling connection for 8889
Handling connection for 8889
E0210 08:13:35.605406 2285 portforward.go:347] error creating error stream for port 8889 -> 8080: Timeout occurred
In the end, I updated the deployment so i would have at least 3 web pods behind the service
$ kubectl edit deployment -n openproject my-openproject-web
deployment.apps/my-openproject-web edited
$ kubectl get rs -n openproject
NAME DESIRED CURRENT READY AGE
my-openproject-web-8d8df9c9b 0 0 0 22h
my-openproject-worker-67bd58494c 0 0 0 22h
my-openproject-memcached-5cf96d79fc 1 1 1 22h
my-openproject-worker-7dd5975d97 1 1 1 19h
my-openproject-web-58845ddbbb 3 3 0 19h
Then when up, I went to the service
$ kubectl port-forward svc/my-openproject -n openproject 9993:8080
Forwarding from 127.0.0.1:9993 -> 8080
Forwarding from [::1]:9993 -> 8080
Handling connection for 9993
Handling connection for 9993
Handling connection for 9993
I can see “My Page” which shows me packages assigned and created by me
By default, we have a couple of Projects created for us
In Demo project, I can see a broken image
I can double click a widget, like the Welcome one to edit
That came back as a broken image on save. I believe this is due to “hostname” settings.
We can go into Administration/General to adjust
Interesting, that field is not editable. I can change the numeric fields, but not the hostname
I believe it’s because by default ingress is enabled meaning it did try and create an ingress with the example hostname
$ kubectl get ingress -n openproject
NAME CLASS HOSTS ADDRESS PORTS AGE
my-openproject <none> openproject.example.com 80, 443 22h
Let’s sort that out now to see if it helps
$ cat r53-openproject.json
{
"Comment": "CREATE openproject fb.s A record ",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "openproject.freshbrewed.science",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "75.73.224.240"
}
]
}
}
]
}
$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-openproject.json
{
"ChangeInfo": {
"Id": "/change/C0358323ML3CD0LEI3D3",
"Status": "PENDING",
"SubmittedAt": "2024-02-10T18:14:10.162Z",
"Comment": "CREATE openproject fb.s A record "
}
}
I’ll make a values file
backgroundReplicaCount: 2
environment:
OPENPROJECT_APP__TITLE: My Freshbrewed OpenProject
openproject:
admin_user:
mail: isaac.johnson@gmail.com
name: builder
password: Testing1234
https: true
ingress:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
ingress.kubernetes.io/proxy-body-size: "0"
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.org/client-max-body-size: "0"
nginx.org/proxy-connect-timeout: "3600"
nginx.org/proxy-read-timeout: "3600"
enabled: true
host: openproject.freshbrewed.science
ingressClassName: null
path: /
pathType: Prefix
tls:
enabled: true
secretName: openproject-tls
and upgrade
$ helm upgrade --create-namespace --namespace openproject --install my-openproject openproject/openproject -f ./openproject-values.yaml
coalesce.go:223: warning: destination for memcached.service.sessionAffinity is a table. Ignoring non-table value ()
Release "my-openproject" has been upgraded. Happy Helming!
NAME: my-openproject
LAST DEPLOYED: Sat Feb 10 12:23:19 2024
NAMESPACE: openproject
STATUS: deployed
REVISION: 3
NOTES:
Thank you for installing OpenProject 🎉
You can access it via https://openproject.freshbrewed.science/
Summary:
--------
OpenProject: 13-slim
PostgreSQL: 15.4.0-debian-11-r45
Memcached: 1.6.23-debian-11-r2
Now I’ll show the ingress
$ kubectl get ingress -n openproject
NAME CLASS HOSTS ADDRESS PORTS AGE
my-openproject <none> openproject.freshbrewed.science 192.168.1.215,192.168.1.36,192.168.1.57,192.168.1.78 80, 443 23h
with SSL enabled, we see bad certs
It actually was just taking time to fetch the LE cert. Soon it came back up
However, even updating via attachments seems to fail. I get a 502 error
I followed the logs and the web pod seems to want to write to /tmp but does not like it being 777 perms
/usr/local/lib/ruby/3.2.0/tmpdir.rb:34:in `block in tmpdir': system temporary path is world-writable: /tmp (StructuredWarnings::StandardWarning)
/usr/local/lib/ruby/3.2.0/tmpdir.rb:34:in `block in tmpdir': /tmp is world-writable: /tmp (StructuredWarnings::StandardWarning)
2024-02-10 21:08:51 +0000 Read: #<Errno::EROFS: Read-only file system @ rb_sysopen - /app/puma20240210-13-1nyzz9>
I, [2024-02-10T21:08:51.710983 #16] INFO -- : [1c9a9a25-8369-40f5-8848-cb2add52aa8e] duration=151.20 db=111.53 view=39.67 status=200 method=GET path=/api/v3/notifications params={"pageSize"=>"0", "filters"=>"[{\"readIAN\":{\"operator\":\"=\",\"values\":[\"f\"]}}]"} host=openproject.freshbrewed.science user=4
I, [2024-02-10T21:08:52.190344 #16] INFO -- : [d2a4fb4a-f9a7-46fd-8185-f3dcea621c17] method=GET path=/health_checks/default format=*/* controller=OkComputer::OkComputerController action=show status=200 allocations=483 duration=15.34 view=0.36 db=0.00 user=3
Which I can see
$ kubectl exec -it my-openproject-web-79d558499c-tjrmd -n openproject -- /bin/sh
Defaulted container "openproject" out of: openproject, wait-for-db (init)
$ ls -ltra /tmp
total 8
drwxrwxrwx 2 root root 4096 Feb 10 18:23 .
drwxr-xr-x 1 root root 4096 Feb 10 18:24 ..
This is a bit of an issue as I cannot just create a task
And then try and add an attachment as I get a 502 error
Also, i can’t just port-forward to the service because it is doing some SSL funny business
$ kubectl port-forward svc/my-openproject -n openproject 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
And switching back to http not only also fails
But I get a footer complaining about it
AKS
I had an AKS cluster up so I decided to circle back and try there
$ helm upgrade --create-namespace --namespace openproject --install my-openproject openproject/openproject
Release "my-openproject" does not exist. Installing it now.
coalesce.go:223: warning: destination for memcached.service.sessionAffinity is a table. Ignoring non-table value ()
NAME: my-openproject
LAST DEPLOYED: Mon Feb 19 08:02:38 2024
NAMESPACE: openproject
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing OpenProject 🎉
You can access it via https://openproject.example.com/
Summary:
--------
OpenProject: 13-slim
PostgreSQL: 15.4.0-debian-11-r45
Memcached: 1.6.23-debian-11-r2
$ kubectl get ingress -n openproject
NAME CLASS HOSTS ADDRESS PORTS AGE
my-openproject <none> openproject.example.com 80, 443 87s
$ kubectl get svc -n openproject
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-openproject ClusterIP 10.0.194.107 <none> 8080/TCP 93s
my-openproject-memcached ClusterIP 10.0.39.226 <none> 11211/TCP 93s
my-openproject-postgresql ClusterIP 10.0.146.225 <none> 5432/TCP 93s
my-openproject-postgresql-hl ClusterIP None <none> 5432/TCP 93s
I’ll use a real DNS entry and create an A record
Then upgrade
$ helm upgrade --create-namespace --namespace openproject --install my-openproject openproject/openproject --set ingress.host=openproject.homehealthsre.site
coalesce.go:223: warning: destination for memcached.service.sessionAffinity is a table. Ignoring non-table value ()
Release "my-openproject" has been upgraded. Happy Helming!
NAME: my-openproject
LAST DEPLOYED: Mon Feb 19 08:06:56 2024
NAMESPACE: openproject
STATUS: deployed
REVISION: 2
NOTES:
Thank you for installing OpenProject 🎉
You can access it via https://openproject.homehealthsre.site/
Summary:
--------
OpenProject: 13-slim
PostgreSQL: 15.4.0-debian-11-r45
Memcached: 1.6.23-debian-11-r2
Which shows it updated
$ kubectl get ingress -n openproject
NAME CLASS HOSTS ADDRESS PORTS AGE
my-openproject <none> openproject.homehealthsre.site 80, 443 5m30s
It’s not requesting an IP so there is nothing to which to route
Nginx Controller
I’ll add an NGinx Ingress controller
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.3.0/deploy/static/provider/cloud/deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
$ kubectl get pods --namespace ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-qzk8s 0/1 Completed 0 37s
ingress-nginx-admission-patch-zwxqm 0/1 Completed 0 37s
ingress-nginx-controller-6649cbd66c-xd8gc 1/1 Running 0 37s
Use the new external IP in the DNS entry
$ kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.0.47.97 172.169.40.54 80:30751/TCP,443:32127/TCP 109s
ingress-nginx-controller-admission ClusterIP 10.0.6.188 <none> 443/TCP 108s
My TTLs are bit long so I’ll use a new A record (openproject2)
I’ll now upgrade the helm to use that name AND set the ingress class
$ helm upgrade --create-namespace --namespace openproject --install my-openproject openproject/openproject --set ingress.host=openproject2.homehealthsre.site --set ingress.ingressClassName=nginx
coalesce.go:223: warning: destination for memcached.service.sessionAffinity is a table. Ignoring non-table value ()
Release "my-openproject" has been upgraded. Happy Helming!
NAME: my-openproject
LAST DEPLOYED: Mon Feb 19 08:15:01 2024
NAMESPACE: openproject
STATUS: deployed
REVISION: 3
NOTES:
Thank you for installing OpenProject 🎉
You can access it via https://openproject2.homehealthsre.site/
Summary:
--------
OpenProject: 13-slim
PostgreSQL: 15.4.0-debian-11-r45
Memcached: 1.6.23-debian-11-r2
This didn’t work, however, a bit later, I tried with a different valid A Record as I realized the old DNS has expired last week.
I set a new A Record
Our updated values
$ cat t.values
ingress:
host: openproject.tpk.pw
ingressClassName: nginx
Then upgrade
$ helm upgrade -f ./t.values --create-namespace --namespace openproject --install my-openproject openproject/openproject --set openproject.https=false --set openproject.admin_user.password="Testing1234" --set openproject.admin_user.name="builder" --set openproject.admin_user.mail="isaac.johnson@gmail.com" --set environment.OPENPROJECT_APP__TITLE='My Freshbrewed OpenProject'
Release "my-openproject" does not exist. Installing it now.
coalesce.go:223: warning: destination for memcached.service.sessionAffinity is a table. Ignoring non-table value ()
NAME: my-openproject
LAST DEPLOYED: Mon Feb 19 18:56:44 2024
NAMESPACE: openproject
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing OpenProject 🎉
You can access it via https://openproject.tpk.pw/
Summary:
--------
OpenProject: 13-slim
PostgreSQL: 15.4.0-debian-11-r45
Memcached: 1.6.23-debian-11-r2
For some reason the DB is not being noticed by the other pods
$ kubectl get pods -n openproject
NAME READY STATUS RESTARTS AGE
my-openproject-memcached-6c498d4dfb-m2rhz 1/1 Running 0 6m46s
my-openproject-postgresql-0 1/1 Running 0 6m46s
my-openproject-seeder-20240219185646-bwf5v 0/1 Pending 0 6m46s
my-openproject-web-58d7dd5b9f-rmdpx 0/1 Pending 0 6m46s
my-openproject-worker-7f9b6f4f5b-4zdng 0/1 Pending 0 6m46s
But the ingress works
$ kubectl get ingress -n openproject
NAME CLASS HOSTS ADDRESS PORTS AGE
my-openproject nginx openproject.tpk.pw 172.169.40.54 80, 443 7m17s
My issue turned out to be PVC storage class issue which I corrected with the values:
$ cat t.values
ingress:
host: openproject.tpk.pw
ingressClassName: nginx
persistence:
storageClassName: azurefile-csi-premium
It took some testing to find the right storageClasses
$ kubectl patch storageclass default -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
$ kubectl patch storageclass managed-csi-premium -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
Then a quick helm upgrade (after a delete and deleting the old psql pvc)
$ helm delete my-openproject -n openproject
$ kubectl delete pvc data-my-openproject-postgresql-0 -n openproject
$ helm upgrade -f ./t.values --create-namespace --namespace openproject --install my-openproject openproject/openproject --set openproject.https=false --set openproject.admin_user.password="Testing1234" --set openproject.admin_user.name="builder" --set openproject.admin_user.mail="isaac.johnson@gmail.com" --set environment.OPENPROJECT_APP__TITLE='My Freshbrewed OpenProject'
$ kubectl get pods -n openproject
NAME READY STATUS RESTARTS AGE
my-openproject-memcached-6c498d4dfb-khb8f 1/1 Running 0 4m51s
my-openproject-postgresql-0 1/1 Running 0 4m51s
my-openproject-seeder-20240219193218-x2q6n 0/1 Completed 0 4m51s
my-openproject-web-58d7dd5b9f-mpntm 1/1 Running 0 4m51s
my-openproject-worker-7f9b6f4f5b-jsxp7 1/1 Running 0 4m51s
It worked, albeit i locked the account up right away
The TLS would be valid I had added a ClusterIssuer to LE, but this proves AKS would work to host the suite
Docker
Let’s go the Docker Compose route instead
builder@builder-T100:~$ git clone https://github.com/opf/openproject-deploy --depth=1 --branch=stable/13 openproject
Cloning into 'openproject'...
remote: Enumerating objects: 21, done.
remote: Counting objects: 100% (21/21), done.
remote: Compressing objects: 100% (16/16), done.
Receiving objects: 100% (21/21), 6.07 KiB | 6.07 MiB/s, done.
remote: Total 21 (delta 0), reused 12 (delta 0), pack-reused 0
builder@builder-T100:~$ cd openproject/compose/
I’ll install Docker Compose
$ sudo apt install docker-compose
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
containerd.io docker-ce docker-ce-cli python3-attr python3-distutils python3-docker python3-dockerpty python3-docopt python3-dotenv python3-jsonschema python3-pyrsistent python3-setuptools
python3-texttable python3-websocket
Suggested packages:
aufs-tools cgroupfs-mount | cgroup-lite python-attr-doc python-jsonschema-doc python-setuptools-doc
Recommended packages:
docker.io
The following NEW packages will be installed:
docker-compose python3-attr python3-distutils python3-docker python3-dockerpty python3-docopt python3-dotenv python3-jsonschema python3-pyrsistent python3-setuptools python3-texttable python3-websocket
The following packages will be upgraded:
containerd.io docker-ce docker-ce-cli
3 upgraded, 12 newly installed, 0 to remove and 135 not upgraded.
Need to get 68.5 MB of archives.
After this operation, 10.3 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://us.archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-distutils all 3.10.8-1~22.04 [139 kB]
Get:2 https://download.docker.com/linux/ubuntu focal/stable amd64 containerd.io amd64 1.6.28-1 [29.6 MB]
Get:3 http://us.archive.ubuntu.com/ubuntu jammy/universe amd64 python3-websocket all 1.2.3-1 [34.7 kB]
Get:4 http://us.archive.ubuntu.com/ubuntu jammy/universe amd64 python3-docker all 5.0.3-1 [89.3 kB]
Get:5 http://us.archive.ubuntu.com/ubuntu jammy/universe amd64 python3-dockerpty all 0.4.1-2 [11.1 kB]
Get:6 http://us.archive.ubuntu.com/ubuntu jammy/universe amd64 python3-docopt all 0.6.2-4 [26.9 kB]
Get:7 http://us.archive.ubuntu.com/ubuntu jammy/universe amd64 python3-dotenv all 0.19.2-1 [20.5 kB]
Get:8 http://us.archive.ubuntu.com/ubuntu jammy/main amd64 python3-attr all 21.2.0-1 [44.0 kB]
Get:9 http://us.archive.ubuntu.com/ubuntu jammy-updates/main amd64 python3-setuptools all 59.6.0-1.2ubuntu0.22.04.1 [339 kB]
Get:10 http://us.archive.ubuntu.com/ubuntu jammy/main amd64 python3-pyrsistent amd64 0.18.1-1build1 [55.5 kB]
Get:11 http://us.archive.ubuntu.com/ubuntu jammy/main amd64 python3-jsonschema all 3.2.0-0ubuntu2 [43.1 kB]
Get:12 http://us.archive.ubuntu.com/ubuntu jammy/universe amd64 python3-texttable all 1.6.4-1 [11.4 kB]
Get:13 http://us.archive.ubuntu.com/ubuntu jammy/universe amd64 docker-compose all 1.29.2-1 [95.8 kB]
Get:14 https://download.docker.com/linux/ubuntu focal/stable amd64 docker-ce-cli amd64 5:25.0.3-1~ubuntu.20.04~focal [13.7 MB]
Get:15 https://download.docker.com/linux/ubuntu focal/stable amd64 docker-ce amd64 5:25.0.3-1~ubuntu.20.04~focal [24.3 MB]
Fetched 68.5 MB in 1s (46.0 MB/s)
(Reading database ... 247222 files and directories currently installed.)
Preparing to unpack .../00-containerd.io_1.6.28-1_amd64.deb ...
Unpacking containerd.io (1.6.28-1) over (1.6.21-1) ...
Preparing to unpack .../01-docker-ce-cli_5%3a25.0.3-1~ubuntu.20.04~focal_amd64.deb ...
Unpacking docker-ce-cli (5:25.0.3-1~ubuntu.20.04~focal) over (5:24.0.2-1~ubuntu.20.04~focal) ...
Preparing to unpack .../02-docker-ce_5%3a25.0.3-1~ubuntu.20.04~focal_amd64.deb ...
Unpacking docker-ce (5:25.0.3-1~ubuntu.20.04~focal) over (5:24.0.2-1~ubuntu.20.04~focal) ...
Selecting previously unselected package python3-distutils.
Preparing to unpack .../03-python3-distutils_3.10.8-1~22.04_all.deb ...
Unpacking python3-distutils (3.10.8-1~22.04) ...
Selecting previously unselected package python3-websocket.
Preparing to unpack .../04-python3-websocket_1.2.3-1_all.deb ...
Unpacking python3-websocket (1.2.3-1) ...
Selecting previously unselected package python3-docker.
Preparing to unpack .../05-python3-docker_5.0.3-1_all.deb ...
Unpacking python3-docker (5.0.3-1) ...
Selecting previously unselected package python3-dockerpty.
Preparing to unpack .../06-python3-dockerpty_0.4.1-2_all.deb ...
Unpacking python3-dockerpty (0.4.1-2) ...
Selecting previously unselected package python3-docopt.
Preparing to unpack .../07-python3-docopt_0.6.2-4_all.deb ...
Unpacking python3-docopt (0.6.2-4) ...
Selecting previously unselected package python3-dotenv.
Preparing to unpack .../08-python3-dotenv_0.19.2-1_all.deb ...
Unpacking python3-dotenv (0.19.2-1) ...
Selecting previously unselected package python3-attr.
Preparing to unpack .../09-python3-attr_21.2.0-1_all.deb ...
Unpacking python3-attr (21.2.0-1) ...
Selecting previously unselected package python3-setuptools.
Preparing to unpack .../10-python3-setuptools_59.6.0-1.2ubuntu0.22.04.1_all.deb ...
Unpacking python3-setuptools (59.6.0-1.2ubuntu0.22.04.1) ...
Selecting previously unselected package python3-pyrsistent:amd64.
Preparing to unpack .../11-python3-pyrsistent_0.18.1-1build1_amd64.deb ...
Unpacking python3-pyrsistent:amd64 (0.18.1-1build1) ...
Selecting previously unselected package python3-jsonschema.
Preparing to unpack .../12-python3-jsonschema_3.2.0-0ubuntu2_all.deb ...
Unpacking python3-jsonschema (3.2.0-0ubuntu2) ...
Selecting previously unselected package python3-texttable.
Preparing to unpack .../13-python3-texttable_1.6.4-1_all.deb ...
Unpacking python3-texttable (1.6.4-1) ...
Selecting previously unselected package docker-compose.
Preparing to unpack .../14-docker-compose_1.29.2-1_all.deb ...
Unpacking docker-compose (1.29.2-1) ...
Setting up python3-dotenv (0.19.2-1) ...
Setting up python3-distutils (3.10.8-1~22.04) ...
Setting up python3-attr (21.2.0-1) ...
Setting up python3-texttable (1.6.4-1) ...
Setting up python3-docopt (0.6.2-4) ...
Setting up python3-setuptools (59.6.0-1.2ubuntu0.22.04.1) ...
Setting up containerd.io (1.6.28-1) ...
Setting up docker-ce-cli (5:25.0.3-1~ubuntu.20.04~focal) ...
Setting up python3-pyrsistent:amd64 (0.18.1-1build1) ...
Setting up python3-websocket (1.2.3-1) ...
Setting up python3-dockerpty (0.4.1-2) ...
Setting up python3-docker (5.0.3-1) ...
Setting up python3-jsonschema (3.2.0-0ubuntu2) ...
Setting up docker-ce (5:25.0.3-1~ubuntu.20.04~focal) ...
Installing new version of config file /etc/default/docker ...
Installing new version of config file /etc/init.d/docker ...
Removing obsolete conffile /etc/init/docker.conf ...
Setting up docker-compose (1.29.2-1) ...
Processing triggers for man-db (2.10.2-1) ...
I can now do a docker compose pull
$ docker-compose pull
Pulling db ... done
Pulling cache ... done
Pulling autoheal ... done
Pulling seeder ... done
Pulling cron ... done
Pulling worker ... done
Pulling web ... done
Pulling proxy ... done
I can now launch it:
builder@builder-T100:~/openproject/compose$ OPENPROJECT_HTTPS=false docker-compose up -d
Creating network "compose_backend" with the default driver
Creating network "compose_default" with the default driver
Creating network "compose_frontend" with the default driver
Creating volume "compose_pgdata" with default driver
Creating volume "compose_opdata" with default driver
Creating compose_cache_1 ... done
Creating compose_autoheal_1 ... done
Creating compose_seeder_1 ... done
Creating compose_db_1 ... done
Creating compose_web_1 ... done
Creating compose_cron_1 ... done
Creating compose_worker_1 ... done
Creating compose_proxy_1 ... done
I can see it came up, but seems to want to take ports 8080 and 80
I can now try to sign in
This time, after setting the admin password, I can see working images
I’m going to delete the helm install
$ helm list -n openproject
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
my-openproject openproject 4 2024-02-10 15:15:29.235846849 -0600 CST deployed openproject-4.5.0 13
$ helm delete my-openproject -n openproject
release "my-openproject" uninstalled
I’ll cleanup the unneeded PVC
$ kubectl get pvc -n openproject
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-my-openproject-postgresql-0 Bound pvc-33c805b4-23b3-45b8-81c8-066ef5653d76 8Gi RWO managed-nfs-storage 2d3h
$ kubectl delete pvc data-my-openproject-postgresql-0 -n openproject
persistentvolumeclaim "data-my-openproject-postgresql-0" deleted
Now that helm is cleared out, we can apply via a Kubernetes manifest YAML using an External IP
$ cat docker.hosted.openproject.yaml
---
apiVersion: v1
kind: Endpoints
metadata:
name: openproject-external-ip
subsets:
- addresses:
- ip: 192.168.1.100
ports:
- name: openproject
port: 8080
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: openproject-external-ip
spec:
clusterIP: None
clusterIPs:
- None
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
- IPv6
ipFamilyPolicy: RequireDualStack
ports:
- name: openproject
port: 80
protocol: TCP
targetPort: 8080
sessionAffinity: None
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.org/websocket-services: openproject-external-ip
generation: 1
labels:
app.kubernetes.io/instance: openprojectingress
name: openprojectingress
spec:
rules:
- host: openproject.freshbrewed.science
http:
paths:
- backend:
service:
name: openproject-external-ip
port:
number: 80
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- openproject.freshbrewed.science
secretName: openproject-tls
$ kubectl apply -f docker.hosted.openproject.yaml
endpoints/openproject-external-ip created
service/openproject-external-ip created
ingress.networking.k8s.io/openprojectingress created
This works, but it clearly isn’t happy about this setup (seeing the footer)
I’ll try setting the values and relaunching
builder@builder-T100:~$ cd openproject/compose/
builder@builder-T100:~/openproject/compose$ docker compose down
[+] Running 11/11
✔ Container compose_proxy_1 Removed 10.5s
✔ Container compose_autoheal_1 Removed 7.8s
✔ Container compose_worker_1 Removed 10.5s
✔ Container compose_cron_1 Removed 10.4s
✔ Container compose_web_1 Removed 0.6s
✔ Container compose_seeder_1 Removed 0.1s
✔ Container compose_db_1 Removed 0.3s
✔ Container compose_cache_1 Removed 1.1s
✔ Network compose_default Removed 0.3s
✔ Network compose_backend Removed 0.8s
✔ Network compose_frontend Removed 0.6s
builder@builder-T100:~/openproject/compose$ OPENPROJECT_HOST__NAME="openproject.freshbrewed.science" OPENPROJECT_HTTPS=true docker-compose up -d
Creating network "compose_backend" with the default driver
Creating network "compose_default" with the default driver
Creating network "compose_frontend" with the default driver
Creating compose_autoheal_1 ... done
Creating compose_cache_1 ... done
Creating compose_seeder_1 ... done
Creating compose_db_1 ... done
Creating compose_web_1 ... done
Creating compose_cron_1 ... done
Creating compose_worker_1 ... done
Creating compose_proxy_1 ... done
And we can see that cleared the footer complaints
Let’s create a new project and card
We created an account in the recording above, but as we can see, it still has to be activated
As an admin, I can go in and see a new account was put in there
and activate it
Advertisement overload
If I go to create a personal page, I can see more advertisements
From an old post it’s clear that OP doesn’t see the need to hide those advertisements.
We see them everywhere - from “Placeholder users”
To Team Planner
To Notification Settings
Even tweaking the color scheme is an Enterprise feature
Which starts, presently, at $435 a year for the smallest offering (5 users at 1 year)
Later, when I setup SaaS, I went back to confirm this; indeed, the SaaS offering is the same price
But that one at least as a monthly option for US $42.5 for 5 users
With everything incrementing by 5
Webhooks
I tried creating a demo webhook. Since I couldn’t set the payload JSON, I set it to use webhook.site to test
However, in creating tasks and updating them showed no results
But adding an attachment did trigger it
Actually, checking back later, the API did trigger several events, just a bit out of order and on a few minute delay.
And the Work Package (ticket) being created
{
"action": "work_package:created",
"work_package": {
"_type": "WorkPackage",
"id": 38,
"lockVersion": 1,
"subject": "test",
"description": {
"format": "markdown",
"raw": "asdfsadfsadf",
"html": "<p class=\"op-uc-p\">asdfsadfsadf</p>"
},
"scheduleManually": false,
"startDate": null,
"dueDate": null,
"derivedStartDate": null,
"derivedDueDate": null,
"estimatedTime": null,
"derivedEstimatedTime": null,
"remainingTime": null,
"derivedRemainingTime": null,
"duration": null,
"ignoreNonWorkingDays": false,
"spentTime": "PT0S",
"percentageDone": 0,
"createdAt": "2024-02-11T23:09:11.222Z",
"updatedAt": "2024-02-11T23:09:26.798Z",
"laborCosts": "0.00 EUR",
"materialCosts": "0.00 EUR",
"overallCosts": "0.00 EUR",
"_embedded": {
"attachments": {
"_type": "Collection",
"total": 0,
"count": 0,
"_embedded": {
"elements": []
},
"_links": {
"self": {
"href": "/api/v3/work_packages/38/attachments"
}
}
},
"relations": {
"_type": "Collection",
"total": 0,
"count": 0,
"_embedded": {
"elements": []
},
"_links": {
"self": {
"href": "/api/v3/work_packages/38/relations"
}
}
},
"type": {
"_type": "Type",
"id": 1,
"name": "Task",
"color": "#1A67A3",
"position": 1,
"isDefault": true,
"isMilestone": false,
"createdAt": "2024-02-11T22:14:51.378Z",
"updatedAt": "2024-02-11T22:14:51.378Z",
"_links": {
"self": {
"href": "/api/v3/types/1",
"title": "Task"
}
}
},
"priority": {
"_type": "Priority",
"id": 8,
"name": "Normal",
"position": 2,
"color": "#74C0FC",
"isDefault": true,
"isActive": true,
"_links": {
"self": {
"href": "/api/v3/priorities/8",
"title": "Normal"
}
}
},
"project": {
"_type": "Project",
"id": 3,
"identifier": "myfreshbrewedproject",
"name": "MyFreshBrewedProject",
"active": true,
"public": true,
"description": {
"format": "markdown",
"raw": "This is my first project in OpenProject",
"html": "<p class=\"op-uc-p\">This is my first project in OpenProject</p>"
},
"createdAt": "2024-02-11T22:36:35.496Z",
"updatedAt": "2024-02-11T22:36:35.525Z",
"statusExplanation": {
"format": "markdown",
"raw": "My Status is here",
"html": "<p class=\"op-uc-p\">My Status is here</p>"
},
"_links": {
"self": {
"href": "/api/v3/projects/3",
"title": "MyFreshBrewedProject"
},
"createWorkPackage": {
"href": "/api/v3/projects/3/work_packages/form",
"method": "post"
},
"createWorkPackageImmediately": {
"href": "/api/v3/projects/3/work_packages",
"method": "post"
},
"workPackages": {
"href": "/api/v3/projects/3/work_packages"
},
"storages": [],
"categories": {
"href": "/api/v3/projects/3/categories"
},
"versions": {
"href": "/api/v3/projects/3/versions"
},
"memberships": {
"href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
},
"types": {
"href": "/api/v3/projects/3/types"
},
"update": {
"href": "/api/v3/projects/3/form",
"method": "post"
},
"updateImmediately": {
"href": "/api/v3/projects/3",
"method": "patch"
},
"delete": {
"href": "/api/v3/projects/3",
"method": "delete"
},
"schema": {
"href": "/api/v3/projects/schema"
},
"ancestors": [],
"projectStorages": {
"href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
},
"parent": {
"href": null
},
"status": {
"href": "/api/v3/project_statuses/on_track",
"title": "On track"
}
}
},
"status": {
"_type": "Status",
"id": 1,
"name": "New",
"isClosed": false,
"color": "#1098AD",
"isDefault": true,
"isReadonly": false,
"defaultDoneRatio": null,
"position": 1,
"_links": {
"self": {
"href": "/api/v3/statuses/1",
"title": "New"
}
}
},
"author": {
"_type": "User",
"id": 4,
"name": "OpenProject Admin",
"createdAt": "2024-02-11T22:14:53.515Z",
"updatedAt": "2024-02-11T22:44:49.584Z",
"login": "admin",
"admin": true,
"firstName": "OpenProject",
"lastName": "Admin",
"email": "admin@example.net",
"avatar": "https://secure.gravatar.com/avatar/cb4f282fed12016bd18a879c1f27ff97?default=404&secure=true",
"status": "active",
"identityUrl": null,
"language": "en",
"_links": {
"self": {
"href": "/api/v3/users/4",
"title": "OpenProject Admin"
},
"memberships": {
"href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
"title": "Members"
},
"showUser": {
"href": "/users/4",
"type": "text/html"
},
"updateImmediately": {
"href": "/api/v3/users/4",
"title": "Update admin",
"method": "patch"
},
"lock": {
"href": "/api/v3/users/4/lock",
"title": "Set lock on admin",
"method": "post"
},
"delete": {
"href": "/api/v3/users/4",
"title": "Delete admin",
"method": "delete"
}
}
},
"customActions": [],
"costsByType": {
"_type": "Collection",
"total": 0,
"count": 0,
"_embedded": {
"elements": []
},
"_links": {
"self": {
"href": "/api/v3/work_packages/38/summarized_costs_by_type"
}
}
}
},
"_links": {
"attachments": {
"href": "/api/v3/work_packages/38/attachments"
},
"addAttachment": {
"href": "/api/v3/work_packages/38/attachments",
"method": "post"
},
"fileLinks": {
"href": "/api/v3/work_packages/38/file_links"
},
"addFileLink": {
"href": "/api/v3/work_packages/38/file_links",
"method": "post"
},
"self": {
"href": "/api/v3/work_packages/38",
"title": "test"
},
"update": {
"href": "/api/v3/work_packages/38/form",
"method": "post"
},
"schema": {
"href": "/api/v3/work_packages/schemas/3-1"
},
"updateImmediately": {
"href": "/api/v3/work_packages/38",
"method": "patch"
},
"delete": {
"href": "/api/v3/work_packages/38",
"method": "delete"
},
"logTime": {
"href": "/api/v3/time_entries",
"title": "Log time on test"
},
"move": {
"href": "/work_packages/38/move/new",
"type": "text/html",
"title": "Move test"
},
"copy": {
"href": "/work_packages/38/copy",
"title": "Copy test"
},
"pdf": {
"href": "/work_packages/38.pdf",
"type": "application/pdf",
"title": "Export as PDF"
},
"atom": {
"href": "/work_packages/38.atom",
"type": "application/rss+xml",
"title": "Atom feed"
},
"availableRelationCandidates": {
"href": "/api/v3/work_packages/38/available_relation_candidates",
"title": "Potential work packages to relate to"
},
"customFields": {
"href": "/projects/myfreshbrewedproject/settings/custom_fields",
"type": "text/html",
"title": "Custom fields"
},
"configureForm": {
"href": "/types/1/edit?tab=form_configuration",
"type": "text/html",
"title": "Configure form"
},
"activities": {
"href": "/api/v3/work_packages/38/activities"
},
"availableWatchers": {
"href": "/api/v3/work_packages/38/available_watchers"
},
"relations": {
"href": "/api/v3/work_packages/38/relations"
},
"revisions": {
"href": "/api/v3/work_packages/38/revisions"
},
"watchers": {
"href": "/api/v3/work_packages/38/watchers"
},
"addWatcher": {
"href": "/api/v3/work_packages/38/watchers",
"method": "post",
"payload": {
"user": {
"href": "/api/v3/users/{user_id}"
}
},
"templated": true
},
"removeWatcher": {
"href": "/api/v3/work_packages/38/watchers/{user_id}",
"method": "delete",
"templated": true
},
"addRelation": {
"href": "/api/v3/work_packages/38/relations",
"method": "post",
"title": "Add relation"
},
"addChild": {
"href": "/api/v3/projects/myfreshbrewedproject/work_packages",
"method": "post",
"title": "Add child of test"
},
"changeParent": {
"href": "/api/v3/work_packages/38",
"method": "patch",
"title": "Change parent of test"
},
"addComment": {
"href": "/api/v3/work_packages/38/activities",
"method": "post",
"title": "Add comment"
},
"previewMarkup": {
"href": "/api/v3/render/markdown?context=/api/v3/work_packages/38",
"method": "post"
},
"timeEntries": {
"href": "/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2238%22%5D%7D%7D%5D",
"title": "Time entries"
},
"ancestors": [],
"category": {
"href": null
},
"type": {
"href": "/api/v3/types/1",
"title": "Task"
},
"priority": {
"href": "/api/v3/priorities/8",
"title": "Normal"
},
"project": {
"href": "/api/v3/projects/3",
"title": "MyFreshBrewedProject"
},
"status": {
"href": "/api/v3/statuses/1",
"title": "New"
},
"author": {
"href": "/api/v3/users/4",
"title": "OpenProject Admin"
},
"responsible": {
"href": null
},
"assignee": {
"href": null
},
"version": {
"href": null
},
"parent": {
"href": null,
"title": null
},
"customActions": [],
"logCosts": {
"href": "/work_packages/38/cost_entries/new",
"type": "text/html",
"title": "Log costs on test"
},
"showCosts": {
"href": "/projects/3/cost_reports?fields%5B%5D=WorkPackageId&operators%5BWorkPackageId%5D=%3D&set_filter=1&values%5BWorkPackageId%5D=38",
"type": "text/html",
"title": "Show cost entries"
},
"costsByType": {
"href": "/api/v3/work_packages/38/summarized_costs_by_type"
},
"meetings": {
"href": "/work_packages/38/tabs/meetings",
"title": "meetings"
},
"github": {
"href": "/work_packages/38/tabs/github",
"title": "github"
},
"github_pull_requests": {
"href": "/api/v3/work_packages/38/github_pull_requests",
"title": "GitHub pull requests"
},
"convertBCF": {
"href": "/api/bcf/2.1/projects/myfreshbrewedproject/topics",
"title": "Convert to BCF",
"payload": {
"reference_links": [
"/api/v3/work_packages/38"
]
},
"method": "post"
}
}
}
}
REST
To use REST, I’ll need to create an API Token under my user’s settings
It will show it once
I can now use that in a Query to get all work packages
$ curl -X GET -H 'Content-Type: application/json' -H 'Accept: application/json' -u "apikey:6c67xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1" https://openproject.freshbrewed.science/api/v3/wor
k_packages
{"_type":"WorkPackageCollection","total":35,"count":20,"pageSize":20,"offset":1,"_embedded":{"elements":[{"derivedStartDate":"2024-02-05","derivedDueDate":"2024-02-19","_type":"WorkPackage","id":2,"lockVersion":0,"subject":"Organize open source conference","description":{"format":"markdown","raw":"","html":""},"scheduleManually":false,"startDate":"2024-02-05","dueDate":"2024-02-19","estimatedTime":null,"derivedEstimatedTime":null,"remainingTime":null,"derivedRemainingTime":null,"duration":"P11D","ignoreNonWorkingDays":false,"percentageDone":0,"createdAt":"2024-02-11T22:14:54.140Z","updatedAt":"2024-02-11T22:14:54.565Z","_links":{"attachments":{"href":"/api/v3/work_packages/2/attachments"},"addAttachment":{"href":"/api/v3/work_packages/2/attachments","method":"post"},"
... snip ...
Using JQ, for example, we could pull the IDs and subjects
$ curl -X GET -H 'Content-Type: application/json' -H 'Accept: application/json' -u "apikey:6c67xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1" https://openproject.freshbrewed.science/api/v3/work_packages | jq '._embedded.elements[] | "\(.id) \(.subject)"'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 109k 100 109k 0 0 427k 0 --:--:-- --:--:-- --:--:-- 427k
"2 Organize open source conference"
"3 Set date and location of conference"
"4 Send invitation to speakers"
"5 Contact sponsoring partners"
"6 Create sponsorship brochure and hand-outs"
"7 Invite attendees to conference"
"8 Setup conference website"
"9 Conference"
"10 Follow-up tasks"
"11 Upload presentations to website"
"12 Party for conference supporters :-)"
"13 End of project"
"14 New login screen"
"15 Password reset does not send email"
"16 New website"
"17 Newsletter registration form"
"18 Implement product tour"
"19 New landing page"
"20 Create wireframes for new landing page"
"21 Contact form"
To create an issue, we can POST a JSON body. Note: Project is a required field and is under “_links”.
$ cat openproject_body.json
{
"subject": "Fresh New Package",
"Description": {
"format": "markdown",
"raw": "howdy howdy",
"html": "<p>howdy Howdy</p>"
},
"scheduleManually": false,
"startDate": "2024-02-12",
"dueDate": "2024-03-12",
"estimatedTime": null,
"_links": {
"project": {
"href": "/api/v3/projects/3",
"title": "MyFreshBrewedProject"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -u "apikey:6c67xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1" https://openproject.freshbrewed.science/api/v3/work_packages -d @openproject_body.json
{"derivedStartDate":null,"derivedDueDate":null,"spentTime":"PT0S","laborCosts":"0.00 EUR","materialCosts":"0.00 EUR","overallCosts":"0.00 EUR","_embedded":{"attachments":{"_type":"Collection","total":0,"count":0,"_embedded":{"elements":[]},"_links":{"self":{"href":"/api/v3/work_packages/40/attachments"}}},"relations":{"_type":"Collection","total":0,"count":0,"_embedded":{"elements":[]},"_links":{"self":{"href":"/api/v3/work_packages/40/relations"}}},"type":{"_type":"Type","id":1,"name":"Task","color":"#1A67A3","position":1,"isDefault":true,"isMilestone":false,"createdAt":"2024-02-11T22:14:51.378Z","updatedAt":"2024-02-11T22:14:51.378Z","_links":{"self":{"href":"/api/v3/types/1","title":"Task"}}},"priority":{"_type":"Priority","id":8,"name":"Normal","position":2,"color":"#74C0FC","isDefault":true,"isActive":true,"_links":{"self":{"href":"/api/v3/priorities/8","title":"Normal"}}},"project":{"_type":"Project","id":3,"identifier":"myfreshbrewedproject","name":"MyFreshBrewedProject","active":true,"public":true,"description":{"format":"markdown","raw":"This is my first project in OpenProject","html":"<p class=\"op-uc-p\">This is my first project in OpenProject</p>"},"createdAt":"2024-02-11T22:36:35.496Z","updatedAt":"2024-02-11T22:36:35.525Z","statusExplanation":{"format":"markdown","raw":"My Status is here","html":"<p class=\"op-uc-p\">My Status is here</p>"},"_links":{"self":{"href":"/api/v3/projects/3","title":"MyFreshBrewedProject"},"createWorkPackage":{"href":"/api/v3/projects/3/work_packages/form","method":"post"},"createWorkPackageImmediately":{"href":"/api/v3/projects/3/work_packages","method":"post"},"workPackages":{"href":"/api/v3/projects/3/work_packages"},"categories":{"href":"/api/v3/projects/3/categories"},"versions":{"href":"/api/v3/projects/3/versions"},"memberships":{"href":"/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"},"types":{"href":"/api/v3/projects/3/types"},"update":{"href":"/api/v3/projects/3/form","method":"post"},"updateImmediately":{"href":"/api/v3/projects/3","method":"patch"},"delete":{"href":"/api/v3/projects/3","method":"delete"},"schema":{"href":"/api/v3/projects/schema"},"status":{"href":"/api/v3/project_statuses/on_track","title":"On track"},"ancestors":[],"projectStorages":{"href":"/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"},"parent":{"href":null}}},"status":{"_type":"Status","id":1,"name":"New","isClosed":false,"color":"#1098AD","isDefault":true,"isReadonly":false,"defaultDoneRatio":null,"position":1,"_links":{"self":{"href":"/api/v3/statuses/1","title":"New"}}},"author":{"_type":"User","id":4,"name":"OpenProject Admin","createdAt":"2024-02-11T22:14:53.515Z","updatedAt":"2024-02-11T22:44:49.584Z","login":"admin","admin":true,"firstName":"OpenProject","lastName":"Admin","email":"admin@example.net","avatar":"https://secure.gravatar.com/avatar/cb4f282fed12016bd18a879c1f27ff97?default=404&secure=true","status":"active","identityUrl":null,"language":"en","_links":{"self":{"href":"/api/v3/users/4","title":"OpenProject Admin"},"memberships":{"href":"/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D","title":"Members"},"showUser":{"href":"/users/4","type":"text/html"},"updateImmediately":{"href":"/api/v3/users/4","title":"Update admin","method":"patch"},"lock":{"href":"/api/v3/users/4/lock","title":"Set lock on admin","method":"post"}}},"customActions":[],"costsByType":{"_type":"Collection","total":0,"count":0,"_embedded":{"elements":[]},"_links":{"self":{"href":"/api/v3/work_packages/40/summarized_costs_by_type"}}}},"_type":"WorkPackage","id":40,"lockVersion":0,"subject":"Fresh New Package","description":{"format":"markdown","raw":"","html":""},"scheduleManually":false,"startDate":"2024-02-12","dueDate":"2024-03-12","estimatedTime":null,"derivedEstimatedTime":null,"remainingTime":null,"derivedRemainingTime":null,"duration":"P22D","ignoreNonWorkingDays":false,"percentageDone":0,"createdAt":"2024-02-12T12:32:56.491Z","updatedAt":"2024-02-12T12:32:56.491Z","_links":{"attachments":{"href":"/api/v3/work_packages/40/attachments"},"addAttachment":{"href":"/api/v3/work_packages/40/attachments","method":"post"},"update":{"href":"/api/v3/work_packages/40/form","method":"post"},"schema":{"href":"/api/v3/work_packages/schemas/3-1"},"updateImmediately":{"href":"/api/v3/work_packages/40","method":"patch"},"delete":{"href":"/api/v3/work_packages/40","method":"delete"},"logTime":{"href":"/api/v3/time_entries","title":"Log time on Fresh New Package"},"move":{"href":"/work_packages/40/move/new","type":"text/html","title":"Move Fresh New Package"},"copy":{"href":"/work_packages/40/copy","title":"Copy Fresh New Package"},"pdf":{"href":"/work_packages/40.pdf","type":"application/pdf","title":"Export as PDF"},"atom":{"href":"/work_packages/40.atom","type":"application/rss+xml","title":"Atom feed"},"availableRelationCandidates":{"href":"/api/v3/work_packages/40/available_relation_candidates","title":"Potential work packages to relate to"},"customFields":{"href":"/projects/myfreshbrewedproject/settings/custom_fields","type":"text/html","title":"Custom fields"},"configureForm":{"href":"/types/1/edit?tab=form_configuration","type":"text/html","title":"Configure form"},"activities":{"href":"/api/v3/work_packages/40/activities"},"availableWatchers":{"href":"/api/v3/work_packages/40/available_watchers"},"relations":{"href":"/api/v3/work_packages/40/relations"},"watchers":{"href":"/api/v3/work_packages/40/watchers"},"addWatcher":{"href":"/api/v3/work_packages/40/watchers","method":"post","payload":{"user":{"href":"/api/v3/users/{user_id}"}},"templated":true},"removeWatcher":{"href":"/api/v3/work_packages/40/watchers/{user_id}","method":"delete","templated":true},"addRelation":{"href":"/api/v3/work_packages/40/relations","method":"post","title":"Add relation"},"addChild":{"href":"/api/v3/projects/myfreshbrewedproject/work_packages","method":"post","title":"Add child of Fresh New Package"},"changeParent":{"href":"/api/v3/work_packages/40","method":"patch","title":"Change parent of Fresh New Package"},"addComment":{"href":"/api/v3/work_packages/40/activities","method":"post","title":"Add comment"},"previewMarkup":{"href":"/api/v3/render/markdown?context=/api/v3/work_packages/40","method":"post"},"timeEntries":{"href":"/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2240%22%5D%7D%7D%5D","title":"Time entries"},"category":{"href":null},"type":{"href":"/api/v3/types/1","title":"Task"},"priority":{"href":"/api/v3/priorities/8","title":"Normal"},"project":{"href":"/api/v3/projects/3","title":"MyFreshBrewedProject"},"status":{"href":"/api/v3/statuses/1","title":"New"},"author":{"href":"/api/v3/users/4","title":"OpenProject Admin"},"responsible":{"href":null},"assignee":{"href":null},"version":{"href":null},"logCosts":{"href":"/work_packages/40/cost_entries/new","type":"text/html","title":"Log costs on Fresh New Package"},"showCosts":{"href":"/projects/3/cost_reports?fields%5B%5D=WorkPackageId&operators%5BWorkPackageId%5D=%3D&set_filter=1&values%5BWorkPackageId%5D=40","type":"text/html","title":"Show cost entries"},"costsByType":{"href":"/api/v3/work_packages/40/summarized_costs_by_type"},"meetings":{"href":"/work_packages/40/tabs/meetings","title":"meetings"},"github_pull_requests":{"href":"/api/v3/work_packages/40/github_pull_requests","title":"GitHub pull requests"},"self":{"href":"/api/v3/work_packages/40","title":"Fresh New Package"},"unwatch":{"href":"/api/v3/work_packages/40/watchers/4","method":"delete"},"ancestors":[],"parent":{"href":null,"title":null},"customActions":[]}}
I can now see the new work package in my list
While the subject looks right and the due dates were set, the Description was not
I typed in a field and then fetched with the API to see my mistake, I should have used lowercase for description
"description": {
"format": "markdown",
"raw": "This is me entering it",
"html": "<p class=\"op-uc-p\">This is me entering it</p>"
},
Let’s try one more time
$ cat openproject_body.json
{
"subject": "Fresh New Package 2",
"description": {
"format": "markdown",
"raw": "Trying again",
"html": "<p>trying again</p>"
},
"scheduleManually": false,
"startDate": "2024-02-14",
"dueDate": "2024-03-14",
"estimatedTime": null,
"_links": {
"project": {
"href": "/api/v3/projects/3",
"title": "MyFreshBrewedProject"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -u "apikey:6c67xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1" https://openproject.freshbrewed.science/api/v3/work_packages -d @openproject_body.json
{"derivedStartDate":null,"derivedDueDate":null,"spentTime":"PT0S","laborCosts":"0.00 EUR","materialCosts":"0.00 EUR","overallCosts":"0.00 EUR","_embedded":{"attachments":{"_type":"Collection","total":0,"count":0,"_embedded":{"elements":[]},"_links":{"self":{"href":"/api/... snip ...
That worked (and it picked the casing from “raw” i might add):
Addendum
I was trying to create some kind of art header for the article so I looked up their Berlin address on Google Maps. They are in the same city block as “The Disgusting Food Museum” with such wonderfully German reviews “Its good, but it’s terrible”
Summary
Today we covered the installation of OpenProject in Kubernetes and Docker (then Exposed through Kubernetes). We covered setup and initial usage, such as adding Work Packages to a board.
We looked into the Enterprise costs (and Advertisements) as well as subscription options. Lastly, we touched on webhooks and the REST API to reading and creating issues.
I actually have so much to cover it warranted a part two where we will dig into the SaaS offering, custom fields, time management, costs, meetings and more. Expect that in the coming days