diff --git a/tests/lifecycle_test.go b/tests/lifecycle_test.go index b73d53b4..daa60ceb 100644 --- a/tests/lifecycle_test.go +++ b/tests/lifecycle_test.go @@ -2,10 +2,14 @@ package tests import ( "context" - "os" + "fmt" "path" - "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/unmango/pulumi-baremetal/tests/util/expect" + + p "github.com/pulumi/pulumi-go-provider" "github.com/pulumi/pulumi-go-provider/integration" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/unmango/pulumi-baremetal/tests/util" @@ -13,86 +17,244 @@ import ( const work = "/tmp/lifecycle" -type lifecycleTest func( - *testing.T, - context.Context, - util.TestProvisioner, -) integration.LifeCycleTest - -func TestLifecycle(t *testing.T) { - ctx := context.Background() - prov, err := util.NewProvisioner("5000", os.Stdout) - if err != nil { - t.Fatalf("failed to create provisioner: %s", err) - } +func containerPath(name string) string { + return path.Join(work, name) +} - if err = prov.Start(ctx); err != nil { - t.Fatalf("failed to start provisioner: %s", err) - } +var _ = Describe("Command Resources", func() { + var server integration.Server + + BeforeEach(func(ctx context.Context) { + By("creating an integration server") + server = util.NewServer() + + By("configuring the provider") + err := provisioner.ConfigureProvider(ctx, server) + Expect(err).NotTo(HaveOccurred()) + + By("creating a workspace in the container") + err = provisioner.Exec(ctx, "mkdir", "-p", work) + Expect(err).NotTo(HaveOccurred()) + }) + + Describe("Tee", Ordered, func() { + stdin := "Test lifecycle stdin" + file := containerPath("create.txt") + + test := integration.LifeCycleTest{ + Resource: "baremetal:cmd:Tee", + Create: integration.Operation{ + Inputs: resource.PropertyMap{ + "create": resource.NewObjectProperty(resource.PropertyMap{ + "content": resource.NewStringProperty(stdin), + "files": resource.NewArrayProperty([]resource.PropertyValue{ + resource.NewStringProperty(file), + }), + }), + }, + ExpectedOutput: resource.NewPropertyMapFromMap(map[string]interface{}{ + "exitCode": 0, + "stdout": stdin, + "stderr": "", + "createdFiles": []string{file}, + "args": map[string]interface{}{ + "append": false, + "content": stdin, + "files": []string{file}, + }, + }), + Hook: func(inputs, output resource.PropertyMap) { + data, err := provisioner.ReadFile(context.Background(), file) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(Equal(stdin)) + }, + }, + } + + It("should complete a full lifecycle", func() { + run(server, test) + }) + + It("should remove created files", func(ctx context.Context) { + Expect(provisioner).NotTo(ContainFile(ctx, file)) + }) + }) +}) + +// Based on https://github.com/pulumi/pulumi-go-provider/blob/main/integration/integration.go + +func run(server integration.Server, l integration.LifeCycleTest) { + urn := resource.NewURN("test", "provider", "", l.Resource, "test") + + runCreate := func(op integration.Operation) (p.CreateResponse, bool) { + By("sending check request") + check, err := server.Check(p.CheckRequest{ + Urn: urn, + Olds: nil, + News: op.Inputs, + }) + Expect(err).NotTo(HaveOccurred()) + + if len(op.CheckFailures) > 0 || len(check.Failures) > 0 { + Expect(check.Failures).To(BeEquivalentTo(op.CheckFailures)) + return p.CreateResponse{}, false + } + + By("sending preview create request") + _, err = server.Create(p.CreateRequest{ + Urn: urn, + Properties: check.Inputs.Copy(), + Preview: true, + }) + // We allow the failure from ExpectFailure to hit at either the preview or the Create. + if op.ExpectFailure && err != nil { + return p.CreateResponse{}, false + } + + By("sending create request") + create, err := server.Create(p.CreateRequest{ + Urn: urn, + Properties: check.Inputs.Copy(), + }) + if op.ExpectFailure { + Expect(err).To(HaveOccurred()) + return p.CreateResponse{}, false + } - if err = prov.Exec(ctx, "mkdir", "-p", work); err != nil { - t.Fatalf("failed to create workspace in container: %s", err) + Expect(err).NotTo(HaveOccurred()) + if err != nil { + return p.CreateResponse{}, false + } + if op.Hook != nil { + op.Hook(check.Inputs, create.Properties.Copy()) + } + if op.ExpectedOutput != nil { + Expect(create.Properties).To(BeEquivalentTo(op.ExpectedOutput)) + } + + return create, true } - suite := map[string]lifecycleTest{ - "tee": TeeTest, + createResponse, keepGoing := runCreate(l.Create) + if !keepGoing { + return } - for name, createTest := range suite { - _ = t.Run(name, func(t *testing.T) { - test := createTest(t, ctx, prov) - server := util.NewServerWithContext(ctx) - err := prov.ConfigureProvider(ctx, server) + id := createResponse.ID + olds := createResponse.Properties + for i, update := range l.Updates { + Describe(fmt.Sprintf("update #%d", i), func() { + By("sending check request") + check, err := server.Check(p.CheckRequest{ + Urn: urn, + Olds: olds, + News: update.Inputs, + }) + + Expect(err).NotTo(HaveOccurred()) if err != nil { - t.Fatalf("failed to configure provider: %s", err) + return + } + if len(update.CheckFailures) > 0 || len(check.Failures) > 0 { + Expect(check.Failures).To(BeEquivalentTo(update.CheckFailures)) + return } - test.Run(t, server) - }) - } -} + By("sending diff request") + diff, err := server.Diff(p.DiffRequest{ + ID: id, + Urn: urn, + Olds: olds, + News: check.Inputs.Copy(), + }) -func TeeTest(t *testing.T, ctx context.Context, p util.TestProvisioner) integration.LifeCycleTest { - stdin := "Test lifecycle stdin" - file := containerPath("create.txt") - - return integration.LifeCycleTest{ - Resource: "baremetal:cmd:Tee", - Create: integration.Operation{ - Inputs: resource.PropertyMap{ - "create": resource.NewObjectProperty(resource.PropertyMap{ - "content": resource.NewStringProperty(stdin), - "files": resource.NewArrayProperty([]resource.PropertyValue{ - resource.NewStringProperty(file), - }), - }), - }, - ExpectedOutput: resource.NewPropertyMapFromMap(map[string]interface{}{ - "exitCode": 0, - "stdout": stdin, - "stderr": "", - "createdFiles": []string{file}, - "args": map[string]interface{}{ - "append": false, - "content": stdin, - "files": []string{file}, - }, - }), - Hook: func(inputs, output resource.PropertyMap) { - data, err := p.ReadFile(ctx, file) - if err != nil { - t.Fatalf("failed to read file: %s", err) + Expect(err).NotTo(HaveOccurred()) + if err != nil { + return + } + if !diff.HasChanges { + return + } + + isDelete := false + for _, v := range diff.DetailedDiff { + switch v.Kind { + case p.AddReplace: + fallthrough + case p.DeleteReplace: + fallthrough + case p.UpdateReplace: + isDelete = true } + } + if isDelete { + runDelete := func() { + err = server.Delete(p.DeleteRequest{ + ID: id, + Urn: urn, + Properties: olds, + }) + Expect(err).NotTo(HaveOccurred()) + } + if diff.DeleteBeforeReplace { + runDelete() + result, keepGoing := runCreate(update) + if !keepGoing { + return + } + id = result.ID + olds = result.Properties + } else { + result, keepGoing := runCreate(update) + if !keepGoing { + return + } - contents := string(data) - if contents != stdin { - t.Errorf("expected '%s' to match '%s'", contents, stdin) + runDelete() + // Set the new block + id = result.ID + olds = result.Properties } - }, - }, + } else { + + // Now perform the preview + _, err = server.Update(p.UpdateRequest{ + ID: id, + Urn: urn, + Olds: olds, + News: check.Inputs.Copy(), + Preview: true, + }) + + if update.ExpectFailure && err != nil { + return + } + + result, err := server.Update(p.UpdateRequest{ + ID: id, + Urn: urn, + Olds: olds, + News: check.Inputs.Copy(), + }) + if update.ExpectFailure { + Expect(err).To(HaveOccurred()) + return + } + if update.Hook != nil { + update.Hook(check.Inputs, result.Properties.Copy()) + } + if update.ExpectedOutput != nil { + Expect(result.Properties.Copy()).To(BeEquivalentTo(update.ExpectedOutput)) + } + olds = result.Properties + } + }) } -} -func containerPath(name string) string { - return path.Join(work, name) + err := server.Delete(p.DeleteRequest{ + ID: id, + Urn: urn, + Properties: olds, + }) + Expect(err).NotTo(HaveOccurred()) } diff --git a/tests/tee_test.go b/tests/tee_test.go deleted file mode 100644 index 8ba455eb..00000000 --- a/tests/tee_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package tests - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/unmango/pulumi-baremetal/tests/util" - . "github.com/unmango/pulumi-baremetal/tests/util/expect" - - p "github.com/pulumi/pulumi-go-provider" - "github.com/pulumi/pulumi-go-provider/integration" - "github.com/pulumi/pulumi/sdk/v3/go/common/resource" -) - -var _ = Describe("Tee", Ordered, func() { - urn := util.Urn("Tee", "cmd") - var server integration.Server - - BeforeAll(func(ctx context.Context) { - By("creating a working directory for the tee test") - err := provisioner.Exec(ctx, "mkdir", "-p", "/tmp/tee") - Expect(err).NotTo(HaveOccurred()) - }) - - BeforeAll(func(ctx context.Context) { - By("creating a provider server") - server = util.NewServer() - }) - - BeforeAll(func(ctx context.Context) { - By("configuring the provider") - err := provisioner.ConfigureProvider(ctx, server) - Expect(err).NotTo(HaveOccurred()) - }) - - Describe("write stdin to a file", func() { - stdin := "Test stdin" - file := "/tmp/tee/create.txt" - - var teeId *string - var stdout *string - var stderr *string - - props := resource.PropertyMap{ - "create": resource.NewObjectProperty(resource.PropertyMap{ - "content": resource.NewStringProperty(stdin), - "files": resource.NewArrayProperty([]resource.PropertyValue{ - resource.NewStringProperty(file), - }), - }), - } - - It("should create", func(ctx context.Context) { - response, err := server.Create(p.CreateRequest{ - Urn: urn, - Preview: false, - Properties: props, - }) - - Expect(err).NotTo(HaveOccurred()) - teeId = &response.ID - - out, ok := response.Properties["stderr"].V.(string) - Expect(ok).To(BeTrueBecause("stderr was not a string")) - stderr = &out - - out, ok = response.Properties["stdout"].V.(string) - Expect(ok).To(BeTrueBecause("stdout was not a string")) - stdout = &out - - Expect(provisioner).To(ContainFile(ctx, file)) - }) - - It("should delete", func(ctx context.Context) { - By("asserting the developer hasn't made an error") - Expect(teeId).NotTo(BeNil()) - - err := server.Delete(p.DeleteRequest{ - Urn: urn, - ID: *teeId, - Properties: resource.PropertyMap{ - "args": resource.NewObjectProperty(resource.PropertyMap{ - "content": resource.NewStringProperty(stdin), - "files": resource.NewArrayProperty([]resource.PropertyValue{ - resource.NewStringProperty(file), - }), - }), - "exitCode": resource.NewNumberProperty(0), - "stdout": resource.NewStringProperty(*stdout), - "stderr": resource.NewStringProperty(*stderr), - "createdFiles": resource.NewArrayProperty([]resource.PropertyValue{ - resource.NewStringProperty(file), - }), - }, - }) - - Expect(err).NotTo(HaveOccurred()) - Expect(provisioner).NotTo(ContainFile(ctx, file)) - }) - }) -})