1package db
2
3import (
4	"database/sql"
5	"errors"
6	"fmt"
7	"strconv"
8
9	sq "github.com/Masterminds/squirrel"
10	"github.com/concourse/concourse/atc"
11	"github.com/concourse/concourse/atc/db/lock"
12)
13
14type BaseResourceTypeNotFoundError struct {
15	Name string
16}
17
18func (e BaseResourceTypeNotFoundError) Error() string {
19	return fmt.Sprintf("base resource type not found: %s", e.Name)
20}
21
22var ErrResourceConfigAlreadyExists = errors.New("resource config already exists")
23var ErrResourceConfigDisappeared = errors.New("resource config disappeared")
24var ErrResourceConfigParentDisappeared = errors.New("resource config parent disappeared")
25var ErrResourceConfigHasNoType = errors.New("resource config has no type")
26
27// ResourceConfig represents a resource type and config source.
28//
29// Resources in a pipeline, resource types in a pipeline, and `image_resource`
30// fields in a task all result in a reference to a ResourceConfig.
31//
32// ResourceConfigs are garbage-collected by gc.ResourceConfigCollector.
33type ResourceConfigDescriptor struct {
34	// A resource type provided by a resource.
35	CreatedByResourceCache *ResourceCacheDescriptor
36
37	// A resource type provided by a worker.
38	CreatedByBaseResourceType *BaseResourceType
39
40	// The resource's source configuration.
41	Source atc.Source
42}
43
44//go:generate counterfeiter . ResourceConfig
45
46type ResourceConfig interface {
47	ID() int
48	CreatedByResourceCache() UsedResourceCache
49	CreatedByBaseResourceType() *UsedBaseResourceType
50	OriginBaseResourceType() *UsedBaseResourceType
51
52	FindResourceConfigScopeByID(int, Resource) (ResourceConfigScope, bool, error)
53}
54
55type resourceConfig struct {
56	id                        int
57	createdByResourceCache    UsedResourceCache
58	createdByBaseResourceType *UsedBaseResourceType
59	lockFactory               lock.LockFactory
60	conn                      Conn
61}
62
63func (r *resourceConfig) ID() int                                   { return r.id }
64func (r *resourceConfig) CreatedByResourceCache() UsedResourceCache { return r.createdByResourceCache }
65func (r *resourceConfig) CreatedByBaseResourceType() *UsedBaseResourceType {
66	return r.createdByBaseResourceType
67}
68
69func (r *resourceConfig) OriginBaseResourceType() *UsedBaseResourceType {
70	if r.createdByBaseResourceType != nil {
71		return r.createdByBaseResourceType
72	}
73	return r.createdByResourceCache.ResourceConfig().OriginBaseResourceType()
74}
75
76func (r *resourceConfig) FindResourceConfigScopeByID(resourceConfigScopeID int, resource Resource) (ResourceConfigScope, bool, error) {
77	var (
78		id           int
79		rcID         int
80		rID          sql.NullString
81		checkErrBlob sql.NullString
82	)
83
84	err := psql.Select("id, resource_id, resource_config_id, check_error").
85		From("resource_config_scopes").
86		Where(sq.Eq{
87			"id":                 resourceConfigScopeID,
88			"resource_config_id": r.id,
89		}).
90		RunWith(r.conn).
91		QueryRow().
92		Scan(&id, &rID, &rcID, &checkErrBlob)
93	if err != nil {
94		if err == sql.ErrNoRows {
95			return nil, false, nil
96		}
97		return nil, false, err
98	}
99
100	var uniqueResource Resource
101	if rID.Valid {
102		var resourceID int
103		resourceID, err = strconv.Atoi(rID.String)
104		if err != nil {
105			return nil, false, err
106		}
107
108		if resource.ID() == resourceID {
109			uniqueResource = resource
110		}
111	}
112
113	var checkErr error
114	if checkErrBlob.Valid {
115		checkErr = errors.New(checkErrBlob.String)
116	}
117
118	return &resourceConfigScope{
119		id:             id,
120		resource:       uniqueResource,
121		resourceConfig: r,
122		checkError:     checkErr,
123		conn:           r.conn,
124		lockFactory:    r.lockFactory}, true, nil
125}
126
127func (r *ResourceConfigDescriptor) findOrCreate(tx Tx, lockFactory lock.LockFactory, conn Conn) (ResourceConfig, error) {
128	rc := &resourceConfig{
129		lockFactory: lockFactory,
130		conn:        conn,
131	}
132
133	var parentID int
134	var parentColumnName string
135	if r.CreatedByResourceCache != nil {
136		parentColumnName = "resource_cache_id"
137
138		resourceCache, err := r.CreatedByResourceCache.findOrCreate(tx, lockFactory, conn)
139		if err != nil {
140			return nil, err
141		}
142
143		parentID = resourceCache.ID()
144
145		rc.createdByResourceCache = resourceCache
146	}
147
148	if r.CreatedByBaseResourceType != nil {
149		parentColumnName = "base_resource_type_id"
150
151		var err error
152		var found bool
153		rc.createdByBaseResourceType, found, err = r.CreatedByBaseResourceType.Find(tx)
154		if err != nil {
155			return nil, err
156		}
157
158		if !found {
159			return nil, BaseResourceTypeNotFoundError{Name: r.CreatedByBaseResourceType.Name}
160		}
161
162		parentID = rc.CreatedByBaseResourceType().ID
163	}
164
165	id, found, err := r.findWithParentID(tx, parentColumnName, parentID)
166	if err != nil {
167		return nil, err
168	}
169
170	if !found {
171		hash := mapHash(r.Source)
172
173		var err error
174		err = psql.Insert("resource_configs").
175			Columns(
176				parentColumnName,
177				"source_hash",
178			).
179			Values(
180				parentID,
181				hash,
182			).
183			Suffix(`
184				ON CONFLICT (`+parentColumnName+`, source_hash) DO UPDATE SET
185					`+parentColumnName+` = ?,
186					source_hash = ?
187				RETURNING id
188			`, parentID, hash).
189			RunWith(tx).
190			QueryRow().
191			Scan(&id)
192
193		if err != nil {
194			return nil, err
195		}
196	}
197
198	rc.id = id
199
200	return rc, nil
201}
202
203func (r *ResourceConfigDescriptor) find(tx Tx, lockFactory lock.LockFactory, conn Conn) (ResourceConfig, bool, error) {
204	rc := &resourceConfig{
205		lockFactory: lockFactory,
206		conn:        conn,
207	}
208
209	var parentID int
210	var parentColumnName string
211	if r.CreatedByResourceCache != nil {
212		parentColumnName = "resource_cache_id"
213
214		resourceCache, found, err := r.CreatedByResourceCache.find(tx, lockFactory, conn)
215		if err != nil {
216			return nil, false, err
217		}
218
219		if !found {
220			return nil, false, nil
221		}
222
223		parentID = resourceCache.ID()
224
225		rc.createdByResourceCache = resourceCache
226	}
227
228	if r.CreatedByBaseResourceType != nil {
229		parentColumnName = "base_resource_type_id"
230
231		var err error
232		var found bool
233		rc.createdByBaseResourceType, found, err = r.CreatedByBaseResourceType.Find(tx)
234		if err != nil {
235			return nil, false, err
236		}
237
238		if !found {
239			return nil, false, nil
240		}
241
242		parentID = rc.createdByBaseResourceType.ID
243	}
244
245	id, found, err := r.findWithParentID(tx, parentColumnName, parentID)
246	if err != nil {
247		return nil, false, err
248	}
249
250	if !found {
251		return nil, false, nil
252	}
253
254	rc.id = id
255
256	return rc, true, nil
257}
258
259func (r *ResourceConfigDescriptor) findWithParentID(tx Tx, parentColumnName string, parentID int) (int, bool, error) {
260	var id int
261	var whereClause sq.Eq
262
263	err := psql.Select("id").
264		From("resource_configs").
265		Where(sq.Eq{
266			parentColumnName: parentID,
267			"source_hash":    mapHash(r.Source),
268		}).
269		Where(whereClause).
270		Suffix("FOR SHARE").
271		RunWith(tx).
272		QueryRow().
273		Scan(&id)
274	if err != nil {
275		if err == sql.ErrNoRows {
276			return 0, false, nil
277		}
278
279		return 0, false, err
280	}
281
282	return id, true, nil
283}
284
285func findOrCreateResourceConfigScope(
286	tx Tx,
287	conn Conn,
288	lockFactory lock.LockFactory,
289	resourceConfig ResourceConfig,
290	resource Resource,
291	resourceType string,
292	resourceTypes atc.VersionedResourceTypes,
293) (ResourceConfigScope, error) {
294
295	var unique bool
296	var uniqueResource Resource
297	var resourceID *int
298
299	if resource != nil {
300		if !atc.EnableGlobalResources {
301			unique = true
302		} else {
303			customType, found := resourceTypes.Lookup(resourceType)
304			if found {
305				unique = customType.UniqueVersionHistory
306			} else {
307				baseType := resourceConfig.CreatedByBaseResourceType()
308				if baseType == nil {
309					return nil, ErrResourceConfigHasNoType
310				}
311				unique = baseType.UniqueVersionHistory
312			}
313		}
314
315		if unique {
316			id := resource.ID()
317
318			resourceID = &id
319			uniqueResource = resource
320		}
321	}
322
323	var scopeID int
324	var checkErr error
325
326	rows, err := psql.Select("id, check_error").
327		From("resource_config_scopes").
328		Where(sq.Eq{
329			"resource_id":        resourceID,
330			"resource_config_id": resourceConfig.ID(),
331		}).
332		RunWith(tx).
333		Query()
334	if err != nil {
335		return nil, err
336	}
337
338	if rows.Next() {
339		var checkErrBlob sql.NullString
340
341		err = rows.Scan(&scopeID, &checkErrBlob)
342		if err != nil {
343			return nil, err
344		}
345
346		if checkErrBlob.Valid {
347			checkErr = errors.New(checkErrBlob.String)
348		}
349
350		err = rows.Close()
351		if err != nil {
352			return nil, err
353		}
354	} else if unique && resource != nil {
355		// delete outdated scopes for resource
356		_, err := psql.Delete("resource_config_scopes").
357			Where(sq.And{
358				sq.Eq{
359					"resource_id": resource.ID(),
360				},
361			}).
362			RunWith(tx).
363			Exec()
364		if err != nil {
365			return nil, err
366		}
367
368		err = psql.Insert("resource_config_scopes").
369			Columns("resource_id", "resource_config_id").
370			Values(resource.ID(), resourceConfig.ID()).
371			Suffix(`
372				ON CONFLICT (resource_id, resource_config_id) WHERE resource_id IS NOT NULL DO UPDATE SET
373					resource_id = ?,
374					resource_config_id = ?
375				RETURNING id
376			`, resource.ID(), resourceConfig.ID()).
377			RunWith(tx).
378			QueryRow().
379			Scan(&scopeID)
380		if err != nil {
381			return nil, err
382		}
383	} else {
384		err = psql.Insert("resource_config_scopes").
385			Columns("resource_id", "resource_config_id").
386			Values(nil, resourceConfig.ID()).
387			Suffix(`
388				ON CONFLICT (resource_config_id) WHERE resource_id IS NULL DO UPDATE SET
389					resource_config_id = ?
390				RETURNING id
391			`, resourceConfig.ID()).
392			RunWith(tx).
393			QueryRow().
394			Scan(&scopeID)
395		if err != nil {
396			return nil, err
397		}
398	}
399
400	return &resourceConfigScope{
401		id:             scopeID,
402		resource:       uniqueResource,
403		resourceConfig: resourceConfig,
404		checkError:     checkErr,
405		conn:           conn,
406		lockFactory:    lockFactory,
407	}, nil
408}
409