Skip to content

Commit

Permalink
Updated readme
Browse files Browse the repository at this point in the history
  • Loading branch information
marccampbell committed Aug 5, 2019
1 parent b20aaf7 commit 3d10347
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 52 deletions.
19 changes: 18 additions & 1 deletion README.md
@@ -1 +1,18 @@
# Outdated: A kubectl Plugin
# `kubectl outdated`

A `kubectl` plugin to show out-of-date images running in a cluster.

## Quick Start

```
kubectl krew install outdated
kubectl outdated
```

The plugin will scan for all pods in all namespaces that you have at least read access to. It will then connect to the registry that hosts the image, and (if there's permission), it will analyze your tag to the list of current tags.

The output is a list of all images, with the most out-of-date images in red, slightly outdated in yellow, and up-to-date in green.

### Example

[![kuebct; ourdated example](https://asciinema.org/a/ExaFOk6ap0GL17GJsJWpExGnM.svg)](https://asciinema.org/a/ExaFOk6ap0GL17GJsJWpExGnM)
35 changes: 32 additions & 3 deletions cmd/outdated/cli/root.go
Expand Up @@ -5,11 +5,13 @@ import (
"os"
"path"
"strings"
"time"

"github.com/replicatedhq/outdated/pkg/logger"
"github.com/replicatedhq/outdated/pkg/outdated"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tj/go-spin"
)

func RootCmd() *cobra.Command {
Expand All @@ -29,16 +31,40 @@ func RootCmd() *cobra.Command {

o := outdated.Outdated{}

log.Info("Finding images in cluster")
images, err := o.ListImages(v.GetString("kubeconfig"))
s := spin.New()
finishedCh := make(chan bool, 1)
foundImageName := make(chan string, 1)
go func() {
lastImageName := ""
for {
select {
case <-finishedCh:
fmt.Printf("\r")
return
case i := <-foundImageName:
lastImageName = i
case <-time.After(time.Millisecond * 100):
if lastImageName == "" {
fmt.Printf("\r \033[36mSearching for images\033[m %s", s.Next())
} else {
fmt.Printf("\r \033[36mSearching for images\033[m %s (%s)", s.Next(), lastImageName)
}
}
}
}()
defer func() {
finishedCh <- true
}()

images, err := o.ListImages(v.GetString("kubeconfig"), foundImageName)
if err != nil {
log.Error(err)
log.Info("")
os.Exit(1)
return nil
}
finishedCh <- true

log.Info("")
head, imageColumnWidth, tagColumnWidth := headerLine(images)
log.Header(head)

Expand All @@ -58,6 +84,9 @@ func RootCmd() *cobra.Command {
log.FinalizeImageLineWithError(erroredImage(image, checkResult, imageColumnWidth, tagColumnWidth))
}
}

log.Info("")

return nil
},
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -4,7 +4,7 @@ go 1.12

require (
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/semver v1.4.2
github.com/Masterminds/sprig v2.20.0+incompatible // indirect
github.com/andrewchambers/go-jqpipe v0.0.0-20180509223707-2d54cef8cd94 // indirect
github.com/blang/semver v3.5.1+incompatible
Expand Down
1 change: 1 addition & 0 deletions go.sum
Expand Up @@ -480,6 +480,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
Expand Down
7 changes: 6 additions & 1 deletion pkg/outdated/list.go
@@ -1,6 +1,7 @@
package outdated

import (
"fmt"
"strings"

"github.com/pkg/errors"
Expand All @@ -18,7 +19,7 @@ type RunningImage struct {
PullableImage string
}

func (o Outdated) ListImages(kubeconfigPath string) ([]RunningImage, error) {
func (o Outdated) ListImages(kubeconfigPath string, imageNameCh chan string) ([]RunningImage, error) {
config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
return nil, errors.Wrap(err, "failed to read kubeconfig")
Expand All @@ -36,6 +37,8 @@ func (o Outdated) ListImages(kubeconfigPath string) ([]RunningImage, error) {

runningImages := []RunningImage{}
for _, namespace := range namespaces.Items {
imageNameCh <- fmt.Sprintf("%s/", namespace.Name)

pods, err := clientset.CoreV1().Pods(namespace.Name).List(metav1.ListOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to list pods")
Expand All @@ -55,6 +58,7 @@ func (o Outdated) ListImages(kubeconfigPath string) ([]RunningImage, error) {
PullableImage: pullable,
}

imageNameCh <- fmt.Sprintf("%s/%s", namespace.Name, runningImage.Image)
runningImages = append(runningImages, runningImage)
}

Expand All @@ -71,6 +75,7 @@ func (o Outdated) ListImages(kubeconfigPath string) ([]RunningImage, error) {
PullableImage: pullable,
}

imageNameCh <- fmt.Sprintf("%s/%s", namespace.Name, runningImage.Image)
runningImages = append(runningImages, runningImage)
}
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/outdated/outdated.go
Expand Up @@ -49,6 +49,7 @@ func (o Outdated) ParseImage(image string, pullableImage string) (*CheckResult,
return o.parseNonSemverImage(reg, imageName, tag, nonSemverTags)
}

// From here on, we can assume that we are on a semver tag
semverTags = append(semverTags, detectedSemver)
collection := SemverTagCollection(semverTags)

Expand All @@ -57,6 +58,7 @@ func (o Outdated) ParseImage(image string, pullableImage string) (*CheckResult,
return nil, errors.Wrap(err, "failed to calculate versions behind")
}
trueVersionsBehind := SemverTagCollection(versionsBehind).RemoveLeastSpecific()

behind := len(trueVersionsBehind) - 1

checkResult := CheckResult{
Expand Down
3 changes: 2 additions & 1 deletion pkg/outdated/registry.go
Expand Up @@ -17,7 +17,8 @@ func initRegistryClient(hostname string) (*registry.Registry, error) {
}

reg, err := registry.New(auth, registry.Opt{
Timeout: time.Duration(time.Second * 5),
SkipPing: true,
Timeout: time.Duration(time.Second * 5),
})
if err != nil {
return nil, errors.Wrap(err, "failed to create registry client")
Expand Down
49 changes: 17 additions & 32 deletions pkg/outdated/version.go
Expand Up @@ -34,34 +34,9 @@ func (c SemverTagCollection) Less(i, j int) bool {
}

func compareVersions(verI *semver.Version, verJ *semver.Version) int {
splitI := strings.Split(verI.Original(), ".")
splitJ := strings.Split(verJ.Original(), ".")

iSegments := verI.Segments()
jSegments := verJ.Segments()

for idx := range splitI {
if idx <= len(splitJ)-1 {
splitIPartInt := iSegments[idx]
splitJPartInt := jSegments[idx]

if splitIPartInt != splitJPartInt {
if splitIPartInt > splitJPartInt {
return 1
}
if splitIPartInt < splitJPartInt {
return -1
}
}
} else {
return -1
}
}

if len(splitI) > len(splitJ) {
if verI.LessThan(verJ) {
return -1
}
if len(splitI) < len(splitJ) {
} else if verI.GreaterThan(verJ) {
return 1
}

Expand All @@ -75,15 +50,21 @@ func (c SemverTagCollection) Swap(i, j int) {
func (c SemverTagCollection) VersionsBehind(currentVersion *semver.Version) ([]*semver.Version, error) {
cleaned, err := c.Unique()
if err != nil {
return []*semver.Version{}, errors.Wrap(err, "deduplicate versions")
return []*semver.Version{}, errors.Wrap(err, "failed to deduplicate versions")
}

for idx := range cleaned {
if compareVersions(cleaned[idx], currentVersion) == 0 {
return cleaned[idx:], nil
sortable := SemverTagCollection(cleaned)
sort.Sort(sortable)

for idx := range sortable {
if sortable[idx].Original() == currentVersion.Original() {
return sortable[idx:], nil
}
}
return []*semver.Version{}, errors.New("no matching version found")

return []*semver.Version{
currentVersion,
}, nil // /shrug
}

// Unique will create a new sorted slice with the same versions that have different tags removed.
Expand Down Expand Up @@ -135,6 +116,10 @@ func (c SemverTagCollection) Unique() ([]*semver.Version, error) {

// RemoveLeastSpecific given a sorted collection will remove the least specific version
func (c SemverTagCollection) RemoveLeastSpecific() []*semver.Version {
if c.Len() == 0 {
return []*semver.Version{}
}

cleanedVersions := []*semver.Version{c[0]}
for i := 0; i < len(c)-1; i++ {
j := i + 1
Expand Down
59 changes: 46 additions & 13 deletions pkg/outdated/version_test.go
Expand Up @@ -179,7 +179,7 @@ func TestCompareVersions(t *testing.T) {
{
name: "minor versions is less than",
versions: []string{"10.1", "10"},
expect: -1,
expect: 1,
},
{
name: "minor versions is greater than",
Expand Down Expand Up @@ -209,7 +209,7 @@ func TestCompareVersions(t *testing.T) {
{
name: "major version only with patch",
versions: []string{"10", "10.1.2"},
expect: 1,
expect: -1,
},
{
name: "minor version greater, patch version less",
Expand All @@ -226,6 +226,11 @@ func TestCompareVersions(t *testing.T) {
versions: []string{"9.1", "10.2.2"},
expect: -1,
},
{
name: "minor with patches",
versions: []string{"v3.0.4-beta.0", "v3.0.4-alpha.1"},
expect: 1,
},
}

for _, test := range tests {
Expand Down Expand Up @@ -307,6 +312,11 @@ func TestRemoveLeastSpecific(t *testing.T) {
versions: []string{"3.5.1.1", "3.5.1", "4.5.1"},
expectVersions: []string{"3.5.1.1", "4.5.1"},
},
{
name: "opa style",
versions: []string{"v3.0.4-beta.0", "v3.0.4-beta.1"},
expectVersions: []string{"v3.0.4-beta.0", "v3.0.4-beta.1"},
},
}

for _, test := range tests {
Expand All @@ -322,19 +332,42 @@ func TestRemoveLeastSpecific(t *testing.T) {
}

func TestResolveTagDates(t *testing.T) {
hostname := "index.docker.io"
imageName := "library/postgres"
versions := []string{"10.0", "10.1", "10.2"}
allVersions := makeVersions(versions)
tests := []struct {
name string
hostname string
imageName string
versions []string
}{
{
name: "postgres",
hostname: "index.docker.io",
imageName: "library/postgres",
versions: []string{"10.0", "10.1", "10.2"},
},
{
name: "tiller",
hostname: "gcr.io",
imageName: "kubernetes-helm/tiller",
versions: []string{"v2.14.1"},
},
}

reg, err := initRegistryClient(hostname)
require.NoError(t, err)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)

allVersions := makeVersions(test.versions)

versionTags, err := resolveTagDates(reg, imageName, allVersions)
require.NoError(t, err)
reg, err := initRegistryClient(test.hostname)
req.NoError(err)

for _, versionTag := range versionTags {
_, err = time.Parse(time.RFC3339, versionTag.Date)
require.NoError(t, err)
versionTags, err := resolveTagDates(reg, test.imageName, allVersions)
req.NoError(err)

for _, versionTag := range versionTags {
_, err = time.Parse(time.RFC3339, versionTag.Date)
req.NoError(err)
}
})
}
}

0 comments on commit 3d10347

Please sign in to comment.