summaryrefslogtreecommitdiff
path: root/pkg/registry/endpoints.go
blob: 3b64fc53cb746379ffdfd87400f2d7520d8852be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package registry

import (
	"crypto/tls"
	"fmt"
	"math"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/cache"
	"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"

	"go.uber.org/ratelimit"
)

// TagListSort defines how the registry returns the list of tags
type TagListSort int

const (
	TagListSortUnknown           TagListSort = -1
	TagListSortUnsorted          TagListSort = 0
	TagListSortLatestFirst       TagListSort = 1
	TagListSortLatestLast        TagListSort = 2
	TagListSortUnsortedString    string      = "unsorted"
	TagListSortLatestFirstString string      = "latest-first"
	TagListSortLatestLastString  string      = "latest-last"
	TagListSortUnknownString     string      = "unknown"
)

const (
	RateLimitNone    = math.MaxInt32
	RateLimitDefault = 10
)

// IsTimeSorted returns whether a tag list is time sorted
func (tls TagListSort) IsTimeSorted() bool {
	return tls == TagListSortLatestFirst || tls == TagListSortLatestLast
}

// TagListSortFromString gets the TagListSort value from a given string
func TagListSortFromString(tls string) TagListSort {
	switch strings.ToLower(tls) {
	case "latest-first":
		return TagListSortLatestFirst
	case "latest-last":
		return TagListSortLatestLast
	case "none", "":
		return TagListSortUnsorted
	default:
		log.Warnf("unknown tag list sort mode: %s", tls)
		return TagListSortUnknown
	}
}

// String returns the string representation of a TagListSort value
func (tls TagListSort) String() string {
	switch tls {
	case TagListSortLatestFirst:
		return TagListSortLatestFirstString
	case TagListSortLatestLast:
		return TagListSortLatestLastString
	case TagListSortUnsorted:
		return TagListSortUnsortedString
	}

	return TagListSortUnknownString
}

// RegistryEndpoint holds information on how to access any specific registry API
// endpoint.
type RegistryEndpoint struct {
	RegistryName   string
	RegistryPrefix string
	RegistryAPI    string
	Username       string
	Password       string
	Ping           bool
	Credentials    string
	Insecure       bool
	DefaultNS      string
	CredsExpire    time.Duration
	CredsUpdated   time.Time
	TagListSort    TagListSort
	Cache          cache.ImageTagCache
	Limiter        ratelimit.Limiter
	IsDefault      bool
	lock           sync.RWMutex
	limit          int
}

// registryTweaks should contain a list of registries whose settings cannot be
// inferred by just looking at the image prefix. Prominent example here is the
// Docker Hub registry, which is referred to as docker.io from the image, but
// its API endpoint is https://registry-1.docker.io (and not https://docker.io)
var registryTweaks map[string]*RegistryEndpoint = map[string]*RegistryEndpoint{
	"docker.io": {
		RegistryName:   "Docker Hub",
		RegistryPrefix: "docker.io",
		RegistryAPI:    "https://registry-1.docker.io",
		Ping:           true,
		Insecure:       false,
		DefaultNS:      "library",
		Cache:          cache.NewMemCache(),
		Limiter:        ratelimit.New(RateLimitDefault),
		IsDefault:      true,
	},
}

var registries map[string]*RegistryEndpoint = make(map[string]*RegistryEndpoint)

// Default registry points to the registry that is to be used as the default,
// e.g. when no registry prefix is given for a certain image.
var defaultRegistry *RegistryEndpoint

// Simple RW mutex for concurrent access to registries map
var registryLock sync.RWMutex

func AddRegistryEndpointFromConfig(epc RegistryConfiguration) error {
	ep := NewRegistryEndpoint(epc.Prefix, epc.Name, epc.ApiURL, epc.Credentials, epc.DefaultNS, epc.Insecure, TagListSortFromString(epc.TagSortMode), epc.Limit, epc.CredsExpire)
	return AddRegistryEndpoint(ep)
}

// NewRegistryEndpoint returns an endpoint object with the given configuration
// pre-populated and a fresh cache.
func NewRegistryEndpoint(prefix, name, apiUrl, credentials, defaultNS string, insecure bool, tagListSort TagListSort, limit int, credsExpire time.Duration) *RegistryEndpoint {
	if limit <= 0 {
		limit = RateLimitNone
	}
	ep := &RegistryEndpoint{
		RegistryName:   name,
		RegistryPrefix: prefix,
		RegistryAPI:    strings.TrimSuffix(apiUrl, "/"),
		Credentials:    credentials,
		CredsExpire:    credsExpire,
		Cache:          cache.NewMemCache(),
		Insecure:       insecure,
		DefaultNS:      defaultNS,
		TagListSort:    tagListSort,
		Limiter:        ratelimit.New(limit),
		limit:          limit,
	}
	return ep
}

// AddRegistryEndpoint adds registry endpoint information with the given details
func AddRegistryEndpoint(ep *RegistryEndpoint) error {
	prefix := ep.RegistryPrefix

	registryLock.Lock()
	// If the endpoint is supposed to be the default endpoint, make sure that
	// any previously set default endpoint is unset.
	if ep.IsDefault {
		if dep := GetDefaultRegistry(); dep != nil {
			dep.IsDefault = false
		}
		SetDefaultRegistry(ep)
	}
	registries[prefix] = ep
	registryLock.Unlock()

	logCtx := log.WithContext()
	logCtx.AddField("registry", ep.RegistryAPI)
	logCtx.AddField("prefix", ep.RegistryPrefix)
	if ep.limit != RateLimitNone {
		logCtx.Debugf("setting rate limit to %d requests per second", ep.limit)
	} else {
		logCtx.Debugf("rate limiting is disabled")
	}
	return nil
}

// inferRegistryEndpointFromPrefix returns a registry endpoint with the API
// URL inferred from the prefix and adds it to the list of the configured
// registries.
func inferRegistryEndpointFromPrefix(prefix string) *RegistryEndpoint {
	apiURL := "https://" + prefix
	return NewRegistryEndpoint(prefix, prefix, apiURL, "", "", false, TagListSortUnsorted, 20, 0)
}

// GetRegistryEndpoint retrieves the endpoint information for the given prefix
func GetRegistryEndpoint(prefix string) (*RegistryEndpoint, error) {
	if prefix == "" {
		if defaultRegistry == nil {
			return nil, fmt.Errorf("no default endpoint configured")
		} else {
			return defaultRegistry, nil
		}
	}

	registryLock.RLock()
	registry, ok := registries[prefix]
	registryLock.RUnlock()

	if ok {
		return registry, nil
	} else {
		var err error
		ep := inferRegistryEndpointFromPrefix(prefix)
		if ep != nil {
			err = AddRegistryEndpoint(ep)
		} else {
			err = fmt.Errorf("could not infer registry configuration from prefix %s", prefix)
		}
		if err == nil {
			log.Debugf("Inferred registry from prefix %s to use API %s", prefix, ep.RegistryAPI)
		}
		return ep, err
	}
}

// SetDefaultRegistry sets a given registry endpoint as the default
func SetDefaultRegistry(ep *RegistryEndpoint) {
	log.Debugf("Setting default registry endpoint to %s", ep.RegistryPrefix)
	ep.IsDefault = true
	if defaultRegistry != nil {
		log.Debugf("Previous default registry was %s", defaultRegistry.RegistryPrefix)
		defaultRegistry.IsDefault = false
	}
	defaultRegistry = ep
}

// GetDefaultRegistry returns the registry endpoint that is set as default,
// or nil if no default registry endpoint is set
func GetDefaultRegistry() *RegistryEndpoint {
	if defaultRegistry != nil {
		log.Debugf("Getting default registry endpoint: %s", defaultRegistry.RegistryPrefix)
	} else {
		log.Debugf("No default registry defined.")
	}
	return defaultRegistry
}

// SetRegistryEndpointCredentials allows to change the credentials used for
// endpoint access for existing RegistryEndpoint configuration
func SetRegistryEndpointCredentials(prefix, credentials string) error {
	registry, err := GetRegistryEndpoint(prefix)
	if err != nil {
		return err
	}
	registry.lock.Lock()
	registry.Credentials = credentials
	registry.lock.Unlock()
	return nil
}

// ConfiguredEndpoints returns a list of prefixes that are configured
func ConfiguredEndpoints() []string {
	registryLock.RLock()
	defer registryLock.RUnlock()
	r := make([]string, 0, len(registries))
	for _, v := range registries {
		r = append(r, v.RegistryPrefix)
	}
	return r
}

// DeepCopy copies the endpoint to a new object, but creating a new Cache
func (ep *RegistryEndpoint) DeepCopy() *RegistryEndpoint {
	ep.lock.RLock()
	newEp := &RegistryEndpoint{}
	newEp.RegistryAPI = ep.RegistryAPI
	newEp.RegistryName = ep.RegistryName
	newEp.RegistryPrefix = ep.RegistryPrefix
	newEp.Credentials = ep.Credentials
	newEp.Ping = ep.Ping
	newEp.TagListSort = ep.TagListSort
	newEp.Cache = cache.NewMemCache()
	newEp.Insecure = ep.Insecure
	newEp.DefaultNS = ep.DefaultNS
	newEp.Limiter = ep.Limiter
	newEp.CredsExpire = ep.CredsExpire
	newEp.CredsUpdated = ep.CredsUpdated
	newEp.IsDefault = ep.IsDefault
	newEp.limit = ep.limit
	ep.lock.RUnlock()
	return newEp
}

// GetTransport returns a transport object for this endpoint
func (ep *RegistryEndpoint) GetTransport() *http.Transport {
	tlsC := &tls.Config{}
	if ep.Insecure {
		tlsC.InsecureSkipVerify = true
	}
	return &http.Transport{
		Proxy:           http.ProxyFromEnvironment,
		TLSClientConfig: tlsC,
	}
}

// init initializes the registry configuration
func init() {
	for k, v := range registryTweaks {
		registries[k] = v.DeepCopy()
		if v.IsDefault {
			if defaultRegistry == nil {
				defaultRegistry = v
			} else {
				panic("only one default registry can be configured")
			}
		}
	}
}