diff --git a/internal/spacelift/repository/context.go b/internal/spacelift/repository/context.go index 8bf41e2..de67da2 100644 --- a/internal/spacelift/repository/context.go +++ b/internal/spacelift/repository/context.go @@ -32,17 +32,19 @@ func NewContextRepository(client client.Client) *contextRepository { return &contextRepository{client: client} } +type contextCreateMutation struct { + ContextCreate struct { + Id string `graphql:"id"` + } `graphql:"contextCreateV2(input: $input)"` +} + func (r *contextRepository) Create(ctx context.Context, context *v1beta1.Context) (*models.Context, error) { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, context.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, context.Namespace) if err != nil { return nil, errors.Wrap(err, "unable to fetch spacelift client while creating context") } - var createMutation struct { - ContextCreate struct { - Id string `graphql:"id"` - } `graphql:"contextCreateV2(input: $input)"` - } + var createMutation contextCreateMutation contextInput, err := structs.FromContextSpec(context) if err != nil { @@ -61,17 +63,19 @@ func (r *contextRepository) Create(ctx context.Context, context *v1beta1.Context }, nil } +type contextUpdateMutation struct { + ContextUpdate struct { + Id string `graphql:"id"` + } `graphql:"contextUpdateV2(id: $id, input: $input, replaceConfigElements: $replaceConfigElements)"` +} + func (r *contextRepository) Update(ctx context.Context, context *v1beta1.Context) (*models.Context, error) { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, context.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, context.Namespace) if err != nil { return nil, errors.Wrap(err, "unable to fetch spacelift client while creating context") } - var updateMutation struct { - ContextUpdate struct { - Id string `graphql:"id"` - } `graphql:"contextUpdateV2(id: $id, input: $input, replaceConfigElements: $replaceConfigElements)"` - } + var updateMutation contextUpdateMutation contextInput, err := structs.FromContextSpec(context) if err != nil { diff --git a/internal/spacelift/repository/context_test.go b/internal/spacelift/repository/context_test.go new file mode 100644 index 0000000..9b0ee01 --- /dev/null +++ b/internal/spacelift/repository/context_test.go @@ -0,0 +1,185 @@ +package repository + +import ( + "context" + "testing" + + "github.com/shurcooL/graphql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/spacelift-io/spacelift-operator/api/v1beta1" + spaceliftclient "github.com/spacelift-io/spacelift-operator/internal/spacelift/client" + "github.com/spacelift-io/spacelift-operator/internal/spacelift/client/mocks" + "github.com/spacelift-io/spacelift-operator/internal/spacelift/repository/structs" + "github.com/spacelift-io/spacelift-operator/internal/utils" +) + +func Test_contextRepository_Create(t *testing.T) { + testCases := []struct { + name string + context v1beta1.Context + assertPayload func(*testing.T, structs.ContextInput) + }{ + { + name: "basic context", + context: v1beta1.Context{ + ObjectMeta: v1.ObjectMeta{ + Name: "name", + }, + Spec: v1beta1.ContextSpec{ + SpaceId: utils.AddressOf("test-space-id"), + Description: utils.AddressOf("test description"), + Labels: []string{"label1", "label2"}, + Attachments: []v1beta1.Attachment{ + { + StackId: utils.AddressOf("test-attached-stack-id"), + }, + }, + Hooks: v1beta1.Hooks{ + AfterApply: []string{"after", "apply"}, + }, + Environment: []v1beta1.Environment{ + { + Id: "id", + Value: utils.AddressOf("secret"), + Secret: utils.AddressOf(true), + Description: utils.AddressOf("test description"), + }, + }, + MountedFiles: []v1beta1.MountedFile{ + { + Id: "id-file", + Value: utils.AddressOf("secret file"), + Secret: utils.AddressOf(true), + Description: utils.AddressOf("test description"), + }, + }, + }, + }, + assertPayload: func(t *testing.T, input structs.ContextInput) { + assert.Equal(t, "name", input.Name) + assert.Equal(t, "test description", *input.Description) + assert.Equal(t, []string{"label1", "label2"}, input.Labels) + assert.Equal(t, []string{"after", "apply"}, input.Hooks.AfterApply) + assert.Equal(t, []structs.StackAttachment{ + { + Stack: "test-attached-stack-id", + }, + }, input.StackAttachments) + assert.Equal(t, []structs.ConfigAttachments{ + { + Description: utils.AddressOf("test description"), + Id: "id", + Type: "ENVIRONMENT_VARIABLE", + Value: "secret", + WriteOnly: true, + }, + { + Description: utils.AddressOf("test description"), + Id: "id-file", + Type: "FILE_MOUNT", + Value: "secret file", + WriteOnly: true, + }, + }, input.ConfigAttachments) + }, + }, + { + name: "basic context with name", + context: v1beta1.Context{ + ObjectMeta: v1.ObjectMeta{ + Name: "name", + }, + Spec: v1beta1.ContextSpec{ + SpaceId: utils.AddressOf("test-space-id"), + Name: utils.AddressOf("test name override"), + }, + }, + assertPayload: func(t *testing.T, input structs.ContextInput) { + assert.Equal(t, "test name override", input.Name) + }, + }, + } + + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + var fakeClient *mocks.Client + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + repo := NewContextRepository(nil) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fakeClient = mocks.NewClient(t) + var actualVars = map[string]any{} + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.contextCreateMutation"), mock.Anything). + Run(func(_ context.Context, _ interface{}, vars map[string]interface{}, _a3 ...graphql.RequestOption) { + actualVars = vars + }).Return(nil) + _, err := repo.Create(context.Background(), &testCase.context) + require.NoError(t, err) + testCase.assertPayload(t, actualVars["input"].(structs.ContextInput)) + }) + } + +} + +func Test_contextRepository_Update(t *testing.T) { + testCases := []struct { + name string + context v1beta1.Context + assertPayload func(*testing.T, map[string]any) + }{ + { + name: "basic context", + context: v1beta1.Context{ + ObjectMeta: v1.ObjectMeta{ + Name: "name", + }, + Spec: v1beta1.ContextSpec{ + SpaceId: utils.AddressOf("test-space-id"), + }, + Status: v1beta1.ContextStatus{ + Id: "test-context-id", + }, + }, + assertPayload: func(t *testing.T, input map[string]any) { + assert.Equal(t, "test-context-id", input["id"]) + assert.Equal(t, graphql.Boolean(true), input["replaceConfigElements"]) + // No need to assert on input details since we use the same code than the Create function + // and this is already covered in Test_contextRepository_Create + assert.IsType(t, structs.ContextInput{}, input["input"]) + }, + }, + } + + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + var fakeClient *mocks.Client + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + repo := NewContextRepository(nil) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fakeClient = mocks.NewClient(t) + var actualVars = map[string]any{} + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.contextUpdateMutation"), mock.Anything). + Run(func(_ context.Context, _ interface{}, vars map[string]interface{}, _a3 ...graphql.RequestOption) { + actualVars = vars + }).Return(nil) + _, err := repo.Update(context.Background(), &testCase.context) + require.NoError(t, err) + testCase.assertPayload(t, actualVars) + }) + } + +} diff --git a/internal/spacelift/repository/run.go b/internal/spacelift/repository/run.go index c8575da..9d2b280 100644 --- a/internal/spacelift/repository/run.go +++ b/internal/spacelift/repository/run.go @@ -29,17 +29,19 @@ func NewRunRepository(client client.Client) *runRepository { type CreateRunQuery struct { } +type createRunMutation struct { + RunTrigger struct { + ID string `graphql:"id"` + State string `graphql:"state"` + } `graphql:"runTrigger(stack: $stack)"` +} + func (r *runRepository) Create(ctx context.Context, stack *v1beta1.Stack) (*models.Run, error) { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, stack.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, stack.Namespace) if err != nil { return nil, errors.Wrap(err, "unable to fetch spacelift client while creating run") } - var mutation struct { - RunTrigger struct { - ID string `graphql:"id"` - State string `graphql:"state"` - } `graphql:"runTrigger(stack: $stack)"` - } + var mutation createRunMutation vars := map[string]any{ "stack": graphql.ID(stack.Status.Id), } diff --git a/internal/spacelift/repository/run_test.go b/internal/spacelift/repository/run_test.go new file mode 100644 index 0000000..7f25452 --- /dev/null +++ b/internal/spacelift/repository/run_test.go @@ -0,0 +1,51 @@ +package repository + +import ( + "context" + "testing" + + "github.com/shurcooL/graphql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/spacelift-io/spacelift-operator/api/v1beta1" + spaceliftclient "github.com/spacelift-io/spacelift-operator/internal/spacelift/client" + "github.com/spacelift-io/spacelift-operator/internal/spacelift/client/mocks" +) + +func Test_runRepository_Create(t *testing.T) { + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + var fakeClient *mocks.Client + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + + var actualVars map[string]any + fakeClient = mocks.NewClient(t) + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.createRunMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, vars map[string]interface{}, _ ...graphql.RequestOption) { + actualVars = vars + runMutation := mutation.(*createRunMutation) + runMutation.RunTrigger.ID = "run-id" + runMutation.RunTrigger.State = "QUEUED" + }).Return(nil) + fakeClient.EXPECT().URL("/stack/%s/run/%s", "stack-id", "run-id").Return("run-url") + + fakeStack := &v1beta1.Stack{ + Status: v1beta1.StackStatus{ + Id: "stack-id", + }, + } + repo := NewRunRepository(nil) + run, err := repo.Create(context.Background(), fakeStack) + assert.NoError(t, err) + + assert.Equal(t, "stack-id", actualVars["stack"]) + assert.Equal(t, "run-id", run.Id) + assert.Equal(t, "QUEUED", run.State) + assert.Equal(t, "run-url", run.Url) + assert.Equal(t, "stack-id", run.StackId) +} diff --git a/internal/spacelift/repository/space.go b/internal/spacelift/repository/space.go index a6fe22b..ce6f1d7 100644 --- a/internal/spacelift/repository/space.go +++ b/internal/spacelift/repository/space.go @@ -30,17 +30,19 @@ func NewSpaceRepository(client client.Client) *spaceRepository { return &spaceRepository{client: client} } +type spaceCreateMutation struct { + SpaceCreate struct { + ID string `graphql:"id"` + } `graphql:"spaceCreate(input: $input)"` +} + func (r *spaceRepository) Create(ctx context.Context, space *v1beta1.Space) (*models.Space, error) { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, space.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, space.Namespace) if err != nil { return nil, errors.Wrap(err, "unable to fetch spacelift client while creating run") } - var mutation struct { - SpaceCreate struct { - ID string `graphql:"id"` - } `graphql:"spaceCreate(input: $input)"` - } + var mutation spaceCreateMutation spaceCreationVars := map[string]any{"input": structs.FromSpaceSpec(space)} @@ -54,17 +56,19 @@ func (r *spaceRepository) Create(ctx context.Context, space *v1beta1.Space) (*mo }, nil } +type spaceUpdateMutation struct { + SpaceUpdate struct { + ID string `graphql:"id"` + } `graphql:"spaceUpdate(space: $space, input: $input)"` +} + func (r *spaceRepository) Update(ctx context.Context, space *v1beta1.Space) (*models.Space, error) { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, space.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, space.Namespace) if err != nil { return nil, errors.Wrap(err, "unable to fetch spacelift client while updating space") } - var spaceUpdateMutation struct { - SpaceUpdate struct { - ID string `graphql:"id"` - } `graphql:"spaceUpdate(space: $space, input: $input)"` - } + var spaceUpdateMutation spaceUpdateMutation spaceUpdateVars := map[string]any{ "space": graphql.ID(space.Status.Id), diff --git a/internal/spacelift/repository/space_test.go b/internal/spacelift/repository/space_test.go new file mode 100644 index 0000000..61c6b54 --- /dev/null +++ b/internal/spacelift/repository/space_test.go @@ -0,0 +1,138 @@ +package repository + +import ( + "context" + "testing" + + "github.com/shurcooL/graphql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/spacelift-io/spacelift-operator/api/v1beta1" + spaceliftclient "github.com/spacelift-io/spacelift-operator/internal/spacelift/client" + "github.com/spacelift-io/spacelift-operator/internal/spacelift/client/mocks" + "github.com/spacelift-io/spacelift-operator/internal/spacelift/repository/structs" + "github.com/spacelift-io/spacelift-operator/internal/utils" +) + +func Test_spaceRepository_Create(t *testing.T) { + testCases := []struct { + name string + space v1beta1.Space + assertPayload func(*testing.T, structs.SpaceInput) + }{ + { + name: "basic space", + space: v1beta1.Space{ + ObjectMeta: v1.ObjectMeta{ + Name: "name", + }, + Spec: v1beta1.SpaceSpec{ + ParentSpace: "parent-space-id", + Description: "test description", + InheritEntities: true, + Labels: &[]string{"label1", "label2"}, + }, + }, + assertPayload: func(t *testing.T, input structs.SpaceInput) { + assert.Equal(t, graphql.String("name"), input.Name) + assert.Equal(t, graphql.String("test description"), input.Description) + assert.Equal(t, graphql.String("parent-space-id"), input.ParentSpace) + assert.Equal(t, graphql.Boolean(true), input.InheritEntities) + assert.Equal(t, &[]graphql.String{"label1", "label2"}, input.Labels) + }, + }, + { + name: "basic space with name", + space: v1beta1.Space{ + ObjectMeta: v1.ObjectMeta{ + Name: "name", + }, + Spec: v1beta1.SpaceSpec{ + ParentSpace: "test-space-id", + Name: utils.AddressOf("test name override"), + }, + }, + assertPayload: func(t *testing.T, input structs.SpaceInput) { + assert.Equal(t, graphql.String("test name override"), input.Name) + }, + }, + } + + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + var fakeClient *mocks.Client + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + repo := NewSpaceRepository(nil) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fakeClient = mocks.NewClient(t) + var actualVars = map[string]any{} + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.spaceCreateMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, vars map[string]interface{}, _ ...graphql.RequestOption) { + actualVars = vars + spaceMutation := mutation.(*spaceCreateMutation) + spaceMutation.SpaceCreate.ID = "space-id" + }).Return(nil) + fakeClient.EXPECT().URL("/spaces/%s", "space-id").Return("") + _, err := repo.Create(context.Background(), &testCase.space) + require.NoError(t, err) + testCase.assertPayload(t, actualVars["input"].(structs.SpaceInput)) + }) + } + +} + +func Test_spaceRepository_Update(t *testing.T) { + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + fakeClient := mocks.NewClient(t) + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + + fakeSpaceId := "space-id" + var actualVars map[string]any + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.spaceUpdateMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, vars map[string]interface{}, _ ...graphql.RequestOption) { + actualVars = vars + spaceMutation := mutation.(*spaceUpdateMutation) + spaceMutation.SpaceUpdate.ID = fakeSpaceId + }).Return(nil) + fakeClient.EXPECT().URL("/spaces/%s", fakeSpaceId).Return("") + + repo := NewSpaceRepository(nil) + + fakeSpace := &v1beta1.Space{ + ObjectMeta: v1.ObjectMeta{ + Name: "space-name", + }, + Spec: v1beta1.SpaceSpec{ + ParentSpace: "parent-space-id", + Description: "test description", + InheritEntities: true, + Labels: &[]string{"label1", "label2"}, + }, + Status: v1beta1.SpaceStatus{ + Id: fakeSpaceId, + }, + } + _, err := repo.Update(context.Background(), fakeSpace) + require.NoError(t, err) + assert.Equal(t, fakeSpaceId, actualVars["space"]) + assert.Equal(t, structs.SpaceInput{ + Name: "space-name", + Description: "test description", + InheritEntities: true, + ParentSpace: "parent-space-id", + Labels: &[]graphql.String{"label1", "label2"}, + }, actualVars["input"]) +} diff --git a/internal/spacelift/repository/stack.go b/internal/spacelift/repository/stack.go index 17ac364..f667366 100644 --- a/internal/spacelift/repository/stack.go +++ b/internal/spacelift/repository/stack.go @@ -36,17 +36,19 @@ func NewStackRepository(client client.Client) *stackRepository { type CreateStackQuery struct { } +type stackCreateMutation struct { + StackCreate struct { + ID string `graphql:"id"` + } `graphql:"stackCreate(input: $input, manageState: $manageState)"` +} + func (r *stackRepository) Create(ctx context.Context, stack *v1beta1.Stack) (*models.Stack, error) { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, stack.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, stack.Namespace) if err != nil { return nil, errors.Wrap(err, "unable to fetch spacelift client while creating stack") } - var stackCreateMutation struct { - StackCreate struct { - ID string `graphql:"id"` - } `graphql:"stackCreate(input: $input, manageState: $manageState)"` - } + var mutation stackCreateMutation stackInput := structs.FromStackSpec(stack) stackCreateMutationVars := map[string]interface{}{ @@ -58,12 +60,12 @@ func (r *stackRepository) Create(ctx context.Context, stack *v1beta1.Stack) (*mo stackCreateMutationVars["manageState"] = graphql.Boolean(*stack.Spec.ManagesStateFile) } - if err := c.Mutate(ctx, &stackCreateMutation, stackCreateMutationVars); err != nil { + if err := c.Mutate(ctx, &mutation, stackCreateMutationVars); err != nil { return nil, errors.Wrap(err, "unable to create stack") } - url := c.URL("/stack/%s", stackCreateMutation.StackCreate.ID) + url := c.URL("/stack/%s", mutation.StackCreate.ID) - stack.Status.Id = stackCreateMutation.StackCreate.ID + stack.Status.Id = mutation.StackCreate.ID if stack.Spec.AWSIntegration != nil { if err := r.attachAWSIntegration(ctx, stack); err != nil { return nil, errors.Wrap(err, "unable to attach AWS integration to stack") @@ -71,52 +73,56 @@ func (r *stackRepository) Create(ctx context.Context, stack *v1beta1.Stack) (*mo } if stack.Spec.CommitSHA != nil && *stack.Spec.CommitSHA != "" { - if err := r.setTrackedCommit(ctx, c, stackCreateMutation.StackCreate.ID, *stack.Spec.CommitSHA); err != nil { + if err := r.setTrackedCommit(ctx, c, mutation.StackCreate.ID, *stack.Spec.CommitSHA); err != nil { return nil, errors.Wrap(err, "unable to set tracked commit on stack") } } return &models.Stack{ - Id: stackCreateMutation.StackCreate.ID, + Id: mutation.StackCreate.ID, Url: url, }, nil } +type awsIntegrationAttachMutation struct { + AWSIntegrationAttach struct { + Id string `graphql:"id"` + } `graphql:"awsIntegrationAttach(id: $id, stack: $stack, read: $read, write: $write)"` +} + func (r *stackRepository) attachAWSIntegration(ctx context.Context, stack *v1beta1.Stack) error { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, stack.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, stack.Namespace) if err != nil { return errors.Wrap(err, "unable to fetch spacelift client while creating stack") } - var awsIntegrationAttachMutation struct { - AWSIntegrationAttach struct { - Id string `graphql:"id"` - } `graphql:"awsIntegrationAttach(id: $id, stack: $stack, read: $read, write: $write)"` - } + var mutation awsIntegrationAttachMutation awsIntegrationAttachVars := map[string]any{ "id": stack.Spec.AWSIntegration.Id, "stack": stack.Status.Id, "read": graphql.Boolean(stack.Spec.AWSIntegration.Read), "write": graphql.Boolean(stack.Spec.AWSIntegration.Write), } - if err := c.Mutate(ctx, &awsIntegrationAttachMutation, awsIntegrationAttachVars); err != nil { + if err := c.Mutate(ctx, &mutation, awsIntegrationAttachVars); err != nil { return err } return nil } +type stackUpdateMutation struct { + StackUpdate struct { + ID string `graphql:"id"` + State string `graphql:"state"` + } `graphql:"stackUpdate(id: $id, input: $input)"` +} + func (r *stackRepository) Update(ctx context.Context, stack *v1beta1.Stack) (*models.Stack, error) { - c, err := spaceliftclient.GetSpaceliftClient(ctx, r.client, stack.Namespace) + c, err := spaceliftclient.DefaultClient(ctx, r.client, stack.Namespace) if err != nil { return nil, errors.Wrap(err, "unable to fetch spacelift client while updating stack") } - var mutation struct { - StackUpdate struct { - ID string `graphql:"id"` - State string `graphql:"state"` - } `graphql:"stackUpdate(id: $id, input: $input)"` - } + var mutation stackUpdateMutation stackInput := structs.FromStackSpec(stack) vars := map[string]interface{}{ @@ -176,20 +182,22 @@ func (r *stackRepository) Get(ctx context.Context, stack *v1beta1.Stack) (*model return s, nil } +type setTrackedCommitMutation struct { + Stack struct { + ID string `graphql:"id"` + } `graphql:"stackSetCurrentCommit(id: $id, sha: $sha)"` +} + func (r *stackRepository) setTrackedCommit(ctx context.Context, c spaceliftclient.Client, stackID, commitSHA string) error { - var setTrackedCommitMutation struct { - Stack struct { - ID string `graphql:"id"` - } `graphql:"stackSetCurrentCommit(id: $id, sha: $sha)"` - } + var mutation setTrackedCommitMutation setTrackedCommitMutationVars := map[string]interface{}{ "id": stackID, "sha": graphql.String(commitSHA), } - if err := c.Mutate(ctx, &setTrackedCommitMutation, setTrackedCommitMutationVars); err != nil { + if err := c.Mutate(ctx, &mutation, setTrackedCommitMutationVars); err != nil { return errors.Wrap(err, "unable to set tracked commit on stack") } diff --git a/internal/spacelift/repository/stack_test.go b/internal/spacelift/repository/stack_test.go new file mode 100644 index 0000000..e8eff6a --- /dev/null +++ b/internal/spacelift/repository/stack_test.go @@ -0,0 +1,193 @@ +package repository + +import ( + "context" + "testing" + + "github.com/shurcooL/graphql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/spacelift-io/spacelift-operator/api/v1beta1" + spaceliftclient "github.com/spacelift-io/spacelift-operator/internal/spacelift/client" + "github.com/spacelift-io/spacelift-operator/internal/spacelift/client/mocks" + "github.com/spacelift-io/spacelift-operator/internal/spacelift/repository/structs" + "github.com/spacelift-io/spacelift-operator/internal/utils" +) + +func Test_stackRepository_Create(t *testing.T) { + testCases := []struct { + name string + stack v1beta1.Stack + assertPayload func(*testing.T, map[string]any) + }{ + { + name: "stack with disabled state management", + stack: v1beta1.Stack{ + ObjectMeta: v1.ObjectMeta{ + Name: "stack-name", + }, + Spec: v1beta1.StackSpec{ + ManagesStateFile: utils.AddressOf(false), + }, + }, + assertPayload: func(t *testing.T, vars map[string]any) { + assert.Equal(t, graphql.Boolean(false), vars["manageState"]) + input := vars["input"].(structs.StackInput) + assert.Equal(t, graphql.String("stack-name"), input.Name) + }, + }, + } + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + var fakeClient *mocks.Client + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + repo := NewStackRepository(nil) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fakeClient = mocks.NewClient(t) + var actualVars = map[string]any{} + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.stackCreateMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, vars map[string]interface{}, _ ...graphql.RequestOption) { + actualVars = vars + createMutation := mutation.(*stackCreateMutation) + createMutation.StackCreate.ID = "stack-id" + }).Return(nil) + fakeClient.EXPECT().URL("/stack/%s", "stack-id").Return("") + _, err := repo.Create(context.Background(), &testCase.stack) + require.NoError(t, err) + testCase.assertPayload(t, actualVars) + }) + } +} + +func Test_stackRepository_Create_WithCommitSHA(t *testing.T) { + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + var fakeClient *mocks.Client + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + repo := NewStackRepository(nil) + + fakeClient = mocks.NewClient(t) + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.stackCreateMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, _ map[string]interface{}, _ ...graphql.RequestOption) { + createMutation := mutation.(*stackCreateMutation) + createMutation.StackCreate.ID = "stack-id" + }).Return(nil) + fakeClient.EXPECT().URL("/stack/%s", "stack-id").Return("") + var setTrackedCommitVars = map[string]any{} + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.setTrackedCommitMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, vars map[string]interface{}, _ ...graphql.RequestOption) { + setTrackedCommitVars = vars + }).Return(nil) + + stack := v1beta1.Stack{ + ObjectMeta: v1.ObjectMeta{ + Name: "stack-name", + }, + Spec: v1beta1.StackSpec{ + CommitSHA: utils.AddressOf("commit-sha"), + SpaceId: utils.AddressOf("space-id"), + }, + } + + _, err := repo.Create(context.Background(), &stack) + require.NoError(t, err) + assert.EqualValues(t, "stack-id", setTrackedCommitVars["id"]) + assert.EqualValues(t, "commit-sha", setTrackedCommitVars["sha"]) +} + +func Test_stackRepository_Create_WithAWSIntegration(t *testing.T) { + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + var fakeClient *mocks.Client + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + repo := NewStackRepository(nil) + + fakeClient = mocks.NewClient(t) + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.stackCreateMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, _ map[string]interface{}, _ ...graphql.RequestOption) { + createMutation := mutation.(*stackCreateMutation) + createMutation.StackCreate.ID = "stack-id" + }).Return(nil) + fakeClient.EXPECT().URL("/stack/%s", "stack-id").Return("") + var attachIntegrationVars = map[string]any{} + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.awsIntegrationAttachMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, vars map[string]interface{}, _ ...graphql.RequestOption) { + attachIntegrationVars = vars + }).Return(nil) + + stack := v1beta1.Stack{ + ObjectMeta: v1.ObjectMeta{ + Name: "stack-name", + }, + Spec: v1beta1.StackSpec{ + SpaceId: utils.AddressOf("space-id"), + AWSIntegration: &v1beta1.AWSIntegration{ + Id: "integration-id", + Read: true, + Write: true, + }, + }, + } + + _, err := repo.Create(context.Background(), &stack) + require.NoError(t, err) + assert.EqualValues(t, "integration-id", attachIntegrationVars["id"]) + assert.EqualValues(t, "stack-id", attachIntegrationVars["stack"]) + assert.EqualValues(t, true, attachIntegrationVars["read"]) + assert.EqualValues(t, true, attachIntegrationVars["write"]) +} + +func Test_stackRepository_Update(t *testing.T) { + originalClient := spaceliftclient.DefaultClient + defer func() { spaceliftclient.DefaultClient = originalClient }() + fakeClient := mocks.NewClient(t) + spaceliftclient.DefaultClient = func(_ context.Context, _ client.Client, _ string) (spaceliftclient.Client, error) { + return fakeClient, nil + } + + fakeStackId := "stack-id" + var actualVars map[string]any + fakeClient.EXPECT(). + Mutate(mock.Anything, mock.AnythingOfType("*repository.stackUpdateMutation"), mock.Anything). + Run(func(_ context.Context, mutation any, vars map[string]interface{}, _ ...graphql.RequestOption) { + actualVars = vars + updateMutation := mutation.(*stackUpdateMutation) + updateMutation.StackUpdate.ID = fakeStackId + }).Return(nil) + fakeClient.EXPECT().URL("/stack/%s", fakeStackId).Return("") + + repo := NewStackRepository(nil) + + fakeStack := &v1beta1.Stack{ + ObjectMeta: v1.ObjectMeta{ + Name: "stack-name", + }, + Spec: v1beta1.StackSpec{ + SpaceId: utils.AddressOf("space-id"), + }, + Status: v1beta1.StackStatus{ + Id: fakeStackId, + }, + } + _, err := repo.Update(context.Background(), fakeStack) + require.NoError(t, err) + assert.Equal(t, "stack-name", actualVars["id"]) + assert.IsType(t, structs.StackInput{}, actualVars["input"]) +} diff --git a/internal/spacelift/repository/structs/stack_input_test.go b/internal/spacelift/repository/structs/stack_input_test.go new file mode 100644 index 0000000..a3471b7 --- /dev/null +++ b/internal/spacelift/repository/structs/stack_input_test.go @@ -0,0 +1,523 @@ +package structs + +import ( + "testing" + + "github.com/shurcooL/graphql" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/spacelift-io/spacelift-operator/api/v1beta1" + "github.com/spacelift-io/spacelift-operator/internal/utils" +) + +func TestFromStackSpec(t *testing.T) { + tests := []struct { + name string + stack v1beta1.Stack + assert func(*testing.T, StackInput) + }{ + { + name: "defaults", + stack: v1beta1.Stack{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stack-name", + }, + }, + assert: func(t *testing.T, input StackInput) { + assert.EqualValues(t, "stack-name", input.Name) + assert.EqualValues(t, "main", input.Branch) + assert.EqualValues(t, false, input.Administrative) + }, + }, + { + name: "stack with name", + stack: v1beta1.Stack{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stack-name", + }, + Spec: v1beta1.StackSpec{ + Name: utils.AddressOf("new name"), + }, + }, + assert: func(t *testing.T, input StackInput) { + assert.EqualValues(t, "new name", input.Name) + }, + }, + { + name: "branch is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + Branch: utils.AddressOf("branch"), + }, + }, + assert: func(t *testing.T, input StackInput) { + assert.EqualValues(t, "branch", input.Branch) + }, + }, + { + name: "repository is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + Repository: "org/repo", + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.Repository) + assert.EqualValues(t, "org", *input.Namespace) + assert.EqualValues(t, "repo", input.Repository) + }, + }, + { + name: "autodeploy is enabled", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + Autodeploy: utils.AddressOf(true), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.Autodeploy) + assert.EqualValues(t, true, *input.Autodeploy) + }, + }, + { + name: "autoretry is enabled", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + Autoretry: utils.AddressOf(true), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.Autoretry) + assert.EqualValues(t, true, *input.Autoretry) + }, + }, + { + name: "github action deploy is enabled", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + GitHubActionDeploy: utils.AddressOf(true), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.GitHubActionDeploy) + assert.EqualValues(t, true, *input.GitHubActionDeploy) + }, + }, + { + name: "local preview is enabled", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + LocalPreviewEnabled: utils.AddressOf(true), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.LocalPreviewEnabled) + assert.EqualValues(t, true, *input.LocalPreviewEnabled) + }, + }, + { + name: "protect from deletion is enabled", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + ProtectFromDeletion: utils.AddressOf(true), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.ProtectFromDeletion) + assert.EqualValues(t, true, *input.ProtectFromDeletion) + }, + }, + { + name: "additional project globs are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + AdditionalProjectGlobs: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.AddditionalProjectGlobs) + require.Len(t, *input.AddditionalProjectGlobs, 1) + assert.EqualValues(t, "test", (*input.AddditionalProjectGlobs)[0]) + }, + }, + { + name: "AfterApply hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + AfterApply: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.AfterApply) + require.Len(t, *input.AfterApply, 1) + assert.EqualValues(t, "test", (*input.AfterApply)[0]) + }, + }, + { + name: "AfterDestroy hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + AfterDestroy: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.AfterDestroy) + require.Len(t, *input.AfterDestroy, 1) + assert.EqualValues(t, "test", (*input.AfterDestroy)[0]) + }, + }, + { + name: "AfterApply hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + AfterInit: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.AfterInit) + require.Len(t, *input.AfterInit, 1) + assert.EqualValues(t, "test", (*input.AfterInit)[0]) + }, + }, + { + name: "AfterPerform hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + AfterPerform: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.AfterPerform) + require.Len(t, *input.AfterPerform, 1) + assert.EqualValues(t, "test", (*input.AfterPerform)[0]) + }, + }, + { + name: "AfterPlan hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + AfterPlan: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.AfterPlan) + require.Len(t, *input.AfterPlan, 1) + assert.EqualValues(t, "test", (*input.AfterPlan)[0]) + }, + }, + { + name: "AfterRun hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + AfterRun: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.AfterRun) + require.Len(t, *input.AfterRun, 1) + assert.EqualValues(t, "test", (*input.AfterRun)[0]) + }, + }, + { + name: "BeforeApply hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + BeforeApply: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.BeforeApply) + require.Len(t, *input.BeforeApply, 1) + assert.EqualValues(t, "test", (*input.BeforeApply)[0]) + }, + }, + { + name: "BeforeDestroy hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + BeforeDestroy: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.BeforeDestroy) + require.Len(t, *input.BeforeDestroy, 1) + assert.EqualValues(t, "test", (*input.BeforeDestroy)[0]) + }, + }, + { + name: "BeforeInit hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + BeforeInit: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.BeforeInit) + require.Len(t, *input.BeforeInit, 1) + assert.EqualValues(t, "test", (*input.BeforeInit)[0]) + }, + }, + { + name: "BeforePerform hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + BeforePerform: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.BeforePerform) + require.Len(t, *input.BeforePerform, 1) + assert.EqualValues(t, "test", (*input.BeforePerform)[0]) + }, + }, + { + name: "BeforePlan hooks are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + BeforePlan: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.BeforePlan) + require.Len(t, *input.BeforePlan, 1) + assert.EqualValues(t, "test", (*input.BeforePlan)[0]) + }, + }, + { + name: "description is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + Description: utils.AddressOf("test"), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.Description) + assert.EqualValues(t, "test", *input.Description) + }, + }, + { + name: "provider is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + Provider: utils.AddressOf("test"), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.Provider) + assert.EqualValues(t, "test", *input.Provider) + }, + }, + { + name: "labels are set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + Labels: &[]string{"test"}, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.Labels) + require.Len(t, *input.Labels, 1) + assert.EqualValues(t, "test", (*input.Labels)[0]) + }, + }, + { + name: "space is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + SpaceId: utils.AddressOf("test"), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.Space) + assert.EqualValues(t, "test", *input.Space) + }, + }, + { + name: "project root is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + ProjectRoot: utils.AddressOf("test"), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.ProjectRoot) + assert.EqualValues(t, "test", *input.ProjectRoot) + }, + }, + { + name: "runner image is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + RunnerImage: utils.AddressOf("test"), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.RunnerImage) + assert.EqualValues(t, "test", *input.RunnerImage) + }, + }, + { + name: "vendor config CF is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + VendorConfig: &v1beta1.VendorConfig{ + CloudFormation: &v1beta1.CloudFormationConfig{ + EntryTemplateFile: "template", + Region: "region", + StackName: "stack name", + TemplateBucket: "bucket", + }, + }, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.VendorConfig) + assert.EqualValues(t, VendorConfigInput{ + CloudFormationInput: &CloudFormationInput{ + EntryTemplateFile: "template", + Region: "region", + StackName: "stack name", + TemplateBucket: "bucket", + }, + }, *input.VendorConfig) + }, + }, + { + name: "vendor config K8S is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + VendorConfig: &v1beta1.VendorConfig{ + Kubernetes: &v1beta1.KubernetesConfig{ + Namespace: "namespace", + KubectlVersion: utils.AddressOf("version"), + }, + }, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.VendorConfig) + assert.EqualValues(t, VendorConfigInput{ + Kubernetes: &KubernetesInput{ + Namespace: "namespace", + KubectlVersion: (*graphql.String)(utils.AddressOf("version")), + }, + }, *input.VendorConfig) + }, + }, + { + name: "vendor config pulumi is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + VendorConfig: &v1beta1.VendorConfig{ + Pulumi: &v1beta1.PulumiConfig{ + LoginURL: "login url", + StackName: "stack name", + }, + }, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.VendorConfig) + assert.EqualValues(t, VendorConfigInput{ + Pulumi: &PulumiInput{ + LoginURL: "login url", + StackName: "stack name", + }, + }, *input.VendorConfig) + }, + }, + { + name: "vendor config ansible is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + VendorConfig: &v1beta1.VendorConfig{ + Ansible: &v1beta1.AnsibleConfig{ + Playbook: "playbook", + }, + }, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.VendorConfig) + assert.EqualValues(t, VendorConfigInput{ + AnsibleInput: &AnsibleInput{ + Playbook: "playbook", + }, + }, *input.VendorConfig) + }, + }, + { + name: "vendor config terragrunt is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + VendorConfig: &v1beta1.VendorConfig{ + Terragrunt: &v1beta1.TerragruntConfig{ + TerraformVersion: "tf version", + TerragruntVersion: "tg version", + UseRunAll: true, + UseSmartSanitization: true, + }, + }, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.VendorConfig) + assert.EqualValues(t, VendorConfigInput{ + TerragruntInput: &TerragruntInput{ + TerraformVersion: "tf version", + TerragruntVersion: "tg version", + UseRunAll: true, + UseSmartSanitization: true, + }, + }, *input.VendorConfig) + }, + }, + { + name: "vendor config TF is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + VendorConfig: &v1beta1.VendorConfig{ + Terraform: &v1beta1.TerraformConfig{ + UseSmartSanitization: true, + Version: utils.AddressOf("version"), + WorkflowTool: utils.AddressOf("workflow"), + Workspace: utils.AddressOf("workspace"), + ExternalStateAccessEnabled: true, + }, + }, + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.VendorConfig) + assert.EqualValues(t, VendorConfigInput{ + Terraform: &TerraformInput{ + UseSmartSanitization: (*graphql.Boolean)(utils.AddressOf(true)), + Version: (*graphql.String)(utils.AddressOf("version")), + WorkflowTool: (*graphql.String)(utils.AddressOf("workflow")), + Workspace: (*graphql.String)(utils.AddressOf("workspace")), + ExternalStateAccessEnabled: (*graphql.Boolean)(utils.AddressOf(true)), + }, + }, *input.VendorConfig) + }, + }, + { + name: "workerpool is set", + stack: v1beta1.Stack{ + Spec: v1beta1.StackSpec{ + WorkerPool: utils.AddressOf("workerpool"), + }, + }, + assert: func(t *testing.T, input StackInput) { + require.NotNil(t, input.WorkerPool) + assert.EqualValues(t, "workerpool", *input.WorkerPool) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.assert(t, FromStackSpec(&tt.stack)) + }) + } +}