podman

Форк
0
/
sqlite_state_internal.go 
580 строк · 19.3 Кб
1
//go:build !remote
2

3
package libpod
4

5
import (
6
	"database/sql"
7
	"errors"
8
	"fmt"
9
	"os"
10
	"strings"
11

12
	"github.com/containers/common/libnetwork/types"
13
	"github.com/containers/podman/v5/libpod/define"
14
	"github.com/sirupsen/logrus"
15

16
	// SQLite backend for database/sql
17
	_ "github.com/mattn/go-sqlite3"
18
)
19

20
func initSQLiteDB(conn *sql.DB) (defErr error) {
21
	// Start with a transaction to avoid "database locked" errors.
22
	// See https://github.com/mattn/go-sqlite3/issues/274#issuecomment-1429054597
23
	tx, err := conn.Begin()
24
	if err != nil {
25
		return fmt.Errorf("beginning transaction: %w", err)
26
	}
27
	defer func() {
28
		if defErr != nil {
29
			if err := tx.Rollback(); err != nil {
30
				logrus.Errorf("Rolling back transaction to create tables: %v", err)
31
			}
32
		}
33
	}()
34

35
	sameSchema, err := migrateSchemaIfNecessary(tx)
36
	if err != nil {
37
		return err
38
	}
39
	if !sameSchema {
40
		if err := createSQLiteTables(tx); err != nil {
41
			return err
42
		}
43
	}
44
	if err := tx.Commit(); err != nil {
45
		return fmt.Errorf("committing transaction: %w", err)
46
	}
47
	return nil
48
}
49

50
func migrateSchemaIfNecessary(tx *sql.Tx) (bool, error) {
51
	// First, check if the DBConfig table exists
52
	checkRow := tx.QueryRow("SELECT 1 FROM sqlite_master WHERE type='table' AND name='DBConfig';")
53
	var check int
54
	if err := checkRow.Scan(&check); err != nil {
55
		if errors.Is(err, sql.ErrNoRows) {
56
			return false, nil
57
		}
58
		return false, fmt.Errorf("checking if DB config table exists: %w", err)
59
	}
60
	if check != 1 {
61
		// Table does not exist, fresh database, no need to migrate.
62
		return false, nil
63
	}
64

65
	row := tx.QueryRow("SELECT SchemaVersion FROM DBConfig;")
66
	var schemaVer int
67
	if err := row.Scan(&schemaVer); err != nil {
68
		if errors.Is(err, sql.ErrNoRows) {
69
			// Brand-new, unpopulated DB.
70
			// Schema was just created, so it has to be the latest.
71
			return false, nil
72
		}
73
		return false, fmt.Errorf("scanning schema version from DB config: %w", err)
74
	}
75

76
	// If the schema version 0 or less, it's invalid
77
	if schemaVer <= 0 {
78
		return false, fmt.Errorf("database schema version %d is invalid: %w", schemaVer, define.ErrInternal)
79
	}
80

81
	// Same schema -> nothing do to.
82
	if schemaVer == schemaVersion {
83
		return true, nil
84
	}
85

86
	// If the DB is a later schema than we support, we have to error
87
	if schemaVer > schemaVersion {
88
		return false, fmt.Errorf("database has schema version %d while this libpod version only supports version %d: %w",
89
			schemaVer, schemaVersion, define.ErrInternal)
90
	}
91

92
	// Perform schema migration here, one version at a time.
93

94
	return false, nil
95
}
96

97
// Initialize all required tables for the SQLite state
98
func createSQLiteTables(tx *sql.Tx) error {
99
	// Technically we could split the "CREATE TABLE IF NOT EXISTS" and ");"
100
	// bits off each command and add them in the for loop where we actually
101
	// run the SQL, but that seems unnecessary.
102
	const dbConfig = `
103
        CREATE TABLE IF NOT EXISTS DBConfig(
104
                ID            INTEGER PRIMARY KEY NOT NULL,
105
                SchemaVersion INTEGER NOT NULL,
106
                OS            TEXT    NOT NULL,
107
                StaticDir     TEXT    NOT NULL,
108
                TmpDir        TEXT    NOT NULL,
109
                GraphRoot     TEXT    NOT NULL,
110
                RunRoot       TEXT    NOT NULL,
111
                GraphDriver   TEXT    NOT NULL,
112
                VolumeDir     TEXT    NOT NULL,
113
                CHECK (ID IN (1))
114
        );`
115

116
	const idNamespace = `
117
        CREATE TABLE IF NOT EXISTS IDNamespace(
118
                ID TEXT PRIMARY KEY NOT NULL
119
        );`
120

121
	const containerConfig = `
122
        CREATE TABLE IF NOT EXISTS ContainerConfig(
123
                ID              TEXT    PRIMARY KEY NOT NULL,
124
                Name            TEXT    UNIQUE NOT NULL,
125
                PodID           TEXT,
126
                JSON            TEXT    NOT NULL,
127
                FOREIGN KEY (ID)    REFERENCES IDNamespace(ID)    DEFERRABLE INITIALLY DEFERRED,
128
                FOREIGN KEY (ID)    REFERENCES ContainerState(ID) DEFERRABLE INITIALLY DEFERRED,
129
                FOREIGN KEY (PodID) REFERENCES PodConfig(ID)
130
        );`
131

132
	const containerState = `
133
        CREATE TABLE IF NOT EXISTS ContainerState(
134
                ID       TEXT    PRIMARY KEY NOT NULL,
135
                State    INTEGER NOT NULL,
136
                ExitCode INTEGER,
137
                JSON     TEXT    NOT NULL,
138
                FOREIGN KEY (ID) REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED,
139
                CHECK (ExitCode BETWEEN -1 AND 255)
140
        );`
141

142
	const containerExecSession = `
143
        CREATE TABLE IF NOT EXISTS ContainerExecSession(
144
                ID          TEXT PRIMARY KEY NOT NULL,
145
                ContainerID TEXT NOT NULL,
146
                FOREIGN KEY (ContainerID) REFERENCES ContainerConfig(ID)
147
        );`
148

149
	const containerDependency = `
150
        CREATE TABLE IF NOT EXISTS ContainerDependency(
151
                ID           TEXT NOT NULL,
152
                DependencyID TEXT NOT NULL,
153
                PRIMARY KEY (ID, DependencyID),
154
                FOREIGN KEY (ID)           REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED,
155
                FOREIGN KEY (DependencyID) REFERENCES ContainerConfig(ID),
156
                CHECK (ID <> DependencyID)
157
        );`
158

159
	const containerVolume = `
160
        CREATE TABLE IF NOT EXISTS ContainerVolume(
161
                ContainerID TEXT NOT NULL,
162
                VolumeName  TEXT NOT NULL,
163
                PRIMARY KEY (ContainerID, VolumeName),
164
                FOREIGN KEY (ContainerID) REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED,
165
                FOREIGN KEY (VolumeName)  REFERENCES VolumeConfig(Name)
166
        );`
167

168
	const containerExitCode = `
169
        CREATE TABLE IF NOT EXISTS ContainerExitCode(
170
                ID        TEXT    PRIMARY KEY NOT NULL,
171
                Timestamp INTEGER NOT NULL,
172
                ExitCode  INTEGER NOT NULL,
173
                CHECK (ExitCode BETWEEN -1 AND 255)
174
        );`
175

176
	const podConfig = `
177
        CREATE TABLE IF NOT EXISTS PodConfig(
178
                ID              TEXT    PRIMARY KEY NOT NULL,
179
                Name            TEXT    UNIQUE NOT NULL,
180
                JSON            TEXT    NOT NULL,
181
                FOREIGN KEY (ID) REFERENCES IDNamespace(ID) DEFERRABLE INITIALLY DEFERRED,
182
                FOREIGN KEY (ID) REFERENCES PodState(ID)    DEFERRABLE INITIALLY DEFERRED
183
        );`
184

185
	const podState = `
186
        CREATE TABLE IF NOT EXISTS PodState(
187
                ID               TEXT PRIMARY KEY NOT NULL,
188
                InfraContainerID TEXT,
189
                JSON             TEXT NOT NULL,
190
                FOREIGN KEY (ID)               REFERENCES PodConfig(ID)       DEFERRABLE INITIALLY DEFERRED,
191
                FOREIGN KEY (InfraContainerID) REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED
192
        );`
193

194
	const volumeConfig = `
195
        CREATE TABLE IF NOT EXISTS VolumeConfig(
196
                Name            TEXT    PRIMARY KEY NOT NULL,
197
                StorageID       TEXT,
198
                JSON            TEXT    NOT NULL,
199
                FOREIGN KEY (Name) REFERENCES VolumeState(Name) DEFERRABLE INITIALLY DEFERRED
200
        );`
201

202
	const volumeState = `
203
        CREATE TABLE IF NOT EXISTS VolumeState(
204
                Name TEXT PRIMARY KEY NOT NULL,
205
                JSON TEXT NOT NULL,
206
                FOREIGN KEY (Name) REFERENCES VolumeConfig(Name) DEFERRABLE INITIALLY DEFERRED
207
        );`
208

209
	tables := map[string]string{
210
		"DBConfig":             dbConfig,
211
		"IDNamespace":          idNamespace,
212
		"ContainerConfig":      containerConfig,
213
		"ContainerState":       containerState,
214
		"ContainerExecSession": containerExecSession,
215
		"ContainerDependency":  containerDependency,
216
		"ContainerVolume":      containerVolume,
217
		"ContainerExitCode":    containerExitCode,
218
		"PodConfig":            podConfig,
219
		"PodState":             podState,
220
		"VolumeConfig":         volumeConfig,
221
		"VolumeState":          volumeState,
222
	}
223

224
	for tblName, cmd := range tables {
225
		if _, err := tx.Exec(cmd); err != nil {
226
			return fmt.Errorf("creating table %s: %w", tblName, err)
227
		}
228
	}
229
	return nil
230
}
231

