Renovate with docker containers

date:

tags: renovate tools

categories: Utilities

Renovate is used to help manage code dependencies. Checkout a previous post for more details on what Renovate is.

Renovate is able to parse dockerfiles to look for newer container images. This only works with either a sha256 digest or a version tag. Tags such as latest or some other arbitrary string will not work.

I tested running renovate as a docker container. Credentials can be used with environment variables within the container.

1
docker run --rm -e RENOVATE_TOKEN=$GITHUB_TOKEN -e RENOVATE_PLATFORM=github -e RENOVATE_AUTODISCOVER=true docker.io/renovate/renovate

With autodiscover enabled, renovate checked out all of the repos that the token had access to. Pull requests were opened in repositories that renovate detected with dependencies.

Here is an example of log output for a repo with only docker detected:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 INFO: Repository started (repository=acaylor/dockertest)
       "renovateVersion": "37.108.1"
 INFO: Branch created (repository=acaylor/dockertest, branch=renovate/configure)
       "commit": "8a094b8c7e9ece2ad27091f5cce6c3dd66b81672",
       "onboarding": true
 INFO: Dependency extraction complete (repository=acaylor/dockertest, baseBranch=master)
       "stats": {
         "managers": {"dockerfile": {"fileCount": 1, "depCount": 1}},
         "total": {"fileCount": 1, "depCount": 1}
       }
 INFO: Onboarding PR created (repository=acaylor/dockertest)
       "pr": "Pull Request #1"
 INFO: Repository finished (repository=acaylor/dockertest)
       "cloned": true,
       "durationMs": 5883

Here is an example of a PR that renovate will open to update a container image.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding MR to help you understand and   configure settings before regular Merge Requests begin.

🚦 To activate Renovate, merge this Merge Request. To disable Renovate, simply close this Merge Request unmerged.



---
### Detected Package Files

* `docker-compose.yml` (docker-compose)
* `Dockerfile` (dockerfile)

### What to Expect

With your current configuration, Renovate will create 1 Merge Request:

<details>
<summary>Update k8s.gcr.io/echoserver Docker tag to v1.10</summary>

 - Schedule: ["at any time"]
 - Branch name: `renovate/k8s.gcr.io-echoserver-1.x`
 - Merge into: `main`
 - Upgrade k8s.gcr.io/echoserver to `1.10`


</details>


---

❓ Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started        section.
If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/     discussions).


---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

<!--renovate-config-hash:94693a990c975907e7f13da3309b9d56ba02b3983519b41786edf5cf031e457c-->

While renovate can generate pull requests to update dependencies, the logs can also be used to get more information.


Run renovate with debug logs and output to a file

Now with debug logs enabled, the data presented via pull requests to you is also available on the command line. I wrote this script to parse data from renovate debug logs.

I can run renovate as a container and redirect the debug output to a file.

1
docker run --rm -e LOG_LEVEL=debug -e LOG_FORMAT=json -e RENOVATE_TOKEN=$GITEA_TOKEN -e RENOVATE_PLATFORM=gitea -e RENOVATE_ENDPOINT="https://git.lan.ayjc.net/api/v1/" -e RENOVATE_AUTODISCOVER=true -e RENOVATE_AUTODISCOVER_FILTER="aj/containers_*" docker.io/renovate/renovate > renovate.log

This should create a file renovate.log

I have a solid foundation for getting more structured data from renovate.

  • Run renovate with logs set to DEBUG and json format.
    • This produces a massive file over 10MB with 20k lines of json.
  • Parse the renovate logs
  • Store the dependency data in a database

Parsing log file full of json objects

There are a few approaches to parsing the debug logs. I used Python.

select log lines

The debug log file will have 20k lines of output. Before importing data into a database, we should clean the output. The logs will be JSON {} objects but not properly enclosed within an array.

Using Python, we can parse the initial log file into one JSON array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json

# Open renovate log file
with open('renovate.log', 'r') as f_in, open('renovate-clean.json', 'w') as f_out:
    data = []

    # iterate over every line of the log fine
    for line in f_in:
        try:
            # parse the line as JSON
            obj = json.loads(line)
            # print("loading log file")

            data.append(obj)
            # print("appending line to new file")
        except ValueError:
            # ignore lines that cannot be parsed as json
            print('line is not JSON...ignoring...')
            pass

    # convert obj back into JSON and write to a new file
    print('Writing to file')
    json.dump(data, f_out)
1
Writing to file

This will create a file in the current directory that is a JSON array.

