Skip to content

Commit 2fb8ecf

Browse files
committed
Add All() to retrieve available versions and v1.UpgradePath(v2)
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
1 parent 6691298 commit 2fb8ecf

File tree

16 files changed

+1452
-51
lines changed

16 files changed

+1452
-51
lines changed

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,46 @@ func main() {
117117
}
118118
```
119119

120+
### List released versions
121+
122+
```go
123+
import (
124+
"context"
125+
"fmt"
126+
127+
"github.com/k0sproject/version"
128+
)
129+
130+
func main() {
131+
ctx := context.Background()
132+
versions, err := version.All(ctx)
133+
if err != nil {
134+
panic(err)
135+
}
136+
for _, v := range versions {
137+
fmt.Println(v)
138+
}
139+
}
140+
```
141+
142+
The first call hydrates a cache under the OS cache directory (honouring `XDG_CACHE_HOME` when set) and reuses it for subsequent listings.
143+
144+
### Plan an upgrade path
145+
146+
```go
147+
from := version.MustParse("v1.24.1+k0s.0")
148+
to := version.MustParse("v1.26.1+k0s.0")
149+
path, err := from.UpgradePath(to)
150+
if err != nil {
151+
panic(err)
152+
}
153+
for _, step := range path {
154+
fmt.Println(step)
155+
}
156+
```
157+
158+
The resulting slice contains the latest patch of each intermediate minor and the target (including prereleases when the target is one).
159+
120160
### `k0s_sort` executable
121161

122162
A command-line interface to the package. Can be used to sort lists of versions or to obtain the latest version number.
@@ -131,4 +171,4 @@ Usage: k0s_sort [options] [filename ...]
131171

132172

133173
## License
134-
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fk0sproject%2Fversion.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fk0sproject%2Fversion?ref=badge_large)
174+
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fk0sproject%2Fversion.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fk0sproject%2Fversion?ref=badge_large)

collection.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
package version
22

33
import (
4+
"bufio"
5+
"context"
6+
"errors"
47
"fmt"
8+
"io"
9+
"maps"
10+
"net/http"
11+
"os"
12+
"path/filepath"
13+
"slices"
14+
"strings"
15+
"time"
16+
17+
"github.com/k0sproject/version/internal/cache"
18+
"github.com/k0sproject/version/internal/github"
519
)
620

21+
// CacheMaxAge is the maximum duration a cached version list is considered fresh
22+
// before forcing a refresh from GitHub.
23+
const CacheMaxAge = 60 * time.Minute
24+
25+
// ErrCacheMiss is returned when no cached version data is available.
26+
var ErrCacheMiss = errors.New("version: cache miss")
27+
728
// Collection is a type that implements the sort.Interface interface
829
// so that versions can be sorted.
930
type Collection []*Version
@@ -31,3 +52,183 @@ func (c Collection) Less(i, j int) bool {
3152
func (c Collection) Swap(i, j int) {
3253
c[i], c[j] = c[j], c[i]
3354
}
55+
56+
// newCollectionFromCache returns the cached versions and the file's modification time.
57+
// It returns ErrCacheMiss when no usable cache exists.
58+
func newCollectionFromCache() (Collection, time.Time, error) {
59+
path, err := cache.File()
60+
if err != nil {
61+
return nil, time.Time{}, fmt.Errorf("locate cache: %w", err)
62+
}
63+
64+
f, err := os.Open(path)
65+
if err != nil {
66+
if errors.Is(err, os.ErrNotExist) {
67+
return nil, time.Time{}, ErrCacheMiss
68+
}
69+
return nil, time.Time{}, fmt.Errorf("open cache: %w", err)
70+
}
71+
defer func() {
72+
_ = f.Close()
73+
}()
74+
75+
info, err := f.Stat()
76+
if err != nil {
77+
return nil, time.Time{}, fmt.Errorf("stat cache: %w", err)
78+
}
79+
80+
collection, readErr := readCollection(f)
81+
if readErr != nil {
82+
return nil, time.Time{}, fmt.Errorf("read cache: %w", readErr)
83+
}
84+
if len(collection) == 0 {
85+
return nil, info.ModTime(), ErrCacheMiss
86+
}
87+
88+
return collection, info.ModTime(), nil
89+
}
90+
91+
// writeCache persists the collection to the cache file, one version per line.
92+
func (c Collection) writeCache() error {
93+
path, err := cache.File()
94+
if err != nil {
95+
return err
96+
}
97+
98+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
99+
return err
100+
}
101+
102+
ordered := slices.Clone(c)
103+
ordered = slices.DeleteFunc(ordered, func(v *Version) bool {
104+
return v == nil
105+
})
106+
slices.SortFunc(ordered, func(a, b *Version) int {
107+
return a.Compare(b)
108+
})
109+
slices.Reverse(ordered)
110+
111+
var b strings.Builder
112+
for _, v := range ordered {
113+
b.WriteString(v.String())
114+
b.WriteByte('\n')
115+
}
116+
117+
return os.WriteFile(path, []byte(b.String()), 0o644)
118+
}
119+
120+
// All returns all known k0s versions using the provided context. It refreshes
121+
// the local cache by querying GitHub for tags newer than the cache
122+
// modification time when the cache is older than CacheMaxAge. The cache is
123+
// skipped if the remote lookup fails and no cached data exists.
124+
func All(ctx context.Context) (Collection, error) {
125+
result, err := loadAll(ctx, sharedHTTPClient, false)
126+
return result.versions, err
127+
}
128+
129+
// Refresh fetches versions from GitHub regardless of cache freshness, updating the cache on success.
130+
func Refresh() (Collection, error) {
131+
return RefreshContext(context.Background())
132+
}
133+
134+
// RefreshContext fetches versions from GitHub regardless of cache freshness,
135+
// updating the cache on success using the provided context.
136+
func RefreshContext(ctx context.Context) (Collection, error) {
137+
result, err := loadAll(ctx, sharedHTTPClient, true)
138+
return result.versions, err
139+
}
140+
141+
type loadResult struct {
142+
versions Collection
143+
usedFallback bool
144+
}
145+
146+
func loadAll(ctx context.Context, httpClient *http.Client, force bool) (loadResult, error) {
147+
cached, modTime, cacheErr := newCollectionFromCache()
148+
if cacheErr != nil && !errors.Is(cacheErr, ErrCacheMiss) {
149+
return loadResult{}, cacheErr
150+
}
151+
152+
known := make(map[string]*Version, len(cached))
153+
for _, v := range cached {
154+
if v == nil {
155+
continue
156+
}
157+
known[v.String()] = v
158+
}
159+
160+
cacheStale := force || errors.Is(cacheErr, ErrCacheMiss) || modTime.IsZero() || time.Since(modTime) > CacheMaxAge
161+
if !cacheStale {
162+
return loadResult{versions: collectionFromMap(known)}, nil
163+
}
164+
165+
client := github.NewClient(httpClient)
166+
tags, err := client.TagsSince(ctx, modTime)
167+
if err != nil {
168+
if force || len(known) == 0 {
169+
return loadResult{}, err
170+
}
171+
return loadResult{versions: collectionFromMap(known), usedFallback: true}, nil
172+
}
173+
174+
var updated bool
175+
for _, tag := range tags {
176+
version, err := NewVersion(tag)
177+
if err != nil {
178+
continue
179+
}
180+
key := version.String()
181+
if _, exists := known[key]; exists {
182+
continue
183+
}
184+
known[key] = version
185+
updated = true
186+
}
187+
188+
result := collectionFromMap(known)
189+
190+
if updated || errors.Is(cacheErr, ErrCacheMiss) || force {
191+
if err := result.writeCache(); err != nil {
192+
return loadResult{}, err
193+
}
194+
}
195+
196+
return loadResult{versions: result}, nil
197+
}
198+
199+
func collectionFromMap(m map[string]*Version) Collection {
200+
if len(m) == 0 {
201+
return nil
202+
}
203+
values := slices.Collect(maps.Values(m))
204+
values = slices.DeleteFunc(values, func(v *Version) bool {
205+
return v == nil
206+
})
207+
slices.SortFunc(values, func(a, b *Version) int {
208+
return a.Compare(b)
209+
})
210+
return Collection(values)
211+
}
212+
213+
func readCollection(r io.Reader) (Collection, error) {
214+
var collection Collection
215+
scanner := bufio.NewScanner(r)
216+
for scanner.Scan() {
217+
line := strings.TrimSpace(scanner.Text())
218+
if line == "" || strings.HasPrefix(line, "#") {
219+
continue
220+
}
221+
222+
v, err := NewVersion(line)
223+
if err != nil {
224+
continue
225+
}
226+
collection = append(collection, v)
227+
}
228+
229+
if err := scanner.Err(); err != nil {
230+
return nil, err
231+
}
232+
233+
return collection, nil
234+
}

0 commit comments

Comments
 (0)