ZenTao: OS PjM

Published: Mar 5, 2024 by Isaac Johnson

ZenTao, named after the Chinese characters for “zen” and “tao,” was founded in 2009 by Attwell Wang and provides an Open-Source project management suite that is surprisingly expansive.

Today we’ll setup the Open-Source community edition using Docker and exposing with Kubernetes for TLS ingress. We’ll cover many, but not all of the features (this suite is huge!). I’ll touch on Project, Work Packages, Screens, Modules, Department and Users. We’ll look into “Spaces” and backups and wrap looking at pricing.

Company

ZenTao, founded in 2009 by ChungSheng (Attwell) Wang is based in Qingdao, China with somewhere between 50 and 1000 employees (no official details. There are 35 employees with LinkedIn profiles). The “Zen” and “Tao” are mean to “embody its roots in seeking harmony and flow within the software development process”. They are seed company with the last round in 2021.

Licenses

Up until 2022, they primarily used the GPL/LGPL licensing to open-source their product. However, to protect logos and links, they moved to a dual licensing model under the Z Public License (ZPL) and the Affero General Public License (AGPL).

The ZPL is a license Nature Easy Corp (the original name of the company) created. The AGPL is a free software license that serves to protect the rights of the users of the software. It is meant to ensure that the users have the freedom to use, study, share, and modify the software. Their site has quite a writeup on Open-Source licensing and why they chose what they did.

Installation

Let’s start with a docker installation. We can follow the steps from the installation page

I’ll pull a recent release

builder@builder-T100:~$ docker pull easysoft/zentao:18.10
18.10: Pulling from easysoft/zentao
b7f91549542c: Pull complete 
6fc18e9d8534: Pull complete 
2983c1dd6d30: Pull complete 
dd480174791a: Pull complete 
a8847cac3d8f: Pull complete 
5a3dfd58e1ad: Pull complete 
2660e43392bd: Pull complete 
d15bee56465f: Pull complete 
a43092a20eb7: Pull complete 
4e8dfdabdb52: Pull complete 
4f4fb700ef54: Pull complete 
0a7ac94fb350: Pull complete 
8ee05c68845c: Pull complete 
ce2b53f47bc2: Pull complete 
4443e5c4fa61: Pull complete 
e3ea9361327c: Pull complete 
660fcd97fabe: Pull complete 
Digest: sha256:65cd5f01db6d18e89c6594df30f7613a10cfea9016e2bc2adb05506f686991a2
Status: Downloaded newer image for easysoft/zentao:18.10
docker.io/easysoft/zentao:18.10

I’ll create a couple of docker volumes

builder@builder-T100:~$ docker volume create zentaodb
zentaodb
builder@builder-T100:~$ docker volume create zentaodata
zentaodata

Now I can use those values to launch an instance

builder@builder-T100:~$ sudo docker run --name zentao -p 4490:80 -v zentaodata:/www/zentaopms -v zentaodb:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=MyDbP4ssW0rd -d easysoft/zentao:18.10
4e6ca2cd2f8bf74e8e3faa7956a8715219bbc6e9489ecda11d55a04e862ae1bd

I’ll now create an A record