Optionally, jq can be used to make the file more human readable:

1
cat renovate-clean.json | jq > clean-jq.json

Parse the remaining data

The remaining data can now be parsed using Python.

This code will open a JSON file that needs to be valid JSON hence in the previous step I transformed the log file into a list of objects.

Then using the psycopg2 library, we connect to a PostgreSQL database and create a table to store dependency data. Install docs

Postgres supports JSON data type and using the jsonb column type will allow for querying the data within each row with JSON data.

Each row will contain:

  • The git repository
  • the file where the dependency is located
  • the data related to the dependency from Renovate logs

I have chosen to parse the data and write it to a postgresql database. There are other approaches that do not require a database server such as using a sqlite3 database which is contained in one file.

Run a postgres server with docker

1
2
3
4
5
6
7
8
9
docker run \
--env=POSTGRES_USER=renovate \
--env=POSTGRES_PASSWORD=renovate \
--env=POSTGRES_DB=renovate \
--volume=/var/lib/postgresql/data \
-p 5432:5432 \
--restart=no \
-d \
postgres

This will pull the image from the public docker hub and run it on your machine if you have docker installed. Eg. swap docker for podman if you use podman.

Install this python package to interface with a PostgreSQL database.

1
pip install psycopg2-binary
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

    Collecting psycopg2-binary
      Downloading psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl.metadata (4.4 kB)
    Downloading psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl (2.6 MB)
       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 8.5 MB/s eta 0:00:00 MB/s eta 0:00:01:02
    [?25hInstalling collected packages: psycopg2-binary
    Successfully installed psycopg2-binary-2.9.9
    
    [notice] A new release of pip is available: 24.0 -> 24.2
    [notice] To update, run: /opt/homebrew/Cellar/jupyterlab/4.2.3/libexec/bin/python -m pip install --upgrade pip
    Note: you may need to restart the kernel to use updated packages.

This is the script described above

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import json
import psycopg2
from psycopg2.extras import Json


# Define the file to parse
file = 'renovate-clean.json'
depType = "docker-compose"
# Open renovate log file
with open(file) as f:
    data = json.load(f)

# Connect to the database
conn = psycopg2.connect(dbname='renovate', user='renovate', password='renovate', host='localhost')
cursor = conn.cursor()

# Create a table for renovate if it doesn't exist
cursor.execute('''CREATE TABLE IF NOT EXISTS renovate (
    id SERIAL PRIMARY KEY,
    repository text,
    packagefile text,
    data JSONB
)''')

# Assuming data is a list of dictionaries
for item in data:
    # if the item does not contain config, skip it
    if 'config' not in item or depType not in item['config']:
        print("No config found in item")
        continue

    # iterate through the items
    for config_item in item['config'][depType]:
        # if the config_item does not contain 'deps', skip it
        if 'deps' not in config_item:
            print("No deps found in config item")
            continue
        # iterate through the dependencies
        for dep_item in config_item['deps']:
            # convert the 'dep' object into JSON
            dep_json = json.dumps(dep_item)
            cursor.execute("INSERT INTO renovate (repository, packagefile, data) VALUES (%s, %s, %s)", (item['repository'], config_item['packageFile'], dep_json))
            print(f"Writing {config_item['packageFile']} to database")

# Commit the changes
conn.commit()
# Close the connection
cursor.close()
conn.close()

print('Done parsing renovate data from log file')

Here is example output not including many lines where no dependencies were found:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    Writing adguard/docker-compose.yml to database
    Writing caddy/docker-compose.yml to database
    Writing cadvisor/docker-compose.yml to database
    Writing dokuwiki/docker-compose.yml to database
    Writing fluent-bit/docker-compose.yml to database
    Writing gitea/docker-compose.yml to database
    Writing ha/docker-compose.yml to database
    Writing kanboard/docker-compose.yml to database
    Writing node-exporter/docker-compose.yml to database
    Writing pihole/docker-compose.yml to database
    Writing portainer/docker-compose.yml to database
    Writing prom/docker-compose.yml to database
    Writing prom/docker-compose.yml to database
    Writing prom/docker-compose.yml to database
    Writing prometheus/docker-compose.yml to database
    Writing prometheus/docker-compose.yml to database
    Writing prometheus/docker-compose.yml to database
    Writing prometheus/docker-compose.yml to database
    Writing prometheus/docker-compose.yml to database
    Writing promtail/docker-compose.yml to database
    Writing proxy-manager/docker-compose.yml to database
    Writing proxy-manager/docker-compose.yml to database
    Writing unpoller/docker-compose.yml to database
    Writing wireguard/docker-compose.yml to database
...
    Writing adguard/docker-compose.yml to database
    Writing cadvisor/docker-compose.yml to database
    Writing promtail/docker-compose.yml to database
    No config found in item
...
    Writing adguard-sync/docker-compose.yml to database
    Writing adguard/docker-compose.yml to database
    Writing archived/adguard-exporter/docker-compose.yml to database
    Writing archived/nginx-exporter/docker-compose.yml to database
    Writing archived/unpoller/docker-compose.yml to database
    Writing caddy/docker-compose.yml to database
    Writing nut-exporter/docker-compose.yml to database
    Writing prometheus-stack/docker-compose.yml to database
    Writing prometheus-stack/docker-compose.yml to database
    Writing prometheus-stack/docker-compose.yml to database
    Writing prometheus-stack/docker-compose.yml to database
    Writing promtail/docker-compose.yml to database
    Writing scrypted/docker-compose.yml to database
...
    No config found in item
    No config found in item
    No config found in item
    Done parsing renovate data from log file

Assuming the program executes without errors, the specified database should have data in the renovate table.

1
SELECT * from renovate;
1
"id","repository","packagefile","data"

querying dependency data

For example, to query all packages that use a certain dependency:

1
select * from renovate where data->>'depName' = 'foo';
1
"id","repository","packagefile","data"

The dependency name is in a column data that is a jsonb type of data.

The data.updates[] array shows if there are updates available for the dependency. We can only show rows with an update proposed:

1
select * from renovate where jsonb_path_exists(data, '$.updates[*]."bucket"');

To summarize all of the dependencies in a file:

1
select packagefile,string_agg(data->>'packageName', ';') as dependency from renovate group by packagefile order by packagefile;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"packagefile","dependency"
adguard/docker-compose.yml,adguard/adguardhome;adguard/adguardhome;adguard/adguardhome
adguard-sync/docker-compose.yml,lscr.io/linuxserver/adguardhome-sync
archived/adguard-exporter/docker-compose.yml,ghcr.io/henrywhitaker3/adguard-exporter
archived/nginx-exporter/docker-compose.yml,docker.io/nginx/nginx-prometheus-exporter
archived/unpoller/docker-compose.yml,golift/unifi-poller
caddy/docker-compose.yml,caddy;caddy
cadvisor/docker-compose.yml,gcr.io/cadvisor/cadvisor-arm64;gcr.io/cadvisor/cadvisor
dokuwiki/docker-compose.yml,lscr.io/linuxserver/dokuwiki
fluent-bit/docker-compose.yml,grafana/fluent-bit-plugin-loki
gitea/docker-compose.yml,docker.io/gitea/gitea
ha/docker-compose.yml,ghcr.io/home-assistant/home-assistant
kanboard/docker-compose.yml,kanboard/kanboard
node-exporter/docker-compose.yml,quay.io/prometheus/node-exporter
nut-exporter/docker-compose.yml,hon95/prometheus-nut-exporter
pihole/docker-compose.yml,pihole/pihole
portainer/docker-compose.yml,portainer/portainer-ce
prom/docker-compose.yml,prom/prometheus;quay.io/prometheus/node-exporter;docker.io/grafana/grafana-oss
prometheus/docker-compose.yml,prom/prometheus;quay.io/prometheus/node-exporter;golift/unifi-poller;prom/blackbox-exporter;grafana/grafana-oss
prometheus-stack/docker-compose.yml,quay.io/prometheus/prometheus;quay.io/prometheus/node-exporter;docker.io/grafana/grafana;gcr.io/cadvisor/cadvisor
promtail/docker-compose.yml,grafana/promtail;grafana/promtail;grafana/promtail
proxy-manager/docker-compose.yml,jc21/nginx-proxy-manager;jc21/mariadb-aria
scrypted/docker-compose.yml,ghcr.io/koush/scrypted
unpoller/docker-compose.yml,golift/unifi-poller
wireguard/docker-compose.yml,lscr.io/linuxserver/wireguard

Clean up database

If you want to keep using the postgres database, you can clear out all rows from the renovate table either with SQL or python:

1
TRUNCATE TABLE renovate RESTART IDENTITY;

Add this to the python script. Hint: before the command to insert new values into the table.

1
cursor.execute(f"TRUNCATE TABLE {depType} RESTART IDENTITY")

Next steps

Now that data is in a database, there are many more queries that can be used by diving into SQL.

comments powered by Disqus