Skip to content

Commit 3d10347

Browse files
committed
Updated readme
1 parent b20aaf7 commit 3d10347

File tree

9 files changed

+125
-52
lines changed

9 files changed

+125
-52
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1-
# Outdated: A kubectl Plugin
1+
# `kubectl outdated`
2+
3+
A `kubectl` plugin to show out-of-date images running in a cluster.
4+
5+
## Quick Start
6+
7+
```
8+
kubectl krew install outdated
9+
kubectl outdated
10+
```
11+
12+
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.
13+
14+
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.
15+
16+
### Example
17+
18+
[![kuebct; ourdated example](https://asciinema.org/a/ExaFOk6ap0GL17GJsJWpExGnM.svg)](https://asciinema.org/a/ExaFOk6ap0GL17GJsJWpExGnM)

cmd/outdated/cli/root.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"os"
66
"path"
77
"strings"
8+
"time"
89

910
"github.com/replicatedhq/outdated/pkg/logger"
1011
"github.com/replicatedhq/outdated/pkg/outdated"
1112
"github.com/spf13/cobra"
1213
"github.com/spf13/viper"
14+
"github.com/tj/go-spin"
1315
)
1416

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

3032
o := outdated.Outdated{}
3133

32-
log.Info("Finding images in cluster")
33-
images, err := o.ListImages(v.GetString("kubeconfig"))
34+
s := spin.New()
35+
finishedCh := make(chan bool, 1)
36+
foundImageName := make(chan string, 1)
37+
go func() {
38+
lastImageName := ""
39+
for {
40+
select {
41+
case <-finishedCh:
42+
fmt.Printf("\r")
43+
return
44+
case i := <-foundImageName:
45+
lastImageName = i
46+
case <-time.After(time.Millisecond * 100):
47+
if lastImageName == "" {
48+
fmt.Printf("\r \033[36mSearching for images\033[m %s", s.Next())
49+
} else {
50+
fmt.Printf("\r \033[36mSearching for images\033[m %s (%s)", s.Next(), lastImageName)
51+
}
52+
}
53+
}
54+
}()
55+
defer func() {
56+
finishedCh <- true
57+
}()
58+
59+
images, err := o.ListImages(v.GetString("kubeconfig"), foundImageName)
3460
if err != nil {
3561
log.Error(err)
3662
log.Info("")
3763
os.Exit(1)
3864
return nil
3965
}
66+
finishedCh <- true
4067

41-
log.Info("")
4268
head, imageColumnWidth, tagColumnWidth := headerLine(images)
4369
log.Header(head)
4470

@@ -58,6 +84,9 @@ func RootCmd() *cobra.Command {
5884
log.FinalizeImageLineWithError(erroredImage(image, checkResult, imageColumnWidth, tagColumnWidth))
5985
}
6086
}
87+
88+
log.Info("")
89+
6190
return nil
6291
},
6392
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.12
44

55
require (
66
github.com/Masterminds/goutils v1.1.0 // indirect
7-
github.com/Masterminds/semver v1.4.2 // indirect
7+
github.com/Masterminds/semver v1.4.2
88
github.com/Masterminds/sprig v2.20.0+incompatible // indirect
99
github.com/andrewchambers/go-jqpipe v0.0.0-20180509223707-2d54cef8cd94 // indirect
1010
github.com/blang/semver v3.5.1+incompatible

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
480480
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
481481
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
482482
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
483+
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
483484
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
484485
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
485486
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=

pkg/outdated/list.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package outdated
22

33
import (
4+
"fmt"
45
"strings"
56

67
"github.com/pkg/errors"
@@ -18,7 +19,7 @@ type RunningImage struct {
1819
PullableImage string
1920
}
2021

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

3738
runningImages := []RunningImage{}
3839
for _, namespace := range namespaces.Items {
40+
imageNameCh <- fmt.Sprintf("%s/", namespace.Name)
41+
3942
pods, err := clientset.CoreV1().Pods(namespace.Name).List(metav1.ListOptions{})
4043
if err != nil {
4144
return nil, errors.Wrap(err, "failed to list pods")
@@ -55,6 +58,7 @@ func (o Outdated) ListImages(kubeconfigPath string) ([]RunningImage, error) {
5558
PullableImage: pullable,
5659
}
5760

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

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

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

pkg/outdated/outdated.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func (o Outdated) ParseImage(image string, pullableImage string) (*CheckResult,
4949
return o.parseNonSemverImage(reg, imageName, tag, nonSemverTags)
5050
}
5151

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

@@ -57,6 +58,7 @@ func (o Outdated) ParseImage(image string, pullableImage string) (*CheckResult,
5758
return nil, errors.Wrap(err, "failed to calculate versions behind")
5859
}
5960
trueVersionsBehind := SemverTagCollection(versionsBehind).RemoveLeastSpecific()
61+
6062
behind := len(trueVersionsBehind) - 1
6163

6264
checkResult := CheckResult{

pkg/outdated/registry.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ func initRegistryClient(hostname string) (*registry.Registry, error) {
1717
}
1818

1919
reg, err := registry.New(auth, registry.Opt{
20-
Timeout: time.Duration(time.Second * 5),
20+
SkipPing: true,
21+
Timeout: time.Duration(time.Second * 5),
2122
})
2223
if err != nil {
2324
return nil, errors.Wrap(err, "failed to create registry client")

pkg/outdated/version.go

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,34 +34,9 @@ func (c SemverTagCollection) Less(i, j int) bool {
3434
}
3535

3636
func compareVersions(verI *semver.Version, verJ *semver.Version) int {
37-
splitI := strings.Split(verI.Original(), ".")
38-
splitJ := strings.Split(verJ.Original(), ".")
39-
40-
iSegments := verI.Segments()
41-
jSegments := verJ.Segments()
42-
43-
for idx := range splitI {
44-
if idx <= len(splitJ)-1 {
45-
splitIPartInt := iSegments[idx]
46-
splitJPartInt := jSegments[idx]
47-
48-
if splitIPartInt != splitJPartInt {
49-
if splitIPartInt > splitJPartInt {
50-
return 1
51-
}
52-
if splitIPartInt < splitJPartInt {
53-
return -1
54-
}
55-
}
56-
} else {
57-
return -1
58-
}
59-
}
60-
61-
if len(splitI) > len(splitJ) {
37+
if verI.LessThan(verJ) {
6238
return -1
63-
}
64-
if len(splitI) < len(splitJ) {
39+
} else if verI.GreaterThan(verJ) {
6540
return 1
6641
}
6742

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

81-
for idx := range cleaned {
82-
if compareVersions(cleaned[idx], currentVersion) == 0 {
83-
return cleaned[idx:], nil
56+
sortable := SemverTagCollection(cleaned)
57+
sort.Sort(sortable)
58+
59+
for idx := range sortable {
60+
if sortable[idx].Original() == currentVersion.Original() {
61+
return sortable[idx:], nil
8462
}
8563
}
86-
return []*semver.Version{}, errors.New("no matching version found")
64+
65+
return []*semver.Version{
66+
currentVersion,
67+
}, nil // /shrug
8768
}
8869

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

136117
// RemoveLeastSpecific given a sorted collection will remove the least specific version
137118
func (c SemverTagCollection) RemoveLeastSpecific() []*semver.Version {
119+
if c.Len() == 0 {
120+
return []*semver.Version{}
121+
}
122+
138123
cleanedVersions := []*semver.Version{c[0]}
139124
for i := 0; i < len(c)-1; i++ {
140125
j := i + 1

pkg/outdated/version_test.go

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func TestCompareVersions(t *testing.T) {
179179
{
180180
name: "minor versions is less than",
181181
versions: []string{"10.1", "10"},
182-
expect: -1,
182+
expect: 1,
183183
},
184184
{
185185
name: "minor versions is greater than",
@@ -209,7 +209,7 @@ func TestCompareVersions(t *testing.T) {
209209
{
210210
name: "major version only with patch",
211211
versions: []string{"10", "10.1.2"},
212-
expect: 1,
212+
expect: -1,
213213
},
214214
{
215215
name: "minor version greater, patch version less",
@@ -226,6 +226,11 @@ func TestCompareVersions(t *testing.T) {
226226
versions: []string{"9.1", "10.2.2"},
227227
expect: -1,
228228
},
229+
{
230+
name: "minor with patches",
231+
versions: []string{"v3.0.4-beta.0", "v3.0.4-alpha.1"},
232+
expect: 1,
233+
},
229234
}
230235

231236
for _, test := range tests {
@@ -307,6 +312,11 @@ func TestRemoveLeastSpecific(t *testing.T) {
307312
versions: []string{"3.5.1.1", "3.5.1", "4.5.1"},
308313
expectVersions: []string{"3.5.1.1", "4.5.1"},
309314
},
315+
{
316+
name: "opa style",
317+
versions: []string{"v3.0.4-beta.0", "v3.0.4-beta.1"},
318+
expectVersions: []string{"v3.0.4-beta.0", "v3.0.4-beta.1"},
319+
},
310320
}
311321

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

324334
func TestResolveTagDates(t *testing.T) {
325-
hostname := "index.docker.io"
326-
imageName := "library/postgres"
327-
versions := []string{"10.0", "10.1", "10.2"}
328-
allVersions := makeVersions(versions)
335+
tests := []struct {
336+
name string
337+
hostname string
338+
imageName string
339+
versions []string
340+
}{
341+
{
342+
name: "postgres",
343+
hostname: "index.docker.io",
344+
imageName: "library/postgres",
345+
versions: []string{"10.0", "10.1", "10.2"},
346+
},
347+
{
348+
name: "tiller",
349+
hostname: "gcr.io",
350+
imageName: "kubernetes-helm/tiller",
351+
versions: []string{"v2.14.1"},
352+
},
353+
}
329354

330-
reg, err := initRegistryClient(hostname)
331-
require.NoError(t, err)
355+
for _, test := range tests {
356+
t.Run(test.name, func(t *testing.T) {
357+
req := require.New(t)
358+
359+
allVersions := makeVersions(test.versions)
332360

333-
versionTags, err := resolveTagDates(reg, imageName, allVersions)
334-
require.NoError(t, err)
361+
reg, err := initRegistryClient(test.hostname)
362+
req.NoError(err)
335363

336-
for _, versionTag := range versionTags {
337-
_, err = time.Parse(time.RFC3339, versionTag.Date)
338-
require.NoError(t, err)
364+
versionTags, err := resolveTagDates(reg, test.imageName, allVersions)
365+
req.NoError(err)
366+
367+
for _, versionTag := range versionTags {
368+
_, err = time.Parse(time.RFC3339, versionTag.Date)
369+
req.NoError(err)
370+
}
371+
})
339372
}
340373
}

0 commit comments

Comments
 (0)