Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

helm package should produce bitwise-deterministic tar files #3612

Open
cmaher opened this issue Mar 5, 2018 · 29 comments · May be fixed by #10178
Open

helm package should produce bitwise-deterministic tar files #3612

cmaher opened this issue Mar 5, 2018 · 29 comments · May be fixed by #10178
Labels
bug Categorizes issue or PR as related to a bug. in progress keep open

Comments

@cmaher
Copy link

cmaher commented Mar 5, 2018

We have a CI process for publishing charts where we try to determine if pushed repository needs to have it's charts built and published. We would like to do this with a bitwise comparison of the generated tgz files. This works for charts without dependencies, but it is inconsistent for charts with dependencies.

For the setup (Using v2.8.1):

charts
├── child1
│   └── Chart.yaml
├── child2
│   └── Chart.yaml
├── child3
│   └── Chart.yaml
└── parent
    ├── Chart.yaml
    └── requirements.yaml

With charts:

# child{1..3}/Chart.yaml
name: child{1..3}
version: 1.0.0

# parent/Chart.yaml
name: parent
version: 1.0.0

# parent/requirements.yaml
dependencies:
- name: child1
  repository: file://../child1
  version: "*"
- name: child2
  repository: file://../child2
  version: "*"
- name: child3
  repository: file://../child3
  version: "*"

Running:

# working dir: parent
$ helm dep update
$ for i in {1..1000}; do helm package . && sha256sum parent-1.0.0.tgz && rm parent-1.0.0.tgz; done

produces several (3, after a significant number of iterations) different shas. The shas also appear to be non-uniformly distributed (e.g. 1 sha appears 70% of the time).

My guess is that https://github.com/kubernetes/helm/blob/master/pkg/chartutil/save.go#L160 is iterating over dependencies in a non-deterministic order, thus producing different tar files.

@bacongobbler
Copy link
Member

bacongobbler commented Mar 5, 2018

I think this is actually due to gzip and is intentional by design. Part of the gzip header has a mod time for whatever is compressed in the file, so by calling helm package multiple times, each one creates their own tarball which is then packaged by gzip and given a unique shasum because the modtime differed. There is a way to disable this header, but idk if we'd be sacrificing something by stripping the modtime header.

If you gunzip the .tgz, is the shasum identical between tarballs?

@cmaher
Copy link
Author

cmaher commented Mar 5, 2018

gunziping the tars produces different tarballs. There still appear to be 3 different shasums.

@bacongobbler
Copy link
Member

bacongobbler commented Mar 5, 2018

Following up on my previous comment, you can reproduce this yourself at home:

><> helm fetch stable/traefik
><> gunzip traefik-1.24.1.tgz 
><> helm fetch stable/traefik
><> shasum -a 256 *
a032f1eba2c54b76d8de38d3763bab5ca5a6383af1156f30caf4595adaec67a0  traefik-1.24.1.tar
ebd78bdbbf63f557db8646c6351bfeeac849951334d4cbfc103ecc6bb1b170c5  traefik-1.24.1.tgz

So in this example, I've shown that I've got stable/traefik, both in compressed and decompressed versions from the official stable/traefik chart. If I gzip this up, we should be able to assert that we come up with the same shasum because the contents are the same, right?

Wrongo.

><> gzip traefik-1.24.1.tar 
><> shasum -a 256 *
144eaa108aff820dd754e16732bbacd29accb41954d6249b4111623587fae415  traefik-1.24.1.tar.gz
ebd78bdbbf63f557db8646c6351bfeeac849951334d4cbfc103ecc6bb1b170c5  traefik-1.24.1.tgz

But the shasums of both the recently-gzipped tarball and the old shasum are still identical.

><> gunzip traefik-1.24.1.tar.gz 
><> mv traefik-1.24.1.tar traefik-1.24.1.tar.1
><> gunzip traefik-1.24.1.tgz 
><> mv traefik-1.24.1.tar traefik-1.24.1.tar.2
><> shasum -a 256 *
a032f1eba2c54b76d8de38d3763bab5ca5a6383af1156f30caf4595adaec67a0  traefik-1.24.1.tar.1
a032f1eba2c54b76d8de38d3763bab5ca5a6383af1156f30caf4595adaec67a0  traefik-1.24.1.tar.2