232
// Get the config of a container with the given ID from the database
233
func (s *SQLiteState) getCtrConfig(id string) (*ContainerConfig, error) {
234
	row := s.conn.QueryRow("SELECT JSON FROM ContainerConfig WHERE ID=?;", id)
235

236
	var rawJSON string
237
	if err := row.Scan(&rawJSON); err != nil {
238
		if errors.Is(err, sql.ErrNoRows) {
239
			return nil, define.ErrNoSuchCtr
240
		}
241
		return nil, fmt.Errorf("retrieving container %s config from DB: %w", id, err)
242
	}
243

244
	ctrCfg := new(ContainerConfig)
245

246
	if err := json.Unmarshal([]byte(rawJSON), ctrCfg); err != nil {
247
		return nil, fmt.Errorf("unmarshalling container %s config: %w", id, err)
248
	}
249

250
	return ctrCfg, nil
251
}
252

253
// Finalize a container that was pulled out of the database.
254
func finalizeCtrSqlite(ctr *Container) error {
255
	// Get the lock
256
	lock, err := ctr.runtime.lockManager.RetrieveLock(ctr.config.LockID)
257
	if err != nil {
258
		return fmt.Errorf("retrieving lock for container %s: %w", ctr.ID(), err)
259
	}
260
	ctr.lock = lock
261

262
	// Get the OCI runtime
263
	if ctr.config.OCIRuntime == "" {
264
		ctr.ociRuntime = ctr.runtime.defaultOCIRuntime
265
	} else {
266
		// Handle legacy containers which might use a literal path for
267
		// their OCI runtime name.
268
		runtimeName := ctr.config.OCIRuntime
269
		ociRuntime, ok := ctr.runtime.ociRuntimes[runtimeName]
270
		if !ok {
271
			runtimeSet := false
272

273
			// If the path starts with a / and exists, make a new
274
			// OCI runtime for it using the full path.
275
			if strings.HasPrefix(runtimeName, "/") {
276
				if stat, err := os.Stat(runtimeName); err == nil && !stat.IsDir() {
277
					newOCIRuntime, err := newConmonOCIRuntime(runtimeName, []string{runtimeName}, ctr.runtime.conmonPath, ctr.runtime.runtimeFlags, ctr.runtime.config)
278
					if err == nil {
279
						// TODO: There is a potential risk of concurrent map modification here.
280
						// This is an unlikely case, though.
281
						ociRuntime = newOCIRuntime
282
						ctr.runtime.ociRuntimes[runtimeName] = ociRuntime
283
						runtimeSet = true
284
					}
285
				}
286
			}
287

288
			if !runtimeSet {
289
				// Use a MissingRuntime implementation
290
				ociRuntime = getMissingRuntime(runtimeName, ctr.runtime)
291
			}
292
		}
293
		ctr.ociRuntime = ociRuntime
294
	}
295

296
	ctr.valid = true
297

298
	return nil
299
}
300

