mirror of https://github.com/helm/helm
Part of #4329 Signed-off-by: Matt Farina <matt@mattfarina.com>pull/4353/head
parent
5ce95c6ea3
commit
d71515ef84
@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm maintainers
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package notify // import "k8s.io/helm/pkg/helm/notify"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver"
|
||||||
|
"k8s.io/helm/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// The time format used in the file recording the last check
|
||||||
|
timeLayout = time.RFC1123Z
|
||||||
|
)
|
||||||
|
|
||||||
|
// Release contains information for a release
|
||||||
|
type Release struct {
|
||||||
|
|
||||||
|
// Version is the version for the release (e.g., `v2.11.0`)
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// Checksums is a map of the file checksums for a release. For example,
|
||||||
|
// a key might be "darwin-amd64" and the value is the checksum for the release
|
||||||
|
// in that environment
|
||||||
|
Checksums map[string]string `json:"checksums,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Releases is an array of Release
|
||||||
|
type Releases []Release
|
||||||
|
|
||||||
|
// IfTime checks if there is a newer version of Helm if the wait period is over
|
||||||
|
// Arguments are:
|
||||||
|
// - lastUpdateTime: The last time an update was checked
|
||||||
|
// - waitTime: The period to wait before checking again
|
||||||
|
// - checkURL: A URL with a JSON file containing the updates
|
||||||
|
// TODO: Change checkURL to the internal object
|
||||||
|
// The return data includes:
|
||||||
|
// - bool: true if an was checked for and false otherwise
|
||||||
|
// - string: the latest version if there is a newer version and an empty string otherwise
|
||||||
|
func IfTime(lastUpdateTime time.Time, waitTime time.Duration, checkURL string) (bool, string, error) {
|
||||||
|
// Check if current time has waited long enough
|
||||||
|
curr := time.Since(lastUpdateTime)
|
||||||
|
if curr <= waitTime {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for an update
|
||||||
|
releases, err := getJSON(checkURL)
|
||||||
|
if err != nil {
|
||||||
|
return true, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is a new release
|
||||||
|
curSemVer, err := semver.NewVersion(version.GetVersion())
|
||||||
|
if err != nil {
|
||||||
|
return true, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var vs []*semver.Version
|
||||||
|
|
||||||
|
for _, release := range releases {
|
||||||
|
tmpVer, err := semver.NewVersion(release.Version)
|
||||||
|
if err == nil {
|
||||||
|
vs = append(vs, tmpVer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(semver.Collection(vs))
|
||||||
|
|
||||||
|
if len(vs) > 0 && vs[len(vs)-1].GreaterThan(curSemVer) {
|
||||||
|
return true, vs[len(vs)-1].String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return new release if one found
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IfTimeFromFile will notify of a new version available if:
|
||||||
|
// - The current time is greater than waitTime + the last time it was updated
|
||||||
|
// - There is a newer version available
|
||||||
|
// This is a helper function wrapping IfTime, reading from the filesystem,
|
||||||
|
// and pretty printing if there is a new version.
|
||||||
|
// The arguments are:
|
||||||
|
// - lastUpdatePath: The path to the local filesystem location containing the
|
||||||
|
// last time an update was checked for
|
||||||
|
// - waitTime: The time in seconds to wait from the last check before checking
|
||||||
|
// again.
|
||||||
|
// - checkURL: A URL to a JSON file with version information to check for updates
|
||||||
|
func IfTimeFromFile(lastUpdatePath string, waitTime int64, checkURL string) (string, error) {
|
||||||
|
// Read contents of the time file
|
||||||
|
content, err := ioutil.ReadFile(lastUpdatePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// The file does not exist so we will assume this is a first run and create
|
||||||
|
// the file with a time of now so it will tell about an update in the future
|
||||||
|
errStr := err.Error()
|
||||||
|
if strings.Contains(errStr, "no such file or directory") {
|
||||||
|
curTime := time.Now().Format(timeLayout)
|
||||||
|
ioutil.WriteFile(lastUpdatePath, []byte(curTime), 0644)
|
||||||
|
content = []byte(curTime)
|
||||||
|
} else {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the string time to a usable Time instance
|
||||||
|
lastChecked, err := time.Parse(timeLayout, strings.TrimSpace(string(content)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the wait period to a Duration instance
|
||||||
|
waitPeriod := time.Duration(waitTime) * time.Second
|
||||||
|
|
||||||
|
// Call NotifyIfTime
|
||||||
|
checked, newVersion, err := IfTime(lastChecked, waitPeriod, checkURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the check time if a check happened
|
||||||
|
if checked {
|
||||||
|
curTime := time.Now().Format(timeLayout)
|
||||||
|
ioutil.WriteFile(lastUpdatePath, []byte(curTime), 0644)
|
||||||
|
|
||||||
|
// If there is a new version and a check happened print the message
|
||||||
|
return newVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJSON(href string) (Releases, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
// Construct the request
|
||||||
|
req, err := http.NewRequest("GET", href, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Helm/"+strings.TrimPrefix(version.GetVersion(), "v"))
|
||||||
|
|
||||||
|
// Perform the request and handle errors
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("Failed to fetch update information at %s : %s", href, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the contents of the JSON file
|
||||||
|
target := Releases{}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return target, nil
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm maintainers
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver"
|
||||||
|
"k8s.io/helm/pkg/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type URLHandler struct {
|
||||||
|
releases Releases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *URLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b, err := json.Marshal(h.releases)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
fmt.Fprintf(w, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkingIfTime(t *testing.T) {
|
||||||
|
|
||||||
|
curSemVer, err := semver.NewVersion(version.GetVersion())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("internal version (%s) could not be parsed: %s", version.GetVersion(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextVer := curSemVer.IncMinor()
|
||||||
|
nextVerString := "v" + nextVer.String()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
checked bool
|
||||||
|
version string
|
||||||
|
newVersion string
|
||||||
|
duration int64
|
||||||
|
}{
|
||||||
|
{true, nextVerString, nextVer.String(), 10},
|
||||||
|
{true, "v" + curSemVer.String(), "", 10},
|
||||||
|
{false, nextVerString, "", 10000},
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpTime := time.Now()
|
||||||
|
tmpTime = tmpTime.Add(-100 * time.Second)
|
||||||
|
|
||||||
|
for i, tc := range tests {
|
||||||
|
handler := &URLHandler{
|
||||||
|
releases: []Release{{Version: tc.version}},
|
||||||
|
}
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
|
||||||
|
checked, newVer, err := IfTime(tmpTime, time.Duration(tc.duration)*time.Second, server.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error checking IfTime: %s", err)
|
||||||
|
}
|
||||||
|
if checked != tc.checked {
|
||||||
|
t.Errorf("new version check of %t but got %t for test case %d", tc.checked, checked, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.newVersion != newVer {
|
||||||
|
t.Errorf("expected a new version of %qbut got %q for test case %d", tc.newVersion, newVer, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkingIfTimeFromFile(t *testing.T) {
|
||||||
|
|
||||||
|
curSemVer, err := semver.NewVersion(version.GetVersion())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("internal version (%s) could not be parsed: %s", version.GetVersion(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextVer := curSemVer.IncMinor()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
version *semver.Version
|
||||||
|
newVersion string
|
||||||
|
duration int64
|
||||||
|
}{
|
||||||
|
{&nextVer, nextVer.String(), 10},
|
||||||
|
{curSemVer, "", 10},
|
||||||
|
{&nextVer, "", 10000},
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir, err := ioutil.TempDir("", "helm-update_check-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
lastUpdatePath := filepath.Join(tempDir, "last_update_check")
|
||||||
|
|
||||||
|
for i, tc := range tests {
|
||||||
|
|
||||||
|
tmpTime := time.Now()
|
||||||
|
tmpTime = tmpTime.Add(-100 * time.Second)
|
||||||
|
ioutil.WriteFile(lastUpdatePath, []byte(tmpTime.Format(timeLayout)), 0644)
|
||||||
|
|
||||||
|
handler := &URLHandler{
|
||||||
|
releases: []Release{{Version: "v" + tc.version.String()}},
|
||||||
|
}
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
|
||||||
|
newVer, err := IfTimeFromFile(lastUpdatePath, tc.duration, server.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error checking IfTime: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.newVersion != newVer {
|
||||||
|
t.Errorf("expected a new version of %qbut got %q for test case %d", tc.newVersion, newVer, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLHandlerNone struct{}
|
||||||
|
|
||||||
|
func (h *URLHandlerNone) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadURLIfTime(t *testing.T) {
|
||||||
|
handler := &URLHandlerNone{}
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
|
||||||
|
tmpTime := time.Now()
|
||||||
|
tmpTime = tmpTime.Add(-100 * time.Second)
|
||||||
|
_, _, err := IfTime(tmpTime, time.Duration(10)*time.Second, server.URL)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error with handler but did not get one")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadURLIfTimeFromFile(t *testing.T) {
|
||||||
|
handler := &URLHandlerNone{}
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
|
||||||
|
tmpTime := time.Now()
|
||||||
|
tmpTime = tmpTime.Add(-100 * time.Second)
|
||||||
|
tempDir, err := ioutil.TempDir("", "helm-update_check-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
lastUpdatePath := filepath.Join(tempDir, "last_update_check")
|
||||||
|
ioutil.WriteFile(lastUpdatePath, []byte(tmpTime.Format(timeLayout)), 0644)
|
||||||
|
|
||||||
|
_, err = IfTimeFromFile(lastUpdatePath, 10, server.URL)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error with handler but did not get one")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFileIfTimeFromFile(t *testing.T) {
|
||||||
|
handler := &URLHandler{
|
||||||
|
releases: []Release{{Version: "v1.2.3"}},
|
||||||
|
}
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
|
||||||
|
tempDir, err := ioutil.TempDir("", "helm-update_check-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
lastUpdatePath := filepath.Join(tempDir, "last_update_check")
|
||||||
|
|
||||||
|
newVer, err := IfTimeFromFile(lastUpdatePath, 10, server.URL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newVer != "" {
|
||||||
|
t.Errorf("expected an empty string version but got %s", newVer)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(lastUpdatePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
_, err = time.Parse(timeLayout, string(content))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue