Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for all-projects to incus image list and API #400

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions client/incus_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ func (r *ProtocolIncus) GetImages() ([]api.Image, error) {
return images, nil
}

// GetImagesAllProjects returns a list of images across all projects as Image structs.
func (r *ProtocolIncus) GetImagesAllProjects() ([]api.Image, error) {
images := []api.Image{}

v := url.Values{}
v.Set("recursion", "1")
v.Set("all-projects", "true")

if !r.HasExtension("images_all_projects") {
return nil, fmt.Errorf("The server is missing the required \"images_all_projects\" API extension")
}

_, err := r.queryStruct("GET", fmt.Sprintf("/images?%s", v.Encode()), nil, "", &images)
if err != nil {
return nil, err
}

return images, nil
}

// GetImagesWithFilter returns a filtered list of available images as Image structs.
func (r *ProtocolIncus) GetImagesWithFilter(filters []string) ([]api.Image, error) {
if !r.HasExtension("api_filtering") {
Expand Down
1 change: 1 addition & 0 deletions client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type ImageServer interface {

// Image handling functions
GetImages() (images []api.Image, err error)
GetImagesAllProjects() (images []api.Image, err error)
GetImageFingerprints() (fingerprints []string, err error)
GetImagesWithFilter(filters []string) (images []api.Image, err error)

Expand Down
5 changes: 5 additions & 0 deletions client/simplestreams_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ func (r *ProtocolSimpleStreams) GetImages() ([]api.Image, error) {
return r.ssClient.ListImages()
}

// GetImagesAllProjects returns a list of available images as Image structs.
func (r *ProtocolSimpleStreams) GetImagesAllProjects() ([]api.Image, error) {
return r.GetImages()
}

// GetImageFingerprints returns a list of available image fingerprints.
func (r *ProtocolSimpleStreams) GetImageFingerprints() ([]string, error) {
// Get all the images from simplestreams
Expand Down
50 changes: 37 additions & 13 deletions cmd/incus/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -1061,8 +1061,9 @@ type cmdImageList struct {
global *cmdGlobal
image *cmdImage

flagFormat string
flagColumns string
flagFormat string
flagColumns string
flagAllProjects bool
}

func (c *cmdImageList) Command() *cobra.Command {
Expand Down Expand Up @@ -1090,13 +1091,15 @@ Column shorthand chars:
F - Fingerprint (long)
p - Whether image is public
d - Description
e - Project
a - Architecture
s - Size
u - Upload date
t - Type`))

cmd.Flags().StringVarP(&c.flagColumns, "columns", "c", "lfpdatsu", i18n.G("Columns")+"``")
cmd.Flags().StringVarP(&c.flagColumns, "columns", "c", defaultImagesColumns, i18n.G("Columns")+"``")
cmd.Flags().StringVarP(&c.flagFormat, "format", "f", "table", i18n.G("Format (csv|json|table|yaml|compact)")+"``")
cmd.Flags().BoolVar(&c.flagAllProjects, "all-projects", false, i18n.G("Display images from all projects"))
cmd.RunE = c.Run

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
Expand All @@ -1110,23 +1113,33 @@ Column shorthand chars:
return cmd
}

const defaultImagesColumns = "lfpdatsu"
const defaultImagesColumnsAllProjects = "elfpdatsu"

func (c *cmdImageList) parseColumns() ([]imageColumn, error) {
columnsShorthandMap := map[rune]imageColumn{
'l': {i18n.G("ALIAS"), c.aliasColumnData},
'L': {i18n.G("ALIASES"), c.aliasesColumnData},
'a': {i18n.G("ARCHITECTURE"), c.architectureColumnData},
'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData},
'e': {i18n.G("PROJECT"), c.projectColumnData},
'f': {i18n.G("FINGERPRINT"), c.fingerprintColumnData},
'F': {i18n.G("FINGERPRINT"), c.fingerprintFullColumnData},
'l': {i18n.G("ALIAS"), c.aliasColumnData},
'L': {i18n.G("ALIASES"), c.aliasesColumnData},
'p': {i18n.G("PUBLIC"), c.publicColumnData},
'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData},
'a': {i18n.G("ARCHITECTURE"), c.architectureColumnData},
's': {i18n.G("SIZE"), c.sizeColumnData},
'u': {i18n.G("UPLOAD DATE"), c.uploadDateColumnData},
't': {i18n.G("TYPE"), c.typeColumnData},
'u': {i18n.G("UPLOAD DATE"), c.uploadDateColumnData},
}

columnList := strings.Split(c.flagColumns, ",")

columns := []imageColumn{}
// Add project column if --all-projects flag specified and
// no --c was passed
if c.flagAllProjects && c.flagColumns == defaultImagesColumns {
c.flagColumns = defaultImagesColumnsAllProjects
}

for _, columnEntry := range columnList {
if columnEntry == "" {
return nil, fmt.Errorf(i18n.G("Empty column entry (redundant, leading or trailing command) in '%s'"), c.flagColumns)
Expand Down Expand Up @@ -1184,6 +1197,10 @@ func (c *cmdImageList) descriptionColumnData(image api.Image) string {
return c.findDescription(image.Properties)
}

func (c *cmdImageList) projectColumnData(image api.Image) string {
return image.Project
}

func (c *cmdImageList) architectureColumnData(image api.Image) string {
return image.Architecture
}
Expand Down Expand Up @@ -1338,15 +1355,22 @@ func (c *cmdImageList) Run(cmd *cobra.Command, args []string) error {

serverFilters, clientFilters := getServerSupportedFilters(filters, api.Image{})

var images []api.Image
allImages, err := remoteServer.GetImagesWithFilter(serverFilters)
if err != nil {
allImages, err = remoteServer.GetImages()
var allImages, images []api.Image
if c.flagAllProjects {
allImages, err = remoteServer.GetImagesAllProjects()
if err != nil {
return err
}
} else {
allImages, err = remoteServer.GetImagesWithFilter(serverFilters)
if err != nil {
allImages, err = remoteServer.GetImages()
if err != nil {
return err
}

clientFilters = filters
clientFilters = filters
}
}

for _, image := range allImages {
Expand Down
59 changes: 49 additions & 10 deletions cmd/incusd/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -1339,30 +1339,46 @@ func getImageMetadata(fname string) (*api.ImageMetadata, string, error) {
return &result, imageType, nil
}

func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectName string, public bool, clauses *filter.ClauseSet, hasPermission auth.PermissionChecker) (any, error) {
func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectName string, public bool, clauses *filter.ClauseSet, hasPermission auth.PermissionChecker, allProjects bool) (any, error) {
mustLoadObjects := recursion || (clauses != nil && len(clauses.Clauses) > 0)

fingerprints, err := tx.GetImagesFingerprints(ctx, projectName, public)
if err != nil {
return err, err
imagesProjectsMap := map[string][]string{}
if allProjects {
var err error

imagesProjectsMap, err = tx.GetImages(ctx)
if err != nil {
return nil, err
}
} else {
fingerprints, err := tx.GetImagesFingerprints(ctx, projectName, public)
if err != nil {
return nil, err
}

for _, fp := range fingerprints {
imagesProjectsMap[fp] = []string{projectName}
}
}

var resultString []string
var resultMap []*api.Image

if recursion {
resultMap = make([]*api.Image, 0, len(fingerprints))
resultMap = make([]*api.Image, 0, len(imagesProjectsMap))
} else {
resultString = make([]string, 0, len(fingerprints))
resultString = make([]string, 0, len(imagesProjectsMap))
}

for _, fingerprint := range fingerprints {
image, err := doImageGet(ctx, tx, projectName, fingerprint, public)
for fingerprint, projects := range imagesProjectsMap {
curProjectName := projects[0]

image, err := doImageGet(ctx, tx, curProjectName, fingerprint, public)
if err != nil {
continue
}

if !image.Public && !hasPermission(auth.ObjectImage(projectName, fingerprint)) {
if !image.Public && !hasPermission(auth.ObjectImage(curProjectName, fingerprint)) {
continue
}

Expand Down Expand Up @@ -1415,6 +1431,10 @@ func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectN
// description: Collection filter
// type: string
// example: default
// - in: query
// name: all-projects
// description: Retrieve images from all projects
// type: boolean
// responses:
// "200":
// description: API endpoints
Expand Down Expand Up @@ -1469,6 +1489,10 @@ func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectN
// description: Collection filter
// type: string
// example: default
// - in: query
// name: all-projects
// description: Retrieve images from all projects
// type: boolean
// responses:
// "200":
// description: API endpoints
Expand Down Expand Up @@ -1518,6 +1542,10 @@ func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectN
// description: Collection filter
// type: string
// example: default
// - in: query
// name: all-projects
// description: Retrieve images from all projects
// type: boolean
// responses:
// "200":
// description: API endpoints
Expand Down Expand Up @@ -1572,6 +1600,11 @@ func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectN
// description: Collection filter
// type: string
// example: default
// - in: query
// name: all-projects
// description: Retrieve images from all projects
// type: boolean
// example: default
// responses:
// "200":
// description: API endpoints
Expand Down Expand Up @@ -1602,8 +1635,14 @@ func doImagesGet(ctx context.Context, tx *db.ClusterTx, recursion bool, projectN
// $ref: "#/responses/InternalServerError"
func imagesGet(d *Daemon, r *http.Request) response.Response {
projectName := request.ProjectParam(r)
allProjects := util.IsTrue(r.FormValue("all-projects"))
filterStr := r.FormValue("filter")

// ProjectParam returns default if not set
if allProjects && projectName != api.ProjectDefaultName {
return response.BadRequest(fmt.Errorf("Cannot specify a project when requesting all projects"))
}

s := d.State()

hasPermission, authorizationErr := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanView, auth.ObjectTypeImage)
Expand All @@ -1620,7 +1659,7 @@ func imagesGet(d *Daemon, r *http.Request) response.Response {

var result any
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
result, err = doImagesGet(ctx, tx, localUtil.IsRecursionRequest(r), projectName, public, clauses, hasPermission)
result, err = doImagesGet(ctx, tx, localUtil.IsRecursionRequest(r), projectName, public, clauses, hasPermission, allProjects)
if err != nil {
return err
}
Expand Down
4 changes: 4 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1901,6 +1901,10 @@ client authentication.

Adds ability to copy image to a project different from the source.

## `images_all_projects`

This adds support for listing images across all projects through the `all-projects` parameter on the `GET /1.0/images`API.

## `cluster_migration_inconsistent_copy`

Adds `allow_inconsistent` field to `POST /1.0/instances/<name>`. Set to `true` to allow inconsistent copying between cluster
Expand Down
22 changes: 22 additions & 0 deletions doc/rest-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,11 @@ definitions:
type: string
type: array
x-go-name: Profiles
project:
description: Project name
example: project1
type: string
x-go-name: Project
properties:
additionalProperties:
type: string
Expand Down Expand Up @@ -7304,6 +7309,10 @@ paths:
in: query
name: filter
type: string
- description: Retrieve images from all projects
in: query
name: all-projects
type: boolean
produces:
- application/json
responses:
Expand Down Expand Up @@ -8055,6 +8064,10 @@ paths:
in: query
name: filter
type: string
- description: Retrieve images from all projects
in: query
name: all-projects
type: boolean
produces:
- application/json
responses:
Expand Down Expand Up @@ -8143,6 +8156,10 @@ paths:
in: query
name: filter
type: string
- description: Retrieve images from all projects
in: query
name: all-projects
type: boolean
produces:
- application/json
responses:
Expand Down Expand Up @@ -8191,6 +8208,11 @@ paths:
in: query
name: filter
type: string
- description: Retrieve images from all projects
example: default
in: query
name: all-projects
type: boolean
produces:
- application/json
responses:
Expand Down
1 change: 1 addition & 0 deletions internal/server/db/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ func (c *ClusterTx) GetImageByFingerprintPrefix(ctx context.Context, fingerprint
image.Cached = object.Cached
image.Public = object.Public
image.AutoUpdate = object.AutoUpdate
image.Project = object.Project

err = c.imageFill(
ctx, object.ID, &image,
Expand Down
1 change: 1 addition & 0 deletions internal/version/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ var APIExtensions = []string{
"projects_restricted_intercept",
"metrics_authentication",
"images_target_project",
"images_all_projects",
"cluster_migration_inconsistent_copy",
"cluster_ovn_chassis",
"container_syscall_intercept_sched_setscheduler",
Expand Down