301
// Finalize a pod that was pulled out of the database.
302
func (s *SQLiteState) createPod(rawJSON string) (*Pod, error) {
303
	config := new(PodConfig)
304
	if err := json.Unmarshal([]byte(rawJSON), config); err != nil {
305
		return nil, fmt.Errorf("unmarshalling pod config: %w", err)
306
	}
307
	lock, err := s.runtime.lockManager.RetrieveLock(config.LockID)
308
	if err != nil {
309
		return nil, fmt.Errorf("retrieving lock for pod %s: %w", config.ID, err)
310
	}
311

312
	pod := new(Pod)
313
	pod.config = config
314
	pod.state = new(podState)
315
	pod.lock = lock
316
	pod.runtime = s.runtime
317
	pod.valid = true
318

319
	return pod, nil
320
}
321

322
// Finalize a volume that was pulled out of the database
323
func finalizeVolumeSqlite(vol *Volume) error {
324
	// Get the lock
325
	lock, err := vol.runtime.lockManager.RetrieveLock(vol.config.LockID)
326
	if err != nil {
327
		return fmt.Errorf("retrieving lock for volume %s: %w", vol.Name(), err)
328
	}
329
	vol.lock = lock
330

331
	// Retrieve volume driver
332
	if vol.UsesVolumeDriver() {
333
		plugin, err := vol.runtime.getVolumePlugin(vol.config)
334
		if err != nil {
335
			// We want to fail gracefully here, to ensure that we
336
			// can still remove volumes even if their plugin is
337
			// missing. Otherwise, we end up with volumes that
338
			// cannot even be retrieved from the database and will
339
			// cause things like `volume ls` to fail.
340
			logrus.Errorf("Volume %s uses volume plugin %s, but it cannot be accessed - some functionality may not be available: %v", vol.Name(), vol.config.Driver, err)
341
		} else {
342
			vol.plugin = plugin
343
		}
344
	}
345

346
	vol.valid = true
347

348
	return nil
349
}
350

351
func (s *SQLiteState) rewriteContainerConfig(ctr *Container, newCfg *ContainerConfig) (defErr error) {
352
	json, err := json.Marshal(newCfg)
353
	if err != nil {
354
		return fmt.Errorf("error marshalling container %s new config JSON: %w", ctr.ID(), err)
355
	}
356

357
	tx, err := s.conn.Begin()
358
	if err != nil {
359
		return fmt.Errorf("beginning transaction to rewrite container %s config: %w", ctr.ID(), err)
360
	}
361
	defer func() {
362
		if defErr != nil {
363
			if err := tx.Rollback(); err != nil {
364
				logrus.Errorf("Rolling back transaction to rewrite container %s config: %v", ctr.ID(), err)
365
			}
366
		}
367
	}()
368

369
	results, err := tx.Exec("UPDATE ContainerConfig SET Name=?, JSON=? WHERE ID=?;", newCfg.Name, json, ctr.ID())
370
	if err != nil {
371
		return fmt.Errorf("updating container config table with new configuration for container %s: %w", ctr.ID(), err)
372
	}
373
	rows, err := results.RowsAffected()
374
	if err != nil {
375
		return fmt.Errorf("retrieving container %s config rewrite rows affected: %w", ctr.ID(), err)
376
	}
377
	if rows == 0 {
378
		ctr.valid = false
379
		return define.ErrNoSuchCtr
380
	}
381

382
	if err := tx.Commit(); err != nil {
383
		return fmt.Errorf("committing transaction to rewrite container %s config: %w", ctr.ID(), err)
384
	}
385

386
	return nil
387
}
388

389
func (s *SQLiteState) addContainer(ctr *Container) (defErr error) {
390
	configJSON, err := json.Marshal(ctr.config)
391
	if err != nil {
392
		return fmt.Errorf("marshalling container config json: %w", err)
393
	}
394

395
	stateJSON, err := json.Marshal(ctr.state)
396
	if err != nil {
397
		return fmt.Errorf("marshalling container state json: %w", err)
398
	}
399
	deps := ctr.Dependencies()
400

401
	podID := sql.NullString{}
402
	if ctr.config.Pod != "" {
403
		podID.Valid = true
404
		podID.String = ctr.config.Pod
405
	}
406

407
	tx, err := s.conn.Begin()
408
	if err != nil {
409
		return fmt.Errorf("beginning container create transaction: %w", err)
410
	}
411
	defer func() {
412
		if defErr != nil {
413
			if err := tx.Rollback(); err != nil {
414
				logrus.Errorf("Rolling back transaction to create container: %v", err)
415
			}
416
		}
417
	}()
418

419
	// TODO: There has to be a better way of doing this
420
	var check int
421
	row := tx.QueryRow("SELECT 1 FROM ContainerConfig WHERE Name=?;", ctr.Name())
422
	if err := row.Scan(&check); err != nil {
423
		if !errors.Is(err, sql.ErrNoRows) {
424
			return fmt.Errorf("checking if container name %s exists in database: %w", ctr.Name(), err)
425
		}
426
	} else if check != 0 {
427
		return fmt.Errorf("name %q is in use: %w", ctr.Name(), define.ErrCtrExists)
428
	}
429

430
	if _, err := tx.Exec("INSERT INTO IDNamespace VALUES (?);", ctr.ID()); err != nil {
431
		return fmt.Errorf("adding container id to database: %w", err)
432
	}
433
	if _, err := tx.Exec("INSERT INTO ContainerConfig VALUES (?, ?, ?, ?);", ctr.ID(), ctr.Name(), podID, configJSON); err != nil {
434
		return fmt.Errorf("adding container config to database: %w", err)
435
	}
436
	if _, err := tx.Exec("INSERT INTO ContainerState VALUES (?, ?, ?, ?);", ctr.ID(), int(ctr.state.State), ctr.state.ExitCode, stateJSON); err != nil {
437
		return fmt.Errorf("adding container state to database: %w", err)
438
	}
439
	for _, dep := range deps {
440
		// Check if the dependency is in the same pod
441
		var depPod sql.NullString
442
		row := tx.QueryRow("SELECT PodID FROM ContainerConfig WHERE ID=?;", dep)
443
		if err := row.Scan(&depPod); err != nil {
444
			if errors.Is(err, sql.ErrNoRows) {
445
				return fmt.Errorf("container dependency %s does not exist in database: %w", dep, define.ErrNoSuchCtr)
446
			}
447
		}
448
		switch {
449
		case ctr.config.Pod == "" && depPod.Valid:
450
			return fmt.Errorf("container dependency %s is part of a pod, but container is not: %w", dep, define.ErrInvalidArg)
451
		case ctr.config.Pod != "" && !depPod.Valid:
452
			return fmt.Errorf("container dependency %s is not part of pod, but this container belongs to pod %s: %w", dep, ctr.config.Pod, define.ErrInvalidArg)
453
		case ctr.config.Pod != "" && depPod.String != ctr.config.Pod:
454
			return fmt.Errorf("container dependency %s is part of pod %s but container is part of pod %s, pods must match: %w", dep, depPod.String, ctr.config.Pod, define.ErrInvalidArg)
455
		}
456

457
		if _, err := tx.Exec("INSERT INTO ContainerDependency VALUES (?, ?);", ctr.ID(), dep); err != nil {
458
			return fmt.Errorf("adding container dependency %s to database: %w", dep, err)
459
		}
460
	}
461
	volMap := make(map[string]bool)
462
	for _, vol := range ctr.config.NamedVolumes {
463
		if _, ok := volMap[vol.Name]; !ok {
464
			if _, err := tx.Exec("INSERT INTO ContainerVolume VALUES (?, ?);", ctr.ID(), vol.Name); err != nil {
465
				return fmt.Errorf("adding container volume %s to database: %w", vol.Name, err)
466
			}
467
			volMap[vol.Name] = true
468
		}
469
	}
470

471
	if err := tx.Commit(); err != nil {
472
		return fmt.Errorf("committing transaction: %w", err)
473
	}
474

475
	return nil
476
}
477

478
// removeContainer remove the specified container from the database.
479
func (s *SQLiteState) removeContainer(ctr *Container) (defErr error) {
480
	tx, err := s.conn.Begin()
481
	if err != nil {
482
		return fmt.Errorf("beginning container %s removal transaction: %w", ctr.ID(), err)
483
	}
484

485
	defer func() {
486
		if defErr != nil {
487
			if err := tx.Rollback(); err != nil {
488
				logrus.Errorf("Rolling back transaction to remove container %s: %v", ctr.ID(), err)
489
			}
490
		}
491
	}()
492

493
	if err := s.removeContainerWithTx(ctr.ID(), tx); err != nil {
494
		return err
495
	}
496

497
	if err := tx.Commit(); err != nil {
498
		return fmt.Errorf("committing container %s removal transaction: %w", ctr.ID(), err)
499
	}
500

501
	return nil
502
}
503

504
// removeContainerWithTx removes the container with the specified transaction.
505
// Callers are responsible for committing.
506
func (s *SQLiteState) removeContainerWithTx(id string, tx *sql.Tx) error {
507
	// TODO TODO TODO:
508
	// Need to verify that at least 1 row was deleted from ContainerConfig.
509
	// Otherwise return ErrNoSuchCtr.
510
	if _, err := tx.Exec("DELETE FROM IDNamespace WHERE ID=?;", id); err != nil {
511
		return fmt.Errorf("removing container %s id from database: %w", id, err)
512
	}
513
	if _, err := tx.Exec("DELETE FROM ContainerConfig WHERE ID=?;", id); err != nil {
514
		return fmt.Errorf("removing container %s config from database: %w", id, err)
515
	}
516
	if _, err := tx.Exec("DELETE FROM ContainerState WHERE ID=?;", id); err != nil {
517
		return fmt.Errorf("removing container %s state from database: %w", id, err)
518
	}
519
	if _, err := tx.Exec("DELETE FROM ContainerDependency WHERE ID=?;", id); err != nil {
520
		return fmt.Errorf("removing container %s dependencies from database: %w", id, err)
521
	}
522
	if _, err := tx.Exec("DELETE FROM ContainerVolume WHERE ContainerID=?;", id); err != nil {
523
		return fmt.Errorf("removing container %s volumes from database: %w", id, err)
524
	}
525
	if _, err := tx.Exec("DELETE FROM ContainerExecSession WHERE ContainerID=?;", id); err != nil {
526
		return fmt.Errorf("removing container %s exec sessions from database: %w", id, err)
527
	}
528
	return nil
529
}
530

531
// networkModify allows you to modify or add a new network, to add a new network use the new bool
532
func (s *SQLiteState) networkModify(ctr *Container, network string, opts types.PerNetworkOptions, new, disconnect bool) error {
533
	if !s.valid {
534
		return define.ErrDBClosed
535
	}
536

537
	if !ctr.valid {
538
		return define.ErrCtrRemoved
539
	}
540

541
	if network == "" {
542
		return fmt.Errorf("network names must not be empty: %w", define.ErrInvalidArg)
543
	}
544

545
	if new && disconnect {
546
		return fmt.Errorf("new and disconnect are mutually exclusive: %w", define.ErrInvalidArg)
547
	}
548

549
	// Grab a fresh copy of the config, in case anything changed
550
	newCfg, err := s.getCtrConfig(ctr.ID())
551
	if err != nil && errors.Is(err, define.ErrNoSuchCtr) {
552
		ctr.valid = false
553
		return define.ErrNoSuchCtr
554
	}
555

556
	_, ok := newCfg.Networks[network]
557
	if new && ok {
558
		return fmt.Errorf("container %s is already connected to network %s: %w", ctr.ID(), network, define.ErrNetworkConnected)
559
	}
560
	if !ok && (!new || disconnect) {
561
		return fmt.Errorf("container %s is not connected to network %s: %w", ctr.ID(), network, define.ErrNoSuchNetwork)
562
	}
563

564
	if !disconnect {
565
		if newCfg.Networks == nil {
566
			newCfg.Networks = make(map[string]types.PerNetworkOptions)
567
		}
568
		newCfg.Networks[network] = opts
569
	} else {
570
		delete(newCfg.Networks, network)
571
	}
572

573
	if err := s.rewriteContainerConfig(ctr, newCfg); err != nil {
574
		return err
575
	}
576

577
	ctr.config = newCfg
578

579
	return nil
580
}
581

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.