├── test ├── fixtures │ ├── build │ │ ├── Dockerfile │ │ └── goss.yaml │ ├── dib │ │ ├── wordlist.txt │ │ ├── wordlist.yml │ │ └── valid_wordlist.txt │ ├── docker │ │ ├── root1 │ │ │ ├── dockerignore │ │ │ │ ├── ignored.txt │ │ │ │ ├── .dockerignore │ │ │ │ └── Dockerfile │ │ │ ├── with-a-file │ │ │ │ ├── included.txt │ │ │ │ └── Dockerfile │ │ │ ├── sub1 │ │ │ │ ├── Dockerfile │ │ │ │ └── sub2 │ │ │ │ │ └── Dockerfile │ │ │ ├── skipbuild │ │ │ │ └── Dockerfile │ │ │ ├── multistage │ │ │ │ └── Dockerfile │ │ │ ├── Dockerfile │ │ │ └── custom-hash-list │ │ │ │ └── Dockerfile │ │ ├── root2 │ │ │ ├── Dockerfile │ │ │ └── root3 │ │ │ │ └── Dockerfile │ │ └── two-parents │ │ │ └── Dockerfile │ ├── dockerfile │ │ ├── simple.dockerfile │ │ ├── simple-arg.dockerfile │ │ ├── simple-tag.dockerfile │ │ ├── multistage.dockerfile │ │ ├── multistage-tag.dockerfile │ │ ├── multistage-alias.dockerfile │ │ ├── multistage-tag-alias.dockerfile │ │ ├── simple-digest.dockerfile │ │ ├── simple-tag-digest.dockerfile │ │ ├── multistage-digest.dockerfile │ │ ├── multistage-tag-digest.dockerfile │ │ ├── multistage-digest-alias.dockerfile │ │ └── multistage-tag-digest-alias.dockerfile │ ├── docker-duplicates │ │ └── root │ │ │ ├── Dockerfile │ │ │ ├── duplicate1 │ │ │ └── Dockerfile │ │ │ └── duplicate2 │ │ │ └── Dockerfile │ ├── junit │ │ ├── junit-invalid.xml │ │ ├── junit-image-test.xml │ │ └── junit-image-test-fail.xml │ └── report │ │ └── build_logs │ │ └── kaniko │ │ ├── 1_parsed_log.txt │ │ ├── 1_raw_log.txt │ │ ├── 2_parsed_log.txt │ │ └── 2_raw_log.txt ├── .dib.yaml ├── end2end │ ├── .gitignore │ ├── docker │ │ ├── base │ │ │ ├── alpine │ │ │ │ ├── 3.19 │ │ │ │ │ └── Dockerfile │ │ │ │ └── 3.20 │ │ │ │ │ └── Dockerfile │ │ │ └── debian │ │ │ │ └── bullseye │ │ │ │ ├── goss.yaml │ │ │ │ └── Dockerfile │ │ └── app │ │ │ ├── curl │ │ │ ├── Dockerfile │ │ │ └── goss.yaml │ │ │ ├── lorem │ │ │ └── Dockerfile │ │ │ └── git │ │ │ ├── Dockerfile │ │ │ └── goss.yaml │ ├── .dib.yaml │ ├── certs │ │ ├── registry.localhost.key.pem │ │ └── registry.localhost.cert.pem │ ├── .dib_list.tmpl │ ├── dib_images_list.md │ ├── Makefile │ └── docker-compose.yml ├── invalid_example.yml ├── example_1.yml └── example_2.yml ├── .github ├── CODEOWNERS ├── actionlint-matcher.json ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── yaml.yml │ ├── release.yml │ ├── docs.yml │ ├── security.yml │ └── qa.yml ├── requirements.txt ├── docs ├── examples │ └── quickstart │ │ ├── .dib.yaml │ │ └── docker │ │ ├── base │ │ └── Dockerfile │ │ └── child │ │ └── Dockerfile ├── images │ ├── favicon.ico │ ├── dib_logo.png │ └── dib_report.png ├── configuration-reference.md ├── extra-tags.md ├── roadmap.md ├── best-practices.md ├── reports.md ├── documentation.md ├── executors.md ├── configuration.md ├── faq.md ├── tests.md ├── install.md ├── quickstart.md └── backends.md ├── pkg ├── report │ ├── assets │ │ ├── favicon.ico │ │ ├── img │ │ │ └── dib.png │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ └── css │ │ │ ├── default-dark.min.css │ │ │ └── styles.css │ ├── doc.go │ ├── templates │ │ ├── graph.go.html │ │ ├── build.go.html │ │ ├── _functions.go.html │ │ ├── _layout.go.html │ │ ├── scan.go.html │ │ ├── debug.go.html │ │ └── _nav.go.html │ ├── utils_test.go │ ├── utils.go │ └── report.go ├── rootlessutil │ ├── rootless_unix.go │ ├── rootless_other.go │ └── rootless_linux.go ├── buildkit │ ├── buildkitutil_darwin.go │ ├── buildkitutil_unix.go │ ├── buildkitutil_linux.go │ ├── context_local.go │ ├── context_local_test.go │ └── s3.go ├── mock │ ├── ratelimiter.go │ ├── tagger.go │ ├── testrunner.go │ ├── registry.go │ ├── writer.go │ ├── builder.go │ └── executor.go ├── ratelimit │ ├── ratelimit.go │ └── channel.go ├── dib │ ├── builder.go │ ├── doc.go │ ├── test.go │ ├── tag.go │ ├── plan.go │ └── tag_test.go ├── testutil │ └── testutil.go ├── graphviz │ ├── doc.go │ ├── graphviz.go │ └── graphviz_test.go ├── dag │ ├── doc.go │ ├── node_test.go │ ├── image.go │ ├── image_test.go │ ├── node.go │ └── printer.go ├── kaniko │ ├── context_local_test.go │ ├── context_local.go │ ├── executor_docker_test.go │ ├── s3.go │ ├── executor_docker.go │ ├── context_remote_test.go │ └── context_remote_internal_test.go ├── goss │ ├── executor_dgoss_test.go │ ├── executor_containerd_test.go │ └── executor_dgoss.go ├── trivy │ ├── executor_local_test.go │ ├── executor_local.go │ └── runner_test.go ├── strutil │ ├── strutil.go │ └── strutil_test.go ├── kubernetes │ ├── types.go │ ├── pod_test.go │ └── exec.go ├── junit │ ├── xml.go │ └── xml_test.go ├── registry │ └── registry.go ├── preflight │ └── check.go ├── executor │ └── executor.go ├── exec │ ├── shell.go │ └── kubernetes.go ├── docker │ └── builder.go └── types │ └── types.go ├── main.go ├── examples ├── Dockerfile └── buildkitd-launcher ├── .gitignore ├── .yamllint ├── cmd ├── build_internal_test.go ├── version.go ├── docgen.go └── list.go ├── internal └── logger │ └── logger_test.go ├── .vscode ├── settings.json └── extensions.json ├── .idea └── runConfigurations │ ├── go test dib.run.xml │ ├── dib_version.xml │ ├── dib_build.xml │ ├── dib_list__console_.xml │ └── dib_list.xml ├── .devcontainer ├── devcontainer.json └── Dockerfile ├── hacks └── run-with-existing-containerd │ ├── Dockerfile │ └── buildkit-entrypoint.sh ├── .goreleaser.yml ├── README.md ├── Makefile ├── CONTRIBUTING.md ├── mkdocs.yml └── renovate.json /test/fixtures/build/Dockerfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.dib.yaml: -------------------------------------------------------------------------------- 1 | log_level: debug 2 | -------------------------------------------------------------------------------- /test/end2end/.gitignore: -------------------------------------------------------------------------------- 1 | reports 2 | -------------------------------------------------------------------------------- /test/fixtures/build/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /test/fixtures/dib/wordlist.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | -------------------------------------------------------------------------------- /test/fixtures/dib/wordlist.yml: -------------------------------------------------------------------------------- 1 | e 2 | f 3 | g 4 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/dockerignore/ignored.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/with-a-file/included.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @radiofrance/contributeurs-devops 2 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/dockerignore/.dockerignore: -------------------------------------------------------------------------------- 1 | ignored.txt 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | pymdown-extensions>=9.10 4 | -------------------------------------------------------------------------------- /docs/examples/quickstart/.dib.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | registry_url: registry.example.com 3 | -------------------------------------------------------------------------------- /test/fixtures/docker/root2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM apache/superset 2 | 3 | LABEL name="root2" 4 | -------------------------------------------------------------------------------- /docs/examples/quickstart/docker/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | LABEL name="base" 4 | -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/docs/images/favicon.ico -------------------------------------------------------------------------------- /test/fixtures/dockerfile/simple.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/example 2 | LABEL name="example" 3 | -------------------------------------------------------------------------------- /docs/images/dib_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/docs/images/dib_logo.png -------------------------------------------------------------------------------- /docs/images/dib_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/docs/images/dib_report.png -------------------------------------------------------------------------------- /test/fixtures/docker-duplicates/root/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | 3 | LABEL name="root" 4 | -------------------------------------------------------------------------------- /test/fixtures/docker/root2/root3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bitnami/elasticsearch 2 | 3 | LABEL name="root3" 4 | -------------------------------------------------------------------------------- /test/invalid_example.yml: -------------------------------------------------------------------------------- 1 | images: 2 | {{- range . }} 3 | - {{ .InexistingField }} 4 | {{- end }} 5 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/simple-arg.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/example:latest 2 | ARG HELLO="there" 3 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/simple-tag.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/example:latest 2 | LABEL name="example" 3 | -------------------------------------------------------------------------------- /pkg/report/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/pkg/report/assets/favicon.ico -------------------------------------------------------------------------------- /pkg/report/assets/img/dib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/pkg/report/assets/img/dib.png -------------------------------------------------------------------------------- /docs/examples/quickstart/docker/child/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.example.com/base:latest 2 | 3 | LABEL name="child" 4 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/sub1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root1:v1 2 | 3 | LABEL name="sub1" 4 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/sub1/sub2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/sub1 2 | 3 | LABEL name="sub2" 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/radiofrance/dib/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/skipbuild/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root1:v1 2 | 3 | LABEL skipbuild="true" 4 | -------------------------------------------------------------------------------- /pkg/report/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/pkg/report/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /test/fixtures/docker-duplicates/root/duplicate1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root 2 | 3 | LABEL name="duplicate" 4 | -------------------------------------------------------------------------------- /test/fixtures/docker-duplicates/root/duplicate2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root 2 | 3 | LABEL name="duplicate" 4 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/dockerignore/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root1:v1 2 | 3 | LABEL name="dockerignore" 4 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/with-a-file/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root1:v1 2 | 3 | LABEL name="with-a-file" 4 | -------------------------------------------------------------------------------- /test/fixtures/junit/junit-invalid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /pkg/report/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/pkg/report/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /pkg/report/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/pkg/report/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /pkg/report/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/pkg/report/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage-tag.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder:latest 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /pkg/report/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofrance/dib/HEAD/pkg/report/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage-alias.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder as builder 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /docs/configuration-reference.md: -------------------------------------------------------------------------------- 1 | Configuration Reference 2 | ======================= 3 | 4 | ```yaml 5 | --8<-- "examples/config/reference.yaml" 6 | ``` 7 | -------------------------------------------------------------------------------- /test/example_1.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | total: {{ len . }} 3 | dib_managed: 4 | {{- range . }} 5 | {{ .ShortName }}: {{ .Hash }} 6 | {{- end }} 7 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/multistage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root1:v1 as builder 2 | FROM vault 3 | 4 | LABEL name="multistage" 5 | -------------------------------------------------------------------------------- /test/fixtures/report/build_logs/kaniko/1_parsed_log.txt: -------------------------------------------------------------------------------- 1 | Retrieving image manifest eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona 2 | -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM moby/buildkit:master-rootless 2 | 3 | COPY buildkitd-launcher /usr/local/bin/buildkitd-launcher 4 | 5 | ENTRYPOINT ["buildkitd-launcher"] 6 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage-tag-alias.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder:latest as builder 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /pkg/rootlessutil/rootless_unix.go: -------------------------------------------------------------------------------- 1 | package rootlessutil 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func IsRootless() bool { 8 | return os.Geteuid() != 0 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | 3 | LABEL name="root1" 4 | LABEL version="v1" 5 | 6 | ARG HELLO="there" 7 | 8 | RUN echo "Hello $HELLO" 9 | -------------------------------------------------------------------------------- /pkg/buildkit/buildkitutil_darwin.go: -------------------------------------------------------------------------------- 1 | package buildkit 2 | 3 | func getRuntimeVariableDataDir() string { 4 | // Per Apple File System (APFS). 5 | return "/var/run" 6 | } 7 | -------------------------------------------------------------------------------- /test/end2end/docker/base/alpine/3.19/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19.2@sha256:af4785ccdbcd5cde71bfd5b93eabd34250b98651f19fe218c91de6c8d10e21c5 2 | LABEL name="base-alpine-3.19" 3 | -------------------------------------------------------------------------------- /test/end2end/docker/base/alpine/3.20/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 2 | LABEL name="base-alpine-3.20" 3 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/simple-digest.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/example@sha256:d23df29669d05462cf55ce2274a3a897aa2e2655d0fad104375f8ef06164b575 2 | LABEL name="example" 3 | -------------------------------------------------------------------------------- /test/fixtures/docker/two-parents/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root1:v1 as builder 2 | FROM eu.gcr.io/my-test-repository/root2:v1 3 | 4 | LABEL name="two-parents" 5 | -------------------------------------------------------------------------------- /test/fixtures/docker/root1/custom-hash-list/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eu.gcr.io/my-test-repository/root1:v1 2 | 3 | LABEL name="custom-hash-list" 4 | 5 | LABEL dib.use-custom-hash-list="true" 6 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/simple-tag-digest.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/example:latest@sha256:d23df29669d05462cf55ce2274a3a897aa2e2655d0fad104375f8ef06164b575 2 | LABEL name="example" 3 | -------------------------------------------------------------------------------- /test/end2end/docker/app/curl/Dockerfile: -------------------------------------------------------------------------------- 1 | # This image build but test always fail 2 | # Build OK, Test KO 3 | FROM registry.localhost/base-debian-bookworm:dib_managed 4 | LABEL name="app-curl" 5 | -------------------------------------------------------------------------------- /test/fixtures/report/build_logs/kaniko/1_raw_log.txt: -------------------------------------------------------------------------------- 1 | time="2022-11-10T09:51:08Z" level=info msg="Retrieving image manifest eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona" 2 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage-digest.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder@sha256:d23df29669d05462cf55ce2274a3a897aa2e2655d0fad104375f8ef06164b575 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /test/end2end/docker/app/lorem/Dockerfile: -------------------------------------------------------------------------------- 1 | # This image is never built (because it parent image "app-git" fail to build) 2 | # Build SKIP, Test SKIP 3 | FROM registry.localhost/app-git:dib_managed 4 | LABEL name="app-skipped" 5 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage-tag-digest.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder:latest@sha256:d23df29669d05462cf55ce2274a3a897aa2e2655d0fad104375f8ef06164b575 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /test/end2end/.dib.yaml: -------------------------------------------------------------------------------- 1 | registry_url: registry.localhost 2 | placeholder_tag: dib_managed 3 | #log_level: debug 4 | backend: docker 5 | local_only: true 6 | rate_limit: 2 7 | include_tests: 8 | - "goss" 9 | - "trivy" 10 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage-digest-alias.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder@sha256:d23df29669d05462cf55ce2274a3a897aa2e2655d0fad104375f8ef06164b575 as builder 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /pkg/mock/ratelimiter.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | // RateLimiter is an noop implementation of RateLimiter. 4 | type RateLimiter struct{} 5 | 6 | func (r RateLimiter) Acquire() { 7 | } 8 | 9 | func (r RateLimiter) Release() { 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/dockerfile/multistage-tag-digest-alias.dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.com/builder:latest@sha256:d23df29669d05462cf55ce2274a3a897aa2e2655d0fad104375f8ef06164b575 as builder 2 | 3 | FROM registry.com/example 4 | LABEL name="example" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | !.idea/runConfigurations 3 | .vscode 4 | !.vscode/settings.json 5 | !.vscode/extensions.json 6 | 7 | dist 8 | docs/cmd 9 | pkg/dib/tests 10 | pkg/trivy/reports 11 | venv 12 | coverage.* 13 | .golangci.yml 14 | golangci-lint-report.xml 15 | -------------------------------------------------------------------------------- /test/end2end/docker/app/git/Dockerfile: -------------------------------------------------------------------------------- 1 | # This image should not build 2 | # Build KO, Test SKIP 3 | FROM registry.localhost/base-debian-bookworm:dib_managed 4 | LABEL name="app-git" 5 | 6 | USER root 7 | 8 | RUN \ 9 | apt-get update \ 10 | && apt-get install -y lorem 11 | -------------------------------------------------------------------------------- /test/example_2.yml: -------------------------------------------------------------------------------- 1 | images: 2 | {{- range . }} 3 | {{ .ShortName }}: 4 | hash: "{{ .Hash }}" 5 | dockerfile: "{{ .Dockerfile.ContextPath }}/{{ .Dockerfile.Filename }}" 6 | extra_tags: {{ .ExtraTags }} 7 | pull_cmd: "docker pull {{ .Name }}:{{ .Hash }}" 8 | {{- end }} 9 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | ignore: | 5 | test/ 6 | 7 | rules: 8 | comments: 9 | require-starting-space: true 10 | ignore-shebangs: true 11 | min-spaces-from-content: 1 12 | line-length: 13 | max: 160 14 | allow-non-breakable-words: true 15 | -------------------------------------------------------------------------------- /pkg/ratelimit/ratelimit.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | // RateLimiter is an abstraction for rate limiting. 4 | type RateLimiter interface { 5 | // Acquire waits until rate limit is available for the build 6 | Acquire() 7 | // Release tells the build is done 8 | Release() 9 | } 10 | -------------------------------------------------------------------------------- /test/end2end/docker/app/git/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | user: 3 | debian: 4 | exists: true 5 | uid: 1664 6 | gid: 1664 7 | groups: 8 | - debian 9 | home: /home/debian 10 | shell: /bin/bash 11 | 12 | group: 13 | debian: 14 | exists: true 15 | gid: 1664 16 | -------------------------------------------------------------------------------- /pkg/dib/builder.go: -------------------------------------------------------------------------------- 1 | package dib 2 | 3 | import ( 4 | "github.com/radiofrance/dib/pkg/dag" 5 | "github.com/radiofrance/dib/pkg/types" 6 | ) 7 | 8 | type Builder struct { 9 | BuildOpts 10 | 11 | Version string 12 | Graph *dag.DAG 13 | TestRunners []types.TestRunner 14 | } 15 | -------------------------------------------------------------------------------- /pkg/testutil/testutil.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func mirrorOf(s string) string { 8 | return fmt.Sprintf("ghcr.io/stargz-containers/%s-org", s) 9 | } 10 | 11 | var ( 12 | AlpineImage = mirrorOf("alpine:3.13") 13 | 14 | CommonImage = AlpineImage 15 | ) 16 | -------------------------------------------------------------------------------- /test/end2end/docker/base/debian/bullseye/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | user: 3 | debian: 4 | exists: true 5 | uid: 1664 6 | gid: 1664 7 | groups: 8 | - debian 9 | home: /home/debian 10 | shell: /bin/bash 11 | 12 | group: 13 | debian: 14 | exists: true 15 | gid: 1664 16 | -------------------------------------------------------------------------------- /test/end2end/docker/app/curl/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | user: 3 | # this test always fail 4 | debiane: 5 | exists: true 6 | uid: 1666 7 | gid: 1664 8 | groups: 9 | - debiane 10 | home: /home/debiane 11 | shell: /bin/sh 12 | 13 | group: 14 | debian: 15 | exists: true 16 | gid: 1666 17 | -------------------------------------------------------------------------------- /pkg/rootlessutil/rootless_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | 3 | package rootlessutil 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | func RootlessKitStateDir() (string, error) { 10 | return "", errors.New("unsupported platform") 11 | } 12 | 13 | func RootlessKitChildPid(_ string) (int, error) { 14 | return 0, errors.New("unsupported platform") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/buildkit/buildkitutil_unix.go: -------------------------------------------------------------------------------- 1 | package buildkit 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | // getBuildkitHostAddress returns the address of the buildkit host for unix distributions. 8 | func getBuildkitHostAddress() string { 9 | run := getRuntimeVariableDataDir() 10 | return "unix://" + filepath.Join(run, "buildkit/buildkitd.sock") 11 | } 12 | -------------------------------------------------------------------------------- /test/end2end/certs/registry.localhost.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCOgb4rASX2EbknriaX 3 | 5ZPSFO3Tc4COn1iKohCK/v43Yhxa45HZG6uGC0UoG3yueZyhZANiAAQFEUmIxE2X 4 | LIwhuRUbMgFE4T3rYFNqgX/JBzP27vjwyq7LMBzOijwwkyxYIJjFa/cIwoVJbxbU 5 | 8FkkkPztpZ7v6j7aysNCT2Sy0AIogNKuIgP3arO/yxpy9NoA2q9JOQo= 6 | -----END PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /pkg/mock/tagger.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | type tagArgs struct { 4 | Src string 5 | Dest string 6 | } 7 | 8 | type Tagger struct { 9 | RecordedCallsArgs []tagArgs 10 | } 11 | 12 | func (r *Tagger) Tag(src, dest string) error { 13 | r.RecordedCallsArgs = append(r.RecordedCallsArgs, tagArgs{ 14 | Src: src, 15 | Dest: dest, 16 | }) 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/report/doc.go: -------------------------------------------------------------------------------- 1 | // Package report provides tools and utilities for generating and managing build reports. 2 | // 3 | // The main functionalities include: 4 | // - Creating and managing build reports. 5 | // - Exporting reports to various formats. 6 | // 7 | // This package is useful for tasks that require detailed reporting of build processes and results. 8 | package report 9 | -------------------------------------------------------------------------------- /pkg/graphviz/doc.go: -------------------------------------------------------------------------------- 1 | // Package graphviz provides tools and utilities for generating Graphviz visualizations. 2 | // 3 | // The main functionalities include: 4 | // - Creating and managing Graphviz graphs. 5 | // - Exporting graphs to various formats such as DOT, PNG. 6 | // 7 | // This package is useful for tasks that require visual representation of data structures and dependencies. 8 | package graphviz 9 | -------------------------------------------------------------------------------- /pkg/buildkit/buildkitutil_linux.go: -------------------------------------------------------------------------------- 1 | package buildkit 2 | 3 | import "github.com/radiofrance/dib/pkg/rootlessutil" 4 | 5 | func getRuntimeVariableDataDir() string { 6 | // Per Linux Foundation "Filesystem Hierarchy Standard" version 3.0 section 3.15. 7 | // Under version 2.3, this was "/var/run". 8 | if rootlessutil.IsRootless() { 9 | return rootlessutil.XDGRuntimeDir() 10 | } 11 | 12 | return "/run" 13 | } 14 | -------------------------------------------------------------------------------- /pkg/dag/doc.go: -------------------------------------------------------------------------------- 1 | // Package dag provides tools for creating and managing directed acyclic graphs (DAGs). 2 | // 3 | // The main functionalities include: 4 | // - Adding nodes to the graph. 5 | // - Walking through the graph with various traversal methods. 6 | // - Listing images in the graph. 7 | // 8 | // This package is useful for tasks that require dependency management and execution order control. 9 | package dag 10 | -------------------------------------------------------------------------------- /pkg/mock/testrunner.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import "github.com/radiofrance/dib/pkg/types" 4 | 5 | type TestRunner struct { 6 | ReturnedError error 7 | } 8 | 9 | func (t *TestRunner) Name() string { 10 | return "testing" 11 | } 12 | 13 | func (t *TestRunner) IsConfigured(_ types.RunTestOptions) bool { 14 | return true 15 | } 16 | 17 | func (t *TestRunner) RunTest(_ types.RunTestOptions) error { 18 | return t.ReturnedError 19 | } 20 | -------------------------------------------------------------------------------- /pkg/report/templates/graph.go.html: -------------------------------------------------------------------------------- 1 | {{- define "title" -}}Image dependency graph | dib{{- end -}} 2 | {{- define "content" -}} 3 |

4 | Image dependency graph 5 | 6 | Generated with graphviz 7 | 8 |

9 |
10 | 11 | DAG visualisation of Docker images 12 | {{- end -}} 13 | -------------------------------------------------------------------------------- /pkg/mock/registry.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "slices" 5 | "sync" 6 | ) 7 | 8 | type Registry struct { 9 | RefExistsCallCount int 10 | ExistingRefs []string 11 | Error error 12 | Lock sync.Locker 13 | } 14 | 15 | func (r *Registry) RefExists(ref string) (bool, error) { 16 | r.Lock.Lock() 17 | defer r.Lock.Unlock() 18 | 19 | r.RefExistsCallCount++ 20 | 21 | if slices.Contains(r.ExistingRefs, ref) { 22 | return true, r.Error 23 | } 24 | 25 | return false, r.Error 26 | } 27 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "message": 4, 12 | "code": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /cmd/build_internal_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestBuildCommand(t *testing.T) { 11 | t.Parallel() 12 | 13 | cmd := buildCommand() 14 | 15 | assert.NotNil(t, cmd) 16 | assert.IsType(t, &cobra.Command{}, cmd) 17 | assert.Equal(t, "build", cmd.Use) 18 | assert.Equal(t, "Run oci images builds", cmd.Short) 19 | assert.Contains(t, cmd.Long, "dib build will compute the graph of images") 20 | assert.NotNil(t, cmd.RunE) 21 | } 22 | -------------------------------------------------------------------------------- /test/end2end/.dib_list.tmpl: -------------------------------------------------------------------------------- 1 | # List of Docker images managed by dib 2 | 3 | 14 | 15 | **Total: `{{ len . }}`** 16 | 17 | | Image Name | Tags | current hash | 18 | |------------|------|--------------| 19 | {{- range . }} 20 | | `{{ .Name }}` | `latest, {{ .ExtraTags }}` | `{{ .Hash }}` | 21 | {{- end }} 22 | -------------------------------------------------------------------------------- /pkg/dib/doc.go: -------------------------------------------------------------------------------- 1 | // Package dib provides tools and utilities for building container images using different backends. 2 | // 3 | // The available backends are: 4 | // - Docker: Uses Docker builder to build images (will be deprecated soon). 5 | // - Kaniko: Builds images inside a container or Kubernetes cluster without requiring a Docker daemon (will be deprecated soon). 6 | // 7 | // The package includes functionalities for managing and executing builds, handling authentication, and configuring build environments. 8 | // 9 | //nolint:lll 10 | package dib 11 | -------------------------------------------------------------------------------- /pkg/mock/writer.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import "sync" 4 | 5 | type Writer struct { 6 | lock sync.Locker 7 | bytes []byte 8 | } 9 | 10 | func NewWriter() *Writer { 11 | return &Writer{ 12 | lock: new(sync.Mutex), 13 | } 14 | } 15 | 16 | func (r *Writer) Write(p []byte) (int, error) { 17 | r.lock.Lock() 18 | defer r.lock.Unlock() 19 | 20 | r.bytes = append(r.bytes, p...) 21 | 22 | return len(r.bytes), nil 23 | } 24 | 25 | func (r *Writer) GetString() string { 26 | r.lock.Lock() 27 | defer r.lock.Unlock() 28 | 29 | return string(r.bytes) 30 | } 31 | -------------------------------------------------------------------------------- /docs/extra-tags.md: -------------------------------------------------------------------------------- 1 | Extra Tags 2 | ========== 3 | 4 | Images managed by dib will get tagged with the human-readable version of the computed hash. This is not very convenient 5 | in some cases, for instance if we want to tag an image with the explicit version of the contained software. 6 | 7 | dib allows additional tags to be definedusing a label in the Dockerfile: 8 | ```dockerfile 9 | LABEL dib.extra-tags="v1.0.0,v1.0,v1" 10 | ``` 11 | 12 | The label may contain a coma-separated list of tags to be created when the image 13 | gets promoted with the `--release` flag. 14 | -------------------------------------------------------------------------------- /pkg/kaniko/context_local_test.go: -------------------------------------------------------------------------------- 1 | package kaniko_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/radiofrance/dib/pkg/kaniko" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_LocalContextProvider(t *testing.T) { 12 | t.Parallel() 13 | 14 | contextProvider := kaniko.NewLocalContextProvider() 15 | 16 | opts := provideDefaultBuildOptions() 17 | 18 | URL, err := contextProvider.PrepareContext(opts) 19 | 20 | require.NoError(t, err) 21 | assert.Equal(t, "dir:///tmp/kaniko-context", URL) 22 | } 23 | -------------------------------------------------------------------------------- /internal/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/radiofrance/dib/internal/logger" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLogger(t *testing.T) { 11 | t.Parallel() 12 | 13 | logger.Infof("this is info") 14 | logger.Debugf("should not be displayed") 15 | 16 | debugLvl := "debug" 17 | logger.SetLevel(&debugLvl) 18 | logger.Debugf("should be displayed") 19 | assert.Equal(t, logger.LogLevelDebug, logger.Get().Level) 20 | 21 | logger.Warnf("this is a warning") 22 | logger.Errorf("this is an error") 23 | } 24 | -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | Roadmap 2 | ======= 3 | 4 | ## Road to v1 5 | 6 | dib is still a work in progress, but we plan to release a stable version (v1.0.0) after we have added the 7 | following features: 8 | 9 | - **Per-image configuration:** Some images may require their own tagging scheme. Being 10 | able to configure those settings for each image is necessary. 11 | 12 | 13 | ## Future additions 14 | 15 | - **Multiplatform builds**: Ability to build images for different platforms, and generate a manifest-list. 16 | - **Image signing**: Sign images to improve supply chain security. 17 | 18 | And more... 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.editor.labelFormat": "short", 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.renderWhitespace": "all", 8 | "files.insertFinalNewline": true, 9 | "files.trimTrailingWhitespace": true, 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit" 12 | }, 13 | "eslint.validate": ["javascript", "svelte"], 14 | "[svelte]": { 15 | "editor.defaultFormatter": "svelte.svelte-vscode" 16 | }, 17 | "[xml]": { 18 | "editor.defaultFormatter": "redhat.vscode-xml" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/best-practices.md: -------------------------------------------------------------------------------- 1 | dib Best Practices 2 | ================== 3 | 4 | ### Pin dependencies versions in Dockerfiles 5 | 6 | As dib only rebuilds images when something changes in the build context (including the Dockerfile), external 7 | dependencies should always be pinned to a specific version, so upgrading the dependency triggers a rebuild. 8 | 9 | Example: 10 | ```dockerfile 11 | RUN apt-get install package@1.0.0 12 | ``` 13 | 14 | ### Use .dockerignore 15 | 16 | The `.dockerignore` lists file patterns that should not be included in the build context. dib also ignores those files 17 | when it computes the checksum, so no rebuild is triggered when they are modified. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/fixtures/junit/junit-image-test.xml: -------------------------------------------------------------------------------- 1 | INFO: Starting docker container 2 | INFO: Container ID: 114e5243 3 | INFO: Sleeping for 0.2 4 | INFO: Container health 5 | INFO: Running Tests 6 | 7 | 8 | 9 | Test results lorem 1 10 | 11 | 12 | Test results lorem 2 13 | 14 | 15 | INFO: Deleting container 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "aaron-bond.better-comments", 8 | "naumovs.color-highlight", 9 | "dbaeumer.vscode-eslint", 10 | "esbenp.prettier-vscode", 11 | "orta.vscode-jest", 12 | "pivaszbs.svelte-autoimport", 13 | "svelte.svelte-vscode", 14 | "vscode-icons-team.vscode-icons" 15 | ], 16 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 17 | "unwantedRecommendations": [] 18 | } 19 | -------------------------------------------------------------------------------- /pkg/ratelimit/channel.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | // ChannelRateLimiter is an implementation of RateLimiter based on a single channel. 4 | type ChannelRateLimiter struct { 5 | limiter chan struct{} 6 | } 7 | 8 | // NewChannelRateLimiter returns an instance of ChannelRateLimiter. 9 | func NewChannelRateLimiter(concurrency int) *ChannelRateLimiter { 10 | return &ChannelRateLimiter{ 11 | limiter: make(chan struct{}, concurrency), 12 | } 13 | } 14 | 15 | // Acquire holds on the channel until it can send a message. 16 | func (r *ChannelRateLimiter) Acquire() { 17 | r.limiter <- struct{}{} 18 | } 19 | 20 | // Release receives a message from the channel to unlock the next one. 21 | func (r *ChannelRateLimiter) Release() { 22 | <-r.limiter 23 | } 24 | -------------------------------------------------------------------------------- /pkg/goss/executor_dgoss_test.go: -------------------------------------------------------------------------------- 1 | package goss_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/radiofrance/dib/pkg/goss" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | //nolint:paralleltest 13 | func Test_DGossExecutor_NewDGossExecutorUsesDefaultShell(t *testing.T) { 14 | err := os.Unsetenv("SHELL") 15 | require.NoError(t, err) 16 | 17 | executor := goss.NewDGossExecutor() 18 | 19 | assert.Equal(t, "/bin/bash", executor.Shell) 20 | } 21 | 22 | func Test_DGossExecutor_NewDGossExecutorDetectsShellFromEnv(t *testing.T) { 23 | t.Setenv("SHELL", "/path/to/shell") 24 | 25 | executor := goss.NewDGossExecutor() 26 | 27 | assert.Equal(t, "/path/to/shell", executor.Shell) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/buildkit/context_local.go: -------------------------------------------------------------------------------- 1 | package buildkit 2 | 3 | import ( 4 | "github.com/radiofrance/dib/pkg/types" 5 | ) 6 | 7 | // LocalContextProvider provides a local build context. 8 | type LocalContextProvider struct{} 9 | 10 | // NewLocalContextProvider creates a new instance of LocalContextProvider. 11 | func NewLocalContextProvider() *LocalContextProvider { 12 | return &LocalContextProvider{} 13 | } 14 | 15 | // PrepareContext returns the local build context path without performing any additional operations. 16 | // Since the context is already available locally, it simply returns the context path from the provided options. 17 | func (c LocalContextProvider) PrepareContext(opts types.ImageBuilderOpts) (string, error) { 18 | return opts.Context, nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/trivy/executor_local_test.go: -------------------------------------------------------------------------------- 1 | package trivy_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/radiofrance/dib/pkg/trivy" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | //nolint:paralleltest 13 | func Test_LocalExecutor_NewLocalExecutorUsesDefaultShell(t *testing.T) { 14 | err := os.Unsetenv("SHELL") 15 | require.NoError(t, err) 16 | 17 | executor := trivy.NewLocalExecutor() 18 | 19 | assert.Equal(t, "/bin/bash", executor.Shell) 20 | } 21 | 22 | func Test_LocalExecutor_NewLocalExecutorDetectsShellFromEnv(t *testing.T) { 23 | t.Setenv("SHELL", "/path/to/shell") 24 | 25 | executor := trivy.NewLocalExecutor() 26 | 27 | assert.Equal(t, "/path/to/shell", executor.Shell) 28 | } 29 | -------------------------------------------------------------------------------- /test/end2end/docker/base/debian/bullseye/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image used to test dib html report 2 | # Build OK, Test OK 3 | FROM debian:bookworm@sha256:a92ed51e0996d8e9de041ca05ce623d2c491444df6a535a566dabd5cb8336946 4 | LABEL name="base-debian-bookworm" 5 | 6 | ENV DOCKER_USER_ID=1664 7 | ENV DOCKER_USER=debian 8 | 9 | USER root 10 | 11 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 12 | RUN set +x \ 13 | # Create dedicated user 14 | && groupadd --gid ${DOCKER_USER_ID} ${DOCKER_USER} \ 15 | && useradd \ 16 | --comment "${DOCKER_USER} user" \ 17 | --uid ${DOCKER_USER_ID} \ 18 | --gid ${DOCKER_USER_ID} \ 19 | --create-home \ 20 | --home-dir /home/${DOCKER_USER} \ 21 | --shell /bin/bash \ 22 | ${DOCKER_USER} 23 | 24 | USER ${DOCKER_USER} 25 | -------------------------------------------------------------------------------- /test/fixtures/junit/junit-image-test-fail.xml: -------------------------------------------------------------------------------- 1 | INFO: Starting docker container 2 | INFO: Container ID: 1aef060a 3 | INFO: Sleeping for 0.2 4 | INFO: Container health 5 | INFO: Running Tests 6 | 7 | 8 | 9 | Test results lorem 1 10 | 11 | 12 | User: debian: uid: doesn't match, expect: [1666] found: [1664] 13 | 14 | 15 | INFO: Deleting container 16 | -------------------------------------------------------------------------------- /pkg/kaniko/context_local.go: -------------------------------------------------------------------------------- 1 | package kaniko 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/radiofrance/dib/pkg/types" 7 | ) 8 | 9 | // LocalContextProvider provides a local build context. 10 | type LocalContextProvider struct{} 11 | 12 | // NewLocalContextProvider creates a new instance of LocalContextProvider. 13 | func NewLocalContextProvider() *LocalContextProvider { 14 | return &LocalContextProvider{} 15 | } 16 | 17 | // PrepareContext has nothing to do because the build context already exists locally. 18 | // It just returns the path of the existing local context, prefixed by the Kaniko `dir://` indicator. 19 | func (c LocalContextProvider) PrepareContext(opts types.ImageBuilderOpts) (string, error) { 20 | return fmt.Sprintf("dir://%s", opts.Context), nil 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. run command '...' 17 | 4. See error 18 | 19 | **Used configuration** 20 | The configuration used to reproduce the error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **System (please complete the following information):** 26 | 27 | - OS: [e.g. osx] 28 | - Go version [e.g. 1.20] 29 | - dib version [e.g. 1.0.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /pkg/goss/executor_containerd_test.go: -------------------------------------------------------------------------------- 1 | package goss_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/radiofrance/dib/pkg/goss" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | //nolint:paralleltest 13 | func Test_ContainerdGossExecutor_NewContainerdGossExecutorUsesDefaultShell(t *testing.T) { 14 | err := os.Unsetenv("SHELL") 15 | require.NoError(t, err) 16 | 17 | executor := goss.NewContainerdGossExecutor() 18 | 19 | assert.Equal(t, "/bin/bash", executor.Shell) 20 | } 21 | 22 | func Test_ContainerdGossExecutor_NewContainerdGossExecutorDetectsShellFromEnv(t *testing.T) { 23 | t.Setenv("SHELL", "/path/to/shell") 24 | 25 | executor := goss.NewContainerdGossExecutor() 26 | 27 | assert.Equal(t, "/path/to/shell", executor.Shell) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/dag/node_test.go: -------------------------------------------------------------------------------- 1 | package dag_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/radiofrance/dib/pkg/dag" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_NewNode(t *testing.T) { 11 | t.Parallel() 12 | 13 | image := &dag.Image{} 14 | node := dag.NewNode(image) 15 | 16 | assert.Same(t, image, node.Image) 17 | assert.Empty(t, node.Children()) 18 | assert.Empty(t, node.Parents()) 19 | } 20 | 21 | func Test_AddChild_SetsParentNode(t *testing.T) { 22 | t.Parallel() 23 | 24 | node := &dag.Node{} 25 | child := &dag.Node{} 26 | 27 | node.AddChild(child) 28 | 29 | children := node.Children() 30 | assert.Len(t, children, 1) 31 | assert.Same(t, child, children[0]) 32 | 33 | parents := child.Parents() 34 | assert.Len(t, parents, 1) 35 | assert.Same(t, node, parents[0]) 36 | } 37 | -------------------------------------------------------------------------------- /test/end2end/certs/registry.localhost.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIB7jCCAXWgAwIBAgIIDuS6JhFQdxQwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV 3 | bWluaWNhIHJvb3QgY2EgNDJkYWFkMB4XDTI0MDYyMjAwMzU1MVoXDTI2MDcyMjAw 4 | MzU1MVowHTEbMBkGA1UEAxMScmVnaXN0cnkubG9jYWxob3N0MHYwEAYHKoZIzj0C 5 | AQYFK4EEACIDYgAEBRFJiMRNlyyMIbkVGzIBROE962BTaoF/yQcz9u748MquyzAc 6 | zoo8MJMsWCCYxWv3CMKFSW8W1PBZJJD87aWe7+o+2srDQk9kstACKIDSriID92qz 7 | v8sacvTaANqvSTkKo38wfTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB 8 | BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUyYht5jTo 9 | kYDsucyTbtJjD00ma8owHQYDVR0RBBYwFIIScmVnaXN0cnkubG9jYWxob3N0MAoG 10 | CCqGSM49BAMDA2cAMGQCMEXavsrDKWKZd7dFd1Y0OBmWLqSzUaj78VW+rrs8VstV 11 | K0KZyY7D7Jx3948tIBDRPAIwW1YOAPxdGwH+4V7YVt09A/Vp2NOSz8hxi9ytS3lU 12 | q63nguC7z34zLjoINa+Tzn1z 13 | -----END CERTIFICATE----- 14 | -------------------------------------------------------------------------------- /.github/workflows/yaml.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: YAML lint 3 | on: # yamllint disable-line rule:truthy 4 | pull_request: 5 | types: [opened, synchronize] 6 | paths: ["**.ya?ml"] 7 | 8 | jobs: 9 | lintyaml: 10 | runs-on: ubuntu-24.04-arm 11 | steps: 12 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 13 | # yamllint is not yet available in ubuntu-24.04-arm (https://github.com/actions/partner-runner-images/blob/main/images/arm-ubuntu-24-image.md) 14 | # request https://github.com/actions/partner-runner-images/issues/56 15 | - name: Install yamllint 16 | run: sudo apt-get update && sudo apt-get install -y --no-install-recommends yamllint 17 | - uses: ibiqlik/action-yamllint@2576378a8e339169678f9939646ee3ee325e845c # tag=v3.1.1 18 | with: 19 | format: github 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: # yamllint disable-line rule:truthy 4 | push: 5 | tags: [v*] 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 15 | with: 16 | go-version-file: "go.mod" 17 | - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 18 | with: 19 | distribution: goreleaser 20 | # renovate: datasource=github-tags depName=goreleaser/goreleaser 21 | version: v2.12.7 22 | args: release --clean 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /docs/reports.md: -------------------------------------------------------------------------------- 1 | Reporting 2 | ========= 3 | 4 | dib generates reports after each build. 5 | By default, the reports are generated in the `reports` directory. You can change it by setting the 6 | `--reports-dir` option to another location. 7 | 8 | ## HTML Report 9 | 10 | The HTML report is the one you are going to use the most. 11 | Just click on the link displayed on the dib output to browse the report. 12 | 13 | In the report you'll find: 14 | 15 | - An overview of all images managed by dib 16 | - The build output 17 | - The graph of dependencies 18 | - Test results and logs 19 | - Vulnerability scan results 20 | 21 | **Preview:** 22 | 23 | ![HTML Report](images/dib_report.png) 24 | 25 | ## jUnit Reports 26 | 27 | Test executors generate reports in jUnit format. 28 | They can then be parsed in a CI pipeline and displayed in a user-friendly fashion. 29 | -------------------------------------------------------------------------------- /pkg/dib/test.go: -------------------------------------------------------------------------------- 1 | package dib 2 | 3 | import ( 4 | "github.com/radiofrance/dib/internal/logger" 5 | "github.com/radiofrance/dib/pkg/types" 6 | "golang.org/x/sync/errgroup" 7 | ) 8 | 9 | // testImage runs the tests on an image. 10 | func testImage(testRunners []types.TestRunner, runTestOpts types.RunTestOptions) error { 11 | logger.Infof("Running tests for \"%s\"", runTestOpts.ImageReference) 12 | 13 | errG := new(errgroup.Group) 14 | for _, runner := range testRunners { 15 | errG.Go(func() error { 16 | if !runner.IsConfigured(runTestOpts) { 17 | return nil 18 | } 19 | 20 | err := runner.RunTest(runTestOpts) 21 | if err != nil { 22 | logger.Errorf("Test runner %s failed on image %s with error: %v", 23 | runner.Name(), runTestOpts.ImageName, err) 24 | 25 | return err 26 | } 27 | 28 | return nil 29 | }) 30 | } 31 | 32 | return errG.Wait() 33 | } 34 | -------------------------------------------------------------------------------- /pkg/strutil/strutil.go: -------------------------------------------------------------------------------- 1 | package strutil 2 | 3 | import "strings" 4 | 5 | // ConvertKVStringsToMap is from https://github.com/moby/moby/blob/v20.10.0-rc2/runconfig/opts/parse.go 6 | // 7 | // ConvertKVStringsToMap converts ["key=value"] to {"key":"value"}. 8 | func ConvertKVStringsToMap(values []string) map[string]string { 9 | result := make(map[string]string, len(values)) 10 | 11 | const splitLimit = 2 12 | for _, value := range values { 13 | kv := strings.SplitN(value, "=", splitLimit) 14 | if len(kv) == 1 { 15 | result[kv[0]] = "" 16 | } else { 17 | result[kv[0]] = kv[1] 18 | } 19 | } 20 | 21 | return result 22 | } 23 | 24 | func DedupeStrSlice(in []string) []string { 25 | m := make(map[string]struct{}) 26 | 27 | var res []string 28 | 29 | for _, s := range in { 30 | if _, ok := m[s]; !ok { 31 | res = append(res, s) 32 | m[s] = struct{}{} 33 | } 34 | } 35 | 36 | return res 37 | } 38 | -------------------------------------------------------------------------------- /pkg/trivy/executor_local.go: -------------------------------------------------------------------------------- 1 | package trivy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/radiofrance/dib/pkg/exec" 11 | ) 12 | 13 | const defaultShell = "/bin/bash" 14 | 15 | // LocalExecutor executes trivy command. 16 | type LocalExecutor struct { 17 | Shell string 18 | } 19 | 20 | // NewLocalExecutor creates a new instance of LocalExecutor. 21 | func NewLocalExecutor() *LocalExecutor { 22 | shell, exists := os.LookupEnv("SHELL") 23 | if !exists { 24 | shell = defaultShell 25 | } 26 | 27 | return &LocalExecutor{ 28 | Shell: shell, 29 | } 30 | } 31 | 32 | // Execute trivy scan on the given image. 33 | func (e LocalExecutor) Execute(_ context.Context, output io.Writer, args ...string) error { 34 | shell := &exec.ShellExecutor{} 35 | 36 | cmd := fmt.Sprintf("trivy %s", strings.Join(args, " ")) 37 | 38 | return shell.ExecuteWithWriter(output, e.Shell, "-c", cmd) 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish docs 3 | on: # yamllint disable-line rule:truthy 4 | workflow_dispatch: 5 | push: 6 | branches: ['docs*'] 7 | tags: ['v*'] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | publish-docs: 14 | runs-on: ubuntu-24.04-arm 15 | permissions: 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 19 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 20 | with: 21 | go-version-file: "go.mod" 22 | - name: Generate cmd docs 23 | run: make docs 24 | - name: Run mkdocs 25 | uses: mhausenblas/mkdocs-deploy-gh-pages@a31c6b13a80e4a4fbb525eeb7a2a78253bb15fa5 # master 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | CONFIG_FILE: mkdocs.yml 29 | REQUIREMENTS: requirements.txt 30 | -------------------------------------------------------------------------------- /docs/documentation.md: -------------------------------------------------------------------------------- 1 | Documentation 2 | ============= 3 | 4 | The documentation is generated with [`mkdocs`](https://www.mkdocs.org/). It generates a static website in plain HTML 5 | from the Markdown files present in the `docs/` directory. 6 | 7 | We also use the [Cobra](https://github.com/spf13/cobra) built-in documentation generator for dib commands. 8 | 9 | ## Local Setup 10 | 11 | Let's set up a local Python environment and run the documentation server with live-reload. 12 | 13 | 1. Create a virtual env: 14 | ```shell 15 | python -m venv venv 16 | source venv/bin/activate 17 | ``` 18 | 19 | 1. Install dependencies: 20 | ```shell 21 | pip install -r requirements.txt 22 | ``` 23 | 24 | 1. Generate docs of dib commands: 25 | ```shell 26 | make docs 27 | ``` 28 | 29 | 1. Run the `mkdocs` server: 30 | ```shell 31 | mkdocs serve 32 | ``` 33 | 34 | 1. Go to [http://localhost:8000](http://localhost:8000) 35 | -------------------------------------------------------------------------------- /test/end2end/dib_images_list.md: -------------------------------------------------------------------------------- 1 | # List of Docker images managed by dib 2 | 3 | 14 | 15 | **Total: `6`** 16 | 17 | | Image Name | Tags | current hash | 18 | |------------|------|--------------| 19 | | `registry.localhost/app-curl` | `latest, []` | `green-autumn-quebec-montana` | 20 | | `registry.localhost/app-git` | `latest, []` | `texas-bluebird-low-ten` | 21 | | `registry.localhost/app-skipped` | `latest, []` | `vegan-alabama-fanta-oranges` | 22 | | `registry.localhost/base-alpine-3.19` | `latest, []` | `angel-leopard-idaho-winner` | 23 | | `registry.localhost/base-alpine-3.20` | `latest, []` | `grey-colorado-pennsylvania-two` | 24 | | `registry.localhost/base-debian-bookworm` | `latest, []` | `butter-social-twenty-whiskey` | 25 | -------------------------------------------------------------------------------- /test/end2end/Makefile: -------------------------------------------------------------------------------- 1 | ##----------------------- 2 | ## Available make targets 3 | ##----------------------- 4 | ## 5 | 6 | ROOT_PATH=${PWD}/../.. 7 | 8 | default: help 9 | 10 | help: ## Display this message 11 | @grep -E '(^[a-zA-Z0-9_.-]+:.*?##.*$$)|(^##)' Makefile | \ 12 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | \ 13 | sed -e 's/\[32m##/[33m/' 14 | 15 | ## 16 | ## ---------------------- 17 | ## dib end2end tests 18 | ## ---------------------- 19 | ## 20 | 21 | registry.start: ## Start required local Docker registry 22 | docker compose up --detach 23 | docker compose ps 24 | 25 | dib.install: ## Generate binary and copy it to $GOPATH/bin (equivalent to go install) 26 | $(MAKE) --directory=${ROOT_PATH} install 27 | 28 | dib.list: ## Update list of Docker images managed by dib 29 | dib list -o go-template-file=.dib_list.tmpl > dib_images_list.md 30 | 31 | dib.build: ## Build updated Docker images managed by dib 32 | dib build 33 | -------------------------------------------------------------------------------- /pkg/mock/builder.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | "github.com/google/uuid" 10 | "github.com/radiofrance/dib/pkg/types" 11 | ) 12 | 13 | type Builder struct { 14 | ID string 15 | } 16 | 17 | func NewBuilder() *Builder { 18 | return &Builder{ 19 | ID: uuid.NewString(), 20 | } 21 | } 22 | 23 | const ReportsDir = "tests/mock-reports" 24 | 25 | //nolint:musttag 26 | func (e *Builder) Build(opts types.ImageBuilderOpts) error { 27 | err := os.MkdirAll(path.Join(ReportsDir, e.ID), 0o750) 28 | if err != nil && !os.IsExist(err) { 29 | return fmt.Errorf("failed to create mock-reports directory: %w", err) 30 | } 31 | 32 | by, err := json.MarshalIndent(opts, "", "\t") 33 | if err != nil { 34 | return err 35 | } 36 | 37 | err = os.WriteFile(path.Join(ReportsDir, e.ID, uuid.NewString()+".json"), by, 0o600) 38 | if err != nil { 39 | return fmt.Errorf("failed to write builds file: %w", err) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /.idea/runConfigurations/go test dib.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /pkg/kubernetes/types.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | // PodConfig hold the configuration for the kubernetes pod to create. 4 | type PodConfig struct { 5 | // Kubernetes generic configuration. 6 | Name string // The name of the pod. Must be unique to avoid collisions with an existing pod. 7 | NameGenerator func() string // A function that generates the pod name. Will override the Name option. 8 | Namespace string // The namespace where the pod should be created. 9 | Labels map[string]string // A map of key/value labels. 10 | Image string // The image for the container. 11 | ImagePullSecrets []string // A list of `imagePullSecret` secret names. 12 | Env map[string]string // A map of key/value env variables. 13 | EnvSecrets []string // A list of `envFrom` secret names. 14 | 15 | // Advanced customisations (raw YAML overrides) 16 | ContainerOverride string // YAML string to override the container object. 17 | PodOverride string // YAML string to override the pod object. 18 | } 19 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "./Dockerfile", 4 | "context": "." 5 | }, 6 | "features": { 7 | "ghcr.io/devcontainers/features/common-utils:2": { 8 | "configureZshAsDefaultShell": "true", 9 | "username": "vscode", 10 | "userUid": "1000", 11 | "userGid": "1000" 12 | }, 13 | "ghcr.io/devcontainers/features/go:1": {}, 14 | // Use Docker-in-Docker to run buildkitd as a container 15 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 16 | }, 17 | "postStartCommand": "docker rm -f buildkitd || true && docker run -d --name buildkitd --privileged moby/buildkit:v0.21.1", 18 | "remoteEnv": { 19 | "BUILDKIT_HOST": "docker-container://buildkitd" 20 | }, 21 | "runArgs": [ 22 | "--cap-add=SYS_PTRACE", 23 | "--security-opt", 24 | "seccomp=unconfined" 25 | ], 26 | "customizations": { 27 | "vscode": { 28 | "settings": { 29 | "go.toolsManagement.checkForUpdates": "local", 30 | "go.useLanguageServer": true 31 | }, 32 | "extensions": [ 33 | "golang.Go" 34 | ] 35 | } 36 | }, 37 | "remoteUser": "vscode" 38 | } 39 | -------------------------------------------------------------------------------- /hacks/run-with-existing-containerd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 2 | 3 | # This Dockerfile creates an image that runs buildkitd with containerd worker 4 | # To run this container: 5 | # docker run --privileged --pid=host \ 6 | # -e CONTAINERD_ADDRESS=/run/containerd/containerd.sock \ 7 | # -e BUILDKIT_VERSION=v0.12.0 \ 8 | # 9 | # 10 | # Note: buildkitd and buildctl will be installed on the host system after nsenter. 11 | # You can specify a specific version of buildkit to install by setting BUILDKIT_VERSION. 12 | # This container only supports rootful mode and will exit with an error if containerd is running in rootless mode. 13 | 14 | # Ensure we're running as root 15 | USER root 16 | 17 | # Install required packages 18 | RUN apk add --no-cache \ 19 | wget \ 20 | tar \ 21 | util-linux 22 | 23 | # Copy the buildkit-entrypoint script and make it executable 24 | COPY buildkit-entrypoint.sh /usr/local/bin/buildkit-entrypoint.sh 25 | RUN chmod +x /usr/local/bin/buildkit-entrypoint.sh 26 | 27 | # Set the entrypoint 28 | ENTRYPOINT ["/usr/local/bin/buildkit-entrypoint.sh"] 29 | -------------------------------------------------------------------------------- /pkg/junit/xml.go: -------------------------------------------------------------------------------- 1 | package junit 2 | 3 | import "encoding/xml" 4 | 5 | type Testsuite struct { 6 | XMLName xml.Name `xml:"testsuite"` 7 | Name string `xml:"name,attr"` 8 | Errors string `xml:"errors,attr"` 9 | Tests string `xml:"tests,attr"` 10 | Failures string `xml:"failures,attr"` 11 | Skipped string `xml:"skipped,attr"` 12 | Time string `xml:"time,attr"` 13 | Timestamp string `xml:"timestamp,attr"` 14 | TestCases []TestCase `xml:"testcase"` 15 | } 16 | 17 | type TestCase struct { 18 | XMLName xml.Name `xml:"testcase"` 19 | ClassName string `xml:"classname,attr"` 20 | File string `xml:"file,attr"` 21 | Name string `xml:"name,attr"` 22 | Time string `xml:"time,attr"` 23 | SystemOut string `xml:"system-out"` 24 | Failure string `xml:"failure"` 25 | } 26 | 27 | // ParseRawLogs cast a raw XML JunitReport (as byte) into a Testsuite structure. 28 | func ParseRawLogs(testsuiteData []byte) (Testsuite, error) { 29 | testSuite := Testsuite{} 30 | 31 | err := xml.Unmarshal(testsuiteData, &testSuite) 32 | if err != nil { 33 | return testSuite, err 34 | } 35 | 36 | return testSuite, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "github.com/radiofrance/dib/internal/logger" 5 | registry "github.com/radiofrance/go-containerregistry" 6 | ) 7 | 8 | // Registry wraps the Google Cloud Registry client library. 9 | type Registry struct { 10 | gcr *registry.Registry 11 | dryRun bool 12 | } 13 | 14 | // NewRegistry creates a new instance of Registry. 15 | func NewRegistry(url string, dryRun bool) (*Registry, error) { 16 | gcr, err := registry.New(url) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return &Registry{gcr, dryRun}, nil 22 | } 23 | 24 | // RefExists checks if the GCR contains the image ref. 25 | func (r Registry) RefExists(imageRef string) (bool, error) { 26 | return r.gcr.RefExists(imageRef) 27 | } 28 | 29 | // Tag creates a new tag from an existing one. 30 | func (r Registry) Tag(existingRef, toCreateRef string) error { 31 | if r.dryRun { 32 | logger.Infof("[DRY-RUN] Retagging image from \"%s\" to \"%s\"", existingRef, toCreateRef) 33 | return nil 34 | } 35 | 36 | logger.Debugf("Retaging image on gcr, source %s, dest %s`", existingRef, toCreateRef) 37 | 38 | return r.gcr.Retag(existingRef, toCreateRef) 39 | } 40 | -------------------------------------------------------------------------------- /.idea/runConfigurations/dib_version.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pkg/goss/executor_dgoss.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/radiofrance/dib/pkg/exec" 11 | "github.com/radiofrance/dib/pkg/types" 12 | ) 13 | 14 | // DGossExecutor executes goss tests using the dgoss wrapper script. 15 | type DGossExecutor struct { 16 | Shell string 17 | } 18 | 19 | // NewDGossExecutor creates a new instance of DGossExecutor. 20 | func NewDGossExecutor() *DGossExecutor { 21 | shell, exists := os.LookupEnv("SHELL") 22 | if !exists { 23 | shell = defaultShell 24 | } 25 | 26 | return &DGossExecutor{ 27 | Shell: shell, 28 | } 29 | } 30 | 31 | // Execute goss tests on the given image. goss.yaml file is expected to be present in the given path. 32 | func (e DGossExecutor) Execute(_ context.Context, output io.Writer, opts types.RunTestOptions, args ...string) error { 33 | shell := &exec.ShellExecutor{ 34 | Dir: opts.DockerContextPath, 35 | Env: append(os.Environ(), fmt.Sprintf("GOSS_OPTS=%s", strings.Join(args, " "))), 36 | } 37 | 38 | cmd := fmt.Sprintf("dgoss run --rm --tty --entrypoint='' %s sh", opts.ImageReference) 39 | 40 | return shell.ExecuteWithWriter(output, e.Shell, "-c", cmd) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | //nolint:forbidigo 2 | package cmd 3 | 4 | import ( 5 | "fmt" 6 | "runtime" 7 | "runtime/debug" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | // Automatically filled by GoReleaser during build process 14 | // @see: https://goreleaser.com/cookbooks/using-main.version/ 15 | version = "unreleased-dev" 16 | commit = "none" 17 | date = "unknown" 18 | builtBy = "unknown" 19 | ) 20 | 21 | func versionCommand() *cobra.Command { 22 | return &cobra.Command{ 23 | Use: "version", 24 | Short: "print current dib version", 25 | Run: versionAction, 26 | } 27 | } 28 | 29 | func versionAction(*cobra.Command, []string) { 30 | goVersion := "unknown" 31 | 32 | buildInfo, available := debug.ReadBuildInfo() 33 | if available { 34 | goVersion = buildInfo.GoVersion 35 | } 36 | 37 | fmt.Printf("version: v%s\n", version) 38 | fmt.Printf("build on %s/%s by %s at %s with %s (from commit %s)\n", 39 | runtime.GOOS, 40 | runtime.GOARCH, 41 | builtBy, 42 | date, 43 | goVersion, 44 | commit, 45 | ) 46 | 47 | if available && buildInfo.Main.Sum != "" { 48 | fmt.Printf("module version: %s, checksum: %q\n", buildInfo.Main.Version, buildInfo.Main.Sum) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.idea/runConfigurations/dib_build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Actions security 3 | on: # yamllint disable-line rule:truthy 4 | pull_request: 5 | types: [opened, synchronize] 6 | paths: [.github/workflows/**] 7 | 8 | jobs: 9 | # Actions security tries to keep your GitHub actions secure by following these simple rules: 10 | # - Check if no issues are found on your GitHub Actions 11 | # - Ensure that all GitHub Actions and reusable workflow are pinned using directly a commit SHA 12 | actions_security: 13 | runs-on: ubuntu-24.04-arm 14 | permissions: 15 | security-events: write 16 | steps: 17 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 18 | - name: Github Actions lint 19 | run: | 20 | curl -O https://raw.githubusercontent.com/rhysd/actionlint/main/.github/actionlint-matcher.json 21 | echo "::add-matcher::.github/actionlint-matcher.json" 22 | bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 23 | ./actionlint -color 24 | - name: Ensure SHA pinned actions 25 | uses: zgosalvez/github-actions-ensure-sha-pinned-actions@9e9574ef04ea69da568d6249bd69539ccc704e74 # v4.0.0 26 | -------------------------------------------------------------------------------- /.idea/runConfigurations/dib_list__console_.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pkg/dib/tag.go: -------------------------------------------------------------------------------- 1 | package dib 2 | 3 | import ( 4 | "github.com/radiofrance/dib/internal/logger" 5 | "github.com/radiofrance/dib/pkg/dag" 6 | "github.com/radiofrance/dib/pkg/types" 7 | ) 8 | 9 | // Retag iterates over the graph to tag all images. 10 | func Retag(graph *dag.DAG, tagger types.ImageTagger, placeholderTag string, release bool) error { 11 | return graph.WalkAsyncErr(func(node *dag.Node) error { 12 | img := node.Image 13 | if img.RetagDone { 14 | return nil 15 | } 16 | 17 | current := img.CurrentRef() 18 | 19 | final := img.DockerRef(img.Hash) 20 | if current != final { 21 | logger.Debugf("Tagging \"%s\" from \"%s\"", final, current) 22 | 23 | err := tagger.Tag(current, final) 24 | if err != nil { 25 | return err 26 | } 27 | } 28 | 29 | if release { 30 | err := tagger.Tag(final, img.DockerRef(placeholderTag)) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | for _, tag := range img.ExtraTags { 36 | extra := img.DockerRef(tag) 37 | logger.Debugf("Tagging \"%s\" from \"%s\"", extra, final) 38 | 39 | err := tagger.Tag(final, extra) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | } 45 | 46 | img.RetagDone = true 47 | 48 | return nil 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /.idea/runConfigurations/dib_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pkg/buildkit/context_local_test.go: -------------------------------------------------------------------------------- 1 | //nolint:testpackage 2 | package buildkit 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/radiofrance/dib/pkg/types" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPrepareLocalContext(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | name string 17 | opts types.ImageBuilderOpts 18 | expected string 19 | err error 20 | }{ 21 | { 22 | name: "valid context", 23 | opts: types.ImageBuilderOpts{ 24 | Context: "valid-context", 25 | }, 26 | expected: "valid-context", 27 | err: nil, 28 | }, 29 | { 30 | name: "empty context", 31 | opts: types.ImageBuilderOpts{ 32 | Context: "", 33 | }, 34 | expected: "", 35 | err: nil, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | t.Parallel() 42 | 43 | provider := LocalContextProvider{} 44 | result, err := provider.PrepareContext(tt.opts) 45 | assert.Equal(t, tt.expected, result) 46 | require.ErrorIs(t, err, tt.err) 47 | }) 48 | } 49 | } 50 | 51 | func TestNewLocalContextProvider(t *testing.T) { 52 | t.Parallel() 53 | 54 | provider := NewLocalContextProvider() 55 | assert.NotNil(t, provider) 56 | } 57 | -------------------------------------------------------------------------------- /docs/executors.md: -------------------------------------------------------------------------------- 1 | Executors 2 | ========= 3 | 4 | dib supports multiple build executors. An executor is a platform able to run image builds and tests. 5 | Unlike the build backends which can be explicitely chosen, the executor is automatically selected depending on the type 6 | of operation (build, test), and the executors configured in the configuration file. 7 | 8 | **Build backend compatibility matrix** 9 | 10 | | Executor | BuildKit (recommended) | Docker (deprecated) | Kaniko (deprecated) | 11 | |------------|------------------------|---------------------|---------------------| 12 | | Local | ✔ | ✔ | ✗ | 13 | | Docker | ✗ | ✗ | ✔ | 14 | | Kubernetes | ✔ | ✗ | ✔ | 15 | 16 | ## Local 17 | 18 | Runs commands using the local exec system call. Use the `--local-only` flag to force the local executor. 19 | 20 | ## Docker 21 | 22 | Runs commands in a docker container, using the `docker run` command. 23 | 24 | ## Kubernetes 25 | 26 | Creates pods in a kubernetes cluster, using the kubernetes API. 27 | dib uses the current kube context, please make do 28 | 29 | See an example configuration in the 30 | [configuration reference](configuration-reference.md) section. 31 | -------------------------------------------------------------------------------- /cmd/docgen.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/cobra/doc" 12 | ) 13 | 14 | const fmTemplate = `--- 15 | title: "%s" 16 | --- 17 | ` 18 | 19 | var cmdDocPath string 20 | 21 | func docgenCommand() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "docgen", 24 | Short: "Generate the documentation for the CLI commands.", 25 | Hidden: true, 26 | RunE: docgenAction, 27 | SilenceUsage: true, 28 | } 29 | cmd.Flags().StringVar(&cmdDocPath, "path", "./docs/cmd", 30 | "path to write the generated documentation to") 31 | 32 | return cmd 33 | } 34 | 35 | func docgenAction(_ *cobra.Command, _ []string) error { 36 | err := os.MkdirAll(cmdDocPath, 0o750) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return doc.GenMarkdownTreeCustom(rootCmd, cmdDocPath, filePrepender, linkHandler) 42 | } 43 | 44 | func filePrepender(filename string) string { 45 | name := filepath.Base(filename) 46 | base := strings.TrimSuffix(name, path.Ext(name)) 47 | title := strings.ReplaceAll(base, "_", " ") 48 | 49 | return fmt.Sprintf(fmTemplate, title) 50 | } 51 | 52 | func linkHandler(name string) string { 53 | base := strings.TrimSuffix(name, path.Ext(name)) 54 | return "../" + strings.ToLower(base) + "/" 55 | } 56 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | dib can be configured either by command-line flags, environment variables or configuration file. 5 | 6 | The command-line flags have the highest priority, then environment variables, then config file. You can set some 7 | default values in the configuration file, and then override with environment variables of command-line flags. 8 | 9 | ### Command-line flags 10 | 11 | Example: 12 | ```shell 13 | dib build --registry-url=gcr.io/project 14 | ``` 15 | 16 | ### Environment variables 17 | 18 | dib auto-discovers configuration from environment variables prefixed with `DIB_`, followed by the capitalized, 19 | snake_cased flag name. 20 | 21 | Example: 22 | ```shell 23 | export DIB_REGISTRY_URL=gcr.io/project 24 | dib build 25 | ``` 26 | 27 | ### Configuration file 28 | 29 | dib uses a YAML configuration file in addition to command-line arguments. It will look for a file names `.dib.yaml` 30 | in the current working directory. You can change the file location by setting the `--config` (`-c`) flag. 31 | 32 | The YAML keys are equivalent to the flag names, in snake_case. 33 | 34 | Example: 35 | ```yaml 36 | # .dib.yaml 37 | registryUrl: gcr.io/project 38 | ... 39 | ``` 40 | 41 | You can find more examples [here](https://github.com/radiofrance/dib/tree/main/docs/examples/config). See also the 42 | [reference configuration file](configuration-reference.md). 43 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - main: . 8 | # Custom ldflags. 9 | # Default: '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser' 10 | ldflags: 11 | - -s -w 12 | - -X github.com/radiofrance/dib/cmd.version={{.Version}} 13 | - -X github.com/radiofrance/dib/cmd.commit={{.Commit}} 14 | - -X github.com/radiofrance/dib/cmd.date={{.CommitDate}} 15 | - -X github.com/radiofrance/dib/cmd.builtBy=goreleaser 16 | mod_timestamp: '{{ .CommitTimestamp }}' 17 | env: 18 | - CGO_ENABLED=0 19 | goos: 20 | - linux 21 | - darwin 22 | goarch: 23 | - arm64 24 | - amd64 25 | checksum: 26 | name_template: 'checksums.txt' 27 | snapshot: 28 | version_template: "{{ incpatch .Version }}-dev" 29 | release: 30 | mode: replace 31 | changelog: 32 | sort: asc 33 | filters: 34 | exclude: 35 | - '^docs:' 36 | - '^test:' 37 | - '^Merge pull request' 38 | groups: 39 | - title: New features 40 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 41 | order: 0 42 | - title: Bug fixes 43 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' 44 | order: 1 45 | - title: Dependency upgrades 46 | regexp: '.*?chore\(deps\):.*$' 47 | order: 2 48 | - title: Other changes 49 | order: 999 50 | -------------------------------------------------------------------------------- /pkg/report/assets/css/default-dark.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Theme: Default Dark 3 | Author: Chris Kempson (http://chriskempson.com) 4 | License: ~ MIT (or more permissive) [via base16-schemes-source] 5 | Maintainer: @highlightjs/core-team 6 | Version: 2021.09.0 7 | */pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#d8d8d8;background:#181818}.hljs ::selection,.hljs::selection{background-color:#383838;color:#d8d8d8}.hljs-comment{color:#585858}.hljs-tag{color:#b8b8b8}.hljs-operator,.hljs-punctuation,.hljs-subst{color:#d8d8d8}.hljs-operator{opacity:.7}.hljs-bullet,.hljs-deletion,.hljs-name,.hljs-selector-tag,.hljs-template-variable,.hljs-variable{color:#ab4642}.hljs-attr,.hljs-link,.hljs-literal,.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:#dc9656}.hljs-class .hljs-title,.hljs-title,.hljs-title.class_{color:#f7ca88}.hljs-strong{font-weight:700;color:#f7ca88}.hljs-addition,.hljs-code,.hljs-string,.hljs-title.class_.inherited__{color:#a1b56c}.hljs-built_in,.hljs-doctag,.hljs-keyword.hljs-atrule,.hljs-quote,.hljs-regexp{color:#86c1b9}.hljs-attribute,.hljs-function .hljs-title,.hljs-section,.hljs-title.function_,.ruby .hljs-property{color:#7cafc2}.diff .hljs-meta,.hljs-keyword,.hljs-template-tag,.hljs-type{color:#ba8baf}.hljs-emphasis{color:#ba8baf;font-style:italic}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-meta .hljs-string{color:#a16946}.hljs-meta .hljs-keyword,.hljs-meta-keyword{font-weight:700} -------------------------------------------------------------------------------- /pkg/preflight/check.go: -------------------------------------------------------------------------------- 1 | package preflight 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | 10 | "github.com/radiofrance/dib/internal/logger" 11 | ) 12 | 13 | func RunPreflightChecks(requiredCommands []string) { 14 | shouldSkipPreflightTest := os.Getenv("SKIP_PREFLIGHT_CHECKS") 15 | if len(shouldSkipPreflightTest) == 0 { 16 | logger.Infof("Running preflights checks...") 17 | 18 | for _, bin := range requiredCommands { 19 | err := isBinInstalled(bin) 20 | if err != nil { 21 | logger.Warnf("%s", err) 22 | } 23 | } 24 | } 25 | } 26 | 27 | // isBinInstalled checks if given binary exist on host system by checking exit code of the command "which xxx" 28 | // If binary does not exist,or exit code is != 0, it will return an error. 29 | func isBinInstalled(bin string) error { 30 | cmd := exec.Command("which", bin) //nolint:noctx 31 | 32 | err := cmd.Run() 33 | if err != nil { 34 | var exitErr *exec.ExitError 35 | if !errors.As(err, &exitErr) { 36 | return fmt.Errorf("unable to check if \"%s\" is installed, error: %w", bin, err) 37 | } 38 | 39 | if _, ok := exitErr.Sys().(syscall.WaitStatus); !ok { 40 | return fmt.Errorf("unable to check if \"%s\" is installed, error: %w", bin, err) 41 | } 42 | 43 | return fmt.Errorf("\"%s\" does not seem to be installed on your system, "+ 44 | "you have to install it before using dib", bin) 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQ) 2 | 3 | ### How to run dib with existing containerd (standalone or created by docker)? 4 | 5 | If you already have containerd running on your system (either standalone or as part of Docker), you can configure dib to use it through BuildKit. Here's how: 6 | 7 | 1. First, make sure BuildKit is installed and running on your system. You can use this Docker image to help with that: 8 | 9 | ```bash 10 | docker run --privileged --pid=host \ 11 | -e CONTAINERD_ADDRESS=/run/containerd/containerd.sock \ 12 | -e BUILDKIT_VERSION=v0.12.0 \ 13 | 14 | ``` 15 | 16 | 2. Once BuildKit is running and connected to your containerd instance, you can configure dib to use it by setting the `buildkit_host` option in your configuration: 17 | 18 | ```yaml 19 | # In .dib.yaml 20 | buildkit_host: unix:///run/buildkit/buildkitd.sock 21 | ``` 22 | 23 | Or you can set it via environment variable: 24 | 25 | ```bash 26 | export DIB_BUILDKIT_HOST=unix:///run/buildkit/buildkitd.sock 27 | ``` 28 | 29 | 3. Now when you run dib, it will use your existing BuildKit daemon, which is connected to your containerd instance: 30 | 31 | ```bash 32 | dib build 33 | ``` 34 | 35 | This approach gives you the best of both worlds - you can use dib's powerful image building capabilities while leveraging your existing containerd setup for efficient container operations. 36 | -------------------------------------------------------------------------------- /pkg/report/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: 'montserrat', serif; 5 | background: #e3e9f7; 6 | } 7 | 8 | .dib-menu { 9 | background: #151f29; 10 | } 11 | 12 | .dib-menu ul { 13 | width: 100%; 14 | } 15 | 16 | .dib-menu ul li { 17 | width: 100%; 18 | } 19 | 20 | .dib-menu ul li a { 21 | font-family: 'montserrat', serif; 22 | text-decoration: none; 23 | text-transform: uppercase; 24 | font-size: 17px; 25 | color: #e3e9f7; 26 | transition: all 0.1s; 27 | } 28 | 29 | .dib-menu ul li a:hover, 30 | .dib-menu ul li a:focus, 31 | .dib-menu ul li a.active { 32 | color: #151f29 !important; 33 | background-color: #e3e9f7 !important; 34 | border-radius: 40px 0 0 40px; 35 | z-index: -1; 36 | transition: all 0.5s; 37 | } 38 | 39 | .accordion { 40 | margin-top: 1em; 41 | } 42 | 43 | .severity-LOW { background-color: #5fbb3160; } 44 | .severity-LOW .severity { background-color: #5fbb31; } 45 | 46 | .severity-MEDIUM { background-color: #e9c60060; } 47 | .severity-MEDIUM .severity { background-color: #e9c600; } 48 | 49 | .severity-HIGH { background-color: #ff880060; } 50 | .severity-HIGH .severity { background-color: #ff8800; } 51 | 52 | .severity-CRITICAL { background-color: #e4000060; } 53 | .severity-CRITICAL .severity { background-color: #e40000; } 54 | 55 | .severity-UNKNOWN { background-color: #74747460; } 56 | .severity-UNKNOWN .severity { background-color: #747474; } 57 | -------------------------------------------------------------------------------- /pkg/executor/executor.go: -------------------------------------------------------------------------------- 1 | package executor 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ) 9 | 10 | /* 11 | Package executor defines interfaces for execution functionalities (producer) 12 | that can be utilized by various builders, such as Kubernetes-based or shell-based executors. 13 | */ 14 | 15 | // KubernetesExecutor defines an interface for executing Kubernetes-based Buildkit builds. 16 | type KubernetesExecutor interface { 17 | // ApplyWithWriters executes a Kubernetes Pod and streams its logs to the provided stdout and stderr writers. 18 | ApplyWithWriters(ctx context.Context, stdout, stderr io.Writer, k8sObject runtime.Object, containerNames string) error 19 | } 20 | 21 | // ShellExecutor defines an interface for executing shell commands with various output handling options. 22 | type ShellExecutor interface { 23 | // Execute a command and return the standard output. 24 | Execute(name string, args ...string) (string, error) 25 | // ExecuteStdout executes a command and prints the standard output instead of returning it. 26 | ExecuteStdout(name string, args ...string) error 27 | // ExecuteWithWriter executes a command and forwards both stdout and stderr to a single io.Writer 28 | ExecuteWithWriter(writer io.Writer, name string, args ...string) error 29 | // ExecuteWithWriters executes a command and forwards stdout and stderr to an io.Writer 30 | ExecuteWithWriters(stdout, stderr io.Writer, name string, args ...string) error 31 | } 32 | -------------------------------------------------------------------------------- /docs/tests.md: -------------------------------------------------------------------------------- 1 | Tests 2 | ===== 3 | 4 | dib can execute tests suites to make assertions on images that it just built. This is useful to prevent regressions, 5 | and ensure everything work as expected at runtime. 6 | 7 | 8 | ## Goss 9 | 10 | [Goss](https://github.com/goss-org/goss) is a YAML-based serverspec alternative tool for validating a server’s configuration. dib runs a container from the 11 | image to test, and injects the goss binary and configuration, then execute the test itself. 12 | 13 | To get started with goss tests, follow the steps below: 14 | 15 | 1. Install goss locally (for local builds only) 16 | 17 | Follow the procedure from the [official docs](https://github.com/goss-org/goss#installation) 18 | 19 | 2. Ensure the goss tests are enabled in configuration: 20 | ```yaml 21 | # .dib.yaml 22 | include_tests: 23 | - goss 24 | ``` 25 | 26 | 3. Create a `goss.yaml` file next to the Dockerfile of the image to test 27 | ``` 28 | debian/ 29 | ├── Dockerfile 30 | └── goss.yaml 31 | ``` 32 | 33 | 4. Add some assertions in the `goss.yaml` 34 | Basic Example: 35 | ```yaml 36 | command: 37 | 'check "hello-world" version': 38 | exec: 'hello-world --version' 39 | exit-status: 0 40 | stdout: 41 | - '/^hello-world version [0-9]+\.[0-9]+\.[0-9]+ linux\/amd64$/' 42 | ``` 43 | 44 | Read the [Goss documentation](https://github.com/goss-org/goss#full-documentation) to learn all possible assertions. 45 | -------------------------------------------------------------------------------- /pkg/rootlessutil/rootless_linux.go: -------------------------------------------------------------------------------- 1 | package rootlessutil 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // XDGRuntimeDir: https://specifications.freedesktop.org/basedir-spec/latest/ 12 | func XDGRuntimeDir() string { 13 | // XDG_RUNTIME_DIR is an environment variable specifying a user-specific directory for runtime files (e.g socket..) 14 | if xrd := os.Getenv("XDG_RUNTIME_DIR"); xrd != "" { 15 | return xrd 16 | } 17 | // Fall back to "/run/user/". 18 | return fmt.Sprintf("/run/user/%d", os.Geteuid()) 19 | } 20 | 21 | func RootlessKitStateDir() (string, error) { 22 | if v := os.Getenv("ROOTLESSKIT_STATE_DIR"); v != "" { 23 | return v, nil 24 | } 25 | 26 | xdr := XDGRuntimeDir() 27 | 28 | // "${XDG_RUNTIME_DIR}/containerd-rootless" is hardcoded in containerd-rootless.sh 29 | // docker is deprecated from v0.25.0 but we keep it for backward compatibility. 30 | stateDir := filepath.Join(xdr, "containerd-rootless") 31 | 32 | _, err := os.Stat(stateDir) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | return stateDir, nil 38 | } 39 | 40 | func RootlessKitChildPid(stateDir string) (int, error) { 41 | pidFilePath := filepath.Join(stateDir, "child_pid") 42 | 43 | _, err := os.Stat(pidFilePath) 44 | if err != nil { 45 | return 0, err 46 | } 47 | 48 | pidFileBytes, err := os.ReadFile(pidFilePath) //nolint:gosec 49 | if err != nil { 50 | return 0, err 51 | } 52 | 53 | pidStr := strings.TrimSpace(string(pidFileBytes)) 54 | 55 | return strconv.Atoi(pidStr) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/kaniko/executor_docker_test.go: -------------------------------------------------------------------------------- 1 | package kaniko_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/radiofrance/dib/pkg/kaniko" 7 | "github.com/radiofrance/dib/pkg/mock" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_DockerExecutor_Execute(t *testing.T) { 13 | t.Setenv("HOME", "/home/dib") 14 | 15 | shell := mock.NewShellExecutor([]mock.ExecutorResult{ 16 | { 17 | Output: "some output", 18 | Error: nil, 19 | }, 20 | }) 21 | 22 | executor := kaniko.NewDockerExecutor(shell, kaniko.ContainerConfig{ 23 | Image: "gcr.io/kaniko-project/executor:latest", 24 | Env: map[string]string{ 25 | "SOME_VARIABLE": "some_value", 26 | }, 27 | Volumes: map[string]string{ 28 | "/host/path/to/context": "/container/path/to/context", 29 | }, 30 | }) 31 | 32 | writer := mock.NewWriter() 33 | err := executor.Execute(t.Context(), writer, []string{"kaniko-arg1", "kaniko-arg2"}) 34 | assert.Equal(t, "some output", writer.GetString()) 35 | 36 | require.NoError(t, err) 37 | 38 | assert.Len(t, shell.Executed, 1) 39 | assert.Equal(t, "docker", shell.Executed[0].Command) 40 | 41 | expectedArgs := []string{ 42 | "run", 43 | "--rm", 44 | "--tty", 45 | "--volume=/home/dib/.docker:/kaniko/.docker", 46 | "--env=DOCKER_CONFIG=/kaniko/.docker", 47 | "--env=SOME_VARIABLE=some_value", 48 | "--volume=/host/path/to/context:/container/path/to/context", 49 | "gcr.io/kaniko-project/executor:latest", 50 | "kaniko-arg1", 51 | "kaniko-arg2", 52 | } 53 | 54 | assert.ElementsMatch(t, shell.Executed[0].Args, expectedArgs) 55 | } 56 | -------------------------------------------------------------------------------- /examples/buildkitd-launcher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # this script spawns buildkitd instance for executing buildctl. 3 | # 4 | # Inspired from https://github.com/moby/buildkit/blob/master/examples/buildctl-daemonless/buildctl-daemonless.sh 5 | # 6 | # Usage: buildctl build ... 7 | # 8 | # Flags for buildkitd can be specified as $BUILDKITD_FLAGS . 9 | set -eu 10 | 11 | : ${BUILDCTL=buildctl} 12 | : ${BUILDCTL_CONNECT_RETRIES_MAX=10} 13 | : ${BUILDKITD=buildkitd} 14 | : ${BUILDKITD_FLAGS=} 15 | : ${ROOTLESSKIT=rootlesskit} 16 | 17 | # $tmp holds the following files: 18 | # * pid 19 | # * addr 20 | # * log 21 | tmp=$(mktemp -d /tmp/buildctl.XXXXXX) 22 | trap "kill \$(cat $tmp/pid) || true; wait \$(cat $tmp/pid) || true; rm -rf $tmp" EXIT 23 | 24 | startBuildkitd() { 25 | addr= 26 | helper= 27 | if [ $(id -u) = 0 ]; then 28 | addr=unix:///run/buildkit/buildkitd.sock 29 | else 30 | addr=unix://$XDG_RUNTIME_DIR/buildkit/buildkitd.sock 31 | helper=$ROOTLESSKIT 32 | fi 33 | $helper $BUILDKITD $BUILDKITD_FLAGS --addr=$addr >$tmp/log 2>&1 & 34 | pid=$! 35 | echo $pid >$tmp/pid 36 | echo $addr >$tmp/addr 37 | } 38 | 39 | waitForBuildkitd() { 40 | addr=$(cat $tmp/addr) 41 | try=0 42 | max=$BUILDCTL_CONNECT_RETRIES_MAX 43 | until $BUILDCTL --addr=$addr debug workers >/dev/null 2>&1; do 44 | if [ $try -gt $max ]; then 45 | echo >&2 "could not connect to $addr after $max trials" 46 | echo >&2 "========== log ==========" 47 | cat >&2 $tmp/log 48 | exit 1 49 | fi 50 | sleep $(awk "BEGIN{print (100 + $try * 20) * 0.001}") 51 | try=$(expr $try + 1) 52 | done 53 | } 54 | 55 | startBuildkitd 56 | waitForBuildkitd 57 | exec $BUILDCTL --addr=$(cat $tmp/addr) "$@" 58 | -------------------------------------------------------------------------------- /pkg/report/templates/build.go.html: -------------------------------------------------------------------------------- 1 | {{- define "title" -}}Build logs | dib{{- end -}} 2 | {{- define "content" -}} 3 |

Build logs

4 |
5 | 6 |
7 | {{ range $imageName, $imageBuildLogs := .Data }} 8 |
9 |

10 | 18 |

19 |
23 |
24 |

25 |                             {{- $imageBuildLogs -}}
26 |                         
27 |
28 |
29 |
30 | {{ end }} 31 |
32 | {{- end -}} 33 | {{- define "extra_javascript" -}} 34 | 35 | 36 | {{- end -}} 37 | -------------------------------------------------------------------------------- /hacks/run-with-existing-containerd/buildkit-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | # Get containerd socket from environment variable 5 | : ${CONTAINERD_ADDRESS:=/run/containerd/containerd.sock} 6 | : ${BUILDKIT_VERSION:=latest} 7 | 8 | 9 | echo "WARNING: This script should be executed only in rootful mode." 10 | 11 | # Enter the host PID namespace and run the commands directly 12 | exec nsenter -t 1 -m -u -n -i -- sh -c " 13 | # Verify if containerd is in rootful mode 14 | if grep -q 'rootless=true' /proc/1/cmdline || grep -q 'rootless=true' /proc/\$(pgrep containerd)/cmdline 2>/dev/null; then 15 | echo 'Error: This container only supports rootful mode, but containerd is running in rootless mode.' 16 | exit 1 17 | fi 18 | 19 | # Install buildkitd and buildctl if not already installed 20 | if ! command -v buildkitd >/dev/null 2>&1 || ! command -v buildctl >/dev/null 2>&1; then 21 | echo 'Installing buildkit...' 22 | 23 | # Create temporary directory for installation 24 | TEMP_DIR=\$(mktemp -d) 25 | cd \$TEMP_DIR 26 | 27 | # Download and extract buildkit 28 | wget -q https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-amd64.tar.gz 29 | tar xzf buildkit-${BUILDKIT_VERSION}.linux-amd64.tar.gz 30 | 31 | # Install binaries 32 | cp bin/buildkitd bin/buildctl /usr/local/bin/ 33 | chmod +x /usr/local/bin/buildkitd /usr/local/bin/buildctl 34 | 35 | # Clean up 36 | cd - >/dev/null 37 | rm -rf \$TEMP_DIR 38 | 39 | echo 'Buildkit installed successfully' 40 | fi 41 | 42 | # Run buildkitd with containerd worker 43 | exec buildkitd \\ 44 | --oci-worker=false \\ 45 | --containerd-worker=true \\ 46 | --containerd-worker-addr=${CONTAINERD_ADDRESS} \\ 47 | --containerd-worker-namespace=default \\ 48 | --containerd-worker-snapshotter=overlayfs \\ 49 | $* 50 | " 51 | -------------------------------------------------------------------------------- /test/fixtures/report/build_logs/kaniko/2_parsed_log.txt: -------------------------------------------------------------------------------- 1 | Retrieving image manifest eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona 2 | Retrieving image eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona from registry eu.gcr.io 3 | Built cross stage deps: map[] 4 | Retrieving image manifest eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona 5 | Returning cached image manifest 6 | Executing 0 build triggers 7 | Unpacking rootfs as cmd RUN apt-get update && apt-get install -y lorem requires it. 8 | Taking snapshot of full filesystem... 9 | LABEL name=\"alpine-git\" 10 | Applying label name=alpine-git 11 | USER root 12 | cmd: USER 13 | RUN apt-get update && apt-get install -y lorem 14 | cmd: /bin/bash 15 | args: [-o pipefail -c apt-get update && apt-get install -y lorem] 16 | util.Lookup returned: &{Uid:0 Gid:0 Username:root Name: HomeDir:/root} 17 | performing slow lookup of group ids for root 18 | Running: [/bin/bash -o pipefail -c apt-get update && apt-get install -y lorem] 19 | Get:1 http://security.debian.org/debian-security bullseye-security InRelease [48.4 kB] 20 | Get:2 http://deb.debian.org/debian bullseye InRelease [116 kB] 21 | Get:3 http://deb.debian.org/debian bullseye-updates InRelease [44.1 kB] 22 | Get:4 http://security.debian.org/debian-security bullseye-security/main amd64 Packages [194 kB] 23 | Get:5 http://deb.debian.org/debian bullseye/main amd64 Packages [8184 kB] 24 | Get:6 http://deb.debian.org/debian bullseye-updates/main amd64 Packages [14.6 kB] 25 | Fetched 8600 kB in 1s (6877 kB/s) 26 | Reading package lists... 27 | Reading package lists... 28 | Building dependency tree... 29 | Reading state information... 30 | E: Unable to locate package lorem 31 | error building image: error building stage: failed to execute command: waiting for process to exit: exit status 100 32 | -------------------------------------------------------------------------------- /pkg/dag/image.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/radiofrance/dib/pkg/dockerfile" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | // Image holds the docker image information. 11 | type Image struct { 12 | Name string `yaml:"name"` 13 | ShortName string `yaml:"short_name"` 14 | // Hash of the build context "At the moment" 15 | Hash string `yaml:"hash"` 16 | // A list of tags to make in addition to image hash. 17 | ExtraTags []string `yaml:"extra_tags,flow,omitempty"` 18 | Dockerfile *dockerfile.Dockerfile `yaml:"dockerfile,omitempty"` 19 | IgnorePatterns []string `yaml:"ignore_patterns,flow,omitempty"` 20 | ContextFiles []string `yaml:"-"` 21 | NeedsRebuild bool `yaml:"-"` 22 | SkipBuild bool `yaml:"-"` 23 | NeedsTests bool `yaml:"-"` 24 | RetagDone bool `yaml:"-"` 25 | RebuildDone bool `yaml:"-"` 26 | RebuildFailed bool `yaml:"-"` 27 | UseCustomHashList bool `yaml:"-"` 28 | } 29 | 30 | // CurrentRef returns the fully-qualified docker ref for the current version. 31 | // If the image needs to be rebuilt, a temporary `dev-` prefix is added to the tag. 32 | func (img Image) CurrentRef() string { 33 | tag := img.Hash 34 | 35 | if img.NeedsRebuild { 36 | tag = "dev-" + img.Hash 37 | } 38 | 39 | return img.DockerRef(tag) 40 | } 41 | 42 | // DockerRef returns the fully-qualified docker ref for a given version. 43 | func (img Image) DockerRef(version string) string { 44 | return fmt.Sprintf("%s:%s", img.Name, version) 45 | } 46 | 47 | //nolint:musttag 48 | func (img Image) Print() string { 49 | strImg, err := yaml.Marshal(img) 50 | if err != nil { 51 | return err.Error() 52 | } 53 | 54 | return string(strImg) 55 | } 56 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base@sha256:36751f1ee2f30745a649afc2b2061f321bacdaa0617159901fe6725b34c93df4 2 | 3 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 4 | && apt-get -y install --no-install-recommends \ 5 | curl \ 6 | yamllint \ 7 | graphviz \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | # renovate: datasource=github-releases depName=moby/buildkit 11 | ENV BUILDKIT_VERSION=v0.26.1 12 | ENV BUILDKIT_URL=https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-amd64.tar.gz 13 | 14 | # renovate: datasource=github-releases depName=goreleaser/goreleaser 15 | ENV GORELEASER_VERSION=v2.12.7 16 | ENV GORELEASER_URL=https://github.com/goreleaser/goreleaser/releases/download/${GORELEASER_VERSION}/goreleaser_Linux_x86_64.tar.gz 17 | 18 | RUN echo "Downloading and installing BuildKit..." && \ 19 | curl -L ${BUILDKIT_URL} -o buildkit.tar.gz && \ 20 | mkdir buildkit_extracted && \ 21 | tar -xzf buildkit.tar.gz -C buildkit_extracted && \ 22 | echo "Copying BuildKit binaries..." && \ 23 | cp buildkit_extracted/bin/buildctl /usr/local/bin/ && \ 24 | cp buildkit_extracted/bin/buildkitd /usr/local/bin/ && \ 25 | rm -rf buildkit_extracted buildkit.tar.gz && \ 26 | \ 27 | echo "Downloading and installing Goreleaser..." && \ 28 | curl -L ${GORELEASER_URL} -o goreleaser.tar.gz && \ 29 | mkdir goreleaser_extracted && \ 30 | tar -xzf goreleaser.tar.gz -C goreleaser_extracted && \ 31 | echo "Copying Goreleaser binary..." && \ 32 | cp goreleaser_extracted/goreleaser /usr/local/bin/ && \ 33 | rm -rf goreleaser_extracted goreleaser.tar.gz && \ 34 | \ 35 | echo "Verifying installations..." && \ 36 | ls -l /usr/local/bin/buildctl /usr/local/bin/buildkitd /usr/local/bin/goreleaser && \ 37 | echo "Verifying shell..." && \ 38 | ls -l /bin/sh && /bin/sh -c "echo Shell test successful" 39 | -------------------------------------------------------------------------------- /pkg/report/templates/_functions.go.html: -------------------------------------------------------------------------------- 1 | {{- define "block_trivy_vulnerabilities" -}} 2 | {{ $results := . }} 3 | {{- range $_, $result := $results -}} 4 | {{- if eq (len $result.Vulnerabilities) 0 }} 5 |

6 | No Vulnerabilities found ! 7 | 8 |

9 | {{- else }} 10 |

11 | Total: 12 | {{ len $result.Vulnerabilities }} 13 |

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {{- range $_, $vuln := $result.Vulnerabilities }} 26 | 27 | 28 | 29 | 30 | 31 | {{ if eq $vuln.FixedVersion "" }} 32 | 33 | {{ else }} 34 | 35 | {{ end }} 36 | 37 | {{- end }} 38 | 39 |
PackageVulnerability IDSeverityInstalled VersionFixed Version
{{ $vuln.PkgName }}{{ $vuln.VulnerabilityID }}{{ $vuln.Severity }}{{ $vuln.InstalledVersion }}Ø{{ $vuln.FixedVersion }}
40 | {{- end }} 41 | {{- end }} 42 | {{- end -}} 43 | -------------------------------------------------------------------------------- /pkg/report/templates/_layout.go.html: -------------------------------------------------------------------------------- 1 | {{- define "layout" -}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{- block "title" . -}}{{- end -}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 | {{- template "nav" . -}} 24 |
25 |
26 | {{- template "content" . -}} 27 |
28 |
29 |
30 | 31 | 32 | 46 | {{- block "extra_javascript" . -}} {{- end -}} 47 | 48 | 49 | {{- end -}} 50 | -------------------------------------------------------------------------------- /test/end2end/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This docker-compose file is used during Dib development. 2 | # It declares 2 services : 3 | # - A Docker registry 4 | # - A webui for the registry 5 | 6 | services: 7 | # https://github.com/distribution/distribution 8 | registry: 9 | image: "registry:2.8.3@sha256:79b29591e1601a73f03fcd413e655b72b9abfae5a23f1ad2e883d4942fbb4351" 10 | container_name: "registry" 11 | ports: 12 | - "443:443" 13 | volumes: 14 | - "registry-data:/var/lib/registry" 15 | - "./certs:/certs" 16 | environment: 17 | - "REGISTRY_HTTP_ADDR=0.0.0.0:443" 18 | - "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.localhost.cert.pem" 19 | - "REGISTRY_HTTP_TLS_KEY=/certs/registry.localhost.key.pem" 20 | - "REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin=[http://registry.localhost]" 21 | - "REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods=[HEAD,GET,OPTIONS,DELETE]" 22 | - "REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials=[true]" 23 | - "REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers=[Authorization,Accept,Cache-Control]" 24 | - "REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers=[Docker-Content-Digest]" 25 | - "REGISTRY_STORAGE_DELETE_ENABLED=true" 26 | networks: 27 | - "dib_registry" 28 | 29 | # https://github.com/Joxit/docker-registry-ui 30 | registry-ui: 31 | image: "joxit/docker-registry-ui:2.5.7@sha256:5594b76bf8dd9de479648e28f38572d020d260568be40b7e52b9758b442275e1" 32 | container_name: "registry-ui" 33 | depends_on: 34 | - registry 35 | ports: 36 | - "80:80" 37 | environment: 38 | - "SINGLE_REGISTRY=true" 39 | - "REGISTRY_SECURED=false" 40 | - "REGISTRY_TITLE=dib local registry" 41 | - "NGINX_PROXY_PASS_URL=https://registry" 42 | - "SHOW_CONTENT_DIGEST=true" 43 | - "SHOW_CATALOG_NB_TAGS=true" 44 | - "SHOW_TAG_HISTORY=true" 45 | - "DELETE_IMAGES=true" 46 | networks: 47 | - "dib_registry" 48 | 49 | volumes: 50 | registry-data: 51 | driver: local 52 | 53 | networks: 54 | dib_registry: 55 | name: dib_registry 56 | driver: bridge 57 | -------------------------------------------------------------------------------- /pkg/mock/executor.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ) 9 | 10 | type ExecutorCommand struct { 11 | Command string 12 | Args []string 13 | Output string 14 | Error error 15 | } 16 | 17 | type ExecutorResult struct { 18 | Output string 19 | Error error 20 | } 21 | 22 | type ShellExecutor struct { 23 | Executed []ExecutorCommand 24 | Expected []ExecutorResult 25 | } 26 | 27 | func NewShellExecutor(expected []ExecutorResult) *ShellExecutor { 28 | return &ShellExecutor{ 29 | Executed: []ExecutorCommand{}, 30 | Expected: expected, 31 | } 32 | } 33 | 34 | type KubernetesExecutor struct { 35 | Applied runtime.Object 36 | Expected runtime.Object 37 | } 38 | 39 | func NewKubernetesExecutor(expected runtime.Object) *KubernetesExecutor { 40 | return &KubernetesExecutor{ 41 | Applied: nil, 42 | Expected: expected, 43 | } 44 | } 45 | 46 | func (e *ShellExecutor) Execute(name string, args ...string) (string, error) { 47 | e.Executed = append(e.Executed, ExecutorCommand{ 48 | Command: name, 49 | Args: args, 50 | }) 51 | 52 | if len(e.Expected) >= len(e.Executed) { 53 | currentIndex := len(e.Executed) - 1 54 | return e.Expected[currentIndex].Output, e.Expected[currentIndex].Error 55 | } 56 | 57 | return "", nil 58 | } 59 | 60 | func (e *ShellExecutor) ExecuteStdout(name string, args ...string) error { 61 | _, err := e.Execute(name, args...) 62 | return err 63 | } 64 | 65 | func (e *ShellExecutor) ExecuteWithWriters(writer, _ io.Writer, name string, args ...string) error { 66 | output, err := e.Execute(name, args...) 67 | _, _ = writer.Write([]byte(output)) 68 | 69 | return err 70 | } 71 | 72 | func (e *ShellExecutor) ExecuteWithWriter(writer io.Writer, name string, args ...string) error { 73 | output, err := e.Execute(name, args...) 74 | _, _ = writer.Write([]byte(output)) 75 | 76 | return err 77 | } 78 | 79 | //nolint:lll 80 | func (m *KubernetesExecutor) ApplyWithWriters(_ context.Context, _, _ io.Writer, k8sObject runtime.Object, _ string) error { 81 | m.Applied = k8sObject 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/kaniko/s3.go: -------------------------------------------------------------------------------- 1 | package kaniko 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/service/s3" 12 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 13 | "github.com/radiofrance/dib/internal/logger" 14 | ) 15 | 16 | // S3Uploader is a FileUploader that uploads files to an AWS S3 bucket. 17 | type S3Uploader struct { 18 | s3 *s3.Client 19 | bucket string 20 | } 21 | 22 | // NewS3Uploader creates a new instance of S3Uploader. 23 | func NewS3Uploader(cfg aws.Config, bucket string) *S3Uploader { 24 | return &S3Uploader{ 25 | s3: s3.NewFromConfig(cfg), 26 | bucket: bucket, 27 | } 28 | } 29 | 30 | // UploadFile uploads a file to an AWS S3 bucket. 31 | func (u S3Uploader) UploadFile(filePath string, targetPath string) error { 32 | file, err := os.Open(filePath) //nolint:gosec 33 | if err != nil { 34 | return fmt.Errorf("can't open file %s: %w", filePath, err) 35 | } 36 | 37 | defer func() { 38 | err := file.Close() 39 | if err != nil { 40 | logger.Errorf("can't close file %s: %v", filePath, err) 41 | } 42 | }() 43 | 44 | // Get file size and read the file content into a buffer 45 | fileInfo, _ := file.Stat() 46 | size := fileInfo.Size() 47 | buffer := make([]byte, size) 48 | 49 | _, err = file.Read(buffer) 50 | if err != nil { 51 | return fmt.Errorf("can't read file %s: %w", filePath, err) 52 | } 53 | 54 | query := &s3.PutObjectInput{ 55 | Bucket: aws.String(u.bucket), 56 | Key: aws.String(targetPath), 57 | ACL: types.ObjectCannedACLPrivate, 58 | Body: bytes.NewReader(buffer), 59 | ContentLength: &size, 60 | ContentType: aws.String(http.DetectContentType(buffer)), 61 | } 62 | 63 | _, err = u.s3.PutObject(context.Background(), query) 64 | if err != nil { 65 | return fmt.Errorf("can't send S3 PUT request: %w", err) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // URL returns the absolute path to the s3 object in the form s3://bucket/target/file. 72 | func (u S3Uploader) URL(targetPath string) string { 73 | return fmt.Sprintf("s3://%s/%s", u.bucket, targetPath) 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dib: DAG Image Builder 2 | ========================= 3 | 4 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/radiofrance/dib?sort=semver) 5 | ![CI Status](https://img.shields.io/github/actions/workflow/status/radiofrance/dib/qa.yml?label=QA&logo=github-actions&logoColor=fff) 6 | [![codecov](https://codecov.io/gh/radiofrance/dib/branch/main/graph/badge.svg)](https://codecov.io/gh/radiofrance/dib) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/radiofrance/dib)](https://goreportcard.com/report/github.com/radiofrance/dib) 8 | 9 | dib is a tool designed to help build multiple Docker images defined within a directory, possibly having dependencies 10 | with one another, in a single command. 11 | 12 | ## Features 13 | 14 | - **Incremental Builds:** Images are only built when something has changed since the last build, saving time and resources. 15 | - **Dependency Resolution:** Supports dependencies between images. Builds will be queued until all parent images are built, ensuring a smooth and efficient build process. 16 | - **Test Suites:** Run test suites on images and ensure the tests pass before promoting images to production. 17 | - **Build Backends:** BuildKit is the recommended and default backend. Docker and Kaniko backends are deprecated and will be removed in a future release. 18 | - **Execution Environments:** dib supports multiple executors, allowing you to build images using different environments such as Shell, Docker, or Kubernetes. 19 | 20 | ## Documentation 21 | 22 | To get started with dib, please read the [documentation](https://radiofrance.github.io/dib). 23 | 24 | ## Contributing 25 | 26 | We welcome contributions from the community! If you'd like to contribute to dib, please review our 27 | [contribution guidelines](https://github.com/radiofrance/dib/blob/main/CONTRIBUTING.md) for more information. 28 | 29 | ## License 30 | 31 | dib is licensed under the [CeCILL V2.1 License](https://cecill.info/licences/Licence_CeCILL_V2.1-en.txt) 32 | 33 | ## Support 34 | 35 | If you have any questions or encounter any issues, please feel free to 36 | [open an issue](https://github.com/radiofrance/dib/issues/new/choose) on GitHub. 37 | -------------------------------------------------------------------------------- /pkg/kaniko/executor_docker.go: -------------------------------------------------------------------------------- 1 | package kaniko 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/radiofrance/dib/pkg/executor" 10 | 11 | "github.com/radiofrance/dib/internal/logger" 12 | ) 13 | 14 | // ContainerConfig holds the configuration options for the docker container. 15 | type ContainerConfig struct { 16 | Image string // Image used to create the Kaniko container. 17 | Env map[string]string // A map of key/value environment variables to inject in the container. 18 | Volumes map[string]string // A map of volumes to mount in the container. 19 | } 20 | 21 | // DockerExecutor will run Kaniko in a docker container. 22 | type DockerExecutor struct { 23 | exec executor.ShellExecutor 24 | config ContainerConfig 25 | DockerConfig string // Path to the docker config directory to mount in the Kaniko container. 26 | } 27 | 28 | // NewDockerExecutor creates a new instance of DockerExecutor. 29 | func NewDockerExecutor(exec executor.ShellExecutor, config ContainerConfig) *DockerExecutor { 30 | dockerCfg := os.Getenv("DOCKER_CONFIG") 31 | if dockerCfg == "" { 32 | dockerCfg = fmt.Sprintf("%s/.docker", os.Getenv("HOME")) 33 | } 34 | 35 | return &DockerExecutor{ 36 | exec: exec, 37 | config: config, 38 | DockerConfig: dockerCfg, 39 | } 40 | } 41 | 42 | // Execute the Kaniko build using a Docker container. 43 | func (e DockerExecutor) Execute(_ context.Context, output io.Writer, args []string) error { 44 | logger.Infof("Building image with kaniko local executor") 45 | 46 | dockerArgs := []string{ 47 | "run", 48 | "--rm", 49 | "--tty", 50 | fmt.Sprintf("--volume=%s:/kaniko/.docker", e.DockerConfig), 51 | "--env=DOCKER_CONFIG=/kaniko/.docker", 52 | } 53 | 54 | for k, v := range e.config.Env { 55 | dockerArgs = append(dockerArgs, fmt.Sprintf("--env=%s=%s", k, v)) 56 | } 57 | 58 | for k, v := range e.config.Volumes { 59 | dockerArgs = append(dockerArgs, fmt.Sprintf("--volume=%s:%s", k, v)) 60 | } 61 | 62 | dockerArgs = append(dockerArgs, e.config.Image) 63 | dockerArgs = append(dockerArgs, args...) 64 | 65 | return e.exec.ExecuteWithWriter(output, "docker", dockerArgs...) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/graphviz/graphviz.go: -------------------------------------------------------------------------------- 1 | package graphviz 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "strings" 8 | 9 | "github.com/radiofrance/dib/pkg/dag" 10 | "github.com/radiofrance/dib/pkg/exec" 11 | ) 12 | 13 | const ( 14 | // graphDot is the name of the file containing the raw graphviz dot language representation of the dib graph. 15 | graphDot = "dib.dot" 16 | 17 | // graphPng is the final file inside we put dib graph. 18 | graphPng = "dib.png" 19 | ) 20 | 21 | // GenerateGraph generates a graphviz representation (png) of the dag.DAG in the given report.Report rootDir. 22 | func GenerateGraph(dag *dag.DAG, reportRootDir string) error { 23 | rawGraphvizOutput := GenerateRawOutput(dag) 24 | 25 | graphvizFile := path.Join(reportRootDir, graphDot) 26 | 27 | err := os.WriteFile(graphvizFile, []byte(rawGraphvizOutput), 0o644) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | shell := &exec.ShellExecutor{ 33 | Dir: reportRootDir, 34 | } 35 | 36 | _, err = shell.Execute("dot", "-Tpng", graphDot, "-o", graphPng) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | 44 | // GenerateRawOutput generates the raw graphviz dot language from the given dag.DAG. 45 | func GenerateRawOutput(graph *dag.DAG) string { 46 | rawGraphvizDotLang := []string{ 47 | "digraph images {\n", 48 | " rankdir = \"LR\";\n", 49 | " node[fontsize=10, shape=cds, height=0.4];\n", 50 | " edge[fontsize=10, arrowhead=vee];\n", 51 | "\n", 52 | } 53 | 54 | if graph != nil { 55 | graph.Walk(func(node *dag.Node) { 56 | img := node.Image 57 | 58 | color := "white" 59 | if img.NeedsRebuild { 60 | color = "red" 61 | } 62 | 63 | rawGraphvizDotLang = append(rawGraphvizDotLang, fmt.Sprintf( 64 | " \"%s\" [fillcolor=%s, style=filled];\n", 65 | img.Name, 66 | color, 67 | )) 68 | 69 | for _, child := range node.Children() { 70 | rawGraphvizDotLang = append(rawGraphvizDotLang, fmt.Sprintf( 71 | " \"%s\" -> \"%s\" [dir=forward];\n", 72 | img.Name, 73 | child.Image.Name, 74 | )) 75 | } 76 | }) 77 | } 78 | 79 | rawGraphvizDotLang = append(rawGraphvizDotLang, "}\n") 80 | 81 | return strings.Join(rawGraphvizDotLang, "") 82 | } 83 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ##----------------------- 2 | ## Available make targets 3 | ##----------------------- 4 | ## 5 | 6 | ########################## 7 | # Configuration 8 | ########################## 9 | 10 | GO ?= go 11 | GOOS ?= $(shell $(GO) env GOOS) 12 | GOPATH ?= $(shell $(GO) env GOPATH) 13 | 14 | default: help 15 | help: ## Display this message 16 | @grep -E '(^[a-zA-Z0-9_.-]+:.*?##.*$$)|(^##)' Makefile | \ 17 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | \ 18 | sed -e 's/\[32m##/[33m/' 19 | 20 | artifact: ## Generate binary in dist folder 21 | goreleaser build --clean --snapshot --single-target 22 | 23 | install: ## Generate binary and copy it to $GOPATH/bin (equivalent to go install) 24 | goreleaser build --clean --snapshot --single-target -o $(GOPATH)/bin/dib 25 | 26 | build: ## Build the CLI binary. 27 | CGO_ENABLED=0 GOOS=$(GOOS) $(GO) build -o ./dist/dib . 28 | 29 | docs: build 30 | ./dist/dib docgen 31 | 32 | ## 33 | ## ---------------------- 34 | ## Q.A 35 | ## ---------------------- 36 | ## 37 | 38 | qa: lint test 39 | 40 | # renovate: datasource=github-releases depName=radiofrance/lint-config 41 | LINT_CONFIG_VERSION = v1.1.1 42 | 43 | lint: ## Lint source code 44 | curl -o .golangci.yml -sS \ 45 | "https://raw.githubusercontent.com/radiofrance/lint-config/$(LINT_CONFIG_VERSION)/.golangci.yml" 46 | golangci-lint run --verbose 47 | 48 | PKG = ./... 49 | RUN = ".*" 50 | RED = $(shell tput setaf 1) 51 | GREEN = $(shell tput setaf 2) 52 | BLUE = $(shell tput setaf 4) 53 | RESET = $(shell tput sgr0) 54 | 55 | .PHONY: test 56 | test: ## Run tests 57 | @go test -v -race -failfast -coverprofile coverage.out -covermode atomic -run $(RUN) $(PKG) | \ 58 | sed 's/RUN/$(BLUE)RUN$(RESET)/g' | \ 59 | sed 's/CONT/$(BLUE)CONT$(RESET)/g' | \ 60 | sed 's/PAUSE/$(BLUE)PAUSE$(RESET)/g' | \ 61 | sed 's/PASS/$(GREEN)PASS$(RESET)/g' | \ 62 | sed 's/FAIL/$(RED)FAIL$(RESET)/g' 63 | 64 | coverage: test ## Run test, then generate coverage html report 65 | @go tool cover -html=coverage.out -o coverage.html 66 | @echo "To open the html coverage file, use one of the following commands:" 67 | @echo "open coverage.html on mac" 68 | @echo "xdg-open coverage.html on linux" 69 | -------------------------------------------------------------------------------- /pkg/report/templates/scan.go.html: -------------------------------------------------------------------------------- 1 | {{- define "title" -}}Trivy scan logs | dib{{- end -}} 2 | {{- define "content" -}} 3 |

4 | Security scan logs 5 | 6 | Generated by Trivy 7 | 8 |

9 |
10 | 11 |
12 | {{ range $imageName, $report := .Data }} 13 |
14 |

15 | 23 |

24 |
28 |
29 | {{ if eq "string" (printf "%T" $report) }} 30 |

Error:

31 |

32 |                                 {{- $report -}}
33 |                             
34 | {{ else }} 35 | {{- template "block_trivy_vulnerabilities" $report.Results -}} 36 | {{ end }} 37 |
38 |
39 |
40 | {{- end }} 41 |
42 | {{- end -}} 43 | {{- define "extra_javascript" -}} 44 | 45 | 46 | {{- end -}} 47 | -------------------------------------------------------------------------------- /pkg/exec/shell.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/radiofrance/dib/internal/logger" 11 | ) 12 | 13 | // ShellExecutor is an implementation of Executor that uses the standard exec package to run shell commands. 14 | type ShellExecutor struct { 15 | Dir string 16 | Env []string 17 | } 18 | 19 | // NewShellExecutor initializes a ShellExecutor with the specified working directory and environment variables. 20 | func NewShellExecutor(workingDir string, env []string) *ShellExecutor { 21 | return &ShellExecutor{ 22 | Dir: workingDir, 23 | Env: env, 24 | } 25 | } 26 | 27 | // Execute a shell command and return the standard output. 28 | func (e ShellExecutor) Execute(name string, args ...string) (string, error) { 29 | cmd := exec.Command(name, args...) //nolint:noctx 30 | cmd.Env = e.Env 31 | 32 | var stdout, stderr bytes.Buffer 33 | 34 | cmd.Stderr = &stderr 35 | cmd.Stdout = &stdout 36 | cmd.Dir = e.Dir 37 | 38 | logger.Debugf("Exec cmd: %s", cmd) 39 | 40 | err := cmd.Run() 41 | if err != nil { 42 | return stderr.String(), fmt.Errorf("failed to execute command: %s: %w", cmd, err) 43 | } 44 | 45 | return stdout.String(), nil 46 | } 47 | 48 | // ExecuteWithWriters executes a command and forwards stdout and stderr to an io.Writer. 49 | func (e ShellExecutor) ExecuteWithWriters(stdout, stderr io.Writer, name string, args ...string) error { 50 | cmd := exec.Command(name, args...) //nolint:noctx 51 | cmd.Env = e.Env 52 | cmd.Stderr = stderr 53 | cmd.Stdout = stdout 54 | cmd.Dir = e.Dir 55 | 56 | logger.Debugf("Exec cmd: %s", cmd) 57 | 58 | err := cmd.Run() 59 | if err != nil { 60 | return fmt.Errorf("failed to execute command: %s: %w", cmd, err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // ExecuteWithWriter executes a command and forwards both stdout and stderr to a single io.Writer. 67 | func (e ShellExecutor) ExecuteWithWriter(writer io.Writer, name string, args ...string) error { 68 | return e.ExecuteWithWriters(writer, writer, name, args...) 69 | } 70 | 71 | // ExecuteStdout executes a shell command and prints to the standard output. 72 | func (e ShellExecutor) ExecuteStdout(name string, args ...string) error { 73 | return e.ExecuteWithWriters(os.Stdout, os.Stderr, name, args...) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/trivy/runner_test.go: -------------------------------------------------------------------------------- 1 | //nolint:gosec 2 | package trivy_test 3 | 4 | import ( 5 | "context" 6 | "io" 7 | "os" 8 | "path" 9 | "testing" 10 | 11 | "github.com/radiofrance/dib/internal/logger" 12 | "github.com/radiofrance/dib/pkg/report" 13 | "github.com/radiofrance/dib/pkg/trivy" 14 | "github.com/radiofrance/dib/pkg/types" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestMain(m *testing.M) { 20 | lvl := "fatal" 21 | logger.SetLevel(&lvl) 22 | os.Exit(m.Run()) 23 | } 24 | 25 | type fakeExecutor struct { 26 | Error error 27 | Output string 28 | RecordedArgs []string 29 | } 30 | 31 | func (e *fakeExecutor) Execute(_ context.Context, output io.Writer, args ...string) error { 32 | e.RecordedArgs = args 33 | 34 | _, err := output.Write([]byte(e.Output)) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return e.Error 40 | } 41 | 42 | func Test_TestRunner_RunTest(t *testing.T) { 43 | t.Parallel() 44 | 45 | cwd, err := os.Getwd() 46 | if err != nil { 47 | t.Fatal("Failed to get current working directory.") 48 | } 49 | 50 | fakeExecutor := &fakeExecutor{} 51 | runner := trivy.NewTestRunner(fakeExecutor, trivy.TestRunnerOptions{ 52 | WorkingDirectory: path.Join(cwd, "../../test"), 53 | }) 54 | 55 | dibReport := report.Init("1.0.0", "reports", false, nil, "") 56 | 57 | require.NoError(t, err) 58 | 59 | opts := types.RunTestOptions{ 60 | ImageName: "image", 61 | ImageReference: "gcr.io/project/image:tag", 62 | DockerContextPath: path.Join(cwd, "../../test/fixtures"), 63 | ReportJunitDir: dibReport.GetJunitReportDir(), 64 | ReportTrivyDir: dibReport.GetTrivyReportDir(), 65 | } 66 | 67 | fakeExecutor.Output = `{}` 68 | 69 | err = runner.RunTest(opts) 70 | require.NoError(t, err) 71 | assert.Equal(t, []string{"image", "--quiet", "--format", "json", "gcr.io/project/image:tag"}, 72 | fakeExecutor.RecordedArgs) 73 | 74 | testReportPath := path.Join(dibReport.GetTrivyReportDir(), "image.json") 75 | assert.FileExists(t, testReportPath) 76 | 77 | expectedContent := `{}` 78 | actualContent, err := os.ReadFile(testReportPath) 79 | require.NoError(t, err) 80 | assert.Equal(t, expectedContent, string(actualContent)) 81 | 82 | _ = os.RemoveAll("reports") 83 | } 84 | -------------------------------------------------------------------------------- /pkg/kaniko/context_remote_test.go: -------------------------------------------------------------------------------- 1 | //nolint:paralleltest 2 | package kaniko_test 3 | 4 | import ( 5 | "os" 6 | "testing" 7 | 8 | "github.com/radiofrance/dib/pkg/kaniko" 9 | "github.com/radiofrance/dib/pkg/types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type fakeUploader struct { 15 | Src string 16 | Dest string 17 | Err error 18 | } 19 | 20 | func (f *fakeUploader) UploadFile(filePath string, targetPath string) error { 21 | f.Src = filePath 22 | f.Dest = targetPath 23 | 24 | return f.Err 25 | } 26 | 27 | func (f *fakeUploader) URL(targetPath string) string { 28 | return "fakes3://bucket/" + targetPath 29 | } 30 | 31 | func provideDefaultBuildOptions() types.ImageBuilderOpts { 32 | return types.ImageBuilderOpts{ 33 | Context: "/tmp/kaniko-context", 34 | Tags: []string{"gcr.io/project-id/image:version"}, 35 | BuildArgs: map[string]string{ 36 | "someArg": "someValue", 37 | }, 38 | Labels: nil, 39 | Push: true, 40 | } 41 | } 42 | 43 | func Test_RemoteContextProvider_FailsWhenContextDirectoryDoesNotExist(t *testing.T) { 44 | fu := &fakeUploader{} 45 | contextProvider := kaniko.NewRemoteContextProvider(fu) 46 | 47 | opts := provideDefaultBuildOptions() 48 | _, err := contextProvider.PrepareContext(opts) 49 | require.Error(t, err) 50 | } 51 | 52 | func Test_RemoteContextProvider_UploadsBuildContext(t *testing.T) { 53 | fu := &fakeUploader{} 54 | contextProvider := kaniko.NewRemoteContextProvider(fu) 55 | 56 | // Create the build context directory 57 | opts := provideDefaultBuildOptions() 58 | require.NoError(t, os.Mkdir(opts.Context, 0o750)) 59 | t.Cleanup(func() { 60 | _ = os.Remove(opts.Context) 61 | }) 62 | 63 | url, err := contextProvider.PrepareContext(opts) 64 | require.NoError(t, err) 65 | 66 | expectedSrc := "/tmp/kaniko-context/context-kaniko-image-version.tar.gz" 67 | assert.Equal(t, expectedSrc, fu.Src) 68 | 69 | expectedDst := "kaniko/image/context-kaniko-image-version.tar.gz" 70 | assert.Equal(t, expectedDst, fu.Dest) 71 | 72 | _, err = os.Stat(expectedSrc) 73 | require.Error(t, err, "Expected context archive to be deleted after upload, but is still present on disk.") 74 | 75 | assert.Equal(t, "fakes3://bucket/kaniko/image/context-kaniko-image-version.tar.gz", url) 76 | } 77 | -------------------------------------------------------------------------------- /pkg/dag/image_test.go: -------------------------------------------------------------------------------- 1 | package dag_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/radiofrance/dib/pkg/dag" 7 | "github.com/radiofrance/dib/pkg/dockerfile" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_CurrentRef_EqualsHashWhenNoRebuildNeeded(t *testing.T) { 12 | t.Parallel() 13 | 14 | image := dag.Image{ 15 | Name: "gcr.io/project-id/nginx", 16 | Hash: "version", 17 | } 18 | 19 | assert.Equal(t, "gcr.io/project-id/nginx:version", image.CurrentRef()) 20 | } 21 | 22 | func Test_CurrentRef_HasDevPrefixWhenNeedsRebuild(t *testing.T) { 23 | t.Parallel() 24 | 25 | image := dag.Image{ 26 | Name: "gcr.io/project-id/nginx", 27 | Hash: "version", 28 | NeedsRebuild: true, 29 | } 30 | 31 | assert.Equal(t, "gcr.io/project-id/nginx:dev-version", image.CurrentRef()) 32 | } 33 | 34 | func Test_DockerRef(t *testing.T) { 35 | t.Parallel() 36 | 37 | image := dag.Image{ 38 | Name: "gcr.io/project-id/nginx", 39 | } 40 | 41 | assert.Equal(t, "gcr.io/project-id/nginx:version", image.DockerRef("version")) 42 | } 43 | 44 | func Test_Print(t *testing.T) { 45 | t.Parallel() 46 | 47 | image := dag.Image{ 48 | Name: "registry.example.org/alpine-base", 49 | ShortName: "alpine-base", 50 | Hash: "hak-una-mat-ata", 51 | Dockerfile: &dockerfile.Dockerfile{ 52 | ContextPath: "/example/project/docker/base/alpine", 53 | Filename: "Dockerfile", 54 | From: []dockerfile.ImageRef{ 55 | { 56 | Name: "alpine", 57 | Tag: "3.17", 58 | Digest: "9ed4aefc74f6792b5a804d1d146fe4b4a2299147b0f50eaf2b08435d7b38c27e", 59 | }, 60 | }, 61 | Labels: map[string]string{ 62 | "dib.extra-tags": "3.17", 63 | }, 64 | }, 65 | } 66 | 67 | expected := "" + 68 | "name: registry.example.org/alpine-base\n" + 69 | "short_name: alpine-base\n" + 70 | "hash: hak-una-mat-ata\n" + 71 | "dockerfile:\n" + 72 | " contextpath: /example/project/docker/base/alpine\n" + 73 | " filename: Dockerfile\n" + 74 | " from:\n" + 75 | " - name: alpine\n" + 76 | " tag: \"3.17\"\n" + 77 | " digest: 9ed4aefc74f6792b5a804d1d146fe4b4a2299147b0f50eaf2b08435d7b38c27e\n" + 78 | " labels:\n" + 79 | " dib.extra-tags: \"3.17\"\n" + 80 | " args: {}\n" 81 | assert.Equal(t, expected, image.Print()) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/docker/builder.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/radiofrance/dib/pkg/executor" 8 | 9 | "github.com/radiofrance/dib/internal/logger" 10 | "github.com/radiofrance/dib/pkg/types" 11 | ) 12 | 13 | // ImageBuilderTagger builds an image using the docker command-line executable. 14 | type ImageBuilderTagger struct { 15 | exec executor.ShellExecutor 16 | dryRun bool 17 | } 18 | 19 | // NewImageBuilderTagger creates a new instance of an ImageBuilderTagger. 20 | func NewImageBuilderTagger(executor executor.ShellExecutor, dryRun bool) *ImageBuilderTagger { 21 | return &ImageBuilderTagger{executor, dryRun} 22 | } 23 | 24 | // Build the image using the docker executable. 25 | // If the image is built successfully, the image will be pushed to the registry. 26 | func (b ImageBuilderTagger) Build(opts types.ImageBuilderOpts) error { 27 | dockerArgs := []string{ 28 | "build", 29 | "--no-cache", 30 | } 31 | 32 | for k, v := range opts.BuildArgs { 33 | dockerArgs = append(dockerArgs, fmt.Sprintf("--build-arg=%s=%s", k, v)) 34 | } 35 | 36 | for k, v := range opts.Labels { 37 | dockerArgs = append(dockerArgs, fmt.Sprintf("--label=%s=%s", k, v)) 38 | } 39 | 40 | for _, tag := range opts.Tags { 41 | dockerArgs = append(dockerArgs, fmt.Sprintf("--tag=%s", tag)) 42 | } 43 | 44 | dockerArgs = append(dockerArgs, opts.Context) 45 | 46 | if b.dryRun { 47 | logger.Infof("[DRY-RUN] docker %s", strings.Join(dockerArgs, " ")) 48 | 49 | if opts.Push { 50 | for _, tag := range opts.Tags { 51 | logger.Infof("[DRY-RUN] docker push %s", tag) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | err := b.exec.ExecuteWithWriter( 59 | opts.LogOutput, "docker", dockerArgs...) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if opts.Push { 65 | for _, tag := range opts.Tags { 66 | err := b.exec.ExecuteWithWriter( 67 | opts.LogOutput, "docker", "push", tag) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | 77 | // Tag runs a docker tag command to re-tag the source tag with the destination tag. 78 | func (b ImageBuilderTagger) Tag(src, dest string) error { 79 | if b.dryRun { 80 | logger.Infof("[DRY-RUN] docker tag %s %s", src, dest) 81 | return nil 82 | } 83 | 84 | return b.exec.ExecuteStdout("docker", "tag", src, dest) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/report/utils_test.go: -------------------------------------------------------------------------------- 1 | package report_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/radiofrance/dib/pkg/report" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestReport_RemoveTerminalColors(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | name string 17 | input string 18 | expected string 19 | }{ 20 | { 21 | name: "valid 1", 22 | input: "\u001b[31mHello World", 23 | expected: "Hello World", 24 | }, 25 | { 26 | name: "valid 2", 27 | input: "\u001b[30mA \u001b[31m B \u001b[32m C \u001b[33m D\u001b[0m", 28 | expected: "A B C D", 29 | }, 30 | { 31 | name: "valid 3", 32 | input: "\u001B[91mE: Unable to locate package lorem", 33 | expected: "E: Unable to locate package lorem", 34 | }, 35 | { 36 | name: "valid 4", 37 | input: "\u001B[0mThe command 'apt-get install -y lorem' returned a non-zero code: 100", 38 | expected: "The command 'apt-get install -y lorem' returned a non-zero code: 100", 39 | }, 40 | } 41 | 42 | for _, test := range tests { 43 | t.Run(test.name, func(t *testing.T) { 44 | t.Parallel() 45 | 46 | actual := report.RemoveTerminalColors([]byte(test.input)) 47 | assert.Equal(t, test.expected, string(actual)) 48 | }) 49 | } 50 | } 51 | 52 | func TestReport_StripKanikoBuildLogs(t *testing.T) { 53 | t.Parallel() 54 | 55 | tests := []struct { 56 | name string 57 | input string 58 | expected string 59 | }{ 60 | { 61 | name: "valid 1 (single line)", 62 | input: "../../test/fixtures/report/build_logs/kaniko/1_raw_log.txt", 63 | expected: "../../test/fixtures/report/build_logs/kaniko/1_parsed_log.txt", 64 | }, 65 | { 66 | name: "valid 1 (real case)", 67 | input: "../../test/fixtures/report/build_logs/kaniko/2_raw_log.txt", 68 | expected: "../../test/fixtures/report/build_logs/kaniko/2_parsed_log.txt", 69 | }, 70 | } 71 | 72 | for _, test := range tests { 73 | t.Run(test.name, func(t *testing.T) { 74 | t.Parallel() 75 | 76 | input, err := os.ReadFile(test.input) 77 | require.NoError(t, err) 78 | 79 | expected, err := os.ReadFile(test.expected) 80 | require.NoError(t, err) 81 | 82 | actual := report.StripKanikoBuildLogs(input) 83 | assert.Equal(t, string(expected), actual) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "strings" 8 | 9 | "github.com/radiofrance/dib/pkg/dib" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func listCommand() *cobra.Command { 14 | const longHelp = `Command list provide different ways to print the list of all Docker images managed by dib. 15 | 16 | The output can be customized with the --output flag : 17 | • console (default output) 18 | ex : dib list 19 | 20 | • go-template-file (render output using a Go template) 21 | ex : dib list -o go-template-file=dib_list.tmpl 22 | 23 | • graphviz (dot language output) 24 | ex : dib list -o graphviz 25 | 26 | You can also generate a PNG image from the graphviz output using the following command : 27 | dib list -o graphviz | dot -Tpng > dib.png 28 | ` 29 | 30 | cmd := &cobra.Command{ 31 | Use: "list", 32 | Short: "List all images managed by dib", 33 | Long: longHelp, 34 | RunE: listAction, 35 | SilenceUsage: true, 36 | } 37 | cmd.Flags().StringP("output", "o", dib.ConsoleFormat, 38 | "Output format : console|graphviz|go-template-file") 39 | cmd.Flags().StringArray("build-arg", []string{}, 40 | "`argument=value` to supply to the builder") 41 | 42 | return cmd 43 | } 44 | 45 | func listAction(cmd *cobra.Command, _ []string) error { 46 | // Bind command flags to viper configuration using snake_case 47 | bindPFlagsSnakeCase(cmd.Flags()) 48 | 49 | opts := dib.ListOpts{} 50 | hydrateOptsFromViper(&opts) 51 | 52 | formatOpts, err := dib.ParseOutputOptions(opts.Output) 53 | if err != nil { 54 | return fmt.Errorf("error while parsing output options: %w", err) 55 | } 56 | 57 | buildArgs := map[string]string{} 58 | 59 | for _, arg := range opts.BuildArg { 60 | key, val, hasVal := strings.Cut(arg, "=") 61 | if hasVal { 62 | buildArgs[key] = os.ExpandEnv(val) 63 | } else { 64 | // check if the env is set in the local environment and use that value if it is 65 | if val, present := os.LookupEnv(key); present { 66 | buildArgs[key] = os.ExpandEnv(val) 67 | } else { 68 | delete(buildArgs, key) 69 | } 70 | } 71 | } 72 | 73 | buildPath := path.Join(workingDir, opts.BuildPath) 74 | 75 | graph, err := dib.GenerateDAG(buildPath, opts.RegistryURL, opts.HashListFilePath, buildArgs) 76 | if err != nil { 77 | return fmt.Errorf("cannot generate DAG: %w", err) 78 | } 79 | 80 | return dib.GenerateList(graph, formatOpts) 81 | } 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to dib 2 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | - Becoming a maintainer 9 | 10 | ## We Develop with Github 11 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `main`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the test suite passes. 20 | 5. Make sure your code lints. 21 | 6. Issue that pull request! 22 | 23 | ## Any contributions you make will be under the CeCILL V2.1 License 24 | In short, when you submit code changes, your submissions are understood to be under the same [CeCILL V2.1 License](https://cecill.info/licences/Licence_CeCILL_V2.1-en.txt) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](https://github.com/radiofrance/dib/issues) 27 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/radiofrance/dib/issues); it's that easy! 28 | 29 | ## Write bug reports with detail, background, and sample code 30 | 31 | **Great Bug Reports** tend to have: 32 | 33 | - A quick summary and/or background 34 | - Steps to reproduce 35 | - Be specific! 36 | - Give sample code if you can. [My stackoverflow question](http://stackoverflow.com/q/12488905/180626) includes sample code that *anyone* with a base R setup can run to reproduce what I was seeing 37 | - What you expected would happen 38 | - What actually happens 39 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 40 | 41 | People *love* thorough bug reports. 42 | 43 | ## References 44 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) 45 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | site_name: dib 3 | site_description: | 4 | dib is a tool designed to help build multiple Docker images defined within a directory, possibly having dependencies 5 | with one another, in a single command. 6 | site_author: Radio France 7 | site_url: https://radiofrance.github.io 8 | repo_name: radiofrance/dib 9 | repo_url: https://github.com/radiofrance/dib 10 | edit_uri: "" 11 | 12 | theme: 13 | name: material 14 | logo: images/dib_logo.png 15 | favicon: images/favicon.ico 16 | language: en 17 | palette: 18 | - media: "(prefers-color-scheme: light)" 19 | scheme: default 20 | primary: blue 21 | accent: light blue 22 | toggle: 23 | icon: material/brightness-7 24 | name: Switch to dark mode 25 | - media: "(prefers-color-scheme: dark)" 26 | scheme: slate 27 | primary: blue 28 | accent: light blue 29 | toggle: 30 | icon: material/brightness-4 31 | name: Switch to light mode 32 | features: 33 | - navigation.sections 34 | - navigation.top 35 | 36 | docs_dir: docs 37 | 38 | plugins: 39 | - search 40 | 41 | markdown_extensions: 42 | - admonition 43 | - attr_list 44 | - codehilite 45 | - md_in_html 46 | - pymdownx.blocks.admonition 47 | - pymdownx.tabbed: 48 | alternate_style: true 49 | - pymdownx.snippets: 50 | base_path: docs 51 | - pymdownx.superfences: 52 | custom_fences: 53 | - name: mermaid 54 | class: mermaid 55 | format: !!python/name:pymdownx.superfences.fence_code_format 56 | 57 | nav: 58 | - Introduction: index.md 59 | - Installation: install.md 60 | - Quickstart: quickstart.md 61 | - Topics: 62 | - Build Backends: backends.md 63 | - Executors: executors.md 64 | - Configuration: configuration.md 65 | - Tests: tests.md 66 | - Reporting: reports.md 67 | - Extra Tags: extra-tags.md 68 | - Reference: 69 | - Configuration: configuration-reference.md 70 | - Command Line: 71 | - Build: cmd/dib_build.md 72 | - List: cmd/dib_list.md 73 | - Version: cmd/dib_version.md 74 | - Completion: 75 | - Bash: cmd/dib_completion_bash.md 76 | - Fish: cmd/dib_completion_fish.md 77 | - Powershell: cmd/dib_completion_powershell.md 78 | - ZSH: cmd/dib_completion_zsh.md 79 | - Development: 80 | - Documentation: documentation.md 81 | - Roadmap: roadmap.md 82 | -------------------------------------------------------------------------------- /pkg/report/templates/debug.go.html: -------------------------------------------------------------------------------- 1 | {{- define "title" -}}Debug | dib{{- end -}} 2 | {{- define "content" -}} 3 |

4 | Debug infos 5 |

6 |
7 | 8 |
9 |
10 |

11 | 19 |

20 |
24 |
25 |

26 |                     {{- .Data -}}
27 |                     
28 |
29 |
30 |
31 |
32 |

33 | 41 |

42 |
46 |
47 |

48 |                         {{- .Opt.BuildOpts -}}
49 |                     
50 |
51 |
52 |
53 |
54 | {{- end -}} 55 | {{- define "extra_javascript" -}} 56 | 57 | 58 | {{- end -}} 59 | -------------------------------------------------------------------------------- /.github/workflows/qa.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: QA 3 | on: # yamllint disable-line rule:truthy 4 | pull_request: 5 | types: [opened, synchronize] 6 | paths: 7 | - ".github/workflows/qa.yml" 8 | - "go.*" 9 | - "**.go" 10 | push: 11 | branches: [main] 12 | 13 | permissions: 14 | contents: read 15 | pull-requests: read 16 | checks: write 17 | 18 | jobs: 19 | lint: 20 | name: Run linters 21 | runs-on: ubuntu-24.04-arm 22 | env: 23 | # renovate: datasource=github-releases depName=radiofrance/lint-config 24 | LINT_CONFIG_VERSION: v1.1.1 25 | steps: 26 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 27 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 28 | with: 29 | go-version-file: "go.mod" 30 | - name: Download golangci-lint config file 31 | run: curl -o .golangci.yml https://raw.githubusercontent.com/radiofrance/lint-config/${{ env.LINT_CONFIG_VERSION }}/.golangci.yml 32 | - uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 33 | with: 34 | # renovate: datasource=github-releases depName=golangci/golangci-lint 35 | version: v2.6.2 36 | 37 | tests: 38 | name: Run tests 39 | runs-on: ubuntu-24.04-arm 40 | steps: 41 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 42 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 43 | with: 44 | go-version-file: "go.mod" 45 | - uses: tlylt/install-graphviz@b2201200d85f06f0189cb74d9b69208504cf12cd # v1.0.0 46 | - run: go test -v -race -covermode=atomic -coverprofile=coverage.out ./... 47 | - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 48 | with: 49 | name: coverage-results 50 | path: coverage.out 51 | retention-days: 1 52 | 53 | codecov: 54 | name: Upload coverage to Codecov 55 | needs: [tests] 56 | runs-on: ubuntu-24.04-arm 57 | steps: 58 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 59 | - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 60 | with: 61 | name: coverage-results 62 | - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 63 | with: 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | fail_ci_if_error: true 66 | verbose: true 67 | -------------------------------------------------------------------------------- /pkg/dib/plan.go: -------------------------------------------------------------------------------- 1 | package dib 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/radiofrance/dib/internal/logger" 8 | "github.com/radiofrance/dib/pkg/dag" 9 | "github.com/radiofrance/dib/pkg/types" 10 | ) 11 | 12 | // Plan decides which actions need to be performed on each image. 13 | func (p *Builder) Plan(registry types.DockerRegistry) error { 14 | if p.ForceRebuild { 15 | logger.Infof("force rebuild mode enabled, all images will be rebuild regardless of their changes") 16 | p.Graph.Walk(func(node *dag.Node) { 17 | node.Image.NeedsRebuild = true 18 | node.Image.NeedsTests = !p.NoTests 19 | }) 20 | 21 | return nil 22 | } 23 | 24 | tagExistsMap, err := refExistsMapForTag(p.Graph, registry) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | err = checkNeedsRebuild(p.Graph, tagExistsMap) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if p.NoTests { 35 | return nil 36 | } 37 | 38 | // Enable tests on images that need to be rebuilt. 39 | p.Graph.Walk(func(node *dag.Node) { 40 | if node.Image.NeedsRebuild { 41 | node.Image.NeedsTests = true 42 | } 43 | }) 44 | 45 | return nil 46 | } 47 | 48 | // checkNeedsRebuild iterates over the graph to find out which images 49 | // can't be found with their current tag, and must be rebuilt. 50 | func checkNeedsRebuild(graph *dag.DAG, tagExistsMap *sync.Map) error { 51 | return graph.WalkErr(func(node *dag.Node) error { 52 | img := node.Image 53 | ref := img.DockerRef(img.Hash) 54 | 55 | tagExists, present := tagExistsMap.Load(ref) 56 | if !present { 57 | return fmt.Errorf("could not check if %s exists", ref) 58 | } 59 | 60 | if tagExists.(bool) { //nolint:forcetypeassert 61 | logger.Debugf("Ref \"%s\" already exists, no rebuild required", ref) 62 | return nil 63 | } 64 | 65 | logger.Infof("Ref \"%s\" is missing, image must be rebuilt", ref) 66 | 67 | img.NeedsRebuild = true 68 | 69 | return nil 70 | }) 71 | } 72 | 73 | func refExistsMapForTag(graph *dag.DAG, registry types.DockerRegistry) (*sync.Map, error) { 74 | refExistsMap := &sync.Map{} 75 | 76 | err := graph.WalkAsyncErr(func(node *dag.Node) error { 77 | img := node.Image 78 | ref := img.DockerRef(img.Hash) 79 | 80 | refAlreadyExists, err := registry.RefExists(ref) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | refExistsMap.Store(ref, refAlreadyExists) 86 | 87 | return nil 88 | }) 89 | if err != nil { 90 | return nil, fmt.Errorf("error during api call to check registry if tag exists: %w", err) 91 | } 92 | 93 | return refExistsMap, nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/dib/tag_test.go: -------------------------------------------------------------------------------- 1 | package dib_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/radiofrance/dib/pkg/dag" 7 | "github.com/radiofrance/dib/pkg/dib" 8 | "github.com/radiofrance/dib/pkg/mock" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_Retag_DoesNotRetagIfNoRetagNeeded(t *testing.T) { 14 | t.Parallel() 15 | 16 | DAG := &dag.DAG{} 17 | DAG.AddNode(dag.NewNode(&dag.Image{ 18 | Name: "registry.example.org/image", 19 | ShortName: "image", 20 | RetagDone: false, 21 | })) 22 | 23 | tagger := &mock.Tagger{} 24 | err := dib.Retag(DAG, tagger, "DIB_MANAGED_VERSION", false) 25 | 26 | require.NoError(t, err) 27 | assert.Empty(t, tagger.RecordedCallsArgs) 28 | } 29 | 30 | func Test_Retag_RetagWhenRebuild(t *testing.T) { 31 | t.Parallel() 32 | 33 | DAG := &dag.DAG{} 34 | DAG.AddNode(dag.NewNode(&dag.Image{ 35 | Name: "registry.example.org/image", 36 | ShortName: "image", 37 | Hash: "myhash", 38 | RetagDone: false, 39 | NeedsRebuild: true, 40 | })) 41 | 42 | tagger := &mock.Tagger{} 43 | err := dib.Retag(DAG, tagger, "DIB_MANAGED_VERSION", false) 44 | 45 | require.NoError(t, err) 46 | require.Len(t, tagger.RecordedCallsArgs, 1) 47 | args := tagger.RecordedCallsArgs[0] 48 | assert.Equal(t, "registry.example.org/image:dev-myhash", args.Src) 49 | assert.Equal(t, "registry.example.org/image:myhash", args.Dest) 50 | } 51 | 52 | func Test_Retag_ReleaseWithPlaceholderTagAndExtraTags(t *testing.T) { 53 | t.Parallel() 54 | 55 | img := &dag.Image{ 56 | Name: "registry.example.org/image", 57 | ShortName: "image", 58 | Hash: "myhash", 59 | ExtraTags: []string{"latest1", "latest2"}, 60 | RetagDone: false, 61 | } 62 | DAG := &dag.DAG{} 63 | DAG.AddNode(dag.NewNode(img)) 64 | 65 | tagger := &mock.Tagger{} 66 | err := dib.Retag(DAG, tagger, "DIB_MANAGED_VERSION", true) 67 | 68 | require.NoError(t, err) 69 | 70 | require.Len(t, tagger.RecordedCallsArgs, 3) 71 | 72 | assert.Equal(t, "registry.example.org/image:myhash", tagger.RecordedCallsArgs[0].Src) 73 | assert.Equal(t, "registry.example.org/image:DIB_MANAGED_VERSION", tagger.RecordedCallsArgs[0].Dest) 74 | assert.Equal(t, "registry.example.org/image:myhash", tagger.RecordedCallsArgs[1].Src) 75 | assert.Equal(t, "registry.example.org/image:latest1", tagger.RecordedCallsArgs[1].Dest) 76 | assert.Equal(t, "registry.example.org/image:myhash", tagger.RecordedCallsArgs[2].Src) 77 | assert.Equal(t, "registry.example.org/image:latest2", tagger.RecordedCallsArgs[2].Dest) 78 | 79 | assert.True(t, img.RetagDone) 80 | } 81 | -------------------------------------------------------------------------------- /test/fixtures/report/build_logs/kaniko/2_raw_log.txt: -------------------------------------------------------------------------------- 1 | time="2022-11-10T09:51:08Z" level=info msg="Retrieving image manifest eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona" 2 | time="2022-11-10T09:51:08Z" level=info msg="Retrieving image eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona from registry eu.gcr.io" 3 | time="2022-11-10T09:51:09Z" level=info msg="Built cross stage deps: map[]" 4 | time="2022-11-10T09:51:09Z" level=info msg="Retrieving image manifest eu.gcr.io/my-example-registry/alpine-test:dev-triple-seventeen-maine-arizona" 5 | time="2022-11-10T09:51:09Z" level=info msg="Returning cached image manifest" 6 | time="2022-11-10T09:51:09Z" level=info msg="Executing 0 build triggers" 7 | time="2022-11-10T09:51:09Z" level=info msg="Unpacking rootfs as cmd RUN apt-get update && apt-get install -y lorem requires it." 8 | time="2022-11-10T09:51:13Z" level=info msg="Taking snapshot of full filesystem..." 9 | time="2022-11-10T09:51:16Z" level=info msg="LABEL name=\"alpine-git\"" 10 | time="2022-11-10T09:51:16Z" level=info msg="Applying label name=alpine-git" 11 | time="2022-11-10T09:51:16Z" level=info msg="USER root" 12 | time="2022-11-10T09:51:16Z" level=info msg="cmd: USER" 13 | time="2022-11-10T09:51:16Z" level=info msg="RUN apt-get update && apt-get install -y lorem" 14 | time="2022-11-10T09:51:16Z" level=info msg="cmd: /bin/bash" 15 | time="2022-11-10T09:51:16Z" level=info msg="args: [-o pipefail -c apt-get update && apt-get install -y lorem]" 16 | time="2022-11-10T09:51:16Z" level=info msg="util.Lookup returned: &{Uid:0 Gid:0 Username:root Name: HomeDir:/root}" 17 | time="2022-11-10T09:51:16Z" level=info msg="performing slow lookup of group ids for root" 18 | time="2022-11-10T09:51:16Z" level=info msg="Running: [/bin/bash -o pipefail -c apt-get update && apt-get install -y lorem]" 19 | Get:1 http://security.debian.org/debian-security bullseye-security InRelease [48.4 kB] 20 | Get:2 http://deb.debian.org/debian bullseye InRelease [116 kB] 21 | Get:3 http://deb.debian.org/debian bullseye-updates InRelease [44.1 kB] 22 | Get:4 http://security.debian.org/debian-security bullseye-security/main amd64 Packages [194 kB] 23 | Get:5 http://deb.debian.org/debian bullseye/main amd64 Packages [8184 kB] 24 | Get:6 http://deb.debian.org/debian bullseye-updates/main amd64 Packages [14.6 kB] 25 | Fetched 8600 kB in 1s (6877 kB/s) 26 | Reading package lists... 27 | Reading package lists... 28 | Building dependency tree... 29 | Reading state information... 30 | E: Unable to locate package lorem 31 | error building image: error building stage: failed to execute command: waiting for process to exit: exit status 100 32 | -------------------------------------------------------------------------------- /pkg/report/templates/_nav.go.html: -------------------------------------------------------------------------------- 1 | {{- define "nav" -}} 2 |
3 | dig_logo 4 | 42 |
43 | Made with ♥ by Radio France 44 | 45 | 46 | {{ .Opt.Version }} 47 | 48 |
49 |
50 | {{- end -}} 51 | -------------------------------------------------------------------------------- /pkg/buildkit/s3.go: -------------------------------------------------------------------------------- 1 | package buildkit 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/service/s3" 13 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 14 | "github.com/radiofrance/dib/internal/logger" 15 | ) 16 | 17 | // S3Uploader is a FileUploader that uploads files to an AWS S3 bucket. 18 | type S3Uploader struct { 19 | s3 *s3.Client 20 | bucket string 21 | } 22 | 23 | // NewS3Uploader creates a new instance of S3Uploader. 24 | func NewS3Uploader(cfg aws.Config, bucket string) *S3Uploader { 25 | return &S3Uploader{ 26 | s3: s3.NewFromConfig(cfg), 27 | bucket: bucket, 28 | } 29 | } 30 | 31 | // UploadFile uploads a file to an AWS S3 bucket. 32 | func (u S3Uploader) UploadFile(filePath string, targetPath string) error { 33 | file, err := os.Open(filePath) //nolint:gosec 34 | if err != nil { 35 | return fmt.Errorf("can't open file %s: %w", filePath, err) 36 | } 37 | 38 | defer func() { 39 | err := file.Close() 40 | if err != nil { 41 | logger.Errorf("can't close file %s: %v", filePath, err) 42 | } 43 | }() 44 | 45 | // Get file size and read the file content into a buffer 46 | fileInfo, _ := file.Stat() 47 | size := fileInfo.Size() 48 | buffer := make([]byte, size) 49 | 50 | _, err = file.Read(buffer) 51 | if err != nil { 52 | return fmt.Errorf("can't read file %s: %w", filePath, err) 53 | } 54 | 55 | query := &s3.PutObjectInput{ 56 | Bucket: aws.String(u.bucket), 57 | Key: aws.String(targetPath), 58 | ACL: types.ObjectCannedACLPrivate, 59 | Body: bytes.NewReader(buffer), 60 | ContentLength: &size, 61 | ContentType: aws.String(http.DetectContentType(buffer)), 62 | } 63 | 64 | _, err = u.s3.PutObject(context.Background(), query) 65 | if err != nil { 66 | return fmt.Errorf("can't send S3 PUT request: %w", err) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // PresignedURL generates a presigned URL for accessing an object in the S3 bucket. 73 | // The URL is valid for a limited time and allows temporary access to the specified object. 74 | func (u S3Uploader) PresignedURL(targetPath string) (string, error) { 75 | presignClient := s3.NewPresignClient(u.s3) 76 | presignParams := &s3.GetObjectInput{ 77 | Bucket: aws.String(u.bucket), 78 | Key: aws.String(targetPath), 79 | } 80 | 81 | presignedURL, err := presignClient.PresignGetObject(context.Background(), presignParams, func(o *s3.PresignOptions) { 82 | o.Expires = 1 * time.Hour 83 | }) 84 | if err != nil { 85 | return "", fmt.Errorf("can't generate presigned URL: %w", err) 86 | } 87 | 88 | return presignedURL.URL, nil 89 | } 90 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:best-practices", 5 | ":automergeMinor", 6 | ":automergeDigest", 7 | ":enableVulnerabilityAlerts", 8 | ":gitSignOff", 9 | ":ignoreModulesAndTests", 10 | ":prConcurrentLimitNone", 11 | ":prHourlyLimitNone", 12 | ":label(dependencies)", 13 | ":semanticCommitScope(deps)", 14 | ":separateMultipleMajorReleases", 15 | "group:kubernetes", 16 | "customManagers:dockerfileVersions" 17 | ], 18 | "packageRules": [ 19 | { 20 | "description": "Delay all updates by 14 days to give the upstream registries time to pull malicious dependencies", 21 | "matchPackageNames": [ 22 | "*" 23 | ], 24 | "minimumReleaseAge": "14 days" 25 | }, 26 | { 27 | "description": "Update digest once a month", 28 | "matchUpdateTypes": [ 29 | "digest" 30 | ], 31 | "extends": [ 32 | "schedule:monthly" 33 | ] 34 | }, 35 | { 36 | "description": "Group lint related bumps", 37 | "groupName": "lint", 38 | "matchPackageNames": [ 39 | "/lint/" 40 | ], 41 | "separateMajorMinor": false 42 | }, 43 | { 44 | "description": "Group aws-sdk-go-v2 related bumps", 45 | "groupName": "aws-sdk-go-v2", 46 | "matchPackageNames": [ 47 | "/aws-sdk-go-v2/" 48 | ], 49 | "separateMajorMinor": false 50 | }, 51 | { 52 | "description": "Group GitHub Actions related bumps", 53 | "groupName": "GitHub Actions", 54 | "matchManagers": [ 55 | "github-actions" 56 | ], 57 | "matchPackageNames": [ 58 | "/actions/" 59 | ], 60 | "separateMajorMinor": false 61 | }, 62 | { 63 | "description": "Update Go directives in go.mod files", 64 | "matchManagers": [ 65 | "gomod" 66 | ], 67 | "matchDepNames": [ 68 | "go" 69 | ], 70 | "matchDepTypes": [ 71 | "golang" 72 | ], 73 | "rangeStrategy": "bump" 74 | } 75 | ], 76 | "postUpdateOptions": [ 77 | "gomodUpdateImportPaths", 78 | "gomodTidy" 79 | ], 80 | "customManagers": [ 81 | { 82 | "description": "Update variables in Makefile and GitHub Actions files", 83 | "customType": "regex", 84 | "managerFilePatterns": [ 85 | "/(^|/)Makefile$/", 86 | "/^\\.github/workflows/.*\\.ya?ml/" 87 | ], 88 | "matchStrings": [ 89 | "#\\s*renovate:\\s*datasource=(?.*?) depName=(?.*?)(?: versioning=(?.*?))?\\s+.*\\s*[:=]\\s*['\"]?(?.+?)['\"]?\\s" 90 | ] 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /pkg/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | const ( 8 | // BackendDocker use "Docker" for building Docker images. 9 | BackendDocker = "docker" 10 | // BackendKaniko use "Kaniko" for building Docker images. 11 | BackendKaniko = "kaniko" 12 | // BuildKitBackend use buildkit for building oci images. 13 | BuildKitBackend = "buildkit" 14 | // TestRunnerGoss use Goss for testing Docker images. 15 | TestRunnerGoss = "goss" 16 | // TestRunnerTrivy use Trivy for testing Docker images. 17 | TestRunnerTrivy = "trivy" 18 | ) 19 | 20 | // ImageBuilder is the interface for building oci images. 21 | type ImageBuilder interface { 22 | Build(opts ImageBuilderOpts) error 23 | } 24 | 25 | // ImageBuilderOpts is a set of options to perform oci image build. 26 | type ImageBuilderOpts struct { 27 | // BuildkitHost is the address of Buildkit host 28 | BuildkitHost string 29 | // Path to the build context. 30 | Context string 31 | // File is the name of the Dockerfile 32 | File string 33 | // LocalOnly is true if the build context is local and not remote. 34 | LocalOnly bool 35 | // Target is the target of the build 36 | Target string 37 | // Name of the tags to build, same as passed to the '-t' flag of the docker build command. 38 | Tags []string 39 | // Labels a key/value set of labels to add to the image. 40 | Labels map[string]string 41 | // BuildArgs a key/value set of build args to pass to the build command. 42 | BuildArgs map[string]string 43 | // Push instructs to push to the remote registry after build, or skip it. 44 | Push bool 45 | // LogOutput is writer where build logs should be written 46 | LogOutput io.Writer 47 | // Progress Set type of progress output (auto, plain, tty). Use plain to show container output 48 | Progress string 49 | } 50 | 51 | // ImageTagger is an abstraction for tagging docker images. 52 | type ImageTagger interface { 53 | Tag(from, to string) error 54 | } 55 | 56 | // TestRunner is an interface for dealing with docker tests, such as goss, trivy. 57 | type TestRunner interface { 58 | Name() string 59 | IsConfigured(opts RunTestOptions) bool 60 | 61 | // RunTest function should execute tests (trivy scan, goss test, etc...). 62 | // It returns nil if test was successfully executed, an error if any problem occurs 63 | RunTest(opts RunTestOptions) error 64 | } 65 | 66 | type RunTestOptions struct { 67 | ImageName string 68 | ImageReference string 69 | DockerContextPath string 70 | BuildkitHost string 71 | ReportJunitDir string 72 | ReportTrivyDir string 73 | } 74 | 75 | // DockerRegistry is an interface for dealing with docker registries. 76 | type DockerRegistry interface { 77 | RefExists(imageRef string) (bool, error) 78 | } 79 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | Installation Guide 2 | ================== 3 | 4 | === "Install with go" 5 | 6 | Install the latest release on macOS or Linux with: 7 | 8 | ```bash 9 | go install github.com/radiofrance/dib@latest 10 | ``` 11 | 12 | === "From binary" 13 | 14 | Binaries are available to download from the [GitHub releases](https://github.com/radiofrance/dib/releases) page. 15 | 16 | ## Shell autocompletion 17 | 18 | Configure your shell to load dib completions: 19 | 20 | === "Bash" 21 | 22 | To load completion run: 23 | 24 | ```shell 25 | . <(dib completion bash) 26 | ``` 27 | 28 | To configure your bash shell to load completions for each session add to your bashrc: 29 | 30 | ```shell 31 | # ~/.bashrc or ~/.bash_profile 32 | command -v dib >/dev/null && . <(dib completion bash) 33 | ``` 34 | 35 | If you have an alias for dib, you can extend shell completion to work with that alias: 36 | 37 | ```shell 38 | # ~/.bashrc or ~/.bash_profile 39 | alias tm=dib 40 | complete -F __start_dib tm 41 | ``` 42 | 43 | === "Fish" 44 | 45 | To configure your fish shell to [load completions](http://fishshell.com/docs/current/index.html#completion-own) 46 | for each session write this script to your completions dir: 47 | 48 | ```shell 49 | dib completion fish > ~/.config/fish/completions/dib.fish 50 | ``` 51 | 52 | === "Powershell" 53 | 54 | To load completion run: 55 | 56 | ```shell 57 | . <(dib completion powershell) 58 | ``` 59 | 60 | To configure your powershell shell to load completions for each session add to your powershell profile: 61 | 62 | Windows: 63 | 64 | ```shell 65 | cd "$env:USERPROFILE\Documents\WindowsPowerShell\Modules" 66 | dib completion >> dib-completion.ps1 67 | ``` 68 | Linux: 69 | 70 | ```shell 71 | cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules" 72 | dib completion >> dib-completions.ps1 73 | ``` 74 | 75 | === "Zsh" 76 | 77 | To load completion run: 78 | 79 | ```shell 80 | . <(dib completion zsh) && compdef _dib dib 81 | ``` 82 | 83 | To configure your zsh shell to load completions for each session add to your zshrc: 84 | 85 | ```shell 86 | # ~/.zshrc or ~/.profile 87 | command -v dib >/dev/null && . <(dib completion zsh) && compdef _dib dib 88 | ``` 89 | 90 | or write a cached file in one of the completion directories in your ${fpath}: 91 | 92 | ```shell 93 | echo "${fpath// /\n}" | grep -i completion 94 | dib completion zsh > _dib 95 | 96 | mv _dib ~/.oh-my-zsh/completions # oh-my-zsh 97 | mv _dib ~/.zprezto/modules/completion/external/src/ # zprezto 98 | ``` 99 | -------------------------------------------------------------------------------- /pkg/exec/kubernetes.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/radiofrance/dib/internal/logger" 10 | k8sutils "github.com/radiofrance/dib/pkg/kubernetes" 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/client-go/kubernetes" 15 | ) 16 | 17 | // KubernetesExecutor will run Buildkit in a Kubernetes cluster. 18 | type KubernetesExecutor struct { 19 | clientSet kubernetes.Interface 20 | } 21 | 22 | // NewKubernetesExecutor creates a new instance of KubernetesExecutor. 23 | func NewKubernetesExecutor(clientSet kubernetes.Interface) *KubernetesExecutor { 24 | return &KubernetesExecutor{ 25 | clientSet: clientSet, 26 | } 27 | } 28 | 29 | // ApplyWithWriters executes a Buildkit build using a Kubernetes Pod. 30 | // Currently, this function is designed to handle only Pod objects. 31 | // It may evolve in the future to support other types of Kubernetes objects. 32 | // 33 | //nolint:lll 34 | func (e KubernetesExecutor) ApplyWithWriters(ctx context.Context, stdout, stderr io.Writer, k8sObject runtime.Object, containerNames string) error { 35 | pod, ok := k8sObject.(*corev1.Pod) 36 | if !ok { 37 | return errors.New("only pod object is supported") 38 | } 39 | 40 | watcher, err := e.clientSet.CoreV1().Pods(pod.Namespace).Watch(ctx, metav1.ListOptions{ 41 | LabelSelector: fmt.Sprintf("app.kubernetes.io/instance=%s", pod.Name), 42 | Watch: true, 43 | }) 44 | if err != nil { 45 | return fmt.Errorf("failed to watch pod: %w", err) 46 | } 47 | defer watcher.Stop() 48 | 49 | readyChan, errChan := k8sutils.MonitorPod(ctx, watcher) 50 | 51 | go func() { 52 | <-readyChan 53 | // Kubernetes logs API returns a single combined stdout+stderr stream. 54 | // Avoid duplicating logs when stdout and stderr point to the same writer. 55 | var out io.Writer 56 | 57 | switch { 58 | case stdout == nil && stderr == nil: 59 | out = io.Discard 60 | case stderr == nil || stderr == stdout: 61 | out = stdout 62 | case stdout == nil: 63 | out = stderr 64 | default: 65 | out = io.MultiWriter(stdout, stderr) 66 | } 67 | 68 | k8sutils.PrintPodLogs(ctx, out, e.clientSet, pod.Namespace, pod.Name, containerNames) 69 | }() 70 | 71 | _, err = e.clientSet.CoreV1().Pods(pod.Namespace).Create(ctx, pod, metav1.CreateOptions{}) 72 | if err != nil { 73 | return fmt.Errorf("failed to create Buildkit pod: %w", err) 74 | } 75 | 76 | defer func() { 77 | err := e.clientSet.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{}) 78 | if err != nil { 79 | logger.Warnf("Failed to delete Buildkit pod %s, ignoring: %v", pod.Name, err) 80 | } 81 | }() 82 | 83 | err = <-errChan 84 | if err != nil { 85 | return fmt.Errorf("error watching Buildkit pod: %w", err) 86 | } 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/kaniko/context_remote_internal_test.go: -------------------------------------------------------------------------------- 1 | //nolint:gosec 2 | package kaniko 3 | 4 | import ( 5 | "archive/tar" 6 | "compress/gzip" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func Test_createArchive(t *testing.T) { 17 | t.Parallel() 18 | 19 | t.Run("successful archive creation", func(t *testing.T) { 20 | t.Parallel() 21 | 22 | srcDir := t.TempDir() 23 | err := os.WriteFile(filepath.Join(srcDir, "file1.txt"), []byte("content1"), 0o644) 24 | require.NoError(t, err) 25 | err = os.WriteFile(filepath.Join(srcDir, "file2.txt"), []byte("content2"), 0o644) 26 | require.NoError(t, err) 27 | 28 | archivePath := filepath.Join(srcDir, "test.tar.gz") 29 | err = createArchive(srcDir, archivePath) 30 | require.NoError(t, err) 31 | 32 | verifyArchive(t, archivePath, []string{"file1.txt", "file2.txt"}) 33 | }) 34 | 35 | t.Run("non-existent destination path", func(t *testing.T) { 36 | t.Parallel() 37 | 38 | srcDir := t.TempDir() 39 | err := os.WriteFile(filepath.Join(srcDir, "file.txt"), []byte("content"), 0o644) 40 | require.NoError(t, err) 41 | 42 | archivePath := filepath.Join(srcDir, "non-existent", "test.tar.gz") 43 | err = createArchive(srcDir, archivePath) 44 | require.NoError(t, err) 45 | 46 | verifyArchive(t, archivePath, []string{"file.txt"}) 47 | }) 48 | 49 | t.Run("empty directory", func(t *testing.T) { 50 | t.Parallel() 51 | 52 | srcDir := t.TempDir() 53 | archivePath := filepath.Join(srcDir, "test.tar.gz") 54 | 55 | err := createArchive(srcDir, archivePath) 56 | require.NoError(t, err) 57 | 58 | verifyArchive(t, archivePath, []string{}) 59 | }) 60 | 61 | t.Run("non-existent source directory", func(t *testing.T) { 62 | t.Parallel() 63 | 64 | srcDir := "/non/existent/directory" 65 | archivePath := "/tmp/test.tar.gz" 66 | 67 | err := createArchive(srcDir, archivePath) 68 | require.Error(t, err) 69 | assert.Contains(t, err.Error(), "can't access directory") 70 | }) 71 | } 72 | 73 | // Helper function to verify the contents of a .tar.gz archive. 74 | func verifyArchive(t *testing.T, archivePath string, expectedFiles []string) { 75 | t.Helper() 76 | 77 | file, err := os.Open(archivePath) 78 | require.NoError(t, err) 79 | t.Cleanup(func() { 80 | _ = file.Close() 81 | }) 82 | 83 | gzipReader, err := gzip.NewReader(file) 84 | require.NoError(t, err) 85 | t.Cleanup(func() { 86 | _ = gzipReader.Close() 87 | }) 88 | 89 | tarReader := tar.NewReader(gzipReader) 90 | 91 | var files []string 92 | 93 | for { 94 | header, err := tarReader.Next() 95 | if err == io.EOF { 96 | break 97 | } 98 | 99 | require.NoError(t, err) 100 | 101 | files = append(files, header.Name) 102 | } 103 | 104 | assert.ElementsMatch(t, expectedFiles, files) 105 | } 106 | -------------------------------------------------------------------------------- /pkg/dag/node.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | import ( 4 | "sync" 5 | 6 | "golang.org/x/sync/errgroup" 7 | ) 8 | 9 | // NodeVisitorFunc visits a node of the graph. 10 | type NodeVisitorFunc func(*Node) 11 | 12 | // NodeVisitorFuncErr visits a node of the graph, and can return an error. 13 | type NodeVisitorFuncErr func(*Node) error 14 | 15 | // Node represents a node of a graph. 16 | type Node struct { 17 | Image *Image 18 | 19 | waitCond *sync.Cond 20 | done bool 21 | 22 | parents []*Node 23 | children []*Node 24 | } 25 | 26 | // NewNode creates a new instance of a Node. 27 | func NewNode(image *Image) *Node { 28 | return &Node{ 29 | Image: image, 30 | waitCond: sync.NewCond(&sync.Mutex{}), 31 | } 32 | } 33 | 34 | // AddChild adds a child node and add the current node to its parents. 35 | func (n *Node) AddChild(node *Node) { 36 | n.children = append(n.children, node) 37 | 38 | node.parents = append(node.parents, n) 39 | } 40 | 41 | // Children returns the children of the node. 42 | func (n *Node) Children() []*Node { 43 | return n.children 44 | } 45 | 46 | // Parents returns the parents of the node. 47 | func (n *Node) Parents() []*Node { 48 | return n.parents 49 | } 50 | 51 | // walk applies the visitor func to the current node, then to every children nodes, recursively. 52 | func (n *Node) walk(visitor NodeVisitorFunc) { 53 | visitor(n) 54 | 55 | for _, childNode := range n.children { 56 | childNode.walk(visitor) 57 | } 58 | } 59 | 60 | // walkErr applies the visitor func to the current node, then to every children nodes, recursively. 61 | // If an error occurs, it stops traversing the graph and returns the error immediately. 62 | func (n *Node) walkErr(visitor NodeVisitorFuncErr) error { 63 | err := visitor(n) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for _, childNode := range n.children { 69 | err = childNode.walkErr(visitor) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | 78 | // walkAsyncErr applies the visitor func to the current node, then to every children nodes, asynchronously. 79 | // If an error occurs, it stops traversing the graph and returns the error immediately. 80 | func (n *Node) walkAsyncErr(visitor NodeVisitorFuncErr) error { 81 | errG := new(errgroup.Group) 82 | errG.Go(func() error { 83 | return visitor(n) 84 | }) 85 | 86 | for _, childNode := range n.children { 87 | errG.Go(func() error { 88 | return childNode.walkAsyncErr(visitor) 89 | }) 90 | } 91 | 92 | return errG.Wait() 93 | } 94 | 95 | // walkInDepth makes a depth-first recursive walk through the graph. 96 | // It applies the visitor func to every children node, then to the current node itself. 97 | func (n *Node) walkInDepth(visitor NodeVisitorFunc) { 98 | for _, childNode := range n.children { 99 | childNode.walkInDepth(visitor) 100 | } 101 | 102 | visitor(n) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/strutil/strutil_test.go: -------------------------------------------------------------------------------- 1 | //nolint:testpackage 2 | package strutil 3 | 4 | import ( 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestDedupeStrSlice(t *testing.T) { 10 | t.Parallel() 11 | 12 | tests := []struct { 13 | name string 14 | input []string 15 | expected []string 16 | }{ 17 | { 18 | name: "No duplicates", 19 | input: []string{"a", "b", "c"}, 20 | expected: []string{"a", "b", "c"}, 21 | }, 22 | { 23 | name: "Duplicates in input", 24 | input: []string{"a", "b", "a", "c", "b"}, 25 | expected: []string{"a", "b", "c"}, 26 | }, 27 | { 28 | name: "All elements are duplicates", 29 | input: []string{"a", "a", "a"}, 30 | expected: []string{"a"}, 31 | }, 32 | { 33 | name: "Mixed empty and non-empty strings", 34 | input: []string{"", "a", "", "b", "a"}, 35 | expected: []string{"", "a", "b"}, 36 | }, 37 | { 38 | name: "Case-sensitive duplicates", 39 | input: []string{"a", "A", "b", "B", "a"}, 40 | expected: []string{"a", "A", "b", "B"}, 41 | }, 42 | { 43 | name: "Special characters and spaces", 44 | input: []string{" ", "!", "@", "!", " "}, 45 | expected: []string{" ", "!", "@"}, 46 | }, 47 | } 48 | 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | t.Parallel() 52 | 53 | result := DedupeStrSlice(tt.input) 54 | if !reflect.DeepEqual(result, tt.expected) { 55 | t.Errorf("DedupeStrSlice(%v) = %v, expected %v", tt.input, result, tt.expected) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestConvertKVStringsToMap(t *testing.T) { 62 | t.Parallel() 63 | 64 | tests := []struct { 65 | name string 66 | input []string 67 | expected map[string]string 68 | }{ 69 | { 70 | name: "Single key-value pair", 71 | input: []string{"key=value"}, 72 | expected: map[string]string{"key": "value"}, 73 | }, 74 | { 75 | name: "Multiple key-value pairs", 76 | input: []string{"name=John", "age=30", "city=Paris"}, 77 | expected: map[string]string{"name": "John", "age": "30", "city": "Paris"}, 78 | }, 79 | { 80 | name: "No equals sign in string", 81 | input: []string{"keyWithoutValue"}, 82 | expected: map[string]string{"keyWithoutValue": ""}, 83 | }, 84 | { 85 | name: "Empty string in input", 86 | input: []string{""}, 87 | expected: map[string]string{"": ""}, 88 | }, 89 | { 90 | name: "Value containing equals sign", 91 | input: []string{"data=this=is=value"}, 92 | expected: map[string]string{"data": "this=is=value"}, 93 | }, 94 | { 95 | name: "Empty input slice", 96 | input: []string{}, 97 | expected: map[string]string{}, 98 | }, 99 | } 100 | 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | t.Parallel() 104 | 105 | result := ConvertKVStringsToMap(tt.input) 106 | if !reflect.DeepEqual(result, tt.expected) { 107 | t.Errorf("ConvertKVStringsToMap(%v) = %v, expected %v", tt.input, result, tt.expected) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/kubernetes/pod_test.go: -------------------------------------------------------------------------------- 1 | package kubernetes_test 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "testing" 7 | 8 | k8sutils "github.com/radiofrance/dib/pkg/kubernetes" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_UniquePodName(t *testing.T) { 13 | t.Parallel() 14 | 15 | dataset := []struct { 16 | identifier string 17 | expectedPrefix string 18 | }{ 19 | { 20 | identifier: "dib", 21 | expectedPrefix: "dib-", 22 | }, 23 | { 24 | identifier: "semicolon:slashes/dib", 25 | expectedPrefix: "semicolon-slashes-dib-", 26 | }, 27 | { 28 | identifier: "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", 29 | expectedPrefix: "veryveryveryveryveryveryveryveryveryveryveryveryveryve-", 30 | }, 31 | } 32 | 33 | // Only alphanumeric characters, or dashes, maximum 63 chars 34 | validationRegexp := regexp.MustCompile(`^[a-z0-9\-]{1,63}`) 35 | 36 | for _, ds := range dataset { 37 | podName := k8sutils.UniquePodName(ds.identifier)() 38 | 39 | assert.Truef(t, strings.HasPrefix(podName, ds.expectedPrefix), 40 | "Pod name %s does not have prefix %s", podName, ds.expectedPrefix) 41 | 42 | assert.Regexp(t, validationRegexp, podName) 43 | } 44 | } 45 | 46 | func Test_UniquePodNameWithImage(t *testing.T) { 47 | t.Parallel() 48 | 49 | dataset := []struct { 50 | identifier string 51 | imageName string 52 | expectedPrefix string 53 | }{ 54 | { 55 | identifier: "buildkit-dib", 56 | imageName: "nginx", 57 | expectedPrefix: "buildkit-dib-nginx-", 58 | }, 59 | { 60 | identifier: "buildkit-dib", 61 | imageName: "registry.example.com/nginx:1.19", 62 | expectedPrefix: "buildkit-dib-registry.example.com-nginx-1.19-", 63 | }, 64 | { 65 | identifier: "buildkit-dib", 66 | imageName: "aquasec_trivy", 67 | expectedPrefix: "buildkit-dib-aquasec-trivy-", 68 | }, 69 | { 70 | identifier: "semicolon:slashes/dib", 71 | imageName: "image:with/special:chars", 72 | expectedPrefix: "semicolon-slashes-dib-image-with-special-chars-", 73 | }, 74 | { 75 | identifier: "short", 76 | imageName: "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", 77 | expectedPrefix: "short-veryveryveryveryveryveryveryveryveryveryveryvery-", 78 | }, 79 | { 80 | identifier: "veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylong", 81 | imageName: "short", 82 | expectedPrefix: "veryveryveryveryveryveryveryveryveryveryveryveryveryve-", 83 | }, 84 | { 85 | identifier: "UPPERCASE", 86 | imageName: "MixedCase", 87 | expectedPrefix: "uppercase-mixedcase-", 88 | }, 89 | } 90 | 91 | // Only alphanumeric characters, or dashes, maximum 63 chars 92 | validationRegexp := regexp.MustCompile(`^[a-z0-9\-]{1,63}`) 93 | 94 | for _, ds := range dataset { 95 | podName := k8sutils.UniquePodNameWithImage(ds.identifier, ds.imageName)() 96 | 97 | assert.Truef(t, strings.HasPrefix(podName, ds.expectedPrefix), 98 | "Pod name %s does not have prefix %s", podName, ds.expectedPrefix) 99 | 100 | assert.Regexp(t, validationRegexp, podName) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/kubernetes/exec.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "k8s.io/cli-runtime/pkg/genericclioptions" 11 | "k8s.io/cli-runtime/pkg/resource" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/kubernetes/scheme" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/kubectl/pkg/cmd/exec" 16 | ) 17 | 18 | // ExecOptions wraps the exec.ExecOptions struct to add helper methods. 19 | type ExecOptions struct { 20 | exec.ExecOptions 21 | } 22 | 23 | // NewExecOptions creates a new instance of ExecOptions with default values. 24 | func NewExecOptions(clientSet kubernetes.Interface, restConfig rest.Config) *ExecOptions { 25 | restConfig.APIPath = "/api" 26 | restConfig.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} 27 | restConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 28 | 29 | return &ExecOptions{ 30 | exec.ExecOptions{ 31 | StreamOptions: exec.StreamOptions{ 32 | IOStreams: genericclioptions.IOStreams{ 33 | In: os.Stdin, 34 | Out: os.Stdout, 35 | ErrOut: os.Stderr, 36 | }, 37 | Stdin: false, 38 | }, 39 | FilenameOptions: resource.FilenameOptions{}, 40 | Executor: &exec.DefaultRemoteExecutor{}, 41 | PodClient: clientSet.CoreV1(), 42 | Config: &restConfig, 43 | }, 44 | } 45 | } 46 | 47 | // WithContainer returns a copy of ExecOptions with pod options set to the given pod. 48 | func (o ExecOptions) WithContainer(pod *corev1.Pod, container string) *ExecOptions { 49 | o.Pod = pod 50 | o.Namespace = pod.Namespace 51 | o.PodName = pod.GetName() 52 | o.ContainerName = container 53 | 54 | return &o 55 | } 56 | 57 | // WithWriters returns a copy of ExecOptions with the given standard output and error output writers. 58 | func (o ExecOptions) WithWriters(out, err io.Writer) *ExecOptions { 59 | o.Out = out 60 | o.ErrOut = err 61 | 62 | return &o 63 | } 64 | 65 | // Exec executes a command in a running pod. 66 | func Exec(o ExecOptions, cmd []string) error { 67 | opts := o.ExecOptions 68 | opts.Command = cmd 69 | opts.Executor = &exec.DefaultRemoteExecutor{} 70 | 71 | err := opts.Validate() 72 | if err != nil { 73 | return fmt.Errorf("invalid exec options: %w", err) 74 | } 75 | 76 | err = opts.Run() 77 | if err != nil { 78 | return fmt.Errorf("error running command: %w", err) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // CopyToContainer copies a file to a container in a running pod. 85 | func CopyToContainer(opts ExecOptions, src string, dest string) error { 86 | file, err := os.Open(src) //nolint:gosec 87 | if err != nil { 88 | return fmt.Errorf("error opening file: %w", err) 89 | } 90 | 91 | defer func() { 92 | _ = file.Close() 93 | }() 94 | 95 | opts.Command = []string{"tee", dest} 96 | 97 | opts.In = file 98 | opts.Out = io.Discard 99 | opts.Stdin = true 100 | opts.Executor = &exec.DefaultRemoteExecutor{} 101 | 102 | err = opts.Validate() 103 | if err != nil { 104 | return fmt.Errorf("invalid exec options: %w", err) 105 | } 106 | 107 | err = opts.Run() 108 | if err != nil { 109 | return fmt.Errorf("error running command: %w", err) 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/dag/printer.go: -------------------------------------------------------------------------------- 1 | package dag 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/pterm/pterm" 9 | ) 10 | 11 | var defaultPrinter = GraphPrinter{ 12 | TopRightCornerString: "└", 13 | TopRightDownString: "├", 14 | HorizontalString: "─", 15 | VerticalString: "│", 16 | RightDownLeftString: "┬", 17 | Indent: 3, 18 | } 19 | 20 | type GraphPrinter struct { 21 | Root *Node 22 | TreeStyle *pterm.Style 23 | TextStyle *pterm.Style 24 | TopRightCornerString string 25 | TopRightDownString string 26 | HorizontalString string 27 | VerticalString string 28 | RightDownLeftString string 29 | Indent int 30 | Writer io.Writer 31 | } 32 | 33 | // WithRoot returns a new GraphPrinter with a specific Root node. 34 | func (p GraphPrinter) WithRoot(root *Node) *GraphPrinter { 35 | p.Root = root 36 | return &p 37 | } 38 | 39 | // Srender renders the graph as a string. 40 | func (p GraphPrinter) Srender() string { 41 | if p.TreeStyle == nil { 42 | p.TreeStyle = pterm.NewStyle() 43 | } 44 | 45 | if p.TextStyle == nil { 46 | p.TextStyle = pterm.NewStyle() 47 | } 48 | 49 | var result string 50 | if p.Root.Image.Name != "" { 51 | result += p.TextStyle.Sprint(p.Root.Image.Name) + "\n" 52 | } 53 | 54 | result += walkOverTree(p.Root.Children(), p, "") 55 | 56 | return result 57 | } 58 | 59 | func walkOverTree(nodes []*Node, printer GraphPrinter, prefix string) string { 60 | var result strings.Builder 61 | 62 | for nodeIndex, node := range nodes { 63 | if node.Image == nil { 64 | continue 65 | } 66 | 67 | txt := fmt.Sprintf("%s [%s]\n", node.Image.ShortName, node.Image.Hash) 68 | if len(nodes) > nodeIndex+1 { // if not last in nodes 69 | if len(node.Children()) == 0 { // if there are no children 70 | result.WriteString(prefix + printer.TreeStyle.Sprint(printer.TopRightDownString) + 71 | strings.Repeat(printer.TreeStyle.Sprint(printer.HorizontalString), printer.Indent) + 72 | printer.TextStyle.Sprint(txt)) 73 | } else { // if there are children 74 | result.WriteString(prefix + printer.TreeStyle.Sprint(printer.TopRightDownString) + 75 | strings.Repeat(printer.TreeStyle.Sprint(printer.HorizontalString), printer.Indent-1) + 76 | printer.TreeStyle.Sprint(printer.RightDownLeftString) + 77 | printer.TextStyle.Sprint(txt)) 78 | result.WriteString(walkOverTree(node.Children(), printer, 79 | prefix+printer.TreeStyle.Sprint(printer.VerticalString)+strings.Repeat(" ", printer.Indent-1))) 80 | } 81 | } else if len(nodes) == nodeIndex+1 { // if last in nodes 82 | if len(node.Children()) == 0 { // if there are no children 83 | result.WriteString(prefix + printer.TreeStyle.Sprint(printer.TopRightCornerString) + 84 | strings.Repeat(printer.TreeStyle.Sprint(printer.HorizontalString), printer.Indent) + 85 | printer.TextStyle.Sprint(txt)) 86 | } else { // if there are children 87 | result.WriteString(prefix + printer.TreeStyle.Sprint(printer.TopRightCornerString) + 88 | strings.Repeat(printer.TreeStyle.Sprint(printer.HorizontalString), printer.Indent-1) + 89 | printer.TreeStyle.Sprint(printer.RightDownLeftString) + 90 | printer.TextStyle.Sprint(txt)) 91 | result.WriteString(walkOverTree(node.Children(), printer, 92 | prefix+strings.Repeat(" ", printer.Indent))) 93 | } 94 | } 95 | } 96 | 97 | return result.String() 98 | } 99 | -------------------------------------------------------------------------------- /pkg/junit/xml_test.go: -------------------------------------------------------------------------------- 1 | package junit_test 2 | 3 | import ( 4 | "encoding/xml" 5 | "os" 6 | "testing" 7 | 8 | "github.com/radiofrance/dib/pkg/junit" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_ParseRawLogs(t *testing.T) { 14 | t.Parallel() 15 | 16 | tests := []struct { 17 | name string 18 | input string 19 | expected junit.Testsuite 20 | expectedErrorMsg string 21 | }{ 22 | { 23 | name: "Goss tests succeed", 24 | input: "../../test/fixtures/junit/junit-image-test.xml", 25 | expected: junit.Testsuite{ 26 | XMLName: xml.Name{Local: "testsuite"}, 27 | Name: "goss", 28 | Errors: "0", 29 | Tests: "2", 30 | Failures: "0", 31 | Skipped: "0", 32 | Time: "0.000", 33 | Timestamp: "2022-10-20T18:29:26Z", 34 | TestCases: []junit.TestCase{ 35 | { 36 | XMLName: xml.Name{Local: "testcase"}, 37 | ClassName: "goss-image-test", 38 | File: "docker/image-test", 39 | Time: "0.000", 40 | Name: "Test lorem 1", 41 | SystemOut: "Test results lorem 1", 42 | }, 43 | { 44 | XMLName: xml.Name{Local: "testcase"}, 45 | ClassName: "goss-image-test", 46 | File: "docker/image-test", 47 | Time: "0.000", 48 | Name: "Test lorem 2", 49 | SystemOut: "Test results lorem 2", 50 | }, 51 | }, 52 | }, 53 | expectedErrorMsg: "", 54 | }, 55 | { 56 | name: "Some Goss tests failed", 57 | input: "../../test/fixtures/junit/junit-image-test-fail.xml", 58 | expected: junit.Testsuite{ 59 | XMLName: xml.Name{Local: "testsuite"}, 60 | Name: "goss", 61 | Errors: "0", 62 | Tests: "2", 63 | Failures: "1", 64 | Skipped: "0", 65 | Time: "0.000", 66 | Timestamp: "2022-10-20T18:29:26Z", 67 | TestCases: []junit.TestCase{ 68 | { 69 | XMLName: xml.Name{Local: "testcase"}, 70 | ClassName: "goss-image-test", 71 | File: "docker/image-test", 72 | Time: "0.000", 73 | Name: "Test lorem 1", 74 | SystemOut: "Test results lorem 1", 75 | }, 76 | { 77 | XMLName: xml.Name{Local: "testcase"}, 78 | ClassName: "goss-image-test", 79 | File: "docker/image-test", 80 | Time: "0.000", 81 | Name: "User debian uid", 82 | Failure: "User: debian: uid: doesn't match, expect: [1666] found: [1664]", 83 | }, 84 | }, 85 | }, 86 | expectedErrorMsg: "", 87 | }, 88 | { 89 | name: "Invalid XML", 90 | input: "../../test/fixtures/junit/junit-invalid.xml", 91 | expected: junit.Testsuite{ 92 | XMLName: xml.Name{Local: "testsuite"}, 93 | TestCases: []junit.TestCase(nil), 94 | }, 95 | expectedErrorMsg: "expected element name after \"registry.localhost/image1-child1\" [dir=forward];\n" + 116 | " \"registry.localhost/image1\" -> \"registry.localhost/image1-child2\" [dir=forward];\n" + 117 | " \"registry.localhost/image1-child1\" [fillcolor=red, style=filled];\n" + 118 | " \"registry.localhost/image1-child1\" -> \"registry.localhost/image1-child1-sub1\" [dir=forward];\n" + 119 | " \"registry.localhost/image1-child1-sub1\" [fillcolor=white, style=filled];\n" + 120 | " \"registry.localhost/image1-child2\" [fillcolor=red, style=filled];\n" + 121 | " \"registry.localhost/image2\" [fillcolor=white, style=filled];\n" + 122 | " \"registry.localhost/image3\" [fillcolor=white, style=filled];\n" + 123 | "}\n" 124 | 125 | actual := graphviz.GenerateRawOutput(inputGraph) 126 | assert.Equal(t, expected, actual) 127 | } 128 | -------------------------------------------------------------------------------- /docs/backends.md: -------------------------------------------------------------------------------- 1 | Build Backends 2 | ============== 3 | 4 | The build backend is a software or service responsible for actually building the images. dib itself is not capable of 5 | building images, it delegates this part to the build backend. 6 | 7 | dib supports multiple build backends. Currently, available backends are `docker`, `kaniko`, and `buildkit`. You can select the 8 | backend to use with the `--backend` option. Note that `docker` and `kaniko` backends are deprecated and will be removed in a future release. `buildkit` is now the recommended and default backend. 9 | 10 | **Executor compatibility matrix** 11 | 12 | | Backend | Local | Docker | Kubernetes | 13 | |----------|-------|--------|------------| 14 | | Docker | ✔ | ✗ | ✗ | 15 | | Kaniko | ✗ | ✔ | ✔ | 16 | | BuildKit | ✔ | ✗ | ✔ | 17 | 18 | ## Docker 19 | 20 | > **Deprecated:** The Docker backend is deprecated and will be removed in a future release. Please use the BuildKit backend instead. 21 | 22 | The `docker` backend uses [Docker](https://www.docker.com/) behind the scenes, and runs `docker build` You need to have 23 | the Docker CLI installed locally to use this backend. 24 | 25 | **Authentication** 26 | 27 | The Docker Daemon requires authentication to pull and push images from private registries. Run the 28 | [`docker login`](https://docs.docker.com/engine/reference/commandline/login/) command to authenticate. 29 | 30 | Authentication settings are stored in a `config.json` file located by default in `$HOME/.docker/`. 31 | If you need to provide a different configuration, you can set the `DOCKER_CONFIG` variable to the path to another 32 | directory, which should contain a `config.json` file. 33 | 34 | **Remote Daemon** 35 | 36 | If you want to set a custom docker daemon host, you can set the `DOCKER_HOST` environment variable. The builds will then 37 | run on the remote host instead of using the local Docker daemon. 38 | 39 | **BuildKit** 40 | 41 | If available, dib will try to use the BuildKit engine to build images, which is faster than the default Docker 42 | build engine. 43 | 44 | ## Kaniko 45 | 46 | > **Deprecated:** The Kaniko backend is deprecated and will be removed in a future release. Please use the BuildKit backend instead. 47 | 48 | [Kaniko](https://github.com/GoogleContainerTools/kaniko) offers a way to build container images inside a container 49 | or Kubernetes cluster, without the security tradeoff of running a docker daemon container with host privileges. 50 | 51 | /// admonition | BuildKit 52 | type: info 53 | 54 | As Kaniko must run in a container, it requires Docker when running local builds as it uses the `docker` executor. 55 | /// 56 | 57 | See the `kaniko` section in the [configuration reference](configuration-reference.md). 58 | 59 | ## BuildKit 60 | 61 | [BuildKit](https://github.com/moby/buildkit) is a toolkit for converting source code to build artifacts in an efficient, expressive and repeatable manner. It provides a more efficient, cache-aware, and concurrent build engine compared to the traditional Docker build. 62 | 63 | **Authentication** 64 | 65 | BuildKit uses the same authentication mechanism as Docker. Run the [`docker login`](https://docs.docker.com/engine/reference/commandline/login/) command to authenticate with your registry. 66 | 67 | **Local Builds** 68 | 69 | For local builds, BuildKit requires the `buildctl` binary to be installed on your system and `buildkitd` daemon to be running. You can install BuildKit by following the instructions in the [official documentation](https://github.com/moby/buildkit#quick-start). 70 | 71 | **Kubernetes Builds** 72 | 73 | For Kubernetes builds, dib will create a pod with the BuildKit image and execute the build inside it. This requires proper configuration of Kubernetes access and Docker registry credentials. 74 | 75 | **BuildKit Host** 76 | 77 | You can specify a custom BuildKit daemon host using the `--buildkit-host` option or by setting the `BUILDKIT_HOST` environment variable. 78 | 79 | See the `buildkit` section in the [configuration reference](configuration-reference.md). 80 | -------------------------------------------------------------------------------- /pkg/report/utils.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "os" 7 | "path" 8 | "regexp" 9 | "sort" 10 | 11 | "github.com/radiofrance/dib/pkg/trivy" 12 | "github.com/radiofrance/dib/pkg/types" 13 | ) 14 | 15 | const ( 16 | BuildReportDir = "builds" 17 | JunitReportDir = "junit" 18 | TrivyReportDir = "trivy" 19 | ) 20 | 21 | var ( 22 | patternAnsiColors = regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]`) 23 | patternKanikoLogs = regexp.MustCompile(`time=".*" level=.* msg="(?P.*)"`) 24 | patternSpecialChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`) 25 | ) 26 | 27 | var templateFuncs = template.FuncMap{ 28 | "sanitize": sanitize, 29 | } 30 | 31 | // renderTemplate Parse and execute given template by its name, taking care of inheritance, 32 | // then write it on the disk, inside the report folder. 33 | func (r Report) renderTemplate(name string, reportOpts Options, reportData any) error { 34 | // The order matter for inheritance 35 | files := []string{ 36 | path.Join(templatesDir, "_layout.go.html"), // base layout 37 | path.Join(templatesDir, "_nav.go.html"), // navbar 38 | path.Join(templatesDir, "_functions.go.html"), // helpers & utils functions 39 | path.Join(templatesDir, fmt.Sprintf("%s.go.html", name)), // report page 40 | } 41 | 42 | tpl, err := template.New("layout").Funcs(templateFuncs).ParseFS(templatesFS, files...) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | writer, err := os.Create(fmt.Sprintf("%s.html", path.Join(r.GetRootDir(), name))) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | defer func(file *os.File) { 53 | _ = file.Close() 54 | }(writer) 55 | 56 | templateData := map[string]any{ 57 | "Name": name, 58 | "Opt": reportOpts, 59 | "Data": reportData, 60 | } 61 | 62 | return tpl.ExecuteTemplate(writer, "layout", templateData) 63 | } 64 | 65 | // isTestRunnerEnabled return true if given types.TestRunner is enabled. False instead. 66 | func isTestRunnerEnabled(name string, testRunners []types.TestRunner) bool { 67 | for _, runner := range testRunners { 68 | if name == runner.Name() { 69 | return true 70 | } 71 | } 72 | 73 | return false 74 | } 75 | 76 | // sortBuildReport sort BuildReport by image name. 77 | func sortBuildReport(buildReports []BuildReport) []BuildReport { 78 | sort.SliceStable(buildReports, func(i, j int) bool { 79 | return buildReports[i].Image.ShortName < buildReports[j].Image.ShortName 80 | }) 81 | 82 | return buildReports 83 | } 84 | 85 | // sortTrivyScan sorts Trivy scan reports by severity. 86 | func sortTrivyScan(parsedTrivyReport trivy.ScanReport) trivy.ScanReport { 87 | order := map[string]int{ 88 | "CRITICAL": 1, 89 | "HIGH": 2, 90 | "MEDIUM": 3, 91 | "LOW": 4, 92 | "UNKNOWN": 5, 93 | } 94 | 95 | for _, result := range parsedTrivyReport.Results { 96 | sort.SliceStable(result.Vulnerabilities, func(i, j int) bool { 97 | return order[result.Vulnerabilities[i].Severity] < order[result.Vulnerabilities[j].Severity] 98 | }) 99 | } 100 | 101 | return parsedTrivyReport 102 | } 103 | 104 | func beautifyBuildsLogs(rawBuildLogs []byte) string { 105 | unescapedBuildLogs := RemoveTerminalColors(rawBuildLogs) 106 | return StripKanikoBuildLogs(unescapedBuildLogs) 107 | } 108 | 109 | // sanitize removes characters from string that are not allowed in document.querySelector calls. 110 | func sanitize(input string) string { 111 | return patternSpecialChars.ReplaceAllString(input, "") 112 | } 113 | 114 | // RemoveTerminalColors strips all ANSI escape codes from the given string. 115 | func RemoveTerminalColors(input []byte) []byte { 116 | results := patternAnsiColors.ReplaceAll(input, []byte{}) 117 | 118 | return results 119 | } 120 | 121 | // StripKanikoBuildLogs Improve readability of kaniko builds logs by removing unwanted stuff from a 122 | // standard logs message. 123 | func StripKanikoBuildLogs(input []byte) string { 124 | results := patternKanikoLogs.ReplaceAll(input, []byte("$message")) 125 | 126 | return string(results) 127 | } 128 | -------------------------------------------------------------------------------- /pkg/report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/radiofrance/dib/internal/logger" 11 | "github.com/radiofrance/dib/pkg/dag" 12 | ) 13 | 14 | const ( 15 | BuildStatusSkipped BuildStatus = iota 16 | BuildStatusSuccess 17 | BuildStatusError 18 | ) 19 | 20 | const ( 21 | TestsStatusSkipped TestsStatus = iota 22 | TestsStatusPassed 23 | TestsStatusFailed 24 | ) 25 | 26 | type ( 27 | BuildStatus int 28 | TestsStatus int 29 | ) 30 | 31 | type Report struct { 32 | Options Options 33 | BuildReports []BuildReport 34 | } 35 | 36 | type Options struct { 37 | RootDir string 38 | Name string 39 | GenerationDate time.Time 40 | Version string 41 | BuildOpts string 42 | WithGraph bool 43 | WithGoss bool 44 | WithTrivy bool 45 | } 46 | 47 | // BuildReport holds the status of the build/tests. 48 | type BuildReport struct { 49 | Image dag.Image 50 | BuildStatus BuildStatus 51 | TestsStatus TestsStatus 52 | FailureMessage string 53 | } 54 | 55 | // GetRootDir return the path of the Report "root" directory. 56 | func (r Report) GetRootDir() string { 57 | return path.Join(r.Options.RootDir, r.Options.Name) 58 | } 59 | 60 | // GetBuildReportDir return the path of the Report "builds" directory. 61 | func (r Report) GetBuildReportDir() string { 62 | return path.Join(r.GetRootDir(), BuildReportDir) 63 | } 64 | 65 | // GetJunitReportDir return the path of the Report "Junit reports" directory. 66 | func (r Report) GetJunitReportDir() string { 67 | return path.Join(r.GetRootDir(), JunitReportDir) 68 | } 69 | 70 | // GetTrivyReportDir return the path of the Report "Trivy reports" directory. 71 | func (r Report) GetTrivyReportDir() string { 72 | return path.Join(r.GetRootDir(), TrivyReportDir) 73 | } 74 | 75 | // GetURL return a string representing the path from which we can browse Report. 76 | func (r Report) GetURL() string { 77 | // GitLab context 78 | gitlabJobURL := os.Getenv("CI_JOB_URL") 79 | if gitlabJobURL != "" { 80 | return fmt.Sprintf("%s/artifacts/file/%s/index.html", gitlabJobURL, r.GetRootDir()) 81 | } 82 | 83 | // Local context 84 | finalReportURL, err := filepath.Abs(r.GetRootDir()) 85 | if err != nil { 86 | return r.GetRootDir() 87 | } 88 | 89 | return fmt.Sprintf("file://%s/index.html", finalReportURL) 90 | } 91 | 92 | // Print display Report.BuildReports to the user. 93 | func (r Report) Print() { 94 | logger.Infof("Build report") 95 | 96 | for _, buildReport := range r.BuildReports { 97 | switch buildReport.BuildStatus { 98 | case BuildStatusSuccess: 99 | logger.Infof("\t[%s]: SUCCESS", buildReport.Image.ShortName) 100 | case BuildStatusSkipped: 101 | logger.Infof("\t[%s]: SKIPPED", buildReport.Image.ShortName) 102 | case BuildStatusError: 103 | logger.Errorf("\t[%s]: FAILURE: %s", buildReport.Image.ShortName, buildReport.FailureMessage) 104 | } 105 | } 106 | 107 | logger.Infof("Tests report") 108 | 109 | for _, buildReport := range r.BuildReports { 110 | switch buildReport.TestsStatus { 111 | case TestsStatusPassed: 112 | logger.Infof("\t[%s]: PASSED", buildReport.Image.ShortName) 113 | case TestsStatusSkipped: 114 | logger.Infof("\t[%s]: SKIPPED", buildReport.Image.ShortName) 115 | case TestsStatusFailed: 116 | logger.Errorf("\t[%s]: FAILED: %s", buildReport.Image.ShortName, buildReport.FailureMessage) 117 | } 118 | } 119 | } 120 | 121 | // CheckError looks for failures in Report.BuildReports and returns an error if any is found. 122 | func (r Report) CheckError() error { 123 | for _, buildReport := range r.BuildReports { 124 | if buildReport.BuildStatus == BuildStatusError { 125 | return fmt.Errorf("one of the image build failed, see the report for more details") 126 | } 127 | 128 | if buildReport.TestsStatus == TestsStatusFailed { 129 | return fmt.Errorf("some tests failed, see report for more details") 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | 136 | // WithError returns a BuildReport. 137 | func (r BuildReport) WithError(err error) BuildReport { 138 | r.BuildStatus = BuildStatusError 139 | r.FailureMessage = err.Error() 140 | 141 | return r 142 | } 143 | --------------------------------------------------------------------------------