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 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
|
package project
import (
"context"
"fmt"
"slices"
"strings"
"github.com/lxc/incus/v6/internal/server/db"
"github.com/lxc/incus/v6/internal/server/db/cluster"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/util"
)
// separator is used to delimit the project name from the suffix.
const separator = "_"
// projectLimitDiskPool is the prefix used for pool-specific disk limits.
var projectLimitDiskPool = "limits.disk.pool."
// Instance adds the "<project>_" prefix to instance name when the given project name is not "default".
func Instance(projectName string, instanceName string) string {
if projectName != api.ProjectDefaultName {
return fmt.Sprintf("%s%s%s", projectName, separator, instanceName)
}
return instanceName
}
// DNS adds ".<project>" as a suffix to instance name when the given project name is not "default".
func DNS(projectName string, instanceName string) string {
if projectName != api.ProjectDefaultName {
return fmt.Sprintf("%s.%s", instanceName, projectName)
}
return instanceName
}
// InstanceParts takes a project prefixed Instance name string and returns the project and instance name.
// If a non-project prefixed Instance name is supplied, then the project is returned as "default" and the instance
// name is returned unmodified in the 2nd return value. This is suitable for passing back into Instance().
// Note: This should only be used with Instance names (because they cannot contain the project separator) and this
// function relies on this rule as project names can contain the project separator.
func InstanceParts(projectInstanceName string) (string, string) {
i := strings.LastIndex(projectInstanceName, separator)
if i < 0 {
// This string is not project prefixed or is part of default project.
return api.ProjectDefaultName, projectInstanceName
}
// As project names can container separator, we effectively split once from the right hand side as
// Instance names are not allowed to container the separator value.
return projectInstanceName[0:i], projectInstanceName[i+1:]
}
// StorageVolume adds the "<project>_prefix" to the storage volume name. Even if the project name is "default".
func StorageVolume(projectName string, storageVolumeName string) string {
return fmt.Sprintf("%s%s%s", projectName, separator, storageVolumeName)
}
// StorageVolumeParts takes a project prefixed storage volume name and returns the project and storage volume
// name as separate variables.
func StorageVolumeParts(projectStorageVolumeName string) (string, string) {
parts := strings.SplitN(projectStorageVolumeName, "_", 2)
// If the given name doesn't contain any project, only return the volume name.
if len(parts) == 1 {
return "", projectStorageVolumeName
}
return parts[0], parts[1]
}
// StorageVolumeProject returns the project name to use to for the volume based on the requested project.
// For image volume types the default project is always returned.
// For custom volume type, if the project specified has the "features.storage.volumes" flag enabled then the
// project name is returned, otherwise the default project name is returned.
// For all other volume types the supplied project name is returned.
func StorageVolumeProject(c *db.Cluster, projectName string, volumeType int) (string, error) {
// Image volumes are effectively a cache and so are always linked to default project.
// Optimisation to avoid loading project record.
if volumeType == db.StoragePoolVolumeTypeImage {
return api.ProjectDefaultName, nil
}
var project *api.Project
err := c.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
dbProject, err := cluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return err
}
project, err = dbProject.ToAPI(ctx, tx.Tx())
return err
})
if err != nil {
return "", fmt.Errorf("Failed to load project %q: %w", projectName, err)
}
return StorageVolumeProjectFromRecord(project, volumeType), nil
}
// StorageVolumeProjectFromRecord returns the project name to use to for the volume based on the supplied project.
// For image volume types the default project is always returned.
// For custom volume type, if the project supplied has the "features.storage.volumes" flag enabled then the
// project name is returned, otherwise the default project name is returned.
// For all other volume types the supplied project's name is returned.
func StorageVolumeProjectFromRecord(p *api.Project, volumeType int) string {
// Image volumes are effectively a cache and so are always linked to default project.
if volumeType == db.StoragePoolVolumeTypeImage {
return api.ProjectDefaultName
}
// Non-custom volumes always use the project specified.
if volumeType != db.StoragePoolVolumeTypeCustom {
return p.Name
}
// Custom volumes only use the project specified if the project has the features.storage.volumes feature
// enabled, otherwise the legacy behaviour of using the default project for custom volumes is used.
if util.IsTrue(p.Config["features.storage.volumes"]) {
return p.Name
}
return api.ProjectDefaultName
}
// StorageBucket adds the "<project>_prefix" to the storage bucket name. Even if the project name is "default".
func StorageBucket(projectName string, storageBucketName string) string {
return fmt.Sprintf("%s%s%s", projectName, separator, storageBucketName)
}
// StorageBucketProject returns the effective project name to use to for the bucket based on the requested project.
// If the project specified has the "features.storage.buckets" flag enabled then the project name is returned,
// otherwise the default project name is returned.
func StorageBucketProject(ctx context.Context, c *db.Cluster, projectName string) (string, error) {
var p *api.Project
err := c.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
dbProject, err := cluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return err
}
p, err = dbProject.ToAPI(ctx, tx.Tx())
return err
})
if err != nil {
return "", fmt.Errorf("Failed to load project %q: %w", projectName, err)
}
return StorageBucketProjectFromRecord(p), nil
}
// StorageBucketProjectFromRecord returns the project name to use to for the bucket based on the supplied project.
// If the project supplied has the "features.storage.buckets" flag enabled then the project name is returned,
// otherwise the default project name is returned.
func StorageBucketProjectFromRecord(p *api.Project) string {
// Buckets only use the project specified if the project has the features.storage.buckets feature
// enabled, otherwise the default project is used.
if util.IsTrue(p.Config["features.storage.buckets"]) {
return p.Name
}
return api.ProjectDefaultName
}
// NetworkProject returns the effective project name to use for the network based on the requested project.
// If the requested project has the "features.networks" flag enabled then the requested project's name is returned,
// otherwise the default project name is returned.
// The second return value is always the requested project's info.
func NetworkProject(c *db.Cluster, projectName string) (string, *api.Project, error) {
var p *api.Project
err := c.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
dbProject, err := cluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return err
}
p, err = dbProject.ToAPI(ctx, tx.Tx())
return err
})
if err != nil {
return "", nil, fmt.Errorf("Failed to load project %q: %w", projectName, err)
}
effectiveProjectName := NetworkProjectFromRecord(p)
return effectiveProjectName, p, nil
}
// NetworkProjectFromRecord returns the project name to use for the network based on the supplied project.
// If the project supplied has the "features.networks" flag enabled then the project name is returned,
// otherwise the default project name is returned.
func NetworkProjectFromRecord(p *api.Project) string {
// Networks only use the project specified if the project has the features.networks feature enabled,
// otherwise the legacy behaviour of using the default project for networks is used.
if util.IsTrue(p.Config["features.networks"]) {
return p.Name
}
return api.ProjectDefaultName
}
// NetworkAllowed returns whether access is allowed to a particular network based on projectConfig.
func NetworkAllowed(reqProjectConfig map[string]string, networkName string, isManaged bool) bool {
// If project is not restricted, then access to network is allowed.
if util.IsFalseOrEmpty(reqProjectConfig["restricted"]) {
return true
}
// If project has no access to NIC devices then also block access to all networks.
if reqProjectConfig["restricted.devices.nic"] == "block" {
return false
}
// Don't allow access to unmanaged networks if only managed network access is allowed.
if slices.Contains([]string{"managed", ""}, reqProjectConfig["restricted.devices.nic"]) && !isManaged {
return false
}
// If restricted.networks.access is not set then allow access to all networks.
if reqProjectConfig["restricted.networks.access"] == "" {
return true
}
// Check if reqquested network is in list of allowed networks.
allowedRestrictedNetworks := util.SplitNTrimSpace(reqProjectConfig["restricted.networks.access"], ",", -1, false)
return slices.Contains(allowedRestrictedNetworks, networkName)
}
// NetworkIntegrationAllowed returns whether access is allowed for a particular network integration based on projectConfig.
func NetworkIntegrationAllowed(reqProjectConfig map[string]string, integrationName string) bool {
// If project is not restricted, then access to network is allowed.
if util.IsFalseOrEmpty(reqProjectConfig["restricted"]) {
return true
}
// If restricted.networks.integrations is not set then allow access to all networks.
if reqProjectConfig["restricted.networks.integrations"] == "" {
return true
}
// Check if reqquested integration is in list of allowed network integrations.
allowedRestrictedIntegrations := util.SplitNTrimSpace(reqProjectConfig["restricted.networks.integrations"], ",", -1, false)
return slices.Contains(allowedRestrictedIntegrations, integrationName)
}
// ImageProjectFromRecord returns the project name to use for the image based on the supplied project.
// If the project supplied has the "features.images" flag enabled then the project name is returned,
// otherwise the default project name is returned.
func ImageProjectFromRecord(p *api.Project) string {
// Images only use the project specified if the project has the features.images feature enabled,
// otherwise the default project for profiles is used.
if util.IsTrue(p.Config["features.images"]) {
return p.Name
}
return api.ProjectDefaultName
}
// ProfileProject returns the effective project to use for the profile based on the requested project.
// If the requested project has the "features.profiles" flag enabled then the requested project's info is returned,
// otherwise the default project's info is returned.
func ProfileProject(c *db.Cluster, projectName string) (*api.Project, error) {
var p *api.Project
err := c.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
dbProject, err := cluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return fmt.Errorf("Failed loading project %q: %w", projectName, err)
}
p, err = dbProject.ToAPI(ctx, tx.Tx())
if err != nil {
return fmt.Errorf("Failed loading config for project %q: %w", projectName, err)
}
effectiveProjectName := ProfileProjectFromRecord(p)
if effectiveProjectName == api.ProjectDefaultName {
dbProject, err = cluster.GetProject(ctx, tx.Tx(), effectiveProjectName)
if err != nil {
return fmt.Errorf("Failed loading project %q: %w", effectiveProjectName, err)
}
}
p, err = dbProject.ToAPI(ctx, tx.Tx())
if err != nil {
return fmt.Errorf("Failed loading config for project %q: %w", dbProject.Name, err)
}
return nil
})
if err != nil {
return nil, err
}
return p, nil
}
// ProfileProjectFromRecord returns the project name to use for the profile based on the supplied project.
// If the project supplied has the "features.profiles" flag enabled then the project name is returned,
// otherwise the default project name is returned.
func ProfileProjectFromRecord(p *api.Project) string {
// Profiles only use the project specified if the project has the features.profiles feature enabled,
// otherwise the default project for profiles is used.
if util.IsTrue(p.Config["features.profiles"]) {
return p.Name
}
return api.ProjectDefaultName
}
// NetworkZoneProject returns the effective project name to use for network zone based on the requested project.
// If the requested project has the "features.networks.zones" flag enabled then the requested project's name is
// returned, otherwise the default project name is returned.
// The second return value is always the requested project's info.
func NetworkZoneProject(c *db.Cluster, projectName string) (string, *api.Project, error) {
var p *api.Project
err := c.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
dbProject, err := cluster.GetProject(ctx, tx.Tx(), projectName)
if err != nil {
return err
}
p, err = dbProject.ToAPI(ctx, tx.Tx())
return err
})
if err != nil {
return "", nil, fmt.Errorf("Failed to load project %q: %w", projectName, err)
}
effectiveProjectName := NetworkZoneProjectFromRecord(p)
return effectiveProjectName, p, nil
}
// NetworkZoneProjectFromRecord returns the project name to use for the network zone based on the supplied project.
// If the project supplied has the "features.networks.zones" flag enabled then the project name is returned,
// otherwise the default project name is returned.
func NetworkZoneProjectFromRecord(p *api.Project) string {
// Network zones only use the project specified if the project has the features.networks.zones feature
// enabled, otherwise the legacy behaviour of using the default project for network zones is used.
if util.IsTrue(p.Config["features.networks.zones"]) {
return p.Name
}
return api.ProjectDefaultName
}
|