Skip to content

Commit

Permalink
Merge pull request #4 from imjasonh/append2
Browse files Browse the repository at this point in the history
implement basic `oci_append`
  • Loading branch information
imjasonh committed Apr 26, 2023
2 parents 146d5b4 + 088462e commit f82273d
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 120 deletions.
1 change: 1 addition & 0 deletions docs/data-sources/ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ Image ref data source

- `digest` (String) Image digest of the image.
- `id` (String) Fully qualified image digest of the image.
- `tag` (String) Image tag of the image.


25 changes: 22 additions & 3 deletions docs/resources/append.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,43 @@
page_title: "oci_append Resource - terraform-provider-oci"
subcategory: ""
description: |-
Image append resource
Append layers to an existing image.
---

# oci_append (Resource)

Image append resource
Append layers to an existing image.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `layers` (Attributes List) Layers to append to the base image. (see [below for nested schema](#nestedatt--layers))

### Optional

- `base_image` (String) Base image to append layers to.

### Read-Only

- `id` (String) Fully qualified image digest of the mutated image.
- `id` (String) The resulting fully-qualified digest (e.g. {repo}@sha256:deadbeef).
- `image_ref` (String) The resulting fully-qualified digest (e.g. {repo}@sha256:deadbeef).

<a id="nestedatt--layers"></a>
### Nested Schema for `layers`

Required:

- `files` (Attributes Map) Files to add to the layer. (see [below for nested schema](#nestedatt--layers--files))

<a id="nestedatt--layers--files"></a>
### Nested Schema for `layers.files`

Required:

- `contents` (String) Content of the file.


196 changes: 147 additions & 49 deletions internal/provider/append_resource.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package provider

import (
"archive/tar"
"bytes"
"context"
"fmt"
"net/http"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/static"
ggcrtypes "github.com/google/go-containerregistry/pkg/v1/types"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
Expand All @@ -23,18 +33,15 @@ func NewAppendResource() resource.Resource {
}

// AppendResource defines the resource implementation.
type AppendResource struct {
client *http.Client
}
type AppendResource struct{}

// AppendResourceModel describes the resource data model.
type AppendResourceModel struct {
// TODO: layer(s) to append

// Id is the output image digest.
Id types.String `tfsdk:"id"`
Id types.String `tfsdk:"id"`
ImageRef types.String `tfsdk:"image_ref"`

BaseImage types.String `tfsdk:"base_image"`
Layers types.List `tfsdk:"layers"`
}

func (r *AppendResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
Expand All @@ -43,20 +50,53 @@ func (r *AppendResource) Metadata(ctx context.Context, req resource.MetadataRequ

func (r *AppendResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Image append resource",

MarkdownDescription: "Append layers to an existing image.",
Attributes: map[string]schema.Attribute{
"base_image": schema.StringAttribute{
MarkdownDescription: "Base image to append layers to.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("cgr.dev/chainguard/static:latest"),
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"layers": schema.ListNestedAttribute{
MarkdownDescription: "Layers to append to the base image.",
Optional: false,
Required: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.RequiresReplace(),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"files": schema.MapNestedAttribute{
MarkdownDescription: "Files to add to the layer.",
Optional: false,
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"contents": schema.StringAttribute{
MarkdownDescription: "Content of the file.",
Optional: false,
Required: true,
},
// TODO: Add support for file mode.
// TODO: Add support for symlinks.
// TODO: Add support for deletion / whiteouts.
},
},
},
},
},
},
"image_ref": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The resulting fully-qualified digest (e.g. {repo}@sha256:deadbeef).",
},

"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Fully qualified image digest of the mutated image.",
MarkdownDescription: "The resulting fully-qualified digest (e.g. {repo}@sha256:deadbeef).",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Expand All @@ -70,81 +110,69 @@ func (r *AppendResource) Configure(ctx context.Context, req resource.ConfigureRe
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*http.Client)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

func (r *AppendResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *AppendResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// TODO: do the appending and update data.Id.

// For the purposes of this example code, hardcoding a response value to
// save into the Terraform state.
data.Id = types.StringValue("TODO") // TODO

// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, "created a resource")
digest, diag := doAppend(ctx, data)
if diag.HasError() {
resp.Diagnostics.Append(diag...)
return
}
data.Id = types.StringValue(digest.String())
data.ImageRef = types.StringValue(digest.String())

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *AppendResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *AppendResourceModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

digest, diag := doAppend(ctx, data)
if diag.HasError() {
resp.Diagnostics.Append(diag...)
return
}

data.Id = types.StringValue(digest.String())
data.ImageRef = types.StringValue(digest.String())

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *AppendResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *AppendResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

// TODO: do the appending and update data.Id.
digest, diag := doAppend(ctx, data)
if diag.HasError() {
resp.Diagnostics.Append(diag...)
return
}

data.Id = types.StringValue(digest.String())
data.ImageRef = types.StringValue(digest.String())

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *AppendResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *AppendResourceModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}
Expand All @@ -155,3 +183,73 @@ func (r *AppendResource) Delete(ctx context.Context, req resource.DeleteRequest,
func (r *AppendResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func doAppend(ctx context.Context, data *AppendResourceModel) (*name.Digest, diag.Diagnostics) {
baseref, err := name.ParseReference(data.BaseImage.ValueString())
if err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to parse base image", fmt.Sprintf("Unable to parse base image %q, got error: %s", data.BaseImage.ValueString(), err))}
}
img, err := remote.Image(baseref,
remote.WithContext(ctx),
remote.WithAuthFromKeychain(authn.DefaultKeychain),
)
if err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to fetch base image", fmt.Sprintf("Unable to fetch base image %q, got error: %s", data.BaseImage.ValueString(), err))}
}

var ls []struct {
Files map[string]struct {
Contents types.String `tfsdk:"contents"`
} `tfsdk:"files"`
}
if diag := data.Layers.ElementsAs(ctx, &ls, false); diag.HasError() {
return nil, diag.Errors()
}

adds := []mutate.Addendum{}
for _, l := range ls {
var b bytes.Buffer
tw := tar.NewWriter(&b)
for name, f := range l.Files {
if err := tw.WriteHeader(&tar.Header{
Name: name,
Size: int64(len(f.Contents.ValueString())),
}); err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to write tar header", fmt.Sprintf("Unable to write tar header for %q, got error: %s", name, err))}
}
if _, err := tw.Write([]byte(f.Contents.ValueString())); err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to write tar contents", fmt.Sprintf("Unable to write tar contents for %q, got error: %s", name, err))}
}
}
if err := tw.Close(); err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to close tar writer", fmt.Sprintf("Unable to close tar writer, got error: %s", err))}
}

adds = append(adds, mutate.Addendum{
Layer: static.NewLayer(b.Bytes(), ggcrtypes.OCILayer),
History: v1.History{CreatedBy: "terraform-provider-oci: oci_append"},
MediaType: ggcrtypes.OCILayer,
})
}

img, err = mutate.Append(img, adds...)
if err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to append layers", fmt.Sprintf("Unable to append layers, got error: %s", err))}
}
if err := remote.Write(baseref, img,
remote.WithContext(ctx),
remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to push image", fmt.Sprintf("Unable to push image, got error: %s", err))}
}
dig, err := img.Digest()
if err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to get image digest", fmt.Sprintf("Unable to get image digest, got error: %s", err))}
}

// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, "created a resource")

d := baseref.Context().Digest(dig.String())
return &d, []diag.Diagnostic{}
}
Loading

0 comments on commit f82273d

Please sign in to comment.