$ cat r53-zentao.json
{
    "Comment": "CREATE zentao fb.s A record ",
    "Changes": [
      {
        "Action": "CREATE",
        "ResourceRecordSet": {
          "Name": "zentao.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-zentao.json
{
    "ChangeInfo": {
        "Id": "/change/C06168121RGUQIZZSA4B9",
        "Status": "PENDING",
        "SubmittedAt": "2024-02-13T01:55:19.036Z",
        "Comment": "CREATE zentao fb.s A record "
    }
}

Once applied, I can create an external endpoint and rule to send Ingress traffic there

$ cat ingress.zentao.yaml
---
apiVersion: v1
kind: Endpoints
metadata:
  name: zentao-external-ip
subsets:
- addresses:
  - ip: 192.168.1.100
  ports:
  - name: zentao
    port: 4490
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: zentao-external-ip
spec:
  clusterIP: None
  clusterIPs:
  - None
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  ports:
  - name: zentao
    port: 80
    protocol: TCP
    targetPort: 4490
  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: zentao-external-ip
  generation: 1
  labels:
    app.kubernetes.io/instance: zentaoingress
  name: zentaoingress
spec:
  rules:
  - host: zentao.freshbrewed.science
    http:
      paths:
      - backend:
          service:
            name: zentao-external-ip
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - zentao.freshbrewed.science
    secretName: zentao-tls

$ kubectl apply -f ./ingress.zentao.yaml
endpoints/zentao-external-ip created
service/zentao-external-ip created
ingress.networking.k8s.io/zentaoingress created

When I was unable to connect, I realized that ZenTao crashed on startup

builder@builder-T100:~$ docker logs zentao
 09:50:05.75 
 09:50:05.75 Welcome to the Easysoft ZenTao 18.10 container
 09:50:05.75 Subscribe to project updates by watching https://www.zentao.net
 09:50:05.75 Submit issues and feature requests at https://www.zentao.net/ask.html
 09:50:05.75 
 09:50:05.76 INFO  ==> Prepare persistence directories.
 09:50:05.95 INFO  ==> Render php.ini with environment variables.
 09:50:05.96 INFO  ==> Check zentao data owner...
 09:50:05.97 INFO  ==> Render apache sites config with envionment variables.
 09:50:05.98 INFO  ==> Prepare custom extensions.
 09:50:06.01 INFO  ==> Check whether the MySQL is available.
 09:50:06.01 INFO  ==> Check whether the Apache is available.
 09:50:07.02 WARN  ==> Apache: Waiting MySQL 1 seconds
 09:50:07.02 WARN  ==> Sentry: Waiting Apache 1 seconds
 09:50:09.02 WARN  ==> Apache: Waiting MySQL 2 seconds
 09:50:09.02 WARN  ==> Sentry: Waiting Apache 2 seconds
 09:50:13.03 WARN  ==> Apache: Waiting MySQL 4 seconds
 09:50:13.03 WARN  ==> Sentry: Waiting Apache 4 seconds
 09:50:21.03 WARN  ==> Apache: Waiting MySQL 8 seconds
 09:50:21.03 WARN  ==> Sentry: Waiting Apache 8 seconds
 09:50:37.04 WARN  ==> Apache: Waiting MySQL 16 seconds
 09:50:37.04 WARN  ==> Sentry: Waiting Apache 16 seconds
 09:51:09.04 WARN  ==> Apache: Waiting MySQL 32 seconds
 09:51:09.05 WARN  ==> Sentry: Waiting Apache 32 seconds
 09:51:09.05 ERROR ==> Apache Maximum number of retries reached!
 09:51:09.05 ERROR ==> Apache Unable to connect to MySQL: 127.0.0.1:3306
 09:51:09.05 ERROR ==> Sentry Maximum number of retries reached!
 09:51:09.05 ERROR ==> Sentry Unable to connect to Apache: 127.0.0.1:80

I changed instead to using Docker-compose:

version: "2.2"
services: 
  zentao:
    image: idoop/zentao:latest
    container_name: zentao
    # if web response code: 310 ERR_TOO_MANY_REDIRECTS, please use host mode.
#    network_mode: "host"
    ports:
      - "80:80"
      - "3306:3306"
    # mysql root account default password is '123456'.
    # the zentao adminstrator account is 'admin',and init password is '123456'.
    environment:
    # specifies Adminer account and password for web login database.
      USER: "root"
      PASSWD: "123456"
      BIND_ADDRESS: "false"
    extra_hosts:
      - "smtp.exmail.qq.com:163.177.90.125"
    volumes:
      - ./data:/opt/zbox/
    restart: always

That worked

/content/images/2024/03/zentao-01.png

And once I switched to English, I could see the login page

/content/images/2024/03/zentao-02.png

The initial password for the “admin” user is “123456”. We can login with that and it prompts us to immediately change passwords

/content/images/2024/03/zentao-03.png

While many of the menus are in English, the setup users and groups were not. Here we see the “Test” user in “Testers” group (ID 3), albeit in Chinese

/content/images/2024/03/zentao-04.png

While I couldn’t read the Plugins, the links went to a Plugin marketplace that converted it to English for me

/content/images/2024/03/zentao-05.png

Projects

Let’s start by creating a Project of type Scrum

/content/images/2024/03/zentao-06.png

and set some details

/content/images/2024/03/zentao-07.png

I now have a project, but it’s a bit empty

/content/images/2024/03/zentao-08.png

Let’s create our first Sprint (Iteration)

/content/images/2024/03/zentao-09.png

Now created, we can click “Create task”

/content/images/2024/03/zentao-10.png

I’ll fill in some details

/content/images/2024/03/zentao-11.png

I now have a spike ticket in the sprint

/content/images/2024/03/zentao-12.png

In the details for the task, I can see duration and other details

/content/images/2024/03/zentao-13.png

I’ll click start and enter a time estimate

/content/images/2024/03/zentao-14.png

We can see we have moved into the state of “Doing” with a start time (albeit not my timezone)

/content/images/2024/03/zentao-15.png

Clicking “Create child task” gives me a pane with a lot more options

/content/images/2024/03/zentao-16.png

System settings

I realized the default timezone was set to Shanghai. We can change that in admin/timezone

/content/images/2024/03/zentao-17.png

Users

We’ve been using the admin user and a “test test” user. Let’s create a real identity we can use

I’ll want to configure email settings. I start in Admin Message Mail

/content/images/2024/03/zentao-19.png

I can now add my SMTP details. Here I’ll use Sendgrid

/content/images/2024/03/zentao-20.png

It saved, but I got a warning about my users missing email addresses

/content/images/2024/03/zentao-21.png

I’ll update my admin user email

/content/images/2024/03/zentao-22.png

Now I see an email sending test option on there

/content/images/2024/03/zentao-23.png

I did a test

/content/images/2024/03/zentao-24.png

But got an error

/content/images/2024/03/zentao-25.png

I changed the port and protocol

/content/images/2024/03/zentao-26.png

This time it worked just fine

/content/images/2024/03/zentao-27.png

and i could see a test mail

/content/images/2024/03/zentao-28.png

I started to add the user but realized I would struggle on Privilege Groups

/content/images/2024/03/zentao-29.png

I can now save the user (though it’s a guess on my part for groups)

/content/images/2024/03/zentao-30.png

I thought perhaps it would use email for password resets, however, if a user tries to login and selects “Forgot password” they see this

/content/images/2024/03/zentao-31.png

Departments

I had to use a translator to realize “禅道软件” translated to “ZenTao Software”. So these sub menus are under “ZenTao Software”

/content/images/2024/03/zentao-32.png

I added some sub-departments

/content/images/2024/03/zentao-33.png

If I switch back to Admin, I can assign users to Departments

/content/images/2024/03/zentao-34.png

I can see the Department now in the User’s profile

/content/images/2024/03/zentao-35.png

This applies to the index page

/content/images/2024/03/zentao-39.png

Translated

/content/images/2024/03/zentao-40.png

Later, I realized we can change our company name in settings

/content/images/2024/03/zentao-51.png

Which I could verify in the Departments definition area

/content/images/2024/03/zentao-53.png

Modules

We can create Modules to assign to our Stories

/content/images/2024/03/zentao-54.png

We can more than five after saving. Modules can also have submodules

/content/images/2024/03/zentao-55.png

We can then use them when defining a story

/content/images/2024/03/zentao-56.png

What I tend to call Sprints or Iterations, they call “Project Plans”. So here we can create a 1 month Project Plan

/content/images/2024/03/zentao-57.png

For which we can then link our stories

/content/images/2024/03/zentao-58.png

Screens

There are “BI” Screens that if we let Google Translate kick in, we can view

/content/images/2024/03/zentao-36.png

So while they are in Chinese

/content/images/2024/03/zentao-37.png

Using translate:

/content/images/2024/03/zentao-38.png

Spaces

We can create a “Space”

/content/images/2024/03/zentao-41.png

With the space created, we can create a Kanban board

/content/images/2024/03/zentao-42.png

From there we can create a Card

/content/images/2024/03/zentao-43.png

I’ll create an initial Spike card for auditing systems

/content/images/2024/03/zentao-44.png

Like one would expect, I can drag the card around to change states

/content/images/2024/03/zentao-45.png

If I go to settings, I can change WIP settings

/content/images/2024/03/zentao-46.png

Such as setting a WIP limit on “doing” / active

/content/images/2024/03/zentao-47.png

For instance, if I set to 2 and create 3 cards in “Doing” we can see we exceeded our Work In Progress limit

/content/images/2024/03/zentao-48.png

Backups

We can see daily backups under the Backus tab

/content/images/2024/03/zentao-18.png

I can then copy from the container:

/content/images/2024/03/zentao-49.png

I’ll copy the files down and tgz them

$ docker cp zentao:/opt/zbox/app/zentao/tmp/backup/202402190030227.code /tmp/zentao-202402190030227.code
$ docker cp zentao:/opt/zbox/app/zentao/tmp/backup/202402190030227.file /tmp/zentao-202402190030227.file
$ docker cp zentao:/opt/zbox/app/zentao/tmp/backup/202402190030227.sql.php /tmp/zentao-202402190030227.sql.php
$ tar -czvf /tmp/zentao-202402190030227.tgz /tmp/zentao-202402190030227.code /tmp/zentao-202402190030227.file  /tmp/zentao-202402190030227.sql.php

And now I have a nice compressed backup I could send off to a NAS or other backup medium

$ ls -ltrah /tmp | grep zentao
-rwxrwxrwx  1 builder builder 5.7M Feb 18 10:30 zentao-202402190030227.sql.php
drwxrwxrwx  6 builder builder 4.0K Feb 18 10:30 zentao-202402190030227.file
drwxrwxrwx 16 builder builder 4.0K Feb 18 10:30 zentao-202402190030227.code
-rw-rw-r--  1 builder builder  87M Feb 18 12:58 zentao-202402190030227.tgz

Pricing

There is quite a lot included in the free community edition. However, if you want some features like Task or Project Gantt Charts, you need to move to a Biz or Enterprise edition license

/content/images/2024/03/zentao-50.png

Seems there is a chatbot (mine is called Philip as you see above), that might offer a 10% discount.

Summary

I came into this post thinking I had a weird foreign app that would likely just be a fail. I really was taken back by how full-featured this entire suite is. Once I got past my English-bias - getting scared and confused by non-ASCII defaults - I found it encompasses everything I could want in a Project and Product management suite. Often, we get tools focused on PjM that are very agile-centric (JIRA, Rallydev, Azure Work Items) but then they do little to account for Waterfall, Product release groupings or cost. Here we get it all. For the small cost of having to work through some Chinese Simplified menus (most I could tweak when it was key), we land with something more than useable for tracking and rolling out projects.

While not perfect and at times using terminology outside the norm, it is fully Open-Source so one can take it and tweak it to one’s desire. I’ll likely continue to experiment with this and add it to my core tools. I would love to circle back on helm eventually or at least use an external PostgreSQL, but for now, this version is very usable as hosted in Docker and exposed through Kubernetes.

ZenTao OpenSource PjM

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes