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 a "path" field to oci_append #164

Merged
merged 1 commit into from
Aug 14, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/resources/append.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Required:
<a id="nestedatt--layers--files"></a>
### Nested Schema for `layers.files`

Required:
Optional:

- `contents` (String) Content of the file.
- `path` (String) Path to a file.
70 changes: 60 additions & 10 deletions internal/provider/append_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"compress/gzip"
"context"
"fmt"
"io"
"os"
"strings"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
Expand Down Expand Up @@ -77,14 +80,16 @@ func (r *AppendResource) Schema(ctx context.Context, req resource.SchemaRequest,
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,
Optional: true,
},
"path": schema.StringAttribute{
MarkdownDescription: "Path to a file.",
Optional: true,
},
// TODO: Add support for file mode.
// TODO: Add support for symlinks.
Expand Down Expand Up @@ -212,6 +217,7 @@ func (r *AppendResource) doAppend(ctx context.Context, data *AppendResourceModel
var ls []struct {
Files map[string]struct {
Contents types.String `tfsdk:"contents"`
Path types.String `tfsdk:"path"`
} `tfsdk:"files"`
}
if diag := data.Layers.ElementsAs(ctx, &ls, false); diag.HasError() {
Expand All @@ -224,14 +230,58 @@ func (r *AppendResource) doAppend(ctx context.Context, data *AppendResourceModel
zw := gzip.NewWriter(&b)
tw := tar.NewWriter(zw)
for name, f := range l.Files {
if err := tw.WriteHeader(&tar.Header{
Name: name,
Size: int64(len(f.Contents.ValueString())),
Mode: 0644,
}); 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))}
var (
size int64
mode int64
datarc io.ReadCloser
)

write := func(rc io.ReadCloser) error {
imjasonh marked this conversation as resolved.
Show resolved Hide resolved
defer rc.Close()
if err := tw.WriteHeader(&tar.Header{
Name: name,
Size: size,
Mode: mode,
}); err != nil {
return fmt.Errorf("unable to write tar header: %w", err)
}

if _, err := io.CopyN(tw, rc, size); err != nil {
return fmt.Errorf("unable to write tar contents: %w", err)
}
return nil
}

if f.Contents.ValueString() != "" {
size = int64(len(f.Contents.ValueString()))
mode = 0644
datarc = io.NopCloser(strings.NewReader(f.Contents.ValueString()))

} else if f.Path.ValueString() != "" {
fi, err := os.Stat(f.Path.ValueString())
if err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to stat file", fmt.Sprintf("Unable to stat file %q, got error: %s", f.Path.ValueString(), err))}
}

// skip any directories or symlinks
if fi.IsDir() || fi.Mode()&os.ModeSymlink != 0 {
continue
}

size = fi.Size()
mode = int64(fi.Mode())

fr, err := os.Open(f.Path.ValueString())
if err != nil {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("Unable to open file", fmt.Sprintf("Unable to open file %q, got error: %s", f.Path.ValueString(), err))}
}
datarc = fr

} else {
return nil, []diag.Diagnostic{diag.NewErrorDiagnostic("No file contents or path specified", fmt.Sprintf("No file contents or path specified for %q", name))}
}
if _, err := tw.Write([]byte(f.Contents.ValueString())); err != nil {

if err := write(datarc); 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))}
}
}
Expand Down
195 changes: 194 additions & 1 deletion internal/provider/append_resource_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package provider

import (
"archive/tar"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"testing"

Expand Down Expand Up @@ -44,6 +48,15 @@ func TestAccAppendResource(t *testing.T) {
t.Fatalf("failed to write image: %v", err)
}

tf := filepath.Join(t.TempDir(), "test_path.txt")
if err := os.WriteFile(tf, []byte("hello world"), 0644); err != nil {
t.Fatalf("failed to write file: %v", err)
}

if err := os.Chmod(tf, 0755); err != nil {
t.Fatalf("failed to chmod file: %v", err)
}

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Expand All @@ -55,9 +68,10 @@ func TestAccAppendResource(t *testing.T) {
layers = [{
files = {
"/usr/local/test.txt" = { contents = "hello world" }
"/usr/local/test_path.txt" = { path = "%s" }
}
}]
}`, ref1),
}`, ref1, tf),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("oci_append.test", "base_image", ref1.String()),
resource.TestMatchResourceAttr("oci_append.test", "image_ref", regexp.MustCompile(`/test@sha256:[0-9a-f]{64}$`)),
Expand All @@ -71,6 +85,134 @@ func TestAccAppendResource(t *testing.T) {
if err := validate.Image(img); err != nil {
return fmt.Errorf("failed to validate image: %v", err)
}
// test that the contents match what we expect
ls, err := img.Layers()
if err != nil {
return fmt.Errorf("failed to get layers: %v", err)
}
if len(ls) != 2 {
return fmt.Errorf("expected 2 layer, got %d", len(ls))
}

flrc, err := ls[1].Uncompressed()
if err != nil {
return fmt.Errorf("failed to get layer contents: %v", err)
}
defer flrc.Close()

// the layer should be a tar file with two files
tw := tar.NewReader(flrc)

hdr, err := tw.Next()
if err != nil {
return fmt.Errorf("failed to read next header: %v", err)
}
if hdr.Size != int64(len("hello world")) {
return fmt.Errorf("expected file size %d, got %d", len("hello world"), hdr.Size)
}

hdr, err = tw.Next()
if err != nil {
return fmt.Errorf("failed to read next header: %v", err)
}
if hdr.Size != int64(len("hello world")) {
return fmt.Errorf("expected file size %d, got %d", len("hello world"), hdr.Size)
}

return nil
}),
),
},
// Update and Read testing
{
Config: fmt.Sprintf(`resource "oci_append" "test" {
base_image = %q
layers = [{
files = {
"/usr/local/test.txt" = { contents = "hello world" }
"/usr/bin/test.sh" = { contents = "echo hello world" }
}
}]
}`, ref2),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("oci_append.test", "base_image", ref2.String()),
resource.TestMatchResourceAttr("oci_append.test", "id", regexp.MustCompile(`/test@sha256:[0-9a-f]{64}$`)),
),
},
// Delete testing automatically occurs in TestCase
},
})

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: fmt.Sprintf(`resource "oci_append" "test" {
base_image = %q
layers = [{
files = {
"/usr/local/test.txt" = { path = "%s" }
}
}]
}`, ref1, tf),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("oci_append.test", "base_image", ref1.String()),
resource.TestMatchResourceAttr("oci_append.test", "image_ref", regexp.MustCompile(`/test@sha256:[0-9a-f]{64}$`)),
resource.TestMatchResourceAttr("oci_append.test", "id", regexp.MustCompile(`/test@sha256:[0-9a-f]{64}$`)),
resource.TestCheckFunc(func(s *terraform.State) error {
rs := s.RootModule().Resources["oci_append.test"]
img, err := crane.Pull(rs.Primary.Attributes["image_ref"])
if err != nil {
return fmt.Errorf("failed to pull image: %v", err)
}
if err := validate.Image(img); err != nil {
return fmt.Errorf("failed to validate image: %v", err)
}
// test that the contents match what we expect
ls, err := img.Layers()
if err != nil {
return fmt.Errorf("failed to get layers: %v", err)
}
if len(ls) != 2 {
return fmt.Errorf("expected 2 layer, got %d", len(ls))
}

flrc, err := ls[1].Uncompressed()
if err != nil {
return fmt.Errorf("failed to get layer contents: %v", err)
}
defer flrc.Close()

// the layer should be a tar file with one file
tr := tar.NewReader(flrc)

hdr, err := tr.Next()
if err != nil {
return fmt.Errorf("failed to read next header: %v", err)
}
if hdr.Name != "/usr/local/test.txt" {
return fmt.Errorf("expected file usr/local/test.txt, got %s", hdr.Name)
}
if hdr.Size != int64(len("hello world")) {
return fmt.Errorf("expected file size %d, got %d", len("hello world"), hdr.Size)
}
// ensure the mode is preserved
if hdr.Mode != 0755 {
return fmt.Errorf("expected mode %d, got %d", 0755, hdr.Mode)
}

// check the actual file contents are what we expect
content := make([]byte, hdr.Size)
if _, err := io.ReadFull(tr, content); err != nil {
return fmt.Errorf("failed to read file contents: %v", err)
}

if string(content) != "hello world" {
return fmt.Errorf("expected file contents %q, got %q", "hello world", string(content))
}

return nil
}),
),
Expand Down Expand Up @@ -135,6 +277,57 @@ func TestAccAppendResource(t *testing.T) {
if err := validate.Index(idx); err != nil {
return fmt.Errorf("failed to validate image: %v", err)
}

iidx, err := idx.IndexManifest()
if err != nil {
return fmt.Errorf("failed to get image index: %v", err)
}

for _, m := range iidx.Manifests {
img, err := idx.Image(m.Digest)
if err != nil {
return fmt.Errorf("failed to get image: %v", err)
}

ls, err := img.Layers()
if err != nil {
return fmt.Errorf("failed to get layers: %v", err)
}
if len(ls) != 2 {
return fmt.Errorf("expected 2 layer, got %d", len(ls))
}

flrc, err := ls[1].Uncompressed()
if err != nil {
return fmt.Errorf("failed to get layer contents: %v", err)
}
defer flrc.Close()

// the layer should be a tar file with one file
tr := tar.NewReader(flrc)

hdr, err := tr.Next()
if err != nil {
return fmt.Errorf("failed to read next header: %v", err)
}
if hdr.Name != "/usr/local/test.txt" {
return fmt.Errorf("expected file usr/local/test.txt, got %s", hdr.Name)
}
if hdr.Size != int64(len("hello world")) {
return fmt.Errorf("expected file size %d, got %d", len("hello world"), hdr.Size)
}

// check the actual file contents are what we expect
content := make([]byte, hdr.Size)
if _, err := io.ReadFull(tr, content); err != nil {
return fmt.Errorf("failed to read file contents: %v", err)
}

if string(content) != "hello world" {
return fmt.Errorf("expected file contents %q, got %q", "hello world", string(content))
}
}

return nil
}),
),
Expand Down
Loading