diff --git a/README.md b/README.md index 4dfbb1668071..364641e58c30 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,107 @@ -# release -Release infrastructure for Kubernetes and related components +Table of Contents +================= +* [Intro](#intro) +* [Instructions (Quick Start)](#instructions-quick-start) +* [Other Tools](#other-tools) + * [Tools](#tools) + * [Release Notes Gathering](#release-notes-gathering) + +# Kubernetes Release Process + +This repo contains the release infrastructure for +[Kubernetes](https://github.com/kubernetes/kubernetes). + +## Intro + +Kubernetes releases are done by the Kubernetes team at Google due to +permissions and other restrictions. This may expand eventually to allow +other Kubernetes contributors to generate releases. + +The current process runs by default in *mock* mode and anyone should +be able to run it in this mode to see exactly how the process works. +In *mock* mode all the code paths are followed for a release, but nothing +is pushed to repositories. + +Sticking with the ancient Greek theme, the release script is called `anago`. +Anago means, in the context of navigators and shipping: +"to launch out, set sail, put to sea." + +Tools in this repository includes a familiar [\*nix-style man +page](https://github.com/kubernetes/release/blob/master/anago) with usage, +process and examples. The link shows how the self-contained doc/man page +makes up the header of the script itself and the same info is available +on the command-line (or get usage simply by calling the script with no options): + +``` +$ anago -man +``` + +The idea is that no external doc updates should be necessary and the +tool itself contains all of the details and instructions and prerequisite +checks needed for anyone to run the tool in at least mock mode. + +There is a simple $USER check to ensure that noone but a certain few people can +run the script with --nomock to perform a real release. + +## Instructions (Quick Start) + +The tool was designed to require minimal inputs. +The only information the tool needs is to know where you want to create a +release with one optional flag `[--official]` \(used on release-\* branches only\). + +Try an alpha release: +``` +$ anago master +``` + +Try a beta release on a branch: +``` +$ anago release-1.2 +``` + +Try an official release on a branch: +``` +$ anago release-1.2 --official +``` + +Try a beta release on a new branch: +``` +$ anago release-9.9 +``` + +Try creating a new branch and beta for an emergency zero-day fix: +``` +$ anago release-9.9.9 +``` + +## Other Tools + +All standalone scripts have embedded man pages. Just use `-man` to view or +your favorite editor. + +### Tools + +* [mailer](https://github.com/kubernetes/release/blob/master/mailer) : Generic mail interface (due to Google's deprecation of sendmail) +* [find_green_job](https://github.com/kubernetes/release/blob/master/find_green_job) : Ask Jenkins for a good build to use +* [script-template](https://github.com/kubernetes/release/blob/master/script-template) : Generate a script template in the kubernetes/release ecosystem +* [relnotes](https://github.com/kubernetes/release/blob/master/relnotes) : Scrape github for release notes \(See below for more info\) + +### Release Notes Gathering + +``` +# get details on how to use the tool +$ relnotes -man +$ cd /kubernetes + +# Show release notes from the last release on a branch to HEAD +$ relnotes + +# Show release notes from the last release on a specific branch to branch HEAD +$ relnotes --branch=release-1.2 + +# Show release notes between two specific releases +$ relnotes v1.2.0..1.2.1 --branch=release-1.2 +``` + +Please report *any* [issues](https://github.com/kubernetes/release/issues) +you encounter. diff --git a/anago b/anago new file mode 100755 index 000000000000..bf7cfe240d1f --- /dev/null +++ b/anago @@ -0,0 +1,925 @@ +#!/bin/bash +# +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Set PROGram name +PROG=${0##*/} +######################################################################## +#+ +#+ NAME +#+ $PROG - Kubernetes Release Tool +#+ +#+ SYNOPSIS +#+ $PROG [--yes] [--nomock] [--noclean] [--official] [--buildversion=] +#+ +#+ $PROG [--helpshort|--usage|-?] +#+ $PROG [--help|-man] +#+ +#+ DESCRIPTION +#+ $PROG produces Kubernetes releases. +#+ +#+ Driven by the named source and an optional [--official] flag, +#+ $PROG determines what needs to be released and calculates the version. +#+ +#+ [--buildversion=] will override the automatic check for a build version +#+ Mostly used for testing as a way to re-use a known good build version. +#+ +#+ What is going to be done is presented to the user and asks for +#+ confirmation before continuing. +#+ +#+ All prerequisites are checked before the process begins. +#+ +#+ $PROG runs in mock/dryrun mode by default. To actually execute a +#+ release, pass in the --nomock flag. +#+ ACLs control who can actually *push*. +#+ +#+ Simply specify the you want to release from and follow the +#+ prompts. is one of master, release-1.2, etc. and the release +#+ is based on the value and the [--official] flag: +#+ +#+ Branch Official Type +#+ ------ -------- ---- +#+ master alpha +#+ master X N/A +#+ release-* beta +#+ release-* X official +#+ +#+ NOTE: can exist already or not. If the branch doesn't exist, +#+ it will be branched from master as part of the run. +#+ +#+ VALUES USED AND DISPLAYED DURING RUNS: +#+ * The RELEASE_VERSION dictionary is indexed by each of the types of +#+ releases that will be processed for a session (alpha,beta,official) +#+ * RELEASE_VERSION_PRIME is the primary release version +#+ * GCRIO_REPO and RELEASE_BUCKET are the publish locations +#+ +#+ +#+ OPTIONS +#+ [--yes] - Assume 'yes' to all queries +#+ [--nomock] - Complete an actual release with upstream +#+ pushes +#+ [--noclean] - Attempt to work in existing workspace +#+ Not always possible and ignored when --nomock +#+ is set +#+ [--official] - Official releases on release branches only +#+ [--buildversion=] - Override Jenkins check and set a specific +#+ build version +#+ [--help | -man] - display man page for this script +#+ [--usage | -?] - display in-line usage +#+ +#+ EXAMPLES +#+ $PROG --yes master - Do a mock alpha release from master +#+ and don't stop/prompt +#+ $PROG release-1.1 - Do a mock beta release from release-1.1 +#+ $PROG --official release-1.1 +#+ - Do a mock official release from release-1.1 +#+ $PROG --nomock --official release-1.1 +#+ - Do an official release from release-1.1 +#+ +#+ FILES +#+ build/release.sh +#+ +#+ SEE ALSO +#+ common.sh - common function definitions +#+ gitlib.sh - git/jenkins function definitions +#+ releaselib.sh - release/push-specific functions +#+ +#+ BUGS/TODO +#+ * Add statefulness and re-entrancy if the need develops +#+ * Allow --buildversion to specify the absolute version that +#+ release::set_build_version() must wait for +#+ - useful when targeting a specific hash for a branch release +#+ +######################################################################## +# If NO ARGUMENTS should return usage, uncomment the following line: +usage=${1:-yes} + +source $(dirname $(readlink -ne $BASH_SOURCE))/lib/common.sh +source $TOOL_LIB_PATH/gitlib.sh +source $TOOL_LIB_PATH/releaselib.sh + +# Validate command-line +common::argc_validate 1 || common::exit 1 "Exiting..." + +# Set positional args +RELEASE_BRANCH=${POSITIONAL_ARGV[0]} + +# Check branch format +[[ $RELEASE_BRANCH =~ $BRANCH_REGEX ]] \ + || common::exit 1 "Invalid branch name!" + +# Check arg conflicts +if ((FLAGS_official)) && [[ "$RELEASE_BRANCH" == "master" ]]; then + common::exit 1 "Can't do official releases on master!" +fi + +############################################################################### +# FUNCTIONS +############################################################################### +############################################################################### +# common::cleanexit prog-specific override function +# Do stuff here to clean up after this specific script +# @param exit code +# +common::cleanexit () { + tput cnorm + + common::strip_control_characters $LOGFILE + + if [[ -d $WORKDIR ]]; then + logecho -n "Copying $LOGFILE to $WORKDIR: " + logrun -s cp -f $LOGFILE $WORKDIR + fi + + common::timestamp end + exit ${1:-0} +} + + +############################################################################### +# Simple ACL check to limit nomock runs to a short list of release folks +check_acls () { + case "$USER" in + filipg|ihmccreery|djmm) ;; + *) logecho "Releases restricted to certain users!" + return 1 + ;; + esac +} + +############################################################################### +# Checks release-specific prereqs +# Sets globals GCLOUD_ACCOUNT GCLOUD_PROJECT +# @param package - A space separated list of packages to verify exist +# +check_prerequisites () { + local useratgoog="$USER@google.com" + local tempfile=/tmp/$PROG-cp.$$ + + # TODO: stub out for non-google + # Verify some hours of LOAS + logecho -n "Checking LOAS state: " + logrun -s common::loascheck 1 || return 1 + + if ! common::set_cloud_binaries; then + logecho "Releasing Kubernetes requires gsutil and gcloud. Please download," + logecho "install and authorize through the Google Cloud SDK:" + logecho + logecho "https://developers.google.com/cloud/sdk/" + return 1 + fi + + # TODO: Users outside google? Guess domain? + # TODO: THe real test here is to verify that whatever auth has access to + # do releasey things + gcloud_auth_list=$($GCLOUD auth list 2>/dev/null) + for user in k8s.production.user@gmail.com $useratgoog; do + logecho -n "Checking cloud auth for $user: " + if [[ "$gcloud_auth_list" =~ -\ $user ]]; then + logecho -r "$OK" + else + logecho -r "$FAILED" + logecho "$user is not in the credentialed accounts list!" + logecho "Sign in with gcloud auth login $user" + return 1 + fi + done + # Ensure $USER is active to start + if ! [[ "$gcloud_auth_list" =~ -\ $useratgoog\ \(active\) ]]; then + logecho "$useratgoog is not the active gcloud user!" + logecho "Set with:" + logecho "$ gcloud config set account $useratgoog" + return 1 + fi + GCLOUD_ACCOUNT=$user + + # Verify write access to $RELEASE_BUCKET + # Insufficient for checking actual writability, but useful: + # ganpati list-members -g cloud-kubernetes-release -u $USER|fgrep -wq $USER + logecho -n "Checking writability to $RELEASE_BUCKET: " + if logrun touch $tempfile && \ + logrun gsutil cp $tempfile gs://$RELEASE_BUCKET && \ + logrun gsutil rm gs://$RELEASE_BUCKET/${tempfile##*/} && \ + logrun rm -f $tempfile; then + logecho -r "$OK" + else + logecho -r "$FAILED" + return 1 + fi + + logecho -n "Checking cloud project state: " + GCLOUD_PROJECT=$($GCLOUD config list project 2>/dev/null |\ + awk '{project = $3} END {print project}') + if [[ -z "$GCLOUD_PROJECT" ]]; then + logecho -r "$FAILED" + logecho "No account authorized through gcloud. Please fix with:" + logecho + logecho "$ gcloud config set project " + return 1 + fi + logecho -r "$OK" +} + +############################################################################### +# Updates pkg/version/base.go with RELEASE_VERSION +# Uses the RELEASE_VERSION global dict +# @param label - label index to RELEASE_VERSION +rev_version_base () { + local label=$1 + local version_file="pkg/version/base.go" + local minor_plus + local gitmajor + local gitminor + + logecho "Updating $version_file to ${RELEASE_VERSION[$label]}..." + + if [[ ${RELEASE_VERSION[$label]} =~ ${VER_REGEX[release]} ]]; then + gitmajor=${BASH_REMATCH[1]} + gitminor=${BASH_REMATCH[2]} + fi + + [[ "$label" =~ ^beta ]] && minor_plus="+" + + sed -i -e "s/\(gitMajor *string = \)\"[^\"]*\"/\1\"$gitmajor\"/g" \ + -e "s/\(gitMinor *string = \)\"[^\"]*\"/\1\"$gitminor$minor_plus\"/g" \ + -e "s/\(gitVersion *string = \)\"[^\"]*\"/\1\"${RELEASE_VERSION[$label]}+\$Format:%h\$\"/g" $version_file + + logecho -n "Formatting $version_file: " + logrun -s gofmt -s -w $version_file + logrun git add $version_file + logecho -n "Committing $version_file: " + logrun -s git commit -m "Kubernetes version ${RELEASE_VERSION[$label]}" +} + + +############################################################################### +# Update CHANGELOG on master +generate_release_notes() { + local tarball=$TREE_ROOT/_output-$RELEASE_VERSION_PRIME/release-tars + tarball+=/kubernetes.tar.gz + + logecho -n "Generating release notes: " + logrun -s relnotes $RELEASE_VERSION_PRIME --tarball=$tarball \ + --branch=${PARENT_BRANCH:-$RELEASE_BRANCH} --htmlize-md \ + --markdown-file=$RELEASE_NOTES_MD \ + --html-file=$RELEASE_NOTES_HTML \ + --release-bucket=$RELEASE_BUCKET || return 1 + + logecho -n "Checkout master branch to make changes: " + logrun -s git checkout master || return 1 + + logecho "Insert $RELEASE_VERSION_PRIME notes into CHANGELOG.md..." + # Pipe to logrun() vs using directly, because quoting. + sed -i -e 's//&\n/' \ + -e "//r $RELEASE_NOTES_MD" CHANGELOG.md |\ + logrun + + # Setup and fetch upstream remote for updated-generated-docs.sh to use + if ! git remote |fgrep -qw upstream; then + logecho -n "Set upstream remote to $K8S_GITHUB_URL: " + logrun -s git remote add upstream $K8S_GITHUB_URL || return 1 + logecho -n "Fetch upstream remote: " + logrun -s git fetch upstream || return 1 + fi + + # TODO: What other checks are necessary here. + # verify-all is an unacceptable superset. Determine which ones are actually + # needed for updating a single markdown file. + logecho -n "Update CHANGELOG.md TOC: " + logrun -s $TREE_ROOT/hack/update-generated-docs.sh || return 1 + logecho -n "Committing CHANGELOG.md: " + + # Pipe to logrun() vs using directly, because quoting. + logrun -s git commit -am "Update CHANGELOG.md for $RELEASE_VERSION_PRIME." \ + || return 1 +} + + +############################################################################## +# Prepare sources for building for a given label +# @param label - The label to process +prepare_tree () { + local label=$1 + local tree_object="$RELEASE_BRANCH" + local branch_arg + local branch_point + local label_common=$label + + # Check for tag first + if git rev-parse "${RELEASE_VERSION[$label]}" >/dev/null 2>&1; then + if ((FLAGS_nomock)); then + logecho "Something horrible went wrong." \ + "The ${RELEASE_VERSION[$label]} tag already exists!" + return 1 + elif ((FLAGS_noclean)); then + logecho "$ATTENTION: Found existing tag ${RELEASE_VERSION[$label]} in" \ + "unclean tree during --noclean run" + logecho -n "Checking out ${RELEASE_VERSION[$label]}: " + logrun -s git checkout ${RELEASE_VERSION[$label]} || return 1 + return 0 + fi + fi + + # if this is a new branched branch, checkout -B + # NOTE: We branch from the head of the $PARENT_BRANCH in this case + if [[ "$PARENT_BRANCH" =~ release- ]]; then + if [[ $RELEASE_VERSION_PRIME == ${RELEASE_VERSION[$label]} ]]; then + # --no-track? + branch_arg="-B" + branch_point="origin/$PARENT_BRANCH" + else + # if this is not the PRIMary version on the named RELEASE_BRANCH, use the + # parent + tree_object=$PARENT_BRANCH + fi + elif [[ $label == "alpha" ]]; then + if ! [[ $JENKINS_BUILD_VERSION =~ ${VER_REGEX[build]} ]]; then + logecho "Unable to set checkout point for alpha release!" \ + "Invalid JENKINS_BUILD_VERSION=$JENKINS_BUILD_VERSION" + return 1 + fi + tree_object=${BASH_REMATCH[2]} + fi + + # Checkout location + logecho -n "Checking out $tree_object: " + logrun -s git checkout $branch_arg $tree_object $branch_point || return 1 + + # rev base.go + case $label in + beta*|official) rev_version_base $label ;; + esac + + # versionize docs + if [[ "$PARENT_BRANCH" == master ]]; then + logecho -n "Versionizing docs for ${RELEASE_VERSION[$label]}: " + logrun -s $TREE_ROOT/build/versionize-docs.sh ${RELEASE_VERSION[$label]} + logecho -n "Committing: " + logrun git commit -am \ + "Versioning docs and examples for ${RELEASE_VERSION[$label]}." + fi + + # Ensure a common name for label in case we're using the special beta indexes + [[ "$label" =~ ^beta ]] && label_common="beta" + + # Tagging + commit_string="Kubernetes $label_common release ${RELEASE_VERSION[$label]}" + logecho -n "Tagging $commit_string on $(gitlib::current_branch): " + logrun -s git tag -a -m "$commit_string" "${RELEASE_VERSION[$label]}" +} + +############################################################################## +# Build the Kubernetes tree +# @param version - The kubernetes version to build +build_tree () { + local version=$1 + + # For official releases we need to build BOTH the official and the beta and + # push those releases. + logecho + logecho -n "Building Kubernetes $version on $RELEASE_BRANCH: " + + # Should be an arg at some point + export KUBE_DOCKER_IMAGE_TAG="$version" + + # TODO: Ideally we update LOCAL_OUTPUT_ROOT in build/common.sh to be + # modifiable. In the meantime just mv the dir after it's done + # Not until https://github.com/kubernetes/kubernetes/issues/23839 + #logrun -s make release OUT_DIR=$BUILD_OUTPUT-${RELEASE_VERSION[$label]} + logrun -s make release || common::exit 1 + + logecho -n "Moving build _output to $BUILD_OUTPUT-$version: " + logrun -s mv $BUILD_OUTPUT $BUILD_OUTPUT-$version +} + +############################################################################## +# Push git objects to github +# NOTES: +# * alpha is alone, pushes tags only +# * beta is alone, pushes branch and tags +# * official pushes both official and beta items - branch and tags +# * New branch tags a new alpha on master, new beta on new branch and pushes +# new branch and tags on both +push_git_objects () { + local b + local dryrun_flag=" --dry-run" + + # The real deal? + ((FLAGS_nomock)) && dryrun_flag="" + + ((FLAGS_yes)) \ + || common::askyorn -e "Pausing here. Confirm push$dryrun_flag of tags" \ + "and bits" \ + || common::exit 1 "Exiting..." + + logecho -n "Checkout master branch to push objects: " + logrun -s git checkout master + + logecho "Pushing$dryrun_flag tags" + for b in ${!RELEASE_VERSION[@]}; do + logecho -n "* ${RELEASE_VERSION[$b]}: " + logrun -s git push$dryrun_flag origin ${RELEASE_VERSION[$b]} || return 1 + done + + if [[ $RELEASE_BRANCH =~ release- ]]; then + #logecho -n "Rebase $RELEASE_BRANCH branch: " + #logrun git fetch origin/$RELEASE_BRANCH || return 1 + #logrun git rebase origin/$RELEASE_BRANCH || return 1 + logecho -n "Pushing$dryrun_flag $RELEASE_BRANCH branch: " + logrun -s git push$dryrun_flag origin $RELEASE_BRANCH || return 1 + if [[ -n "$PARENT_BRANCH" ]]; then + logecho -n "Pushing$dryrun_flag $PARENT_BRANCH branch: " + logrun -s git push$dryrun_flag origin $PARENT_BRANCH || return 1 + fi + fi + + # For new branches and for CHANGELOG, update the master + logecho -n "Rebase master branch: " + logrun git fetch origin || return 1 + logrun -s git rebase origin/master || return 1 + logecho -n "Pushing$dryrun_flag master branch: " + logrun -s git push$dryrun_flag origin master || return 1 +} + + +############################################################################### +# generate the announcement text to be mailed and published +create_branch_announcement () { + cat <<+ +Kubernetes team, +