So in other words, if you want a deterministic shasum of the contents of the chart, I'd suggest decompressing the packages first before checking the shasum.

Would that work for you?

@bmperrea
Copy link

bmperrea commented Mar 6, 2018

The timestamp issue in the default gzip settings appears to be irrelevant since there are only a few unique shas that appear in thousands of tests on helm package (whereas the shas from the gzip example appear to be purely random). It looks like writeToTar in pkg/chartutil/save.go specifies its own header here: https://github.com/kubernetes/helm/blob/master/pkg/chartutil/save.go#L205.

Perhaps sorting the dependencies in a deterministic manner before serialization is all that is needed, but any help on where to put the sort would be appreciated.

For now a workaround is to do a file diff on the decompressed packages after removing the dependency tars. This won't catch differences in dependencies except version differences (which can happen for local dependencies, e.g.), but will catch most differences until this can be patched.

@bacongobbler
Copy link
Member

I was unable to reproduce this behaviour using a stable chart. Can you provide a sample chart that produces this behaviour?

My steps to test this:

$ helm fetch --untar stable/traefik
$ for i in {1..1000}; do helm package traefik/ && mv traefik-1.24.1.tgz traefik-1.24.1.tgz.$i; done
$ shasum -a 256 *.tgz* | awk '{print $1}' | uniq
ebd78bdbbf63f557db8646c6351bfeeac849951334d4cbfc103ecc6bb1b170c5

Every invocation of helm package returned a consistent shasum.

/shrug

@cmaher
Copy link
Author

cmaher commented Mar 7, 2018

https://github.com/cmaher/nondeterministic-charts demonstrates this using Client: &version.Version{SemVer:"v2.8.1", GitCommit:"6af75a8fd72e2aa18a2b278cfe5c7a1c5feca7f2", GitTreeState:"clean"}

I don't think stable/traefik is going to be affected by this, because it doesn't have any dependencies.

@fejta-bot
Copy link

Issues go stale after 90d of inactivity.
Mark the issue as fresh with /remove-lifecycle stale.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
/lifecycle stale

@fejta-bot
Copy link

Stale issues rot after 30d of inactivity.
Mark the issue as fresh with /remove-lifecycle rotten.
Rotten issues close after an additional 30d of inactivity.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
/lifecycle rotten
/remove-lifecycle stale

@jeff-knurek
Copy link

I found out a way to reproduce this. One thing that seems to be helpful in reproducing is having more than one dependency in the requirements file. Also, the large the values.yaml file the easier it is to reproduce as well.

spinnaker is an example stable chart to reproduce:

mkdir pkgs
cd spinnaker
helm dep up
for i in {1..30}; do helm package . && mv spinnaker-0.5.0.tgz ../pkgs/spinnaker-0.5.0.tgz.$i; done
cd ../pkgs
shasum -a 256 *.tgz* | awk '{print $1}' | uniq

@jeff-knurek
Copy link

/remove-lifecycle rotten

@diclophis
Copy link

We have been able to reproduce this issue with some of our local charts as well.

@adrian-gierakowski
Copy link

The problem definitely manifests itself on macOS. None of the suggestions mentioned in this tread solve the issues. I've created a repo to demonstrate the problem and a workaround: https://github.com/adrian-gierakowski/helm-package-sha-test.

@Michael-Sinz
Copy link

Michael-Sinz commented Aug 27, 2019

The problem is due to the order of entries in the tar file. It is not a gzip issue but rather an issue that the two tar files, while the same size, actually have the files within them in a different order.

The issue is that the POSIX directory enumeration API does not define a specific order for file names to be returned. There may be a "common" order but since sorting/ordering is not something that can be done generically (it is locale/user specific), the core filesystems are not forced to provide a consistent order. Result orders may depend on available cache entries or order of data returned from elevator algorithms that read from the disk or a number of other out-of-order execution behaviors.

We noticed this specifically when multiple sub-charts were involved. The order of the sub-charts in the tar file were sometimes different. One form was more common (70% or so).

Note that we had not noticed this problem until we had a chart that had sub-charts like this one has. That seems to be what triggered the whole problem or ordering within the tar file.

Also note that the two tar files, when extracted, produce exactly the same files and directories - they just are in the tar file in a different order which will cause different hashes.

You can see this with tar -t on each of the helm tgz (or extract tar) files.

$ tar -tzf A/speech-onprem-0.1.5.tgz
speech-onprem/Chart.yaml                                                            
speech-onprem/values.yaml
speech-onprem/templates/NOTES.txt
speech-onprem/templates/tests/test-speech-on-prem-readiness.yaml
speech-onprem/.helmignore
speech-onprem/README.md
speech-onprem/charts/speechToText/Chart.yaml
speech-onprem/charts/speechToText/values.yaml
speech-onprem/charts/speechToText/templates/NOTES.txt
speech-onprem/charts/speechToText/templates/_helpers.tpl
speech-onprem/charts/speechToText/templates/_image.tpl
speech-onprem/charts/speechToText/templates/speechToText-autoscaler.yaml
speech-onprem/charts/speechToText/templates/speechToText-deployment.yaml
speech-onprem/charts/speechToText/templates/speechToText-poddisruption.yaml
speech-onprem/charts/speechToText/templates/speechToText-service.yaml
speech-onprem/charts/textToSpeech/Chart.yaml
speech-onprem/charts/textToSpeech/values.yaml
speech-onprem/charts/textToSpeech/templates/NOTES.txt
speech-onprem/charts/textToSpeech/templates/_helpers.tpl
speech-onprem/charts/textToSpeech/templates/_image.tpl
speech-onprem/charts/textToSpeech/templates/textToSpeech-autoscaler.yaml
speech-onprem/charts/textToSpeech/templates/textToSpeech-deployment.yaml
speech-onprem/charts/textToSpeech/templates/textToSpeech-poddisruption.yaml
speech-onprem/charts/textToSpeech/templates/textToSpeech-service.yaml



$ tar -tzf B/speech-onprem-0.1.5.tgz
speech-onprem/Chart.yaml
speech-onprem/values.yaml
speech-onprem/templates/NOTES.txt
speech-onprem/templates/tests/test-speech-on-prem-readiness.yaml
speech-onprem/.helmignore
speech-onprem/README.md
speech-onprem/charts/textToSpeech/Chart.yaml
speech-onprem/charts/textToSpeech/values.yaml
speech-onprem/charts/textToSpeech/templates/NOTES.txt
speech-onprem/charts/textToSpeech/templates/_helpers.tpl
speech-onprem/charts/textToSpeech/templates/_image.tpl
speech-onprem/charts/textToSpeech/templates/textToSpeech-autoscaler.yaml
speech-onprem/charts/textToSpeech/templates/textToSpeech-deployment.yaml
speech-onprem/charts/textToSpeech/templates/textToSpeech-poddisruption.yaml
speech-onprem/charts/textToSpeech/templates/textToSpeech-service.yaml
speech-onprem/charts/speechToText/Chart.yaml
speech-onprem/charts/speechToText/values.yaml
speech-onprem/charts/speechToText/templates/NOTES.txt
speech-onprem/charts/speechToText/templates/_helpers.tpl
speech-onprem/charts/speechToText/templates/_image.tpl
speech-onprem/charts/speechToText/templates/speechToText-autoscaler.yaml
speech-onprem/charts/speechToText/templates/speechToText-deployment.yaml
speech-onprem/charts/speechToText/templates/speechToText-poddisruption.yaml
speech-onprem/charts/speechToText/templates/speechToText-service.yaml

@jdolitsky
Copy link
Contributor

Is this related to dependencies, or is the issue just increasingly more likely to occur the more files a chart contains?

The issue is that the POSIX directory enumeration API does not define a specific order for file names to be returned. There may be a "common" order but since sorting/ordering is not something that can be done generically (it is locale/user specific), the core filesystems are not forced to provide a consistent order. Result orders may depend on available cache entries or order of data returned from elevator algorithms that read from the disk or a number of other out-of-order execution behaviors.

Is there really not a way enforce this order in Go? @Michael-Sinz

@Michael-Sinz
Copy link

Michael-Sinz commented May 28, 2020 via email

@github-actions
Copy link

This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.

@github-actions github-actions bot added the Stale label Nov 22, 2020
@Michael-Sinz
Copy link

Is there no progress on this issue. The lack of deterministic output for constant input is a big problem. It means that helm package is not deterministic and we can not depend on it producing the same results. Imaging if a compiler could make different code for the same source code from run to run - how would you debug such a program?

@rjeberhard
Copy link

Agreed. We've had to make clumsy workarounds to only package the chart if one of the source files changes. One issue is the embedded time stamps.

@bacongobbler
Copy link
Member

@Michael-Sinz I have not seen any activity from the community on this bug. Please feel free to work on a fix!

@github-actions github-actions bot removed the Stale label Nov 26, 2020
@github-actions
Copy link

This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.

@github-actions github-actions bot added the Stale label Feb 24, 2021
@Michael-Sinz
Copy link

We still would like to have deterministic helm chart building. The fact that the same helm chart from the same source tree from the same git hash with the same helm version does not produce deterministic hashes is concerning.

@github-actions
Copy link

github-actions bot commented Jun 3, 2021

This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.

@sathieu
Copy link
Contributor

sathieu commented Oct 20, 2021

Deterministic output is the goal of the Reproducible builds project.

It has a recommended tar example:

# requires GNU Tar 1.28+
$ tar --sort=name \
      --mtime="@${SOURCE_DATE_EPOCH}" \
      --owner=0 --group=0 --numeric-owner \
      --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \
      -cf product.tar build

gz seems to produce reproducible output since 2018.

But as helm uses go libs, maybe the fix is around file order:

helm/pkg/chartutil/save.go

Lines 205 to 228 in f41f46c

// Save templates
for _, f := range c.Templates {
n := filepath.Join(base, f.Name)
if err := writeToTar(out, n, f.Data); err != nil {
return err
}
}
// Save files
for _, f := range c.Files {
n := filepath.Join(base, f.Name)
if err := writeToTar(out, n, f.Data); err != nil {
return err
}
}
// Save dependencies
for _, dep := range c.Dependencies() {
if err := writeTarContents(out, dep, filepath.Join(base, ChartsDir)); err != nil {
return err
}
}
return nil
}

@geowalrus4gh
Copy link

geowalrus4gh commented Mar 30, 2022

Hi, we are really waiting for this issue to be fixed. The real advantage of using OCI is to make use of the delta changes in a chart that are only committed to the repository and thus save space at the repository heavily. We find this feature very useful in OCI container images. Without this feature, helm OCI support is will not be full.

@hickeyma
Copy link
Contributor

/cc @jdolitsky @scottrigby

@dinvlad
Copy link

dinvlad commented Sep 20, 2022

Any updates?

@h0tbird
Copy link

h0tbird commented Oct 4, 2022

I can reproduce it like this:

$ helm version
version.BuildInfo{Version:"v3.10.0", GitCommit:"ce66412a723e4d89555dc67217607c6579ffcb21", GitTreeState:"clean", GoVersion:"go1.19.1"}
$ helm create foo
Creating foo
$ for i in {1..10}; do helm package foo >/dev/null; md5sum foo-0.1.0.tgz; sleep 0.5; done
39e2b6bff940ed840873926f271d72f8  foo-0.1.0.tgz
f34d771d8039d77c9b76340c7940ab55  foo-0.1.0.tgz
f34d771d8039d77c9b76340c7940ab55  foo-0.1.0.tgz
afe15ee8c34f069e967e88e70c7bbd29  foo-0.1.0.tgz
afe15ee8c34f069e967e88e70c7bbd29  foo-0.1.0.tgz
7c1b20acd7a9a73944af13fec2303cf0  foo-0.1.0.tgz
7c1b20acd7a9a73944af13fec2303cf0  foo-0.1.0.tgz
16cef47516265bed7f8dbd9ef145042c  foo-0.1.0.tgz
16cef47516265bed7f8dbd9ef145042c  foo-0.1.0.tgz
1532cb3b9a5d828e19d74b034caebace  foo-0.1.0.tgz

I tried setting the GZIP="-n" environment variable with no luck.

@dinvlad
Copy link

dinvlad commented Oct 4, 2022

FWIW the example from reproducible builds worked well for me, but that obviously requires custom scripting, which is not ideal.

@FrenchBen
Copy link
Contributor

Fix for the above has been pending review/merge. Not sure what the blocker is TBH:
#10178

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Categorizes issue or PR as related to a bug. in progress keep open
Projects
None yet
Development

Successfully merging a pull request may close this issue.