Skip to content

Implement incus image alias nested block #250

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/resources/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ resource "incus_instance" "test1" {
}
```

## Image alias Example

```hcl
resource "incus_image" "alpine" {
source_image = {
remote = "images"
name = "alpine/edge"
}

alias {
name = "alpine"
description = "Alpine Linux"
}

alias {
name = "alpine-edge"
description = "Alpine Linux Edge"
}
}

```

## Argument Reference

* `source_file` - *Optional* - The image file from the local file system from which the image will be created. See reference below.
Expand Down Expand Up @@ -63,6 +85,11 @@ The `source_instance` block supports:

* `snapshot`- *Optional* - Name of the snapshot of the source instance

The `alias` block supports:

* `name` - **Required** - The name of the alias.
* `description` - *Optional* - A description for the alias.

## Attribute Reference

The following attributes are exported:
Expand Down
230 changes: 225 additions & 5 deletions internal/image/resource_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type ImageModel struct {
SourceImage types.Object `tfsdk:"source_image"`
SourceInstance types.Object `tfsdk:"source_instance"`
Aliases types.Set `tfsdk:"aliases"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new syntax, we probably should introduce a breaking change and remove aliases as we now have the alias blocks. @stgraber what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, agreed, we don't want the redundancy

Alias types.Set `tfsdk:"alias"`
Project types.String `tfsdk:"project"`
Remote types.String `tfsdk:"remote"`

Expand Down Expand Up @@ -70,6 +71,11 @@ type SourceInstanceModel struct {
Snapshot types.String `tfsdk:"snapshot"`
}

type ImageAliasModel struct {
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
}

// ImageResource represent Incus cached image resource.
type ImageResource struct {
provider *provider_config.IncusProviderConfig
Expand Down Expand Up @@ -226,6 +232,30 @@ func (r ImageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
ElementType: types.StringType,
},
},

Blocks: map[string]schema.Block{
"alias": schema.SetNestedBlock{
Description: "Image alias",
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
Description: "The name of the image alias.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"description": schema.StringAttribute{
Optional: true,
Description: "The description for the image alias.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
},
},
},
},
}
}