+Kubernetes' $RELEASE_BRANCH branch has been created and is ready to accept patches. +

+Refer to the Cherrypick Guide for instructions. + +Announced by $PROG, the Kubernetes Release Tool ++ +} + +############################################################################### +# generate the announcement text to be mailed and published +create_announcement () { + cat <<+ +Kubernetes team, +

+Kubernetes $RELEASE_VERSION_PRIME has been built and pushed. +

+The release notes have been updated in CHANGELOG.md with a pointer to it on github: +

+


+$(cat $RELEASE_NOTES_HTML) +
+


+Leads, the CHANGELOG.md has been bootstrapped with $RELEASE_VERSION_PRIME release notes and you may edit now as needed. +


+Published by $PROG, the Kubernetes Release Tool ++ +} + +############################################################################### +# Mail out the announcement +# @param subject - the subject for the email notification +announce () { + local mailto="cloud-kubernetes-team@google.com" + mailto+=",kubernetes-dev@googlegroups.com" + mailto+=",kubernetes-announce@googlegroups.com" + local subject="$*" + local announcement_text=/tmp/$PROG-announce.$$ + + ((FLAGS_nomock)) || mailto=$USER + + if [[ -n "$PARENT_BRANCH" ]]; then + create_branch_announcement + else + create_announcement + fi > $announcement_text + + logecho "Announcing k8s $RELEASE_VERSION_PRIME to $mailto..." + + # Always cc invoker + mailer --to="$mailto" --cc="$USER" --file="$announcement_text" --html \ + --from="K8s-Anago" \ + --subject="$subject" || return 1 + + logrun rm -f $announcement_text +} + +############################################################################### +# Update the releases page on github +update_github_release () { + local release_id + local id_suffix + local release_verb="Posting" + local prerelease="true" + local draft="true" + local tarball="$TREE_ROOT/_output-$RELEASE_VERSION_PRIME/release-tars" + tarball+="/kubernetes.tar.gz" + + ((FLAGS_official)) && prerelease="false" + if ((FLAGS_nomock)); then + draft="false" + + # Check to see that a tag exists. + # non-draft release posts to github create a tag. We don't want to + # create any tags on the repo this way. The tag should already exist + # as a result of the release process. + if ! $GHCURL $K8S_GITHUB_API/git/refs/tags |jq -r '.[] | .ref' |\ + egrep "^refs/tags/$RELEASE_VERSION_PRIME$"; then + logecho + logecho "$FATAL: How did we get here?" + logecho "The $RELEASE_VERSION_PRIME tag doesn't exist yet on github." \ + "That can't be good." + logecho "We certainly cannot publish a release without a tag." + return 1 + fi + fi + + # Does the release exist yet? + release_id=$($GHCURL $K8S_GITHUB_API/releases/tags/$RELEASE_VERSION_PRIME |\ + jq -r '.id') + + if [[ -n "$release_id" ]]; then + logecho "The $RELEASE_VERSION_PRIME is already published on github." + if ((FLAGS_yes)) || common::askyorn -e "Would you like to update it"; then + logecho "Setting post data id to $release_id to update existing release." + id_suffix="/$release_id" + release_verb="Updating" + else + logecho "Existing release (id #$release_id) left intact." + return 1 + fi + fi + + # post release data + logecho "$release_verb the $RELEASE_VERSION_PRIME release on github..." + release_id=$($GHCURL $K8S_GITHUB_API/releases$id_suffix --data \ + '{ + "tag_name": "'$RELEASE_VERSION_PRIME'", + "target_commitish": "'$RELEASE_BRANCH'", + "name": "'$RELEASE_VERSION_PRIME'", + "body": "See [kubernetes-announce@](https://groups.google.com/forum/#!forum/kubernetes-announce) and [CHANGELOG]('$K8S_GITHUB_URL'/blob/master/CHANGELOG.md/#'${RELEASE_VERSION_PRIME//\./}') for details.", + "draft": '$draft', + "prerelease": '$prerelease' + }' |jq -r '.id') + + # verify it was created + if [[ -z "$release_id" ]]; then + logecho + logecho -r "$FAILED to create the $RELEASE_VERSION_PRIME release on github!" + return 1 + fi + + # publish binary + logecho -n "Uploading binary to github: " + logrun -s $GHCURL -H "Content-Type:application/x-compressed" \ + --data-binary @$tarball \ + "${K8S_GITHUB_API/api\./uploads\.}/releases/$release_id/assets?name=${tarball##*/}" + + if $draft; then + logecho + logecho "$ATTENTION: A draft release of $RELEASE_VERSION_PRIME was" \ + "created at $K8S_GITHUB_URL/releases." + logecho + + # delete it + if ((FLAGS_yes)) || \ + common::askyorn -y "Delete draft release (id #$release_id) now"; then + logecho -n "Deleting the draft release (id #$release_id): " + logrun $GHCURL -X DELETE $K8S_GITHUB_API/releases/$release_id + fi + + # verify it was deleted + release_id=$($GHCURL $K8S_GITHUB_API/releases/$release_id | jq -r '.id') + if [[ -n "$release_id" ]]; then + logecho -r $FAILED + logecho "The draft release (id #$release_id) was NOT deleted." \ + "Deal with it by hand" + logecho "$K8S_GITHUB_URL/releases/$RELEASE_VERSION_PRIME" + else + logecho -r $OK + fi + fi +} + +############################################################################## +# Calls into Jenkins looking for a build to use for release +# Sets global PARENT_BRANCH when a new branch is created +get_build_candidate () { + local testing_branch + + # Are we branching to a new branch? + if gitlib::branch_exists $RELEASE_BRANCH; then + testing_branch=$RELEASE_BRANCH + else + [[ $RELEASE_BRANCH =~ $BRANCH_REGEX ]] + + # if 3 part branch name, check parent exists + if [[ -z "${BASH_REMATCH[4]}" ]]; then + if ((FLAGS_official)); then + common::exit 1 "Can't do official releases when creating a new branch!" + fi + + PARENT_BRANCH=master + testing_branch=$PARENT_BRANCH + elif gitlib::branch_exists ${RELEASE_BRANCH%.*}; then + PARENT_BRANCH=${RELEASE_BRANCH%.*} + testing_branch=$PARENT_BRANCH + else + common::exit 1 "$FATAL! We should never get here! branch=$RELEASE_BRANCH" + fi + fi + + if [[ -n $JENKINS_BUILD_VERSION ]]; then + logecho -r "$ATTENTION: Using --buildversion=$JENKINS_BUILD_VERSION" + else + logecho "Asking Jenkins for a good build (this may take some time)..." + FLAGS_verbose=1 release::set_build_version $testing_branch || return 1 + fi + + # The RELEASE_BRANCH should always match with the JENKINS_BUILD_VERSION + if [[ $RELEASE_BRANCH =~ release- ]] && \ + [[ ! $JENKINS_BUILD_VERSION =~ ^v${RELEASE_BRANCH/release-/} ]]; then + logecho + logecho "$FATAL! branch/build mismatch!" + logecho "buildversion=$JENKINS_BUILD_VERSION branch=$RELEASE_BRANCH" + common::exit 1 + fi + + FLAGS_verbose=1 \ + release::set_release_version $JENKINS_BUILD_VERSION $RELEASE_BRANCH \ + $PARENT_BRANCH || return 1 +} + +############################################################################## +# Prepare the workspace and sync the tree +prepare_workspace () { + local outdir + + # Clean up or not + if ((FLAGS_noclean)); then + logecho "Working in existing workspace..." + else + if [[ -d $TREE_ROOT ]]; then + logecho "Checking for _output directories..." + logrun cd $TREE_ROOT + # set -e sillyness - yes that's a ||true there that would otherwise not + # be needed except for 'set -e' in all its glory + for outdir in $(ls -1d _output* 2>/dev/null ||true); do + # This craziness due to + # https://github.com/kubernetes/kubernetes/issues/23839 + if [[ $outdir != "_output" ]]; then + logrun mv $outdir _output + fi + logecho -n "make clean for $outdir: " + logrun -s make clean || return 1 + logecho -n "Removing _output: " + logrun -s rm -rf _output || return 1 + done + fi + logecho -n "Removing/recreating $WORKDIR: " + logrun cd /tmp + logrun -s rm -rf $WORKDIR || return 1 + fi + + # Sync the tree + logrun mkdir -p $WORKDIR + gitlib::sync_repo $K8S_GITHUB_URL $TREE_ROOT || return 1 + logrun cd $TREE_ROOT + + logecho -n "Check/make release bucket $RELEASE_BUCKET: " + logrun -s release::gcs::ensure_release_bucket $RELEASE_BUCKET \ + || return 1 +} + +############################################################################### +# MAIN +############################################################################### +# Default mode is a mocked release workflow +: ${FLAGS_nomock:=0} +: ${FLAGS_noclean:=0} +: ${FLAGS_official:=0} +# Set with --buildversion or set it later in release::set_build_version() +JENKINS_BUILD_VERSION=$FLAGS_buildversion + +RELEASE_BUCKET="kubernetes-release" +if ((FLAGS_nomock)); then + # Override any --noclean setting if nomock + FLAGS_noclean=0 + GCRIO_REPO="google_containers" +else + # This is passed to logrun() where appropriate when we want to mock + # specific activities like pushes + LOGRUN_MOCK="-m" + # Point to a $USER playground + RELEASE_BUCKET+=-$USER + GCRIO_REPO="kubernetes-release-test" +fi +BASEDIR="/usr/local/google/$USER" + +# TODO: +# These KUBE_ globals extend beyond the scope of the new release refactored +# tooling so to pass these through as flags will require fixes across +# kubernetes/kubernetes and kubernetes/release which we can do at a later time +export KUBE_DOCKER_REGISTRY="gcr.io/$GCRIO_REPO" +export KUBE_RELEASE_RUN_TESTS=n +export KUBE_SKIP_CONFIRMATIONS=y + +############################################################################## +# Initialize logs +############################################################################## +# Initialize and save up to 10 (rotated logs) +LOGFILE=/tmp/$PROG.log +common::logfileinit $LOGFILE 10 +# BEGIN script +common::timestamp begin + +############################################################################## +common::stepheader "CHECK CREDENTIALS" +############################################################################## +gitlib::check_credentials + +# Simple check to validate who can do actual releases +((FLAGS_nomock)) && check_acls + +############################################################################## +common::stepheader "CHECK PREREQUISITES" +############################################################################## +common::check_packages jq docker-engine pandoc || common::exit 1 "Exiting..." +check_prerequisites || common::exit 1 "Exiting..." + +############################################################################## +common::stepheader "FIND A VALID BUILD CANDIDATE" +############################################################################## +common::runstep get_build_candidate || common::exit 1 "Exiting..." + +# WORK/BUILD area +WORKDIR=$BASEDIR/$PROG-$RELEASE_VERSION_PRIME +# TOOL_ROOT is release/ +# TREE_ROOT is working branch/tree +TREE_ROOT=$WORKDIR/kubernetes +BUILD_OUTPUT=$TREE_ROOT/_output +RELEASE_NOTES_MD=$WORKDIR/release-notes.md +RELEASE_NOTES_HTML=$WORKDIR/release-notes.html + +############################################################################## +common::stepheader "DISK SPACE CHECK" +############################################################################## +# 18G per build +common::disk_space_check $BASEDIR $((18*${#RELEASE_VERSION[*]})) ||\ + common::exit 1 "Exiting..." + +if [[ $RELEASE_BRANCH =~ release- ]]; then + ############################################################################## + common::stepheader "PENDING PRS ON THE $RELEASE_BRANCH BRANCH" + ############################################################################## + gitlib::pending_prs $RELEASE_BRANCH +fi + +############################################################################## +common::stepheader "SESSION VALUES" +############################################################################## +# Show versions and ask for confirmation to continue +# Pass in the indexed RELEASE_VERSION dict key by key +ALL_RELEASE_VERSIONS=($(for key in ${!RELEASE_VERSION[@]}; do + echo RELEASE_VERSION[$key]; done)) +common::printvars -p WORKDIR WORKDIR TREE_ROOT JENKINS_BUILD_VERSION \ + RELEASE_VERSION_PRIME ${ALL_RELEASE_VERSIONS[@]} \ + RELEASE_BRANCH GCRIO_REPO RELEASE_BUCKET \ + FLAGS_nomock FLAGS_noclean FLAGS_official LOGFILE + +if [[ -n "$PARENT_BRANCH" ]]; then + logecho + logecho "$ATTENTION: $RELEASE_BRANCH is a NEW branch off $PARENT_BRANCH!" +fi + +logecho +((FLAGS_yes)) || common::askyorn -e "Do these values look ok for a release" \ + || common::exit 1 "Exiting..." + +logecho +logecho -r "${TPUT[BOLD]}>>>>>>>>${TPUT[OFF]}" \ + "View detailed session output with: tailf $LOGFILE" +logecho -r "${TPUT[BOLD]}>>>>>>>>${TPUT[OFF]}" \ + "(Previous logs can be found in $LOGFILE.{1..10})" + +############################################################################## +common::stepheader "PREPARE WORKSPACE" +############################################################################## +common::runstep prepare_workspace || common::exit 1 "Exiting..." + +# Need to check git push direct credentials here, deeper into the process +# than I'd optimally like - preferably this is done way earlier up where the +# other prerequisites are checked, but the nature of the check requires +# an actual git repo. +############################################################################## +common::stepheader "CHECK GIT PUSH ACCESS" +############################################################################## +# TODO: capture state of access without forcing us into a prompt I have to +# expose. +logecho -n "Checking git push access (verbosely to accept password if needed)" +logrun -v git push -q --dry-run $K8S_GITHUB_URL || common::exit 1 "Exiting..." + +# Iterate over session release versions for setup, tagging and building +for label in ${!RELEASE_VERSION[@]}; do + ############################################################################## + common::stepheader "TAG AND BUILD ${RELEASE_VERSION[$label]}" + ############################################################################## + # Prepare the tree for each set of actions (keys of RELEASE_VERSION) + common::runstep prepare_tree $label || common::exit 1 "Exiting..." + common::runstep build_tree ${RELEASE_VERSION[$label]} +done + +# No publishing for X.Y.Z-beta.0 releases +if [[ -z "$PARENT_BRANCH" ]]; then + ############################################################################## + common::stepheader "GENERATE RELEASE NOTES" + ############################################################################## + common::runstep generate_release_notes || common::exit 1 "Exiting..." +fi + +# On the master branch update cmd/mungedocs/mungedocs.go and +# run update-generated-docs.sh +if [[ "$PARENT_BRANCH" == master ]]; then + ############################################################################## + common::stepheader "MUNGEDOCS" + ############################################################################## + logecho -n "Checkout master branch to make changes: " + logrun -s git checkout master + logecho -n "Update docs for new branch $RELEASE_BRANCH: " + sed -i 's,\(const latestReleaseBranch =\) ,\1 "'$RELEASE_BRANCH'",g' \ + cmd/mungedocs/mungedocs.go cmd/mungedocs/unversioned_warning_test.go + logrun -s $TREE_ROOT/hack/update-generated-docs.sh + logecho -n "Committing: " + logrun -s git commit -am \ + "Update the latestReleaseBranch to $RELEASE_BRANCH in the munger." +fi + +############################################################################## +common::stepheader "PUSH GIT OBJECTS" +############################################################################## +common::runstep push_git_objects || common::exit 1 "Exiting..." + +# Get back on $RELEASE_BRANCH because we source branch-specific +# hack/lib/golang.sh in release::docker::release() +if [[ $RELEASE_BRANCH =~ release- ]]; then + logecho -n "Checkout $RELEASE_BRANCH branch to continue: " + logrun -s git checkout $RELEASE_BRANCH || common::exit 1 "Exiting..." +fi + +# Set branch-specific KUBE_SERVER_PLATFORMS from current tree +# Used in release::docker::release() +source $TREE_ROOT/hack/lib/golang.sh + +# Push for each release version of this session +for label in ${!RELEASE_VERSION[@]}; do + ############################################################################## + common::stepheader "PUSH ${RELEASE_VERSION[$label]} IMAGES" + ############################################################################## + common::runstep release::gcs::copy_release_artifacts \ + ${RELEASE_VERSION[$label]} \ + $BUILD_OUTPUT-${RELEASE_VERSION[$label]} $RELEASE_BUCKET \ + || common::exit 1 "Exiting..." + + common::runstep release::docker::release \ + $KUBE_DOCKER_REGISTRY ${RELEASE_VERSION[$label]} \ + || common::exit 1 "Exiting..." + + common::runstep release::gcs::publish_official \ + ${RELEASE_VERSION[$label]} \ + $BUILD_OUTPUT-${RELEASE_VERSION[$label]} $RELEASE_BUCKET \ + || common::exit 1 "Exiting..." +done + +if [[ -n "$PARENT_BRANCH" ]]; then + ############################################################################## + common::stepheader "ANNOUNCE NEW BRANCH" + ############################################################################## + common::runstep announce "k8s $RELEASE_BRANCH branch has been created" \ + || common::exit 1 "Exiting..." +else + ############################################################################## + common::stepheader "ANNOUNCE RELEASE" + ############################################################################## + common::runstep announce "k8s $RELEASE_VERSION_PRIME is live!" \ + || common::exit 1 "Exiting..." + + ############################################################################## + common::stepheader "UPDATE GITHUB RELEASES PAGE" + ############################################################################## + common::runstep update_github_release || common::exit 1 "Exiting..." +fi + +# END script +common::cleanexit 0 diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 000000000000..c4d69f8c3f78 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,16 @@ +# Kubernetes Releases + +TBD + +## Types of Releases + +* Alpha releases (`vX.Y.0-alpha.W`) are cut directly from `master`. +* Beta releases (`vX.Y.Z-beta.W`) are cut from their respective release branch, + `release-X.Y`. +* Official releases (`vX.Y.Z`) are cut from their respective release branch, + `release-X.Y`. +* New release series are also cut directly from `master`. + +## More + +TBD diff --git a/find_green_build b/find_green_build new file mode 100755 index 000000000000..e4d557161e94 --- /dev/null +++ b/find_green_build @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Set PROGram name +PROG=${0##*/} +######################################################################## +#+ +#+ NAME +#+ $PROG - Find a good Kubernetes build from Jenkins +#+ +#+ SYNOPSIS +#+ $PROG [--official] [] +#+ [--github-token=] +#+ $PROG [--helpshort|--usage|-?] +#+ $PROG [--help|-man] +#+ +#+ DESCRIPTION +#+ Scan defined Jenkins instances for 2 contiguous successful runs +#+ to validate the state of the tree and then return build and release +#+ versions for use by the release tools or just for fun. +#+ +#+ With no args, $PROG works on master. Otherwise it works on the named +#+ release branch (ie. release-1.2) and optionally produces +#+ --official versions. +#+ +#+ OPTIONS +#+ [--official] - Produce an official release version for a +#+ release branch +#+ --github-token= - Must be specified if GITHUB_TOKEN not set +#+ [--help | -man] - display man page for this script +#+ [--usage | -?] - display in-line usage +#+ +#+ EXAMPLES +#+ $PROG - Produce versions for master +#+ $PROG release-1.2 - Produce versions for release-1.2 +#+ +#+ FILES +#+ +#+ SEE ALSO +#+ common.sh - base function definitions +#+ anago - release tool +#+ relnotes - release notes generator +#+ +#+ BUGS/TODO +#+ +######################################################################## +# If NO ARGUMENTS should return *usage*, uncomment the following line: +#usage=${1:-yes} + +source $(dirname $(readlink -ne $BASH_SOURCE))/lib/common.sh +source $TOOL_LIB_PATH/gitlib.sh +source $TOOL_LIB_PATH/releaselib.sh + +# Validate command-line +common::argc_validate 1 || common::exit 1 "Exiting..." + +############################################################################### +# common::cleanexit prog-specific override function +# Do stuff here to clean up after this specific script +# @param exit code +# +common::cleanexit () { + common::timestamp end + exit ${1:-0} +} + +############################################################################### +# MAIN +############################################################################### +# BEGIN script +common::timestamp begin + +# Initialize and save up to 10 (rotated logs) +MYLOG=/tmp/$PROG.log +common::logfileinit $MYLOG 10 + +gitlib::check_credentials +common::check_packages jq +common::set_cloud_binaries + +# Blank for master +RELEASE_BRANCH=${POSITIONAL_ARGV[0]:-"master"} + +if (($FLAGS_official)) && [[ "$RELEASE_BRANCH" == "master" ]]; then + common::exit 1 "Can't do official releases on master!" +fi + +# Are we branching to a new branch? +if gitlib::branch_exists $RELEASE_BRANCH; then + TESTING_BRANCH=$RELEASE_BRANCH + BRANCH_OFF_MASTER=0 +else + (($FLAGS_official)) && \ + common::exit 1 "Can't do official releases when creating a new branch!" + TESTING_BRANCH=master + BRANCH_OFF_MASTER=1 +fi + +logecho +release::set_build_version $TESTING_BRANCH || common::exit 1 +release::set_release_version $JENKINS_BUILD_VERSION $RELEASE_BRANCH \ + $BRANCH_OFF_MASTER || common::exit 1 + +common::exit 0 diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 000000000000..ac87d9ae04e8 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,822 @@ +#!/bin/bash +# +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Provide a default $PROG (for use in most functions that use a $PROG: prefix +: ${PROG:="common"} +export PROG + +############################################################################## +# Common library of useful functions and GLOBALS. +############################################################################## + +set -o errtrace + +# TODO: +# - Figure out a way to share common bits with other Kubernetes sub repos +# - cleanup / function headers + +############################################################################## +# COMMON CONSTANTS +# +TOOL_LIB_PATH=${TOOL_LIB_PATH:-$(dirname $(realpath $BASH_SOURCE))} +TOOL_ROOT=${TOOL_ROOT:-$(realpath $TOOL_LIB_PATH/..)} +PATH=$TOOL_ROOT:$PATH +# Provide a default EDITOR for those that don't have this set +: ${EDITOR:="vi"} +export PATH TOOL_ROOT TOOL_LIB_PATH EDITOR + +# Pretty curses stuff for terminals +if [[ -t 1 ]]; then + # Set some video text attributes for use in error/warning msgs. + declare -A TPUT=([BOLD]=$(tput bold 2>/dev/null)) + TPUT+=( + [REVERSE]=$(tput rev 2>/dev/null) + [UNDERLINE]=$(tput smul 2>/dev/null) + [BLINK]=$(tput blink 2>/dev/null) + [GREEN]=${TPUT[BOLD]}$(tput setaf 2 2>/dev/null) + [RED]=${TPUT[BOLD]}$(tput setaf 1 2>/dev/null) + [YELLOW]=${TPUT[BOLD]}$(tput setaf 3 2>/dev/null) + [OFF]=$(tput sgr0 2>/dev/null) + [COLS]=$(tput cols 2>/dev/null) + ) + + # HR + HR="$(for ((i=1;i<=${TPUT[COLS]};i++)); do echo -en '\u2500'; done)" + + # Save original TTY State + TTY_SAVED_STATE="$(stty -g)" +else + HR="$(for ((i=1;i<=80;i++)); do echo -en '='; done)" +fi + +# Set some usable highlighted keywords for functions like logrun -s +YES="${TPUT[GREEN]}YES${TPUT[OFF]}" +OK="${TPUT[GREEN]}OK${TPUT[OFF]}" +DONE="${TPUT[GREEN]}DONE${TPUT[OFF]}" +PASSED="${TPUT[GREEN]}PASSED${TPUT[OFF]}" +FAILED="${TPUT[RED]}FAILED${TPUT[OFF]}" +FATAL="${TPUT[RED]}FATAL${TPUT[OFF]}" +NO="${TPUT[RED]}NO${TPUT[OFF]}" +WARNING="${TPUT[YELLOW]}WARNING${TPUT[OFF]}" +ATTENTION="${TPUT[YELLOW]}ATTENTION${TPUT[OFF]}" +MOCK="${TPUT[YELLOW]}MOCK${TPUT[OFF]}" + +# Ensure USER is set +USER=${USER:-$LOGNAME} + +# Set a PID for use throughout. +export PID=$$ + +# Save original cmd-line. +ORIG_CMDLINE="$*" + +PROGSTATE=/tmp/$PROG-runstate + +############################################################################### +# Define logecho() function to display to both log and stdout. +# As this is widely used and to reduce clutter, we forgo the common:: prefix +# Options can be -n or -p or -np/-pn. +# @optparam -p Add $PROG: prefix to stdout +# @optparam -r Exclude log prefix (used to output status' like $OK $FAILED) +# @optparam -n no newline (just like echo -n) +# @param a string to echo to stdout +logecho () { + local log_prefix="$PROG::${FUNCNAME[1]:-"main"}(): " + local prefix + # Dynamically set fmtlen + local fmtlen=$((${TPUT[COLS]:-"80"})) + local n + local raw=0 + #local -a sed_pat=() + + while [[ "$#" -gt 0 ]]; do + case "$1" in + -r) raw=1; shift ;; + -n) n="-n"; shift ;; + -p) prefix="$PROG: "; ((fmtlen+=${#prefix})); shift ;; + *) break ;; + esac + done + + if ((raw)) || [[ -z "$*" ]]; then + # Clean log_prefix for blank lines + log_prefix="" + #else + # Increase fmtlen to account for control characters + #((fmtlen+=$(echo "$*" grep -o '[[:cntrl:]]' |wc -l))) + #sed_pat=(-e '2,${s}/^/ ... /g') + fi + + # Allow widespread use of logecho without having to + # determine if $LOGFILE exists first. + [[ -f $LOGFILE ]] || LOGFILE="/dev/null" + ( + # If -n is set, do not provide autoformatting or you lose the -n effect + # Use of -n should only be used on short status lines anyway. + if ((raw)) || [[ $n == "-n" ]]; then + echo -e $n "$log_prefix$*" + else + # Add FUNCNAME to line prefix, but strip it from visible output + # Useful for viewing log detail + echo -e "$*" | fmt -$fmtlen | sed -e "1s,^,$log_prefix,g" "${sed_pat[@]}" + fi + ) | tee -a "$LOGFILE" |sed "s,^$log_prefix,$prefix,g" +} + +############################################################################### +# logrun() function to run commands to both log and stdout. +# As this is widely used and to reduce clutter, we forgo the common:: prefix +# +# The calling function is added to the line prefix. +# NOTE: All optparam's for logrun() (obviously) must preceed the command string +# @optparam -v Run verbosely +# @optparam -s Provide a $OK or $FAILED status from running command +# @optparam -m MOCK command by printing out command line rather than running it. +# @optparam -r Retry attempts. Integer arg follows -r (Ex. -r 2) +# Typically used together with -v to show retry attempts. +# @param a command string +# GLOBALS used in this function: +# * LOGFILE (Set by common::logfileinit()), if set, gets full command output +# * FLAGS_verbose (Set by caller - defaults to false), if true, full output to stdout +logrun () { + local mock=0 + local status=0 + local arg + local retries=0 + local try + local retry_string + local scope="::${FUNCNAME[1]:-main}()" + local ret + local verbose=0 + + while [[ "$#" -gt 0 ]]; do + case "$1" in + -v) verbose=1; shift ;; + -s) status=1; shift ;; + -m) mock=1; shift ;; + -r) retries=$2; shift 2;; + *) break ;; + esac + done + + for ((try=0; try<=$retries; try++)); do + if [[ $try -gt 0 ]]; then + if ((verbose)) || ((FLAGS_verbose)); then + # if global FLAGS_verbose, be very verbose + logecho "Retry #$try..." + elif ((status)); then + # if we're reporting a status (-v), then just ... + logecho -n "." + fi + # Add some minimal wait between retries assuming we're retrying due to + # something resolvable by waiting 'just a bit' + sleep 2 + fi + + # if no args, take stdin + if (($#==0)); then + if ((verbose)) || ((FLAGS_verbose)); then + tee -a $LOGFILE + else + tee -a $LOGFILE &>/dev/null + fi + ret=$? + elif [[ -f "$LOGFILE" ]]; then + printf "\n$PROG$scope: %s\n" "$*" >> $LOGFILE + + if ((mock)); then + logecho "($MOCK)" + logecho "(CMD): $@" + return 0 + fi + + # Special case "cd" which cannot be run through a pipe (subshell) + if (! ((FLAGS_verbose)) && ! ((verbose)) ) || [[ "$1" == "cd" ]]; then + "${@:-:}" >> $LOGFILE 2>&1 + else + printf "\n$PROG$scope: %s\n" "$*" + "${@:-:}" 2>&1 | tee -a $LOGFILE + fi + ret=${PIPESTATUS[0]} + else + if ((mock)); then + logecho "($MOCK)" + logecho "(CMD): $@" + return 0 + fi + + if ((verbose)) || ((FLAGS_verbose)); then + printf "\n$PROG$scope: %s\n" "$*" + "${@:-:}" + else + "${@:-:}" &>/dev/null + fi + ret=${PIPESTATUS[0]} + fi + + [[ "$ret" = 0 ]] && break + done + + [[ -n "$retries" && $try > 0 ]] && retry_string=" (retry #$try)" + + if ((status)); then + [[ "$ret" = 0 ]] && logecho -r "$OK$retry_string" + [[ "$ret" != 0 ]] && logecho -r "$FAILED" + fi + + return $ret +} + +############################################################################### +# common::timestamp() Capture block timings and display them +# The calling function is added to the line prefix. +# NOTE: All optparam's for logrun() (obviously) must preceed the command string +# @param begin|end|done +# @optparam section defaults to main, but can be specified to time sub sections +common::timestamp () { + local action=$1 + local section=${2:-main} + # convert illegal characters to (legal) underscore + section=${section//[-\.:\/]/_} + local start_var="${section}start_seconds" + local end_var="${section}end_seconds" + local elapsed + local d + local h + local m + local s + local prettyd + local prettyh + local prettym + local prettys + local pretty + + case $action in + begin) + + # Get time(date) for display and calc. + eval $start_var=$(date '+%s') + + # Print BEGIN message for $PROG. + echo "$PROG: BEGIN $section on ${HOSTNAME%%.*} $(date)" + + if [[ $section == "main" ]]; then + echo + fi + ;; + end|done) + # Check for "START" values before calcing. + if [[ -z ${!start_var} ]]; then + #display_time="EE:EE:EE - 'end' run without 'begin' in this scope or sourced script using common::timestamp" + return 1 + fi + + # Get time(date) for display and calc. + eval $end_var=$(date '+%s') + + elapsed=$(( ${!end_var} - ${!start_var} )) + d=$(( elapsed / 86400 )) + h=$(( (elapsed % 86400) / 3600 )) + m=$(( (elapsed % 3600) / 60 )) + s=$(( elapsed % 60 )) + (($d>0)) && local prettyd="${d}d" + (($h>0)) && local prettyh="${h}h" + (($m>0)) && local prettym="${m}m" + prettys="${s}s" + pretty="$prettyd$prettyh$prettym$prettys" + + [[ $section == "main" ]] && echo + echo "$PROG: DONE $section on ${HOSTNAME%%.*} $(date) in $pretty" + ;; + esac +} + +# Write our own trap to capture signal +common::trap () { + local func="$1" + shift + local sig + + for sig; do + trap "$func $sig" "$sig" + done +} + +common::trapclean () { + local sig=$1 + local frame=0 + + # If user ^C's at read then tty is hosed, so make it sane again. + [[ -n "$TTY_SAVED_STATE" ]] && stty "$TTY_SAVED_STATE" + + logecho;logecho + logecho "Signal $sig caught!" + logecho + logecho "Traceback (line function script):" + while caller $frame; do + ((frame++)) + done + common::exit 2 "Exiting..." +} + +############################################################################# +# Clean exit with an ending timestamp +# @param Exit code +common::cleanexit () { + # Display end common::timestamp when an existing common::timestamp begin + # was run. + [[ -n ${mainstart_seconds} ]] && common::timestamp end + exit ${1:-0} +} + +############################################################################# +# common::cleanexit() entry point with some formatting and message printing +# @param Exit code +# @param message +common::exit () { + local etype=${1:-0} + shift + + [[ -n "$1" ]] && (logecho;logecho "$@";logecho) + common::cleanexit $etype +} + +############################################################################# +# Simple yes/no prompt +# +# @optparam default -n(default)/-y/-e (default to n, y or make (e)xplicit) +# @param message +common::askyorn () { + local yorn + local def=n + local msg="y/N" + + case $1 in + -y) # yes default + def="y" msg="Y/n" + shift + ;; + -e) # Explicit + def="" msg="y/n" + shift + ;; + -n) shift + ;; + esac + + while [[ $yorn != [yYnN] ]]; do + logecho -n "$*? ($msg): " + read yorn + : ${yorn:=$def} + done + + # Final test to set return code + [[ $yorn == [yY] ]] +} + +############################################################################### +# Print step header text in a consistent way +common::stepheader () { + # If called with no args, assume the key is the caller's function name + local msg="$*" + + logecho + logecho -r $HR + logecho "$msg" + logecho -r $HR + logecho +} + +# Save a specified number of backups to a file +common::rotatelog () { + local file=$1 + local num=$2 + local tmpfile=/tmp/rotatelog.$PID + local counter=$num + + # Quiet exit + [[ ! -f "$file" ]] && return + + cp -p $file $tmpfile + + while ((counter>=0)); do + if ((counter==num)); then + rm -f $file.$counter + elif ((counter==0)); then + if [[ -f "$file" ]]; then + next=$((counter+1)) + mv $file $file.$next + fi + else + next=$((counter+1)) + [[ -f $file.$counter ]] && mv $file.$counter $file.$next + fi + ((counter==0)) && break + ((counter--)) + done + + mv $tmpfile $file +} + +# --norotate assumes you're passing in a unique LOGFILE. +# $2 then indicates the number of unique filenames prefixed up to the last +# dot extension that will be saved. The rest of those files will be deleted +# For example, common::logfileinit --norotate foo.log.234 100 +# common::logfileinit maintains up to 100 foo.log.* files. Anything else named +# foo.log.* > 100 are removed. +common::logfileinit () { + local nr=false + + if [[ "$1" == "--norotate" ]]; then + local nr=true + shift + fi + LOGFILE=${1:-$PWD/$PROG.log} + local num=$2 + + # Ensure LOG directory exists + mkdir -p $(dirname $LOGFILE 2>&-) + + # Initialize Logfile. + if ! $nr; then + common::rotatelog "$LOGFILE" ${num:-3} + fi + # Truncate the logfile. + > "$LOGFILE" + + echo "CMD: $PROG $ORIG_CMDLINE" >> "$LOGFILE" + + # with --norotate, remove the list of files that start with $PROG.log + if $nr; then + ls -1tr ${LOGFILE%.*}.* |head --lines=-$num |xargs rm -f + fi +} + +# An alternative that has a dependency on external program - pandoc +# store markdown man pages in companion files. Allow prog -man to still read +# those and display a man page using: +# pandoc -s -f markdown -t man prog.md |man -l - +common::manpage () { + [[ "$usage" == "yes" ]] && set -- -usage + [[ "$man" == "yes" ]] && set -- -man + [[ "$comments" == "yes" ]] && set -- -comments + + case $1 in + -*usage|"-?") + sed -n '/#+ SYNOPSIS/,/^#+ DESCRIPTION/p' $0 |sed '/^#+ DESCRIPTION/d' |\ + envsubst | sed -e 's,^#+ ,,g' -e 's,^#+$,,g' + exit 1 + ;; + -*man|-h|-*help) + grep "^#+" "$0" |\ + sed -e 's,^#+ ,,g' -e 's,^#+$,,g' |envsubst |${PAGER:-"less"} + exit 1 + ;; + esac +} + +############################################################################### +# General command-line parser converting -*arg="value" to $FLAGS_arg="value" +# Set -name/--name booleans to FLAGS_name=1 +# As a convenience, flags can contain dashes or underscores, but dashes are +# converted to underscores in the final FLAGS_name to conform to variable +# naming standards. +# Sets global array POSITIONAL_ARGV holding all non-dash command-line arguments +common::namevalue () { + local arg + local name + local value + local -A arg_aliases=([v]="verbose" [n]="dryrun") + + for arg in "$@"; do + case $arg in + -*[[:alnum:]]*) # Strip off any leading - or -- + arg=$(printf "%s\n" $arg |sed 's/^-\{1,2\}//') + # Handle global aliases + arg=${arg_aliases[$arg]:-"$arg"} + if [[ $arg =~ =(.*) ]]; then + name=${arg%%=*} + value=${arg#*=} + # change -'s to _ in name for legal vars in bash + eval export FLAGS_${name/-/_}=\""$value"\" + else + # bool=1 + # change -'s to _ in name for legal vars in bash + eval export FLAGS_${arg/-/_}=1 + fi + ;; + *) POSITIONAL_ARGV+=("$arg") + ;; + esac + done +} + +############################################################################### +# Print vars in simple or pretty format with text highlighting, columnized, +# logged. +# Prints the shell-quoted values of all of the given variables. +# Arrays and associative arrays are supported; all their elements will be +# printed. +# @optparam -p Pretty print the values +# @param space separated list of variables +common::printvars () { + local var + local var_str + local key + local tmp + local pprint=0 + local pprintvar + local pprintval + local -a quoted + + # Pretty/format print? + if [[ "$1" == "-p" ]]; then + pprint=1 + pprintvar=$2 + shift 2 + fi + + for var in "$@"; do + (($pprint)) && var_str=$var + + # if var is an array, do special tricks + # bash wizardry courtesy of + # http://stackoverflow.com/questions/4582137/bash-indirect-array-addressing + if [[ "$(declare -p $var 2>/dev/null)" =~ ^declare\ -[aA] ]]; then + tmp="$var[@]" + quoted=("${!tmp}") # copy the variable + for key in "${!quoted[@]}"; do + # shell-quote each element + quoted[$key]="$(printf %q "${quoted[$key]}")" + done + if (($pprint)); then + logecho -r "$(printf '%-32s%s\n' "${var_str}:" "${quoted[*]}")" + else + printf '%s=%s\n' "$var" "${quoted[*]}" + fi + else + if (($pprint)); then + pprintval=$(eval echo \$$pprintvar) + logecho -r \ + "$(printf '%-32s%s\n' "${var_str}:" "${!var/$pprintval\//\$$pprintvar/}")" + else + echo "$var=${!var}" + fi + fi + done +} + + +############################################################################### +# Simple argc validation with a usage return +# @param num - number of POSITIONAL_ARGV that should be on the command-line +# return 1 if any number other than num +common::argc_validate () { + local args=$1 + + # Validate number of args + if ((${#POSITIONAL_ARGV[@]}>args)); then + logecho + logecho "Exceeded maximum argument limit of $args!" + logecho + $PROG -? + logecho + common::exit 1 + fi +} + + +############################################################################### +# Get the md5 hash of a file +# @param file - The file +# @print the md5 hash +common::md5 () { + if which md5 >/dev/null 2>&1; then + md5 -q "$1" + else + md5sum "$1" | awk '{print $1}' + fi +} + +############################################################################### +# Get the sha1 hash of a file +# @param file - The file +# @print the sha1 hash +common::sha1 () { + if which shasum >/dev/null 2>&1; then + shasum -a1 "$1" + else + sha1sum "$1" + fi | awk '{print $1}' +} + +############################################################################### +# Check state of LOAS +# +# @param hour - hours remaining on prodcert +# @optparam user - Check a specific user +# Returns: +# 0 if LOAS is active and will still be active for more than N hours. +# 1 if LOAS is inactive or will expire in less than N hours. +common::loascheck () { + local hour=${1:-0} + local user=${2:-} + local becomeuser='' + local athostname='' + local isroleacct=0 + local tty_session=1 + + # Role account? + id |fgrep -q role-accts && isroleacct=1 + + # Interactive/tty session? + tty -s || tty_session=0 + + [[ -n "$user" ]] && becomeuser="become $user --" + + if ! $becomeuser \ + prodcertstatus --quiet --check_remaining_hours=$hour &>/dev/null; then + logecho + logecho "EXCEPTION: LOAS credentials for ${user:-$USER} will expire" \ + "in $hour hours. Run:" + + if [[ -n "$user" ]] || (($isroleacct)); then + # If run via cron, assume we need to instruct user where to run + # prodaccess + (($tty_session)) || athostname="@$HOSTNAME" + + logecho "$ become -t ${user:-$LOGNAME}$athostname -- prodaccess --sslenroll" + else + logecho "$ prodaccess" + fi + return 1 + else + # Issue a WARNING if we get close to a reasonable threshold + # The latest version of prodcertstatus spews useless text into stderr + # and we want to display the useful part of the stderr so explicitly + # strip it out + # We also have to do a dance with stderr/stdout + $becomeuser prodcertstatus --check_remaining_hours=4 2>&1 1>/dev/null |\ + egrep -v 'Reusing existing SSO cookie.' + return 0 + fi +} + +############################################################################### +# Check packages for a K8s release +# @param package - A space separated list of packages to verify exist +# +common::check_packages () { + local prereq + local -a missing=() + + # Make sure a bunch of packages are available + logecho -n "Checking required system packages: " + for prereq in $*; do + dpkg --get-selections 2>/dev/null | fgrep -qw $prereq || missing+=($prereq) + done + + if ((${#missing[@]}>0)); then + logecho -r "$FAILED" + logecho "PREREQ: Missing prerequisites: ${missing[@]}" \ + "Run the following and try again:" + logecho + for prereq in ${missing[@]}; do + if [[ $prereq == "sendgmr" ]]; then + logecho "sudo goobuntu-add-repo $prereq && sudo apt-get update" + fi + logecho "sudo apt-get install $prereq" + done + return 1 + fi + logecho -r "$OK" +} + + +############################################################################### +# Check disk space +# @param disk - a path +# @param threshold - int in GB +# +common::disk_space_check () { + local disk=$1 + local threshold=$2 + local avail=$(df -BG $disk |\ + sed -nr -e "s|^\S+\s+\S+\s+\S+\s+([0-9]+).*$|\1|p") + + logecho -n "Checking for at least $threshold GB on $disk: " + + if ((threshold>avail)); then + logecho -r "$FAILED" + logecho "AVAILABLE SPACE: $avail" + logecho "THRESHOLD: $threshold" + return 1 + else + logecho -r "$OK" + fi +} + +############################################################################### +# Run a function and display time metrics +# @param function - a function name to run and time +common::runstep () { + local function=$1 + local finishtime + local retcode + + common::timestamp begin $function &>/dev/null + + $* + retcode=$? + + finishtime=$(common::timestamp end $function | sed 's/.* //') + logecho "${TPUT[BOLD]}>>>>>>>> $PROG::$function() finished in" \ + "$finishtime${TPUT[OFF]}" + return $retcode +} + +############################################################################### +# Absolutify incoming path +# +# @param relative or absolute path +# @print absolute path +common::absolute_path () { + local arg=$1 + + [[ -z "$arg" ]] && return 0 + + [[ "$arg" =~ ^/ ]] || dir="$PWD/$arg" + logecho $arg +} + +############################################################################### +# Strip all control characters out of a text file +# Useful for stripping color codes and things from text files after runs +# @param file text file +common::strip_control_characters () { + local file=$1 + + sed -ri -e "s/\x1B[\[(]([0-9]{1,2}(;[0-9]{1,2})?)?[m|K|B]//g" \ + -e 's/\o015$//g' $file +} + +############################################################################### +# Print a number of characters (with no newline) +# @param char single character +# @param num number to print +common::print_n_char () { + local char=$1 + local num=$2 + local sep + + printf -v sep '%*s' $num + echo "${sep// /$char}" +} + +############################################################################### +# Set the global GSUTIL and GCLOUD binaries +# Returns: +# 0 if both GSUTIL and GCLOUD are set to executables +# 1 if both GSUTIL and GCLOUD are not set to executables +common::set_cloud_binaries () { + + logecho -n "Checking/setting cloud tools: " + + for GSUTIL in $(which gsutil) /opt/google/google-cloud-sdk/bin/gsutil; do + [[ -x $GSUTIL ]] && break + done + + # gcloud should be in the same place + GCLOUD=${GSUTIL/gsutil/gcloud} + + if [[ -x "$GSUTIL" && -x "$GCLOUD" ]]; then + logecho -r $OK + return 0 + else + logecho -r $FAILED + return 1 + fi +} + +# Set a common::trap() to capture ^C's and other unexpected exits and do the +# right thing in common::trapclean(). +common::trap common::trapclean ERR SIGINT SIGQUIT SIGTERM SIGHUP + +# parse cmdline +common::namevalue "$@" + +# Run common::manpage to show usage and man pages +common::manpage "$@" diff --git a/lib/gitlib.sh b/lib/gitlib.sh new file mode 100644 index 000000000000..e3dd75b8083b --- /dev/null +++ b/lib/gitlib.sh @@ -0,0 +1,169 @@ +#!/bin/bash +# +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +############################################################################### +# GIT-related constants and functions + +############################################################################### +# CONSTANTS +############################################################################### +GHCURL="curl -s --fail -u ${GITHUB_TOKEN:-$FLAGS_github_token}:x-oauth-basic" +JCURL="curl -g -s" +K8S_GITHUB_API='https://api.github.com/repos/kubernetes/kubernetes' +K8S_GITHUB_URL='https://github.com/kubernetes/kubernetes' +JENKINS_URL="http://kubekins.dls.corp.google.com/job" + +# Regular expressions for bash regex matching +# 0=entire branch name +# 1=Major +# 2=Minor +# 3=.Patch +# 4=Patch +BRANCH_REGEX="master|release-([0-9]{1,})\.([0-9]{1,})(\.([0-9]{1,}))*$" +# release - 1=Major, 2=Minor, 3=Patch, 4=-(alpha|beta), 5=rev +# dotzero - 1=Major, 2=Minor +# build - 1=build number, 2=sha1 +declare -A VER_REGEX=([release]="v([0-9]{1,})\.([0-9]{1,})\.([0-9]{1,})(-alpha|-beta)*\.*([0-9]{1,})*" + [dotzero]="v([0-9]{1,})\.([0-9]{1,})\.0$" + [build]="([0-9]{1,})\+([0-9a-f]{5,40})" + ) + +############################################################################### +# FUNCTIONS +############################################################################### + +############################################################################### +# Looks up the list of releases on github and puts the last release per branch +# into a global branch-indexed dictionary LAST_RELEASE[$branch] +# +# USEFUL: LAST_TAG=$(git describe --abbrev=0 --tags) +gitlib::last_releases () { + local release + local branch_name + local latest_branch + declare -Ag LAST_RELEASE + + logecho -n "Setting last releases by branch: " + for release in $($GHCURL $K8S_GITHUB_API/releases|\ + jq -r '.[] | select(.draft==false) | .tag_name'); do + if [[ $release =~ v([0-9]+\.[0-9]+)\.[0-9]+ ]]; then + branch_name=release-${BASH_REMATCH[1]} + # Keep the latest(first) branch + : ${latest_branch:=$branch_name} + # Does branch exist? If not, default tag to master branch + git rev-parse --verify origin/$branch_name &>/dev/null ||\ + branch_name=master + + LAST_RELEASE[$branch_name]=${LAST_RELEASE[$branch_name]:-$release} + fi + done + + # If ${LAST_RELEASE[master]} is unset, set it to the last release-* branch + : ${LAST_RELEASE[master]:=${LAST_RELEASE[$latest_branch]}} + + logecho -r "$OK" +} + +############################################################################### +# What branch am I on? +# prints current branch name +# returns 1 if current working directory is not git repository +gitlib::current_branch () { + if ! git rev-parse --abbrev-ref HEAD 2>/dev/null; then + ( + logecho + logecho "Not a git repository!" + logecho + ) >&2 + return 1 + fi +} + +############################################################################### +# Show the pending/open PRs on a branch +# @param branch +# returns 1 if current working directory is not git repository +gitlib::pending_prs () { + local branch=$1 + local pr + local login + local date + local msg + local sep + + if ((FLAGS_htmlize_md)); then + echo "PR | User | Date | Commit Message" + echo "-- | ---- | ---- | --------------" + sep="|" + fi + + while read pr login date msg; do + printf "%-8s $sep %-10s $sep %-18s $sep %s\n" \ + "#$pr" "@$login" "$(date +"%F %R" -d "$date")" "$msg" + done < <($GHCURL $K8S_GITHUB_API/pulls\?state\=open\&base\=$branch |\ + jq -r \ + '.[] | "\(.number)\t\(.user.login)\t\(.updated_at)\t\(.title)"') +} + +############################################################################### +# Validates github credentials using the standard $GITHUB_TOKEN in your env +# returns 0 if credentials are valid +# returns 1 if credentials are invalid +gitlib::check_credentials () { + logecho -n "Checking for valid github credentials: " + if ! $GHCURL $K8S_GITHUB_API >/dev/null 2>&1; then + logecho -r "$FAILED" + logecho + logecho "You must set a github token one of two ways:" + logecho "* Set GITHUB_TOKEN in your environment" + logecho "* Specify your --github-token= on the command line" + logecho + logecho "If you don't have a token yet, go get one at" \ + "https://github.com/settings/tokens" + common::exit 1 + fi + logecho -r "$OK" +} + +############################################################################## +# Git repo sync +# @param repo - full git url +# @param dest - destination directory +gitlib::sync_repo () { + local repo=$1 + local dest=$2 + + logecho -n "Syncing ${repo##*/} to $dest: " + if [[ -d $dest ]]; then + ( + cd $dest + logrun git checkout master + logrun -s git pull + ) || return 1 + else + logrun -s git clone $repo $dest || return 1 + fi +} + +############################################################################## +# Does git branch exist? +# @param branch - branch +gitlib::branch_exists () { + local branch=$1 + + git ls-remote --exit-code $K8S_GITHUB_URL \ + refs/heads/$branch &>/dev/null +} diff --git a/lib/releaselib.sh b/lib/releaselib.sh new file mode 100644 index 000000000000..399e298bea84 --- /dev/null +++ b/lib/releaselib.sh @@ -0,0 +1,718 @@ +#!/bin/bash +# +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# GLOBALS +JOB_CACHE_DIR=/tmp/buildresults-cache + +############################################################################## +# Builds a cache mapping Jenkins run numbers to build versions including +# build numbers and associated hashes. +# Used by release::set_build_version() +# +# @param job - Jenkins job name +# +release::update_job_cache () { + local job=$1 + local logroot="gs://kubernetes-jenkins/logs" + local cache_limit=50 + local buildlog + local j + local i + local run + local cnt + local last_value + local -a JOB + + mkdir -p $JOB_CACHE_DIR + + job_file=$JOB_CACHE_DIR/$job + + if ((FLAGS_verbose)); then + if [[ -f $job_file ]]; then + logecho -n "Updating" + source $job_file + else + logecho -n "Creating" + fi + logecho -r " $job cache..." + fi + + run=$($GSUTIL cat $logroot/$job/latest-build.txt 2>/dev/null) || return + + for ((cnt=0;cnt<=$cache_limit;cnt++)); do + # Once we hit one that is set, break out + # the cache should be complete after that + [[ -n ${JOB[$run]} ]] && break + + buildlog=$($GSUTIL cat $logroot/$job/$run/build-log.txt 2>/dev/null) + if [[ "$buildlog" =~ Uploading\ build\ result:\ SUCCESS ]] && + [[ "$buildlog" =~ \ + (build_version=|Found Kubernetes version: )(${VER_REGEX[release]}\.${VER_REGEX[build]}) ]]; then + JOB[$run]=${BASH_REMATCH[2]} + fi + ((run--)) + done + + # Write out cache file + # Reverse sort the array + for i in $(for n in ${!JOB[*]}; do echo $n; done|sort -rh); do + # and dedup + [[ ${JOB[$i]} != $last_value ]] && echo JOB[$i]=${JOB[$i]} + last_value=${JOB[$i]} + done > $job_file +} + +############################################################################## +# Sets the JENKINS_BUILD_VERSION global by cross checking it against a set of +# critical jenkins build jobs +# @param branch - branch name +# +# TODO: +# * Ability to point to a particular primary job hash and validate it +# - Useful if wanting to go back to last good from a few days ago +# * Make use of builder average time to find a better search algo +# - find longest running green and use reasonable offsets to search for +# dependents to reduce search time +# * e2e-gce: :20 +# * e2e-gce-scalability: :42 +# * e2e-gce-serial: 3:30 +# * e2e-gce-slow: 1:30 +# * kubemark-5-gce: 1:10 +# * e2e-gce-reboot: :47 +# * test-go: :45 +# * e2e-gke: :37 +# * e2e-gke-slow: :50 +release::set_build_version () { + local branch=$1 + local build_version + local build_number + local build_sha1 + local run + local giveup_build_number=999999 + local job_count=0 + local max_job_length + local other_job + local good_job + local branch_head + local branch_suffix + [[ $branch =~ release- ]] && branch_suffix="-$branch" + local main_job="kubernetes-e2e-gce$branch_suffix" + declare -a job_array good_jobs + + # Would be nice to pull/generate these jobs dynamically filtered through + # a pattern like gce,gke so new/changed testing jobs don't require + # updating here, but it turns out way too much hardcoding would need + # to be done to make this 'dynamic', so just list these explicitly. + #all_jobs=$($JCURL -s "http://kubekins.dls.corp.google.com/view/Critical%20Builds/api/json" |jq -r '.jobs[] | .name' | egrep -v '-soak-|-build') + local -a gce_jobs=("kubernetes-e2e-gce-serial$branch_suffix" + "kubernetes-e2e-gce-slow$branch_suffix" + "kubernetes-kubemark-5-gce$branch_suffix" + "kubernetes-e2e-gce-reboot$branch_suffix" + "kubernetes-e2e-gce-scalability$branch_suffix" + "kubernetes-test-go$branch_suffix" + ) + + # kubernetes-e2e-gke-subnet - Uses a branch version? + # kubernetes-e2e-gke-test - Uses a branch version? + #"kubernetes-e2e-gke-serial" + local -a gke_jobs=( + "kubernetes-e2e-gke$branch_suffix" + "kubernetes-e2e-gke-slow$branch_suffix" + ) + + # Combined list for cross-checking against $main_job + local -a secondary_jobs=(${gce_jobs[@]} ${gke_jobs[@]}) + + # Update cache + for other_job in $main_job ${secondary_jobs[@]}; do + release::update_job_cache $other_job + done + + if ((FLAGS_verbose)); then + # Get the longest line for formatting + max_job_length=$(echo ${secondary_jobs[*]} |\ + awk '{for (i=1;i<=NF;++i) {l=length($i);if(l>x) x=l}}END{print x}') + # Pad it a bit + ((max_job_length+2)) + + logecho + logecho "(*) Primary job (-) Secondary jobs" + logecho + logecho " $(printf '%-'$max_job_length's' "Jenkins Job")" \ + "Run # Build # Time/Status" + logecho "= $(common::print_n_char = $max_job_length)" \ + "====== ======= ===========" + fi + + while read good_job; do + if [[ $good_job =~ \ + JOB\[([0-9]+)\]=(${VER_REGEX[release]})\.${VER_REGEX[build]} ]]; then + main_run=${BASH_REMATCH[1]} + build_number=${BASH_REMATCH[8]} + build_sha1=${BASH_REMATCH[9]} + build_version=${BASH_REMATCH[2]}.$build_number+$build_sha1 + build_sha1_date=$($GHCURL $K8S_GITHUB_API/commits?sha=$build_sha1 |\ + jq -r '.[0] | .commit .author .date') + build_sha1_date=$(date +"%R %m/%d" -d "$build_sha1_date") + else + logecho "Bad build version!" + return 1 + fi + + # For release branch, the HEAD revision has to be the one we use because + # we make code changes on the HEAD of the branch (version.go). + # Verify if discovered build_version's SHA1 hash == HEAD if branch + if [[ "$branch" =~ release- ]]; then + branch_head=$($GHCURL $K8S_GITHUB_API/commits/$branch |jq -r '.sha') + + if [[ $build_sha1 != ${branch_head:0:14} ]]; then + # TODO: Figure out how to curl a list of last N commits + # So we can return a message about how far ahead the top of the + # release branch is from the last good commit. + #commit_count=$(git rev-list $build_sha1..${branch_head:0:14} |wc -l) + commit_count=some + logecho + logecho "$ATTENTION: The $branch branch HEAD is ahead of the last" \ + "good Jenkins run by $commit_count commits." \ + "Wait for Jenkins to catch up." + return 1 + fi + fi + + # Deal with far-behind secondary builds and just skip forward + ((build_number>giveup_build_number)) && continue + + ((FLAGS_verbose)) && \ + logecho "* $(printf \ + '%-'$max_job_length's %-7s %-7s' \ + $main_job \#$main_run \#$build_number) [$build_sha1_date]" + + # Check secondaries to ensure that build number is green across "all" + for other_job in ${secondary_jobs[@]}; do + ((FLAGS_verbose)) \ + && logecho -n "- $(printf '%-'$max_job_length's ' $other_job)" + + # Need to kick out when a secondary doesn't exist (anymore) + if [[ ! -f $JOB_CACHE_DIR/$other_job ]]; then + ((FLAGS_verbose)) \ + && logecho -r "Does not exist SKIPPING" + ((job_count++)) || true + continue + elif [[ $(wc -l <$JOB_CACHE_DIR/$other_job) -lt 1 ]]; then + ((FLAGS_verbose)) \ + && logecho -r "No Good Runs SKIPPING" + ((job_count++)) || true + continue + fi + + run=$(awk -F[][] '/\.'$build_number'\+'$build_sha1'$/ {print $2}' \ + $JOB_CACHE_DIR/$other_job) + + if [[ -n $run ]]; then + ((FLAGS_verbose)) && \ + logecho "$(printf '%-7s %-7s' \#$run \#$build_number) $PASSED" + ((job_count++)) || true + continue + else + ((FLAGS_verbose)) && \ + logecho "$(printf '%-7s %-7s' -- --)" \ + "${TPUT[RED]}GIVE UP${TPUT[OFF]}" + giveup_build_number=$build_number + job_count=0 + break + fi + done + + ((FLAGS_verbose)) && logecho + ((job_count>=${#secondary_jobs[@]})) && break + done < $JOB_CACHE_DIR/$main_job + + if ((job_count==0)); then + logecho "Unable to find a green set of test results!" + return 1 + else + JENKINS_BUILD_VERSION=$build_version + fi + + ((FLAGS_verbose)) && logecho JENKINS_BUILD_VERSION=$JENKINS_BUILD_VERSION + + return 0 +} + + +############################################################################## +# Sets global dictionary RELEASE_VERSION based on passed in build version and +# release branch +# @param version - Jenkins build version +# @param branch - branch to check +# @param parent_branch - the parent of a new branch (if new) +release::set_release_version () { + local version=$1 + local branch=$2 + local parent_branch=$3 + local label + declare -A release_branch build_version + declare -Ag RELEASE_VERSION + + if ! [[ $branch =~ $BRANCH_REGEX ]]; then + logecho "Invalid branch format! $branch" + return 1 + fi + + release_branch[major]=${BASH_REMATCH[1]} + release_branch[minor]=${BASH_REMATCH[2]} + + # if branch == master, version is an alpha + # if branch == release, version is a beta + # if branch == release+1, version is an alpha + if ! [[ $version =~ ${VER_REGEX[release]} ]]; then + logecho "Invalid version format! $version" + return 1 + fi + + # Split incoming version up into components + build_version[major]=${BASH_REMATCH[1]} + build_version[minor]=${BASH_REMATCH[2]} + build_version[patch]=${BASH_REMATCH[3]} + build_version[label]=${BASH_REMATCH[4]} + build_version[labelid]=${BASH_REMATCH[5]} + + # RELEASE_VERSION_PRIME is the default release version for this session/type + # Other labels such as alpha and beta are set as needed + # Index ordering is important here as it's how they are processed + if [[ "$parent_branch" == master ]]; then + # This is a new branch, set new alpha and beta versions + RELEASE_VERSION[alpha]="v${release_branch[major]}" + RELEASE_VERSION[alpha]+=".$((${release_branch[minor]}+1)).0-alpha.0" + RELEASE_VERSION[beta]="v${release_branch[major]}.${release_branch[minor]}" + RELEASE_VERSION[beta]+=".0-beta.0" + RELEASE_VERSION_PRIME=${RELEASE_VERSION[beta]} + elif [[ "$parent_branch" =~ release- ]]; then + # When we do branched branches we end up with two betas so deal with it + # by creating a couple of beta indexes. + RELEASE_VERSION[beta0]="v${build_version[major]}.${build_version[minor]}" + RELEASE_VERSION[beta0]+=".${build_version[patch]}${build_version[label]}" + # Need to increment the labelid since the original branch will have its + # predecessors. + RELEASE_VERSION[beta0]+=".$((${build_version[labelid]}+1))" + RELEASE_VERSION[beta1]="v${build_version[major]}.${build_version[minor]}" + RELEASE_VERSION[beta1]+=".$((${build_version[patch]}+1))-beta.0" + RELEASE_VERSION_PRIME="${RELEASE_VERSION[beta0]}" + elif [[ $branch =~ release- ]]; then + # Build out the RELEASE_VERSION dict + RELEASE_VERSION_PRIME="v${build_version[major]}.${build_version[minor]}" + RELEASE_VERSION_PRIME+=".${build_version[patch]}" + if ((FLAGS_official)); then + RELEASE_VERSION[official]="$RELEASE_VERSION_PRIME" + RELEASE_VERSION[beta]="v${build_version[major]}.${build_version[minor]}" + RELEASE_VERSION[beta]+=".$((${build_version[patch]}+1))-beta.0" + else + RELEASE_VERSION[beta]="$RELEASE_VERSION_PRIME${build_version[label]}" + RELEASE_VERSION[beta]+=".$((${build_version[labelid]}+1))" + RELEASE_VERSION_PRIME="${RELEASE_VERSION[beta]}" + fi + else + RELEASE_VERSION[alpha]="v${build_version[major]}.${build_version[minor]}" + RELEASE_VERSION[alpha]+=".${build_version[patch]}${build_version[label]}" + RELEASE_VERSION[alpha]+=".$((${build_version[labelid]}+1))" + RELEASE_VERSION_PRIME="${RELEASE_VERSION[alpha]}" + fi + + if ((FLAGS_verbose)); then + for label in ${!RELEASE_VERSION[*]}; do + logecho "RELEASE_VERSION[$label]=${RELEASE_VERSION[$label]}" + done + logecho "RELEASE_VERSION_PRIME=$RELEASE_VERSION_PRIME" + fi + + return 0 +} + +############################################################################### +# Create a unique bucket name for releasing Kube and make sure it exists. +# @param bucket - The gs release bucket name +# @return 1 if bucket can't be made +release::gcs::ensure_release_bucket() { + local bucket=$1 + + if ! $GSUTIL ls "gs://$bucket" >/dev/null 2>&1 ; then + logecho -n "Creating Google Cloud Storage bucket $bucket: " + logrun -s $GSUTIL mb -p "$GCLOUD_PROJECT" "gs://$bucket" || return 1 + fi +} + +############################################################################### +# Create a unique bucket name for releasing Kube and make sure it exists. +# TODO: There is a version of this in kubernetes/build/common.sh. Refactor. +# @param gcs_stage - the staging directory +# @param source and destination arguments +# @return 1 if tar fails +release::gcs::stage_and_hash() { + local gcs_stage=$1 + shift + + # Split the args into srcs... and dst + local args=("$@") + local split=$((${#args[@]}-1)) # Split point for src/dst args + local srcs=("${args[@]::$split}" ) + local dst="${args[$split]}" + + for src in ${srcs[@]}; do + srcdir=$(dirname $src) + srcthing=$(basename $src) + logrun mkdir -p $gcs_stage/$dst || return 1 + tar c -C $srcdir $srcthing | tar x -C $gcs_stage/$dst || return 1 + done +} + +############################################################################### +# Copy the release artifacts to staging and push them up to GS +# TODO: There is a version of this in kubernetes/build/common.sh that is also +# used by the build/push-*-build.sh scripts. Refactor. +# @param version - release version +# @param build_output - build output directory +# @param bucket - GS bucket +# @return 1 on failure +release::gcs::copy_release_artifacts() { + local version=$1 + local build_output=$2 + local bucket=$3 + local platform + local platforms + local release_stage=$build_output/release-stage + local release_tars=$build_output/release-tars + local gcs_stage=$build_output/gcs-stage + local src + local dst + local gcs_destination="gs://$bucket/release/$version/" + + logrun rm -rf $gcs_stage || return 1 + logrun mkdir -p $gcs_stage || return 1 + + logecho "Publish release artifacts to gs://$bucket..." + + # Stage everything in release directory + logecho "- Staging locally to ${gcs_stage##*/}..." + release::gcs::stage_and_hash $gcs_stage "$release_tars"/* . || return 1 + + # Having the configure-vm.sh script and and trusty code from the GCE cluster + # deploy hosted with the release is useful for GKE. + release::gcs::stage_and_hash $gcs_stage \ + "$release_stage/full/kubernetes/cluster/gce/configure-vm.sh" extra/gce \ + || return 1 + release::gcs::stage_and_hash $gcs_stage \ + "$release_stage/full/kubernetes/cluster/gce/gci/node.yaml" extra/gce \ + || return 1 + release::gcs::stage_and_hash $gcs_stage \ + "$release_stage/full/kubernetes/cluster/gce/gci/master.yaml" extra/gce \ + || return 1 + release::gcs::stage_and_hash $gcs_stage \ + "$release_stage/full/kubernetes/cluster/gce/gci/configure.sh" extra/gce \ + || return 1 + + # Upload the "naked" binaries to GCS. This is useful for install scripts that + # download the binaries directly and don't need tars. + platforms=($(cd "$release_stage/client"; echo *)) + for platform in "${platforms[@]}"; do + src="$release_stage/client/$platform/kubernetes/client/bin/*" + dst="bin/${platform/-//}/" + # We assume here the "server package" is a superset of the "client package" + if [[ -d "$release_stage/server/$platform" ]]; then + src="$release_stage/server/$platform/kubernetes/server/bin/*" + fi + release::gcs::stage_and_hash $gcs_stage "$src" "$dst" || return 1 + done + + logecho "- Hashing files in ${gcs_stage##*/}..." + find $gcs_stage -type f | while read path; do + common::md5 $path > "$path.md5" || return 1 + common::sha1 $path > "$path.sha1" || return 1 + done + + logecho -n "- Copying release artifacts to $gcs_destination: " + logrun -s $GSUTIL -qm cp -r $gcs_stage/* $gcs_destination || return 1 + + # TODO(jbeda): Generate an HTML page with links for this release so it is easy + # to see it. For extra credit, generate a dynamic page that builds up the + # release list using the GCS JSON API. Use Angular and Bootstrap for extra + # extra credit. + + logecho -n "- Marking all uploaded objects public: " + logrun -s $LOGRUN_MOCK $GSUTIL -q -m acl ch -R -g all:R \ + "$gcs_destination" || return 1 + + logecho -n "- Listing final contents to log file: " + logrun -s $GSUTIL ls -lhr "$gcs_destination" || return 1 +} + + +############################################################################### +# Publish a new official version, (latest or stable,) but only if the release +# files actually exist on GCS and the release we're dealing with is newer than +# the contents in GCS. +# @param version - release version +# @param build_output - build output directory +# @param bucket - GS bucket +# @return 1 on failure +release::gcs::publish_official () { + local version=$1 + local build_output=$2 + local bucket=$3 + local release_dir="gs://$bucket/release/$version" + local version_major + local version_minor + local publish_file + local publish_files + local type="latest" + [[ "$version" =~ alpha|beta ]] || type="stable" + + logecho + logecho "Publish official pointer text files to $bucket..." + + if ! $GSUTIL ls $release_dir >/dev/null 2>&1 ; then + logecho "Release files don't exist at $release_dir" + return 1 + fi + + if [[ $version =~ ${VER_REGEX[release]} ]]; then + version_major=${BASH_REMATCH[1]} + version_minor=${BASH_REMATCH[2]} + fi + + publish_files=($type + $type-$version_major + $type-$version_major.$version_minor + ) + + for publish_file in ${publish_files[@]}; do + # If there's a version that's above the one we're trying to release, don't + # do anything, and just try the next one. + release::gcs::verify_release_gt release/$publish_file.txt \ + $bucket $version || continue + release::gcs::publish release/$publish_file.txt $build_output \ + $bucket $version || return 1 + done +} + + +############################################################################### +# Check if the new version is greater than the version currently published on +# GCS. +# @param publish_file - the GCS location to look in +# @param bucket - GS bucket +# @param version - release version +# @return 1 if new version is not greater than the GCS version +# +release::gcs::verify_release_gt() { + local -r publish_file=$1 + local -r bucket=$2 + local -r version=$3 + local -r publish_file_dst="gs://$bucket/$publish_file" + local gcs_version + local greater=true + + logecho -n "Test $version > $publish_file (published): " + if ! [[ $version =~ ${VER_REGEX[release]} ]]; then + logecho -r "$FAILED" + logecho "* Invalid version format! $version" + return 1 + fi + + local -r version_major="${BASH_REMATCH[1]}" + local -r version_minor="${BASH_REMATCH[2]}" + local -r version_patch="${BASH_REMATCH[3]}" + local -r version_prerelease="${BASH_REMATCH[4]}" + local -r version_prerelease_rev="${BASH_REMATCH[5]}" + + if gcs_version="$($GSUTIL cat $publish_file_dst 2>/dev/null)"; then + if ! [[ $gcs_version =~ ${VER_REGEX[release]} ]]; then + logecho -r "$FAILED" + logecho "* file contains invalid release version," \ + "can't compare: '$gcs_version'" + return 1 + fi + + local -r gcs_version_major="${BASH_REMATCH[1]}" + local -r gcs_version_minor="${BASH_REMATCH[2]}" + local -r gcs_version_patch="${BASH_REMATCH[3]}" + local -r gcs_version_prerelease="${BASH_REMATCH[4]}" + local -r gcs_version_prerelease_rev="${BASH_REMATCH[5]}" + + if [[ "$version_major" -lt "$gcs_version_major" ]]; then + greater=false + elif [[ "$version_major" -gt "$gcs_version_major" ]]; then + : # fall out + elif [[ "$version_minor" -lt "$gcs_version_minor" ]]; then + greater=false + elif [[ "$version_minor" -gt "$gcs_version_minor" ]]; then + : # fall out + elif [[ "$version_patch" -lt "$gcs_version_patch" ]]; then + greater=false + elif [[ "$version_patch" -gt "$gcs_version_patch" ]]; then + : # fall out + # Use lexicographic (instead of integer) comparison because + # version_prerelease is a string, ("alpha" or "beta",) but first check if + # either is an official release (i.e. empty prerelease string). + # + # We have to do this because lexicographically "beta" > "alpha" > "", but + # we want official > beta > alpha. + elif [[ -n "$version_prerelease" && -z "$gcs_version_prerelease" ]]; then + greater=false + elif [[ -z "$version_prerelease" && -n "$gcs_version_prerelease" ]]; then + : # fall out + elif [[ "$version_prerelease" < "$gcs_version_prerelease" ]]; then + greater=false + elif [[ "$version_prerelease" > "$gcs_version_prerelease" ]]; then + : # fall out + # Finally resort to -le here, since we want strictly-greater-than. + elif [[ "$version_prerelease_rev" -le "$gcs_version_prerelease_rev" ]]; then + greater=false + fi + + if $greater; then + logecho -r "$OK" + logecho "* $version > $gcs_version (published), updating" + else + logecho "$WARNING" + logecho "* $version <= $gcs_version (published) - not updating." + return 1 + fi + else + # gsutil cat failed; file does not exist + logecho -r "$OK" + logecho "* $publish_file_dst does not exist yet. It will be created..." + return 0 + fi +} + + +############################################################################### +# Publish a release to GCS: upload a version file, if KUBE_GCS_MAKE_PUBLIC, +# make it public, and verify the result. +# TODO: There is a version of this in kubernetes/build/common.sh that is also +# used by kube::release::gcs::publish_ci(). Possible refactor. +# @param publish_file - the GCS location to look in +# @param build_output - build output directory +# @param bucket - GS bucket +# @param version - release version +# @return 1 on failure +release::gcs::publish() { + local publish_file=$1 + local build_output=$2 + local bucket=$3 + local version=$4 + local release_stage=$build_output/release-stage + local publish_file_dst="gs://$bucket/$publish_file" + local contents + local public_link="https://storage.googleapis.com/$bucket/$publish_file" + + logrun mkdir -p "$release_stage/upload" || return 1 + echo "$version" > "$release_stage/upload/latest" || return 1 + + logrun $GSUTIL -m cp "$release_stage/upload/latest" \ + "$publish_file_dst" || return 1 + + if ((FLAGS_nomock)); then + logecho -n "Making uploaded version file public and non-cacheable: " + logrun -s $GSUTIL acl ch -R -g all:R $publish_file_dst || return 1 + $GSUTIL setmeta -h "Cache-Control:private, max-age=0" \ + "$publish_file_dst" >/dev/null 2>&1 || return 1 + + # If public, validate public link + logecho -n "* Validating uploaded version file at $public_link: " + contents="$(curl -s $public_link)" + else + # If not public, validate using gsutil + logecho -n "* Validating uploaded version file at $publish_file_dst: " + contents="$($GSUTIL cat $publish_file_dst)" + fi + + if [[ "$contents" == "$version" ]]; then + logecho "$OK" + else + logecho "$FAILED (file contents: $contents)" + return 1 + fi +} + +############################################################################### +# Releases all docker images to a docker registry. +# +# @param registry - docker registry +# @param version - version tag +# @return 1 on failure +release::docker::release () { + local registry=$1 + local version=$2 + local docker_push_cmd=(docker) + local docker_target + local legacy_docker_target + local arch + local binary + local binaries=( + "kube-apiserver" + "kube-controller-manager" + "kube-scheduler" + "kube-proxy" + "hyperkube" + ) + + [[ "$registry" =~ gcr.io/ ]] && docker_push_cmd=("$GCLOUD" "docker") + + # Activate credentials for the k8s.production.user@gmail.com + [[ "$registry" == "gcr.io/google_containers" ]] \ + && logrun $GCLOUD config set account k8s.production.user@gmail.com + + logecho + logecho "Send docker containers to $registry..." + + # 'gcloud docker' gives lots of internal_failure's so add retries to + # all of the invocations + for arch in "${KUBE_SERVER_PLATFORMS[@]##*/}"; do + for binary in "${binaries[@]}"; do + docker_target="$binary-$arch:$version" + if ! logrun -r 5 ${docker_push_cmd[@]} \ + history "$registry/$docker_target"; then + logecho "$WARNING - Skipping non-existent $docker_target..." + continue + fi + + logecho "Release $docker_target:" + logecho -n "- Pushing: " + logrun -r 5 -s ${docker_push_cmd[@]} push "$registry/$docker_target" + + # If we have a amd64 docker image. Tag it without -amd64 also + # and push it for compatibility with earlier versions + if [[ $arch == "amd64" ]]; then + legacy_docker_target="$binary:$version" + logecho "Release legacy $legacy_docker_target:" + + logecho -n "- Tagging: " + logrun -r 5 -s docker tag -f "$registry/$docker_target" \ + "$registry/$legacy_docker_target" 2>/dev/null + + logecho -n "- Pushing: " + logrun -r 5 -s ${docker_push_cmd[@]} \ + push "$registry/$legacy_docker_target" + fi + done + done + + # Activate default account + if [[ "$registry" == "gcr.io/google_containers" ]]; then + logrun $GCLOUD config set account $USER@google.com + fi +} diff --git a/mailer b/mailer new file mode 100755 index 000000000000..f9dc6215cc63 --- /dev/null +++ b/mailer @@ -0,0 +1,97 @@ +#!/bin/bash +# +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Set PROGram name +PROG=${0##*/} +######################################################################## +#+ +#+ NAME +#+ $PROG - General (send)mail interface +#+ +#+ SYNOPSIS +#+ $PROG --to= --subject="..." --file= +#+ [--html] [--cc=] [--from=address] +#+ $PROG [--helpshort|--usage|-?] +#+ $PROG [--help|-man] +#+ +#+ DESCRIPTION +#+ Google has deprecated sendmail for internal use -- go/nosmtp. +#+ $PROG provides an common interface for sending email using a sendmail +#+ replacement (for use within Google). +#+ +#+ $PROG by default sends plain text. Use --html to send html. +#+ +#+ OPTIONS +#+ --to="address,address,..." - space separated list of people to mail +#+ --subject="..." - mail subject +#+ --file= - file to mail +#+ [--html] - Send html content +#+ [--cc="address,address,..."] - space separated list of people to cc +#+ [--from]="address"] - file to mail +#+ [--help | -man] - display man page for this script +#+ [--helpshort] - display gbash-usage +#+ [--usage | -?] - display in-line usage +#+ +#+ EXAMPLES +#+ $PROG --to="$USER" --subject="This PASSED!" --file=/tmp/mailfile.23561 +#+ - Mail the contents of /tmp/mailfile.23561 +#+ +#+ FILES +#+ +#+ SEE ALSO +#+ common.sh - function definitions for timestamp, etc. +#+ +#+ BUGS/TODO +#+ +######################################################################## +# If NO ARGUMENTS should return *usage*, uncomment the following line: +usage=${1:-yes} + +source $(dirname $(readlink -ne $BASH_SOURCE))/lib/common.sh + +if [[ $(hostname) =~ \.google\. ]]; then + # Quietly check for sendgmr + common::check_packages sendgmr || common::exit 1 "Exiting..." + + # sendgmr doesn't do much checking so hold its hand and talk nicely to it + [[ -n "$FLAGS_cc" ]] && CC_ARG="-cc=$FLAGS_cc" + + if ((FLAGS_html)); then + FILE_ARGS="--html_file=$FLAGS_file --body_file=/dev/null" + else + FILE_ARGS="--html_file=/dev/null --body_file=$FLAGS_file" + fi + + # Send it! + sendgmr \ + -to="$FLAGS_to" \ + -from="$FLAGS_from" \ + -subject="$FLAGS_subject" \ + $CC_ARG \ + $FILE_ARGS +else + # Nice and simple + ( + cat <] [--branch=] +#+ [--markdown-file=] [--html-file=] +#+ [--release-bucket=] [--preview] +#+ $PROG [--helpshort|--usage|-?] +#+ $PROG [--help|-man] +#+ +#+ DESCRIPTION +#+ $PROG scans 'git log' for 'merge pull's and collects and displays +#+ release notes based on release-note-* labels from PR titles and PR +#+ release-note blocks (in the body). +#+ +#+ By default, $PROG produces release notes for the last github release +#+ to the HEAD of current branch and uses a standard git range otherwise. +#+ You may omit the end of the range to terminate the range at the HEAD +#+ of the current branch. +#+ +#+ The default output is pure markdown and unless [--markdown-file=] is +#+ specified, the output file is in /tmp/$PROG-release-notes.md. +#+ If [--html-file=] is set, $PROG will also produce a pure html version +#+ of the notes at that location. +#+ +#+ If [--quiet] is not specified, the output to stdout will always be +#+ only the markdown version of the output. +#+ +#+ [--branch=] is used to specify a branch other than the current one. +#+ +#+ Other options detailed in EXAMPLES below. +#+ +#+ OPTIONS +#+ --quiet - Don't display the notes when done +#+ --htmlize-md - Output markdown with html for PRs and +#+ contributors (for use in CHANGELOG.md) +#+ --full - Force 'full' release format to show all +#+ sections of release notes. (This is the +#+ *default* for new branch X.Y.0 notes) +#+ --tarball= - tarball to md5/sha1 sum for display +#+ --markdown-file= - Specify an alt file to use to store notes +#+ --html-file= - Produce a html version of the notes +#+ --release-bucket= - Specify gs bucket to point to in +#+ generated notes (informational only) +#+ --preview - Report additional branch statistics (used for +#+ reporting outside of releases) +#+ --github-token= - Must be specified if GITHUB_TOKEN not set +#+ --branch= - Specify a branch other than the current one +#+ [--help | -man] - Display man page for this script +#+ [--usage | -?] - Display in-line usage +#+ +#+ EXAMPLES +#+ $PROG - Notes for last release to HEAD +#+ on current branch +#+ $PROG v1.1.4.. - Notes for v1.1.4 to HEAD on current branch +#+ $PROG v1.1.4..v1.1.7 - Notes for v1.1.4..v1.1.7 +#+ $PROG v1.1.7 - Notes for last release +#+ on current branch to v1.1.7 +#+ +#+ FILES +#+ /tmp/$PROG-release-htmls.md +#+ /tmp/$PROG-release-htmls.html +#+ +#+ SEE ALSO +#+ common.sh - Base function definitions +#+ gitlib.sh - git-related function definitions +#+ https://stedolan.github.io/jq - JSON CLI +#+ +#+ BUGS/TODO +#+ +######################################################################## +# If NO ARGUMENTS should return *usage*, uncomment the following line: +#usage=${1-yes} + +source $(dirname $(readlink -ne $BASH_SOURCE))/lib/common.sh +source $TOOL_LIB_PATH/gitlib.sh + +# Validate command-line +common::argc_validate 1 +[[ -n $FLAGS_tarball && ! -f $FLAGS_tarball ]] \ + && common::exit 1 "--tarball=$FLAGS_tarball doesn't exist! Exiting..." + +############################################################################### +# FUNCTIONS +############################################################################### +############################################################################### +# Get titles from a list of PRs +# @param prs - A space separated list of PRs to extract +# +extract_pr_title () { + local prs="$*" + local pr + local content + local body + local author + local pull_json + + for pr in $prs; do + pull_json="$($GHCURL $K8S_GITHUB_API/pulls/$pr)" + body="$(echo "$pull_json" |jq -r '.body' |tr -d '\r')" + + # Look for a body release note first and default to title + content=$(echo "$body" |\ + sed -n '/```release-note/,/^```/{/^```/!p;/^```$/q}') + + # if the release-note block is empty or the template is unchanged, use title + if [[ -z "$content" ]] || ! [[ "$content" =~ -OR- ]]; then + content=$(echo "$pull_json" | jq -r '.title') + fi + + author=$(echo "$pull_json" | jq -r '.user.login') + + logecho -r "* $content (#$pr, @$author)" + done +} + + +############################################################################### +# Create the release note markdown body +# @param file - A file (tarball) to link to on google storage +# @param start_tag - The start tag of range +# @param release_tag - The release tag of range +# +create_body () { + local file=$1 + local start_tag=$2 + local release_tag=$3 + local release_bucket=${FLAGS_release_bucket:-"kubernetes-release"} + local title + + ((FLAGS_preview)) && title="Branch " + # Show a more useful header if release_tag == HEAD + if [[ "$release_tag" == "HEAD" ]]; then + title+=$CURRENT_BRANCH + else + title+=$release_tag + fi + + ((FLAGS_preview)) && echo "**Release Note Preview - generated on $(date)**" + echo + echo "# $title" + echo "[Documentation](http://kubernetes.github.io) &" \ + "[Examples](http://releases.k8s.io/$CURRENT_BRANCH/examples)" + echo + if [[ -f "$file" ]]; then + echo "## Downloads" + echo + echo "binary | sha1 hash | md5 hash" + echo "------ | --------- | --------" + echo "[${file##*/}](https://storage.googleapis.com/$release_bucket/release/$release_tag/${file##*/}) | \`$(common::sha1 $file)\` | \`$(common::md5 $file)\`" + echo + fi + echo "## Changes since $start_tag" + echo + cat $PR_NOTES + echo +} + +############################################################################### +# Jenkins status +# Uses global CURRENT_BRANCH +jenkins_status () { + local content + local red=${TPUT[RED]} + local green=${TPUT[GREEN]} + local off=${TPUT[OFF]} + local official + + if ((FLAGS_htmlize_md)); then + red="" + green="" + off="" + fi + + # If working on a release branch assume --official for the + # purpose of displaying find_green_build output + [[ $CURRENT_BRANCH =~ release- ]] && official="--official" + + # State of tree + echo + echo "## State of $CURRENT_BRANCH branch" + if content=$(find_green_build -v $official $CURRENT_BRANCH); then + echo "${green}GOOD TO GO!$off" + else + echo "${red}NOT READY$off" + fi + echo + echo "### Details" + echo '```' + echo "$content" + echo '```' +} + +############################################################################### +# Scan PRs for release-note-* labels and generate markdown for the actual +# release notes section of the report +# Uses global LAST_RELEASE CURRENT_BRANCH +generate_notes () { + local branch_head + local range + local start_tag + local release_tag + local pretty_range + local labels + local body + local counter=0 + local tempcss=/tmp/$PROG-ca.$$ + local -a notes_normal + local -a notes_action + local -a notes_experimental + local -a prs + + branch_head=$(git rev-parse refs/remotes/origin/$CURRENT_BRANCH) + + # Default + range="${POSITIONAL_ARGV[0]:-"${LAST_RELEASE[$CURRENT_BRANCH]}..$branch_head"}" + + if [[ "${POSITIONAL_ARGV[0]}" =~ ([v0-9.]*-*(alpha|beta)*\.*[0-9]*)\.\.([v0-9.]*-*(alpha|beta)*\.*[0-9]*)$ ]]; then + start_tag=${BASH_REMATCH[1]} + release_tag=${BASH_REMATCH[3]} + else + start_tag="${LAST_RELEASE[$CURRENT_BRANCH]}" + release_tag=${POSITIONAL_ARGV[0]} + fi + + if [[ -z "$start_tag" ]]; then + common::exit 1 "Unable to set beginning of range automatically." \ + "Specify on the command-line. Exiting..." + fi + + range="$start_tag..${release_tag:-$branch_head}" + + # If range is unterminated, finish it with $branch_head + [[ $range =~ \.\.$ ]] && range+=$branch_head + + # Validate range + if ! git rev-parse $range &>/dev/null; then + logecho + logecho "Invalid tags/range $range !" + return 1 + fi + + # For pretty printing + pretty_range=${range/$branch_head/HEAD} + + # Deref all the PRs back to master, paying special attention to + # automated cherrypicks that could have multiple sources + while read line; do + if [[ "$line" =~ automated-cherry-pick-of-(#[0-9]+-){1,} ]]; then + prs+=($(echo "${BASH_REMATCH[0]}" | egrep -o "#[0-9]*" |tr -d '#')) + else + prs+=($(echo "$line" | awk '{gsub(/#/,"");print $4 }')) + fi + done < <(git log $range --format="%s" --grep="Merge pull") + + logecho + for pr in ${prs[*]}; do + ((counter++)) + # Reset line + printf "\r%-80s" " " + echo -ne "\rScanning PRs between $pretty_range on the" \ + "$CURRENT_BRANCH branch ($counter/${#prs[*]})" + labels="$($GHCURL $K8S_GITHUB_API/issues/$pr/labels |\ + jq -r '.[] | (.name| tostring)')" + + if [[ "$labels" =~ $'\n'release-note-breaking-change$'\n' || + "$labels" =~ $'\n'release-note-action-required$'\n' ]]; then + notes_action+=("$pr") + elif [[ "$labels" =~ $'\n'release-note-experimental$'\n' ]]; then + notes_experimental+=("$pr") + elif [[ "$labels" =~ $'\n'release-note$'\n' ]]; then + notes_normal+=("$pr") + fi + done + logecho + + logecho "Generating release notes..." + # Bootstrap notes for major (new branch) releases + if ((FLAGS_full)) || [[ $release_tag =~ ${VER_REGEX[dotzero]} ]]; then + cat <> $PR_NOTES +### Major Themes +* Add to or delete this section + +### Other notable improvements +* Add to or delete this section + +EOF+ + fi + + if [[ -n "${notes_experimental[*]}" ]]; then + echo "### Experimental Features" >> $PR_NOTES + extract_pr_title "${notes_experimental[*]}" >> $PR_NOTES + echo >> $PR_NOTES + fi + + if [[ -n "${notes_action[*]}" ]]; then + echo "### Action Required" >> $PR_NOTES + extract_pr_title "${notes_action[*]}" >> $PR_NOTES + echo >> $PR_NOTES + fi + + if ((FLAGS_full)) || [[ $release_tag =~ ${VER_REGEX[dotzero]} ]]; then + cat <> $PR_NOTES +### Known Issues +* Add to or delete this section + +### Provider-specific Notes +* Add to or delete this section + +EOF+ + fi + + if [[ -n "${notes_normal[*]}" ]]; then + echo "### Other notable changes" >> $PR_NOTES + extract_pr_title "${notes_normal[*]}" >> $PR_NOTES + fi + + if [[ -z "${notes_normal[*]}" && -z "${notes_action[*]}" ]]; then + logecho + logecho "**No notable changes for this release**" >> $PR_NOTES + logecho + fi + + logecho "Preparing layout..." + create_body ${FLAGS_tarball:-""} $start_tag ${release_tag:-HEAD} \ + > $RELEASE_NOTES_MD + + if ((FLAGS_preview)); then + # Pending PRS + logecho "Adding pending PR status..." + ( + echo "-------" + echo "## PENDING PRs on the $CURRENT_BRANCH branch" + gitlib::pending_prs $CURRENT_BRANCH + ) >> $RELEASE_NOTES_MD + fi + + if ((FLAGS_htmlize_md)); then + sed -i -e "s,#\([0-9]\{5\,\}\),[#\1]($K8S_GITHUB_URL/pull/\1),g" \ + -e "s,@\([a-zA-Z0-9-]*\),[@\1](https://github.com/\1),g" \ + $RELEASE_NOTES_MD + fi + + if ((FLAGS_preview)); then + # We do this after htmlizing because we don't want to update the + # issues in the block of this section + logecho "Adding jenkins build status (this may take a while)..." + jenkins_status >> $RELEASE_NOTES_MD + fi + + if [[ -n "$RELEASE_NOTES_HTML" ]]; then + echo "" > $tempcss + + pandoc -H $tempcss --from markdown_github --to html \ + $RELEASE_NOTES_MD > $RELEASE_NOTES_HTML + + # Remove temp file + logrun rm -f $tempcss + fi +} + +############################################################################## +# CONSTANTS +############################################################################## +CURRENT_BRANCH=${FLAGS_branch:-$(gitlib::current_branch)} \ + || common::exit 1 + +PR_NOTES=/tmp/$PROG-$CURRENT_BRANCH-prnotes +# Initialize new PR_NOTES for session +>$PR_NOTES +RELEASE_NOTES_MD=$(common::absolute_path \ + ${FLAGS_markdown_file:-/tmp/$PROG-$CURRENT_BRANCH.md}) +RELEASE_NOTES_HTML=$(common::absolute_path $FLAGS_html_file) +ANNOUNCEMENT_TEXT=/tmp/$PROG-announcement + +############################################################################### +# MAIN +############################################################################### +# Initialize and save up to 10 (rotated logs) +MYLOG=/tmp/$PROG.log +common::logfileinit $MYLOG 10 + +# BEGIN script +common::timestamp begin + +# Check credentials +gitlib::check_credentials + +# Check for packages +common::check_packages jq pandoc + +# Build LAST_RELEASE dictionary +gitlib::last_releases + +generate_notes || common::exit 1 + +logecho +if ((FLAGS_quiet)); then + logecho -n "Notes written to $RELEASE_NOTES_MD" + if [[ -f $RELEASE_NOTES_HTML ]]; then + logecho " and $RELEASE_NOTES_HTML" + else + logecho + fi +else + logecho -r "$HR" + cat $RELEASE_NOTES_MD + logecho -r "$HR" +fi + +common::timestamp end diff --git a/script-template b/script-template new file mode 100755 index 000000000000..321521aa39b9 --- /dev/null +++ b/script-template @@ -0,0 +1,168 @@ +#!/bin/bash +# +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Set PROGram name +PROG=${0##*/} +######################################################################## +#+ NAME +#+ $PROG - Create a kubernetes release script template +#+ +#+ SYNOPSIS +#+ $PROG +#+ $PROG [--help | -man] +#+ $PROG [--usage | -?] +#+ +#+ DESCRIPTION +#+ $PROG produces a general template for use within the kubernetes/release +#+ script/tool ecosystem with a *nix-style header and some useful comments +#+ to get you started. +#+ +#+ OPTIONS +#+ [--help | -man] - display man page for this script +#+ [--usage | -?] - display usage information +#+ +#+ EXAMPLES +#+ +#+ FILES +#+ +#+ SEE ALSO +#+ +#+ BUGS +#+ +#- +######################################################################## +source $(dirname $(readlink -ne $0))/lib/common.sh + +cat < [--optionalarg=[value1|value2]] +#+ \$PROG [--helpshort|--usage|-?] +#+ \$PROG [--help|-man] +#+ +#+ DESCRIPTION +#+ Detailed description of script and options +#+ +#+ OPTIONS +#+ --requiredarg= - Detail of --requiredarg +#+ [--optionalarg=] - Detail of option2 and arguments +#+ [--help | -man] - display man page for this script +#+ [--usage | -?] - display in-line usage +#+ +#+ EXAMPLES +#+ \$PROG --requiredarg=value - How script works with --requiredarg=value +#+ \$PROG --optionalarg=value - How script works with --optionalarg=value +#+ +#+ FILES +#+ Applicable files +#+ Related files +#+ +#+ SEE ALSO +#+ common.sh - base function definitions +#+ Other related scripts +#+ +#+ BUGS/TODO +#+ Known problems with script +#+ +######################################################################## +# If NO ARGUMENTS should return *usage*, uncomment the following line: +#usage=\${1:-yes} + +source \$(dirname \$(readlink -ne \$BASH_SOURCE))/lib/common.sh + +# Process Command-line arguments +# POSITIONAL_ARGV is provided by common::namevalue after arg preprocessing +# * --name=value becomes FLAGS_name=value +# * --name becomes FLAGS_name=1 (boolean) + +# Optionally validate number of POSITIONAL_ARGV +#common::argc_validate 2 + +############################################################################### +# FUNCTIONS +############################################################################### +# OPTIONAL: Overwrite common.sh's common::cleanexit +#common::cleanexit () { +#rm -rf \$TMPDIR \$TMPFILE +#tput cnorm +# +## Do stuff here to clean up after this specific script +# +#common::timestamp end +#exit \${1:-0} +#} + + +############################################################################### +# MAIN +############################################################################### + +############################################################################## +# Initialize logs +############################################################################## +# Initialize and save up to 10 (rotated logs) +#MYLOG=/tmp/\$PROG.log +#common::logfileinit \$MYLOG 10 +# BEGIN script +common::timestamp begin + +############################################################################## +# OTHER HELPFUL FUNCTIONS (More in common.sh) +############################################################################## +# logecho - echo to stdout and MYLOG if set +# logrun - run a cmd to stdout and MYLOG if set +# common::askyorn - Ask a simple yes or no question (see common.sh for details) +# common::stepheader - Bolded, logged bullet points for your output +# common::exit - Exit cleanly +# common::check_packages - Check for package prereqs +# common::disk_space_check - Check disk space + + +############################################################################## +common::stepheader MAJOR STEP 1 +############################################################################## + + +############################################################################## +common::stepheader MAJOR STEP 2 +############################################################################## + + +# END script +common::timestamp end +EOF_CAT