File: projects.go

package info (click to toggle)
incus 6.0.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 24,392 kB
  • sloc: sh: 16,313; ansic: 3,121; python: 457; makefile: 337; ruby: 51; sql: 50; lisp: 6
file content (238 lines) | stat: -rw-r--r-- 6,494 bytes parent folder | download | duplicates (4)
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
//go:build linux && cgo && !agent

package cluster

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/lxc/incus/v6/internal/server/db/query"
	"github.com/lxc/incus/v6/shared/api"
	"github.com/lxc/incus/v6/shared/util"
)

// Code generation directives.
//
//generate-database:mapper target projects.mapper.go
//generate-database:mapper reset -i -b "//go:build linux && cgo && !agent"
//
//generate-database:mapper stmt -e project objects
//generate-database:mapper stmt -e project objects-by-Name
//generate-database:mapper stmt -e project objects-by-ID
//generate-database:mapper stmt -e project create struct=Project
//generate-database:mapper stmt -e project id
//generate-database:mapper stmt -e project rename
//generate-database:mapper stmt -e project update struct=Project
//generate-database:mapper stmt -e project delete-by-Name
//
//generate-database:mapper method -i -e project GetMany references=Config
//generate-database:mapper method -i -e project GetOne struct=Project
//generate-database:mapper method -i -e project Exists struct=Project
//generate-database:mapper method -i -e project Create references=Config
//generate-database:mapper method -i -e project ID struct=Project
//generate-database:mapper method -i -e project Rename
//generate-database:mapper method -i -e project DeleteOne-by-Name

// ProjectFeature indicates the behaviour of a project feature.
type ProjectFeature struct {
	// DefaultEnabled
	// Whether the feature should be enabled by default on new projects.
	DefaultEnabled bool

	// CanEnableNonEmpty
	// Whether or not the feature can be changed to enabled on a non-empty project.
	CanEnableNonEmpty bool
}

// ProjectFeatures lists available project features and their behaviours.
var ProjectFeatures = map[string]ProjectFeature{
	"features.images": {
		DefaultEnabled: true,
	},
	"features.profiles": {
		DefaultEnabled: true,
	},
	"features.storage.volumes": {
		DefaultEnabled: true,
	},
	"features.storage.buckets": {
		DefaultEnabled: true,
	},
	"features.networks": {},
	"features.networks.zones": {
		CanEnableNonEmpty: true,
	},
}

// Project represents a project.
type Project struct {
	ID          int
	Description string
	Name        string `db:"omit=update"`
}

// ProjectFilter specifies potential query parameter fields.
type ProjectFilter struct {
	ID   *int
	Name *string `db:"omit=update"` // If non-empty, return only the project with this name.
}

// ToAPI converts the database Project struct to an api.Project entry.
func (p *Project) ToAPI(ctx context.Context, tx *sql.Tx) (*api.Project, error) {
	apiProject := &api.Project{
		ProjectPut: api.ProjectPut{
			Description: p.Description,
		},
		Name: p.Name,
	}

	var err error
	apiProject.Config, err = GetProjectConfig(ctx, tx, p.ID)
	if err != nil {
		return nil, fmt.Errorf("Failed loading project config: %w", err)
	}

	return apiProject, nil
}

// ProjectHasProfiles is a helper to check if a project has the profiles
// feature enabled.
func ProjectHasProfiles(ctx context.Context, tx *sql.Tx, name string) (bool, error) {
	stmt := `
SELECT projects_config.value
  FROM projects_config
  JOIN projects ON projects.id=projects_config.project_id
 WHERE projects.name=? AND projects_config.key='features.profiles'
`
	values, err := query.SelectStrings(ctx, tx, stmt, name)
	if err != nil {
		return false, fmt.Errorf("Fetch project config: %w", err)
	}

	if len(values) == 0 {
		return false, nil
	}

	return util.IsTrue(values[0]), nil
}

// GetProjectNames returns the names of all availablprojects.
func GetProjectNames(ctx context.Context, tx *sql.Tx) ([]string, error) {
	stmt := "SELECT name FROM projects"

	names, err := query.SelectStrings(ctx, tx, stmt)
	if err != nil {
		return nil, fmt.Errorf("Fetch project names: %w", err)
	}

	return names, nil
}

// GetProjectIDsToNames returns a map associating each prect ID to its
// project name.
func GetProjectIDsToNames(ctx context.Context, tx *sql.Tx) (map[int64]string, error) {
	stmt := "SELECT id, name FROM projects"

	rows, err := tx.QueryContext(ctx, stmt)
	if err != nil {
		return nil, err
	}

	defer func() { _ = rows.Close() }()

	result := map[int64]string{}
	for i := 0; rows.Next(); i++ {
		var id int64
		var name string

		err := rows.Scan(&id, &name)
		if err != nil {
			return nil, err
		}

		result[id] = name
	}

	err = rows.Err()
	if err != nil {
		return nil, err
	}

	return result, nil
}

// ProjectHasImages is a helper to check if a project has the images
// feature enabled.
func ProjectHasImages(ctx context.Context, tx *sql.Tx, name string) (bool, error) {
	project, err := GetProject(ctx, tx, name)
	if err != nil {
		return false, fmt.Errorf("fetch project: %w", err)
	}

	config, err := GetProjectConfig(ctx, tx, project.ID)
	if err != nil {
		return false, err
	}

	enabled := util.IsTrue(config["features.images"])

	return enabled, nil
}

// UpdateProject updates the project matching the given key parameters.
func UpdateProject(ctx context.Context, tx *sql.Tx, name string, object api.ProjectPut) error {
	id, err := GetProjectID(ctx, tx, name)
	if err != nil {
		return fmt.Errorf("Fetch project ID: %w", err)
	}

	stmt, err := Stmt(tx, projectUpdate)
	if err != nil {
		return fmt.Errorf("Failed to get \"projectUpdate\" prepared statement: %w", err)
	}

	result, err := stmt.Exec(object.Description, id)
	if err != nil {
		return fmt.Errorf("Update project: %w", err)
	}

	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Fetch affected rows: %w", err)
	}

	if n != 1 {
		return fmt.Errorf("Query updated %d rows instead of 1", n)
	}

	// Clear config.
	_, err = tx.Exec(`
DELETE FROM projects_config WHERE projects_config.project_id = ?
`, id)
	if err != nil {
		return fmt.Errorf("Delete project config: %w", err)
	}

	err = UpdateConfig(ctx, tx, "projects", "project", int(id), object.Config)
	if err != nil {
		return fmt.Errorf("Insert config for project: %w", err)
	}

	return nil
}

// InitProjectWithoutImages populates the images_profiles table with
// all images from the default project when a project is created with
// features.images=false.
func InitProjectWithoutImages(ctx context.Context, tx *sql.Tx, project string) error {
	defaultProfileID, err := GetProfileID(ctx, tx, project, "default")
	if err != nil {
		return fmt.Errorf("Fetch project ID: %w", err)
	}

	stmt := `INSERT INTO images_profiles (image_id, profile_id)
	SELECT images.id, ? FROM images WHERE project_id=1`
	_, err = tx.Exec(stmt, defaultProfileID)
	return err
}