Expand Down Expand Up @@ -338,14 +368,33 @@ func (r ImageResource) Update(ctx context.Context, req resource.UpdateRequest, r
diags = req.State.GetAttribute(ctx, path.Root("aliases"), &newAliases)
resp.Diagnostics.Append(diags...)

// Extract old and new nested alias blocks
newAliasModels, diags := ToAliasModelList(ctx, plan.Alias)
resp.Diagnostics.Append(diags...)

newAliasBlocks, diags := ToAliasBlockList(ctx, plan.Alias)
resp.Diagnostics.Append(diags...)

oldAliasModels := make([]ImageAliasModel, 0, len(plan.Alias.Elements()))
diags = req.State.GetAttribute(ctx, path.Root("alias"), &oldAliasModels)
resp.Diagnostics.Append(diags...)

oldAliasBlocks := make([]string, 0, len(oldAliasModels))
for _, oldAliasModel := range oldAliasModels {
oldAliasBlocks = append(oldAliasBlocks, oldAliasModel.Name.ValueString())
}

if resp.Diagnostics.HasError() {
return
}

removed, added := utils.DiffSlices(oldAliases, newAliases)

// Combine all removals
allRemoved := append(removed, oldAliasBlocks...)

// Delete removed aliases.
for _, alias := range removed {
for _, alias := range allRemoved {
err := server.DeleteImageAlias(alias)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Failed to delete alias %q for cached image with fingerprint %q", alias, imageFingerprint), err.Error())
Expand All @@ -366,6 +415,22 @@ func (r ImageResource) Update(ctx context.Context, req resource.UpdateRequest, r
}
}

// Add new nested alias blocks (with descriptions)
for _, newAliasModel := range newAliasModels {
if utils.ValueInSlice(newAliasModel.Name.ValueString(), newAliasBlocks) {
req := api.ImageAliasesPost{}
req.Name = newAliasModel.Name.ValueString()
req.Description = newAliasModel.Description.ValueString()
req.Target = imageFingerprint

err := server.CreateImageAlias(req)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Failed to create alias %q for cached image with fingerprint %q", newAliasModel.Name.ValueString(), imageFingerprint), err.Error())
return
}
}
}

// Update Terraform state.
diags = r.SyncState(ctx, &resp.State, server, plan)
resp.Diagnostics.Append(diags...)
Expand Down Expand Up @@ -451,21 +516,37 @@ func (r ImageResource) SyncState(ctx context.Context, tfState *tfsdk.State, serv
copiedAliases, diags := ToAliasList(ctx, m.CopiedAliases)
respDiags.Append(diags...)

configAliasBlocks, diags := ToAliasBlockList(ctx, m.Alias)
respDiags.Append(diags...)

// Copy aliases from image state that are present in user defined
// config or are not copied.
var aliases []string
var aliasBlocks []api.ImageAlias
for _, a := range image.Aliases {
if utils.ValueInSlice(a.Name, configAliases) || !utils.ValueInSlice(a.Name, copiedAliases) {
aliases = append(aliases, a.Name)
if utils.ValueInSlice(a.Name, configAliasBlocks) {
aliasBlocks = append(aliasBlocks, a)
} else {
aliases = append(aliases, a.Name)
}
}
}

aliasSet, diags := ToAliasSetType(ctx, aliases)
respDiags.Append(diags...)

copiedAliasesSet, diags := ToAliasSetType(ctx, copiedAliases)
respDiags.Append(diags...)

aliasBlocksSet, diags := ToAliasBlocksSetType(ctx, aliasBlocks)
respDiags.Append(diags...)

m.Fingerprint = types.StringValue(image.Fingerprint)
m.CreatedAt = types.Int64Value(image.CreatedAt.Unix())
m.Aliases = aliasSet
m.CopiedAliases = copiedAliasesSet
m.Alias = aliasBlocksSet

if respDiags.HasError() {
return respDiags
Expand Down Expand Up @@ -568,7 +649,13 @@ func (r ImageResource) createImageFromSourceFile(ctx context.Context, resp *reso
return
}

imageAliases := make([]api.ImageAlias, 0, len(aliases))
aliasModels, diags := ToAliasModelList(ctx, plan.Alias)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

imageAliases := make([]api.ImageAlias, 0, len(aliases)+len(aliasModels))
for _, alias := range aliases {
// Ensure image alias does not already exist.
aliasTarget, _, _ := server.GetImageAlias(alias)
Expand All @@ -583,6 +670,26 @@ func (r ImageResource) createImageFromSourceFile(ctx context.Context, resp *reso

imageAliases = append(imageAliases, ia)
}

for _, aliasModel := range aliasModels {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the same code block in both createImageFromSourceImage and createImageFromSourceInstance. Could you extract this into a function that can be used in all other places?

// Ensure image alias does not already exist.
name := aliasModel.Name.ValueString()
description := aliasModel.Description.ValueString()

aliasTarget, _, _ := server.GetImageAlias(name)
if aliasTarget != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Image alias %q already exists", name), "")
return
}

ia := api.ImageAlias{
Name: name,
Description: description,
}

imageAliases = append(imageAliases, ia)
}

image.Aliases = imageAliases

op, err := server.CreateImage(image, createArgs)
Expand Down Expand Up @@ -690,7 +797,13 @@ func (r ImageResource) createImageFromSourceImage(ctx context.Context, resp *res
return
}

imageAliases := make([]api.ImageAlias, 0, len(aliases))
aliasModels, diags := ToAliasModelList(ctx, plan.Alias)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

imageAliases := make([]api.ImageAlias, 0, len(aliases)+len(aliasModels))
for _, alias := range aliases {
// Ensure image alias does not already exist.
aliasTarget, _, _ := server.GetImageAlias(alias)
Expand All @@ -706,6 +819,25 @@ func (r ImageResource) createImageFromSourceImage(ctx context.Context, resp *res
imageAliases = append(imageAliases, ia)
}

for _, aliasModel := range aliasModels {
// Ensure image alias does not already exist.
name := aliasModel.Name.ValueString()
description := aliasModel.Description.ValueString()

aliasTarget, _, _ := server.GetImageAlias(name)
if aliasTarget != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Image alias %q already exists", name), "")
return
}

ia := api.ImageAlias{
Name: name,
Description: description,
}

imageAliases = append(imageAliases, ia)
}

// Get data about remote image (also checks if image exists).
imageInfo, _, err := imageServer.GetImage(image)
if err != nil {
Expand Down Expand Up @@ -804,7 +936,14 @@ func (r ImageResource) createImageFromSourceInstance(ctx context.Context, resp *
return
}

imageAliases := make([]api.ImageAlias, 0, len(aliases))
aliasModels, diags := ToAliasModelList(ctx, plan.Alias)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

imageAliases := make([]api.ImageAlias, 0, len(aliases)+len(aliasModels))
for _, alias := range aliases {
// Ensure image alias does not already exist.
aliasTarget, _, _ := server.GetImageAlias(alias)
Expand All @@ -820,6 +959,25 @@ func (r ImageResource) createImageFromSourceInstance(ctx context.Context, resp *
imageAliases = append(imageAliases, ia)
}

for _, aliasModel := range aliasModels {
// Ensure image alias does not already exist.
name := aliasModel.Name.ValueString()
description := aliasModel.Description.ValueString()

aliasTarget, _, _ := server.GetImageAlias(name)
if aliasTarget != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Image alias %q already exists", name), "")
return
}

ia := api.ImageAlias{
Name: name,
Description: description,
}

imageAliases = append(imageAliases, ia)
}

var source *api.ImagesPostSource
if !sourceInstanceModel.Snapshot.IsNull() {
snapsnotName := sourceInstanceModel.Snapshot.ValueString()
Expand Down Expand Up @@ -885,6 +1043,68 @@ func ToAliasSetType(ctx context.Context, aliases []string) (types.Set, diag.Diag
return types.SetValueFrom(ctx, types.StringType, aliases)
}

// ToAliasModelList converts image alias blocks from types.Set into
// a list of ImageAliasModel.
func ToAliasModelList(ctx context.Context, aliasSet types.Set) ([]ImageAliasModel, diag.Diagnostics) {
if aliasSet.IsNull() || aliasSet.IsUnknown() {
return []ImageAliasModel{}, nil
}

aliasModels := make([]ImageAliasModel, 0, len(aliasSet.Elements()))
diags := aliasSet.ElementsAs(ctx, &aliasModels, false)
if diags.HasError() {
return nil, diags
}

return aliasModels, diags
}

// ToAliasBlockList converts alias of type types.Set into a slice of API ImageAlias.
func ToAliasBlockList(ctx context.Context, aliasSet types.Set) ([]string, diag.Diagnostics) {
if aliasSet.IsNull() || aliasSet.IsUnknown() {
return []string{}, nil
}

modelAliases := make([]ImageAliasModel, 0, len(aliasSet.Elements()))
diags := aliasSet.ElementsAs(ctx, &modelAliases, false)
if diags.HasError() {
return nil, diags
}

aliasBlocks := make([]string, 0, len(modelAliases))
for _, a := range modelAliases {
aliasBlocks = append(aliasBlocks, a.Name.ValueString())
}

return aliasBlocks, diags
}

// ToAliasBlocksSetType converts slice of strings into aliases of type types.Set.
func ToAliasBlocksSetType(ctx context.Context, aliases []api.ImageAlias) (types.Set, diag.Diagnostics) {
aliasList := make([]ImageAliasModel, 0, len(aliases))

for _, a := range aliases {
alias := ImageAliasModel{
Name: types.StringValue(a.Name),
}

if a.Description != "" {
alias.Description = types.StringValue(a.Description)
} else {
alias.Description = types.StringValue("")
}

aliasList = append(aliasList, alias)
}

aliasType := map[string]attr.Type{
"name": types.StringType,
"description": types.StringType,
}

return types.SetValueFrom(ctx, types.ObjectType{AttrTypes: aliasType}, aliasList)
}

// createImageResourceID creates new image ID by concatenating remote and
// image fingerprint using colon.
func createImageResourceID(remote string, fingerprint string) string {
Expand Down
Loading