Repository: kubernetes/release
Pull request #940 (2019-11-24 03:49:46)

Add binary downloads table to release notes
To be able to use the golang based release notes tool for patch
releases, we need to add support for generating the downloads table.
This commit implements this support, where it only works when the tags
to be used are specified via the `--start-rev` and `--end-rev` flags.

---

### Demo

For example, if we now generate the notes for v1.16.1 like this:
```
> go run cmd/release-notes/main.go  \
    --start-rev v1.16.0 \
    --end-rev v1.16.1 \
    --branch release-1.16 \
    --repo-path=$GOPATH/src/k8s.io/kubernetes \
    --output v1.16.1.md \
    --release-tars ./demo-tars
```

Then the output looks like this:
https://gist.github.com/saschagrunert/7dbd0e72c003d4ca9d8b331dd3d7b55e

Looks pretty similar to the current script based generation here:
https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.16.md#v1161

We have now the benefit of getting all notes into the changelog, since we're now using the golang based tool for generation (compare the notes).

After this PR, we would still have to add some features to the golang implementation until we can remove the  `relnotes` bash script, like:

- Adding support for rendering HTML (conversion via pandoc right now, the HTML version is used in the announcing mail)
- Adding support for properly marshaling release notes into one document (multiple versions)

We can think about linking the notes in the mail only, then we would need no HTML conversion at all.

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/release/pull/940#" title="Author self-approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Frelease).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[cmd/release-notes/OWNERS](https://github.com/kubernetes/release/blob/master/cmd/release-notes/OWNERS)~~ [saschagrunert]
- ~~[pkg/notes/OWNERS](https://github.com/kubernetes/release/blob/master/pkg/notes/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
saschagrunert: /cc @justaugustus @tpepper @hasheddan @hoegaarden 

I propose that we switch to the golang based release notes tool as soon as possible since the bash implementation contains bugs which miss the notes of cherry-pick edits. The format of the notes is different which might cause problems, right? Beside this, we could also discuss if we want to go with https://github.com/kubernetes/release/pull/923.
cpanato: /lgtm
/hold
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/release/pull/940#issuecomment-557873382" title="LGTM">cpanato</a>*, *<a href="https://github.com/kubernetes/release/pull/940#" title="Author self-approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Frelease).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[cmd/release-notes/OWNERS](https://github.com/kubernetes/release/blob/master/cmd/release-notes/OWNERS)~~ [saschagrunert]
- ~~[pkg/notes/OWNERS](https://github.com/kubernetes/release/blob/master/pkg/notes/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/release/pull/940#issuecomment-557873382" title="LGTM">cpanato</a>*, *<a href="https://github.com/kubernetes/release/pull/940#pullrequestreview-321997730" title="LGTM">hasheddan</a>*, *<a href="https://github.com/kubernetes/release/pull/940#" title="Author self-approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Frelease).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[cmd/release-notes/OWNERS](https://github.com/kubernetes/release/blob/master/cmd/release-notes/OWNERS)~~ [saschagrunert]
- ~~[pkg/notes/OWNERS](https://github.com/kubernetes/release/blob/master/pkg/notes/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
tpepper: I definitely like this and would love to see it merge as soon as 1.17.0 is out the door.
justaugustus: > I definitely like this and would love to see it merge as soon as 1.17.0 is out the door.

+1. Explicit hold until then.
saschagrunert: @tpepper @justaugustus do you think we can move forward with this? I’d love to see the patch release notes generated by the golang tool in the near future. :)
k8s-ci-robot: @saschagrunert: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
saschagrunert: Ah, I’ll rebase tomorrow 👍 Thanks, @k8s-ci-robot !
k8s-ci-robot: New changes are detected. LGTM label has been removed.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/release/pull/940#issuecomment-557873382" title="LGTM">cpanato</a>*, *<a href="https://github.com/kubernetes/release/pull/940#pullrequestreview-321997730" title="LGTM">hasheddan</a>*, *<a href="https://github.com/kubernetes/release/pull/940#" title="Author self-approved">saschagrunert</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **idealhack**
You can assign the PR to them by writing `/assign @idealhack` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Frelease).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/release/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["idealhack"]} -->
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-release-test | 4cedafc9e0a2436ab2027bed2ba8d82fce123c99 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/release/940/pull-release-test/1205037257925332992) | `/test pull-release-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes&repo=release&pr=940). [Your PR dashboard](https://gubernator.k8s.io/pr/saschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes/release/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /test pull-release-test
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-release-test | 4cedafc9e0a2436ab2027bed2ba8d82fce123c99 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/release/940/pull-release-test/1205038767107215360) | `/test pull-release-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes&repo=release&pr=940). [Your PR dashboard](https://gubernator.k8s.io/pr/saschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes/release/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/release/pull/940#issuecomment-557873382" title="LGTM">cpanato</a>*, *<a href="https://github.com/kubernetes/release/pull/940#pullrequestreview-321997730" title="LGTM">hasheddan</a>*, *<a href="https://github.com/kubernetes/release/pull/940#" title="Author self-approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Frelease).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[cmd/release-notes/OWNERS](https://github.com/kubernetes/release/blob/master/cmd/release-notes/OWNERS)~~ [saschagrunert]
- ~~[pkg/notes/OWNERS](https://github.com/kubernetes/release/blob/master/pkg/notes/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-release-test | 919a0369ed1ee9bf5dda3a8cdc4d80ca0e56ae4e | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/release/940/pull-release-test/1205042414226509826) | `/test pull-release-test`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes&repo=release&pr=940). [Your PR dashboard](https://gubernator.k8s.io/pr/saschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes/release/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/cmd/release-notes/main.go b/cmd/release-notes/main.go
index 9b26cc4f..f01bd986 100644
--- a/cmd/release-notes/main.go
+++ b/cmd/release-notes/main.go
@@ -50,6 +50,8 @@ type options struct {
 	requiredAuthor string
 	debug          bool
 	discoverMode   string
+	releaseBucket  string
+	releaseTars    string
 }
 
 const (
@@ -185,6 +187,20 @@ func (o *options) BindFlags() *flag.FlagSet {
 			revisionDiscoveryModeMinorToLatest),
 	)
 
+	flags.StringVar(
+		&o.releaseBucket,
+		"release-bucket",
+		envDefault("RELEASE_BUCKET", "kubernetes-release"),
+		"Specify gs bucket to point to in generated notes",
+	)
+
+	flags.StringVar(
+		&o.releaseTars,
+		"release-tars",
+		envDefault("RELEASE_TARS", ""),
+		"Directory of tars to sha512 sum for display",
+	)
+
 	return flags
 }
 
@@ -292,7 +308,9 @@ func (o *options) WriteReleaseNotes(releaseNotes notes.ReleaseNotes, history not
 			return err
 		}
 
-		if err := notes.RenderMarkdown(doc, output); err != nil {
+		if err := notes.RenderMarkdown(
+			output, doc, o.releaseBucket, o.releaseTars, o.startRev, o.endRev,
+		); err != nil {
 			logrus.Errorf("rendering release note document to markdown: %v", err)
 			return err
 		}
@@ -407,8 +425,7 @@ func run(args []string) error {
 		return err
 	}
 
-	err = opts.WriteReleaseNotes(releaseNotes, history)
-	if err != nil {
+	if err := opts.WriteReleaseNotes(releaseNotes, history); err != nil {
 		logrus.Errorf("writing to file: %v", err)
 		return err
 	}
diff --git a/pkg/notes/document.go b/pkg/notes/document.go
index c99d490e..0066dbb4 100644
--- a/pkg/notes/document.go
+++ b/pkg/notes/document.go
@@ -17,10 +17,15 @@ limitations under the License.
 package notes
 
 import (
+	"crypto/sha512"
 	"fmt"
 	"io"
+	"os"
+	"path/filepath"
 	"sort"
 	"strings"
+
+	"github.com/pkg/errors"
 )
 
 // Document represents the underlying structure of a release notes document.
@@ -106,7 +111,11 @@ func CreateDocument(notes ReleaseNotes, history ReleaseNotesHistory) (*Document,
 
 // RenderMarkdown accepts a Document and writes a version of that document to
 // supplied io.Writer in markdown format.
-func RenderMarkdown(doc *Document, w io.Writer) error {
+func RenderMarkdown(w io.Writer, doc *Document, bucket, tars, prevTag, newTag string) error {
+	if err := createDownloadsTable(w, bucket, tars, prevTag, newTag); err != nil {
+		return err
+	}
+
 	// we always want to render the document with SIGs in alphabetical order
 	sortedSIGs := []string{}
 	for sig := range doc.SIGs {
@@ -158,7 +167,7 @@ func RenderMarkdown(doc *Document, w io.Writer) error {
 
 	// the "API Changes" section
 	if len(doc.APIChanges) > 0 {
-		write("## API Changes\n\n")
+		write("### API Changes\n\n")
 		for _, note := range doc.APIChanges {
 			writeNote(note)
 		}
@@ -167,9 +176,9 @@ func RenderMarkdown(doc *Document, w io.Writer) error {
 
 	// the "Duplicate Notes" section
 	if len(doc.Duplicates) > 0 {
-		write("## Notes From Multiple SIGs\n\n")
+		write("### Notes from Multiple SIGs\n\n")
 		for header, notes := range doc.Duplicates {
-			write(fmt.Sprintf("### %s\n\n", header))
+			write(fmt.Sprintf("#### %s\n\n", header))
 			for _, note := range notes {
 				writeNote(note)
 			}
@@ -180,9 +189,9 @@ func RenderMarkdown(doc *Document, w io.Writer) error {
 
 	// each SIG gets a section (in alphabetical order)
 	if len(sortedSIGs) > 0 {
-		write("## Notes from Individual SIGs\n\n")
+		write("### Notes from Individual SIGs\n\n")
 		for _, sig := range sortedSIGs {
-			write("### SIG " + prettySIG(sig) + "\n\n")
+			write("#### SIG " + prettySIG(sig) + "\n\n")
 			for _, note := range doc.SIGs[sig] {
 				writeNote(note)
 			}
@@ -193,7 +202,7 @@ func RenderMarkdown(doc *Document, w io.Writer) error {
 
 	// the "Bug Fixes" section
 	if len(doc.BugFixes) > 0 {
-		write("## Bug Fixes\n\n")
+		write("### Bug Fixes\n\n")
 		for _, note := range doc.BugFixes {
 			writeNote(note)
 		}
@@ -203,7 +212,7 @@ func RenderMarkdown(doc *Document, w io.Writer) error {
 	// we call the uncategorized notes "Other Notable Changes". ideally these
 	// notes would at least have a SIG label.
 	if len(doc.Uncategorized) > 0 {
-		write("## Other Notable Changes\n\n")
+		write("### Other Notable Changes\n\n")
 		for _, note := range doc.Uncategorized {
 			writeNote(note)
 		}
@@ -253,3 +262,74 @@ func prettifySigList(sigs []string) string {
 
 	return sigList
 }
+
+// createDownloadsTable creates the markdown table with the links to the tarballs.
+// The function does nothing if the `tars` variable is empty.
+func createDownloadsTable(w io.Writer, bucket, tars, prevTag, newTag string) error {
+	// Do not add the table if not explicitely requested
+	if tars == "" {
+		return nil
+	}
+	if prevTag == "" || newTag == "" {
+		return errors.New("release tags not specified")
+	}
+
+	fmt.Fprintf(w, "# %s\n\n", newTag)
+	fmt.Fprintf(w, "[Documentation](https://docs.k8s.io)\n\n")
+
+	fmt.Fprintf(w, "## Downloads for %s\n\n", newTag)
+
+	urlPrefix := fmt.Sprintf("https://storage.googleapis.com/%s/release", bucket)
+	if bucket == "kubernetes-release" {
+		urlPrefix = "https://dl.k8s.io"
+	}
+
+	for _, item := range []struct {
+		heading  string
+		patterns []string
+	}{
+		{"", []string{"kubernetes.tar.gz", "kubernetes-src.tar.gz"}},
+		{"Client Binaries", []string{"kubernetes-client*.tar.gz"}},
+		{"Server Binaries", []string{"kubernetes-server*.tar.gz"}},
+		{"Node Binaries", []string{"kubernetes-node*.tar.gz"}},
+	} {
+		if item.heading != "" {
+			fmt.Fprintf(w, "### %s\n\n", item.heading)
+		}
+		fmt.Fprintln(w, "filename | sha512 hash")
+		fmt.Fprintln(w, "-------- | -----------")
+
+		for _, pattern := range item.patterns {
+			pattern := filepath.Join(tars, pattern)
+
+			matches, err := filepath.Glob(pattern)
+			if err != nil {
+				return err
+			}
+
+			for _, file := range matches {
+				f, err := os.Open(file)
+				if err != nil {
+					return err
+				}
+				defer f.Close()
+
+				h := sha512.New()
+				if _, err := io.Copy(h, f); err != nil {
+					return err
+				}
+
+				fileName := filepath.Base(file)
+				fmt.Fprintf(w,
+					"[%s](%s/%s/%s) | `%x`\n",
+					fileName, urlPrefix, newTag, fileName, h.Sum(nil),
+				)
+			}
+		}
+
+		fmt.Fprintln(w, "")
+	}
+
+	fmt.Fprintf(w, "## Changelog since %s\n\n", prevTag)
+	return nil
+}


Test patch:
diff --git a/pkg/notes/document_test.go b/pkg/notes/document_test.go
index 6fd76266..857bccc6 100644
--- a/pkg/notes/document_test.go
+++ b/pkg/notes/document_test.go
@@ -17,6 +17,10 @@ limitations under the License.
 package notes
 
 import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -37,3 +41,97 @@ func TestPrettySIG(t *testing.T) {
 		require.Equal(t, expected, (prettySIG(input)))
 	}
 }
+
+func TestCreateDownloadsTable(t *testing.T) {
+	// Given
+	output := &bytes.Buffer{}
+	dir, err := ioutil.TempDir("", "")
+	require.Nil(t, err)
+	defer os.RemoveAll(dir)
+
+	for _, file := range []string{
+		"kubernetes-client-darwin-386.tar.gz",
+		"kubernetes-client-darwin-amd64.tar.gz",
+		"kubernetes-client-linux-386.tar.gz",
+		"kubernetes-client-linux-amd64.tar.gz",
+		"kubernetes-client-linux-arm.tar.gz",
+		"kubernetes-client-linux-arm64.tar.gz",
+		"kubernetes-client-linux-ppc64le.tar.gz",
+		"kubernetes-client-linux-s390x.tar.gz",
+		"kubernetes-client-windows-386.tar.gz",
+		"kubernetes-client-windows-amd64.tar.gz",
+		"kubernetes-node-linux-amd64.tar.gz",
+		"kubernetes-node-linux-arm.tar.gz",
+		"kubernetes-node-linux-arm64.tar.gz",
+		"kubernetes-node-linux-ppc64le.tar.gz",
+		"kubernetes-node-linux-s390x.tar.gz",
+		"kubernetes-node-windows-amd64.tar.gz",
+		"kubernetes-server-linux-amd64.tar.gz",
+		"kubernetes-server-linux-arm.tar.gz",
+		"kubernetes-server-linux-arm64.tar.gz",
+		"kubernetes-server-linux-ppc64le.tar.gz",
+		"kubernetes-server-linux-s390x.tar.gz",
+		"kubernetes-src.tar.gz",
+		"kubernetes.tar.gz",
+	} {
+		require.Nil(t, ioutil.WriteFile(
+			filepath.Join(dir, file), []byte{1, 2, 3}, 0o0644,
+		))
+	}
+
+	// When
+	require.Nil(t, createDownloadsTable(output, "kubernetes-release", dir,
+		"v1.16.0", "v1.16.1"))
+
+	// Then
+	require.Equal(t, output.String(), `# v1.16.1
+
+[Documentation](https://docs.k8s.io)
+
+## Downloads for v1.16.1
+
+filename | sha512 hash
+-------- | -----------
+[kubernetes.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-src.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-src.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+
+### Client Binaries
+
+filename | sha512 hash
+-------- | -----------
+[kubernetes-client-darwin-386.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-darwin-386.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-darwin-amd64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-darwin-amd64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-linux-386.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-linux-386.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-linux-amd64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-linux-amd64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-linux-arm.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-linux-arm.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-linux-arm64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-linux-arm64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-linux-ppc64le.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-linux-s390x.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-linux-s390x.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-windows-386.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-windows-386.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-client-windows-amd64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-client-windows-amd64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+
+### Server Binaries
+
+filename | sha512 hash
+-------- | -----------
+[kubernetes-server-linux-amd64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-server-linux-amd64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-server-linux-arm.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-server-linux-arm.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-server-linux-arm64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-server-linux-arm64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-server-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-server-linux-ppc64le.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-server-linux-s390x.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-server-linux-s390x.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+
+### Node Binaries
+
+filename | sha512 hash
+-------- | -----------
+[kubernetes-node-linux-amd64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-node-linux-amd64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-node-linux-arm.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-node-linux-arm.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-node-linux-arm64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-node-linux-arm64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-node-linux-ppc64le.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-node-linux-ppc64le.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-node-linux-s390x.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-node-linux-s390x.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+[kubernetes-node-windows-amd64.tar.gz](https://dl.k8s.io/v1.16.1/kubernetes-node-windows-amd64.tar.gz) | `+"`"+`27864cc5219a951a7a6e52b8c8dddf6981d098da1658d96258c870b2c88dfbcb51841aea172a28bafa6a79731165584677066045c959ed0f9929688d04defc29`+"`"+`
+
+## Changelog since v1.16.0
+
+`)
+}