diff --git a/cmd/test-server/main.go b/cmd/test-server/main.go index 2f2dd6b..3ea04de 100644 --- a/cmd/test-server/main.go +++ b/cmd/test-server/main.go @@ -21,12 +21,10 @@ import ( "fmt" "log" "net" - "strings" eventsv1 "github.com/innabox/fulfillment-common/api/events/v1" ffv1 "github.com/innabox/fulfillment-common/api/fulfillment/v1" metadatav1 "github.com/innabox/fulfillment-common/api/metadata/v1" - sharedv1 "github.com/innabox/fulfillment-common/api/shared/v1" "google.golang.org/grpc" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" @@ -85,140 +83,6 @@ func (s *clustersServer) List(ctx context.Context, request *ffv1.ClustersListReq return &ffv1.ClustersListResponse{}, nil } -// Simple mock compute instances server for testing -type computeInstancesServer struct { - ffv1.UnimplementedComputeInstancesServer -} - -func (s *computeInstancesServer) Create(ctx context.Context, request *ffv1.ComputeInstancesCreateRequest) (*ffv1.ComputeInstancesCreateResponse, error) { - instance := request.GetObject() - - // Set mock ID and state if not already set - if instance.Id == "" { - instance.Id = "ci-mock-12345" - } - if instance.Status == nil { - instance.Status = &ffv1.ComputeInstanceStatus{ - State: ffv1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PROGRESSING, - } - } - - log.Printf("Created compute instance: %s (name: %s, template: %s)", - instance.Id, - instance.GetMetadata().GetName(), - instance.GetSpec().GetTemplate()) - - return &ffv1.ComputeInstancesCreateResponse{Object: instance}, nil -} - -func (s *computeInstancesServer) Get(ctx context.Context, request *ffv1.ComputeInstancesGetRequest) (*ffv1.ComputeInstancesGetResponse, error) { - // Return a mock instance - instance := &ffv1.ComputeInstance{ - Id: request.Id, - Metadata: &sharedv1.Metadata{ - Name: "mock-instance", - }, - Spec: &ffv1.ComputeInstanceSpec{ - Template: "small-instance", - }, - Status: &ffv1.ComputeInstanceStatus{ - State: ffv1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_READY, - IpAddress: "192.168.1.100", - }, - } - - log.Printf("Retrieved compute instance: %s", request.Id) - return &ffv1.ComputeInstancesGetResponse{Object: instance}, nil -} - -func (s *computeInstancesServer) List(ctx context.Context, request *ffv1.ComputeInstancesListRequest) (*ffv1.ComputeInstancesListResponse, error) { - // Return a mock list with one instance - instance := &ffv1.ComputeInstance{ - Id: "ci-mock-12345", - Metadata: &sharedv1.Metadata{ - Name: "mock-instance", - }, - Spec: &ffv1.ComputeInstanceSpec{ - Template: "small-instance", - }, - Status: &ffv1.ComputeInstanceStatus{ - State: ffv1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_READY, - IpAddress: "192.168.1.100", - }, - } - - size := int32(1) - total := int32(1) - log.Printf("Listed compute instances") - return &ffv1.ComputeInstancesListResponse{ - Items: []*ffv1.ComputeInstance{instance}, - Size: &size, - Total: &total, - }, nil -} - -// Simple mock compute instance templates server -type computeInstanceTemplatesServer struct { - ffv1.UnimplementedComputeInstanceTemplatesServer -} - -func (s *computeInstanceTemplatesServer) Get(ctx context.Context, request *ffv1.ComputeInstanceTemplatesGetRequest) (*ffv1.ComputeInstanceTemplatesGetResponse, error) { - // Return a mock template - template := &ffv1.ComputeInstanceTemplate{ - Id: request.Id, - Metadata: &sharedv1.Metadata{ - Name: request.Id, - }, - } - - log.Printf("Retrieved compute instance template: %s", request.Id) - return &ffv1.ComputeInstanceTemplatesGetResponse{Object: template}, nil -} - -func (s *computeInstanceTemplatesServer) List(ctx context.Context, request *ffv1.ComputeInstanceTemplatesListRequest) (*ffv1.ComputeInstanceTemplatesListResponse, error) { - // All available templates - allTemplates := []*ffv1.ComputeInstanceTemplate{ - { - Id: "tpl-small-001", - Metadata: &sharedv1.Metadata{ - Name: "small-instance", - }, - }, - { - Id: "tpl-large-001", - Metadata: &sharedv1.Metadata{ - Name: "large-instance", - }, - }, - } - - // Apply filter if provided (simple string matching for mock purposes) - filter := request.GetFilter() - var templates []*ffv1.ComputeInstanceTemplate - - if filter != "" { - // Simple filter: check if filter contains the template ID or name - // This is a mock implementation - real server would parse CEL expressions - for _, tmpl := range allTemplates { - // Check if filter mentions this template's ID or name - if strings.Contains(filter, tmpl.Id) || strings.Contains(filter, tmpl.GetMetadata().GetName()) { - templates = append(templates, tmpl) - } - } - } else { - templates = allTemplates - } - - size := int32(len(templates)) - total := int32(len(templates)) - log.Printf("Listed compute instance templates (filter: %q, matches: %d)", filter, len(templates)) - return &ffv1.ComputeInstanceTemplatesListResponse{ - Items: templates, - Size: &size, - Total: &total, - }, nil -} - // Dummy metadata server - required for login type metadataServer struct { metadatav1.UnimplementedMetadataServer @@ -234,12 +98,12 @@ func main() { scenarioFile := flag.String("scenario", defaultScenarioFile, "Path to event scenario YAML file") flag.Parse() - // Load scenario from file + // Load event scenario from file scenario, err := testing.LoadScenarioFromFile(*scenarioFile) if err != nil { - log.Fatalf("Failed to load scenario from %s: %v", *scenarioFile, err) + log.Fatalf("Failed to load event scenario from %s: %v", *scenarioFile, err) } - log.Printf("Loaded scenario: %s - %s", scenario.Name, scenario.Description) + log.Printf("Loaded event scenario: %s - %s", scenario.Name, scenario.Description) listener, err := net.Listen("tcp", "127.0.0.1:"+serverPort) if err != nil { @@ -255,8 +119,6 @@ func main() { eventsv1.RegisterEventsServer(grpcServer, &loggingEventsServer{EventsServerFuncs: eventsServerFuncs}) ffv1.RegisterClustersServer(grpcServer, &clustersServer{}) - ffv1.RegisterComputeInstancesServer(grpcServer, &computeInstancesServer{}) - ffv1.RegisterComputeInstanceTemplatesServer(grpcServer, &computeInstanceTemplatesServer{}) metadatav1.RegisterMetadataServer(grpcServer, &metadataServer{}) // Register health service diff --git a/internal/cmd/create/computeinstance/computeinstance_e2e_test.go b/internal/cmd/create/computeinstance/computeinstance_e2e_test.go new file mode 100644 index 0000000..1a99b67 --- /dev/null +++ b/internal/cmd/create/computeinstance/computeinstance_e2e_test.go @@ -0,0 +1,156 @@ +/* +Copyright (c) 2025 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package computeinstance + +import ( + "context" + + ffv1 "github.com/innabox/fulfillment-common/api/fulfillment/v1" + sharedv1 "github.com/innabox/fulfillment-common/api/shared/v1" + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/gomega" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/innabox/fulfillment-cli/internal/testing" +) + +var _ = Describe("Compute Instance E2E", func() { + var ( + ctx context.Context + cancel context.CancelFunc + server *testing.Server + conn *grpc.ClientConn + instanceClient ffv1.ComputeInstancesClient + templateClient ffv1.ComputeInstanceTemplatesClient + ) + + BeforeEach(func() { + var err error + + // Create cancellable context + ctx, cancel = context.WithCancel(context.Background()) + DeferCleanup(cancel) + + // Create test server + server = testing.NewServer() + DeferCleanup(server.Stop) + + // Create and load compute instance scenario + scenario := &testing.ComputeInstanceScenario{ + Name: "e2e-test-scenario", + Description: "E2E test scenario for compute instances", + Templates: []*testing.TemplateData{ + { + ID: "tpl-test-001", + Name: "test-template", + Title: "Test Template", + Description: "A test compute instance template", + }, + }, + Instances: []*testing.InstanceData{ + { + ID: "ci-existing-001", + Name: "existing-instance", + Template: "tpl-test-001", + State: ffv1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_READY, + IPAddress: "192.168.1.100", + }, + }, + } + + // Create and register mock compute instance server + ciServer := testing.NewMockComputeInstancesServer(scenario) + ffv1.RegisterComputeInstancesServer(server.Registrar(), ciServer) + + // Create and register mock compute instance templates server + citServer := testing.NewMockComputeInstanceTemplatesServer(scenario) + ffv1.RegisterComputeInstanceTemplatesServer(server.Registrar(), citServer) + + // Start the server + server.Start() + + // Create client connection + conn, err = grpc.NewClient( + server.Address(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(conn.Close) + + // Create clients + instanceClient = ffv1.NewComputeInstancesClient(conn) + templateClient = ffv1.NewComputeInstanceTemplatesClient(conn) + }) + + It("should create, list, and delete a compute instance", func() { + // Step 1: List templates to get a valid template + listTemplatesResp, err := templateClient.List(ctx, &ffv1.ComputeInstanceTemplatesListRequest{}) + Expect(err).ToNot(HaveOccurred()) + Expect(listTemplatesResp.Items).ToNot(BeEmpty()) + template := listTemplatesResp.Items[0] + + // Step 2: List existing instances (should have 1) + listResp, err := instanceClient.List(ctx, &ffv1.ComputeInstancesListRequest{}) + Expect(err).ToNot(HaveOccurred()) + initialCount := len(listResp.Items) + Expect(initialCount).To(Equal(1)) + + // Step 3: Create a new compute instance + createResp, err := instanceClient.Create(ctx, &ffv1.ComputeInstancesCreateRequest{ + Object: &ffv1.ComputeInstance{ + Metadata: &sharedv1.Metadata{ + Name: "new-test-instance", + }, + Spec: &ffv1.ComputeInstanceSpec{ + Template: template.Id, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(createResp.Object).ToNot(BeNil()) + Expect(createResp.Object.Id).ToNot(BeEmpty()) + createdID := createResp.Object.Id + + // Step 4: List instances again (should have 2 now) + listResp, err = instanceClient.List(ctx, &ffv1.ComputeInstancesListRequest{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(listResp.Items)).To(Equal(initialCount + 1)) + + // Step 5: Get the created instance by ID + getResp, err := instanceClient.Get(ctx, &ffv1.ComputeInstancesGetRequest{ + Id: createdID, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(getResp.Object.Id).To(Equal(createdID)) + Expect(getResp.Object.Spec.Template).To(Equal(template.Id)) + + // Step 6: Delete the created instance + _, err = instanceClient.Delete(ctx, &ffv1.ComputeInstancesDeleteRequest{ + Id: createdID, + }) + Expect(err).ToNot(HaveOccurred()) + + // Step 7: List instances again (should be back to initial count) + listResp, err = instanceClient.List(ctx, &ffv1.ComputeInstancesListRequest{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(listResp.Items)).To(Equal(initialCount)) + + // Step 8: Verify the instance was deleted (Get should fail) + _, err = instanceClient.Get(ctx, &ffv1.ComputeInstancesGetRequest{ + Id: createdID, + }) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/internal/cmd/create/computeinstance/computeinstance_suite_test.go b/internal/cmd/create/computeinstance/computeinstance_suite_test.go new file mode 100644 index 0000000..1b309b2 --- /dev/null +++ b/internal/cmd/create/computeinstance/computeinstance_suite_test.go @@ -0,0 +1,26 @@ +/* +Copyright (c) 2025 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package computeinstance + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/gomega" +) + +func TestComputeInstance(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Compute Instance Suite") +} diff --git a/internal/testing/compute_instance_scenario.go b/internal/testing/compute_instance_scenario.go new file mode 100644 index 0000000..8cb7498 --- /dev/null +++ b/internal/testing/compute_instance_scenario.go @@ -0,0 +1,146 @@ +/* +Copyright (c) 2025 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package testing + +import ( + "fmt" + "os" + + ffv1 "github.com/innabox/fulfillment-common/api/fulfillment/v1" + sharedv1 "github.com/innabox/fulfillment-common/api/shared/v1" + "gopkg.in/yaml.v3" +) + +// ComputeInstanceScenario represents test data for compute instances and templates +type ComputeInstanceScenario struct { + Name string + Description string + Templates []*TemplateData + Instances []*InstanceData +} + +// TemplateData contains compute instance template data +type TemplateData struct { + ID string + Name string + Title string + Description string +} + +// InstanceData contains compute instance data +type InstanceData struct { + ID string + Name string + Template string + State ffv1.ComputeInstanceState + IPAddress string +} + +// YAML parsing structures +type computeInstanceScenarioFile struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Templates []*templateFile `yaml:"templates"` + Instances []*instanceFile `yaml:"instances"` +} + +type templateFile struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Title string `yaml:"title"` + Description string `yaml:"description"` +} + +type instanceFile struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Template string `yaml:"template"` + State string `yaml:"state"` + IPAddress string `yaml:"ipAddress"` +} + +// LoadComputeInstanceScenarioFromFile loads a compute instance scenario from a YAML file +func LoadComputeInstanceScenarioFromFile(filename string) (*ComputeInstanceScenario, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read scenario file: %w", err) + } + + var file computeInstanceScenarioFile + if err := yaml.Unmarshal(data, &file); err != nil { + return nil, fmt.Errorf("failed to parse scenario YAML: %w", err) + } + + return file.toScenario(), nil +} + +// toScenario converts a computeInstanceScenarioFile to a ComputeInstanceScenario with proper proto enums +func (f *computeInstanceScenarioFile) toScenario() *ComputeInstanceScenario { + scenario := &ComputeInstanceScenario{ + Name: f.Name, + Description: f.Description, + Templates: make([]*TemplateData, len(f.Templates)), + Instances: make([]*InstanceData, len(f.Instances)), + } + + for i, t := range f.Templates { + scenario.Templates[i] = &TemplateData{ + ID: t.ID, + Name: t.Name, + Title: t.Title, + Description: t.Description, + } + } + + for i, inst := range f.Instances { + scenario.Instances[i] = &InstanceData{ + ID: inst.ID, + Name: inst.Name, + Template: inst.Template, + State: ffv1.ComputeInstanceState(ffv1.ComputeInstanceState_value[inst.State]), + IPAddress: inst.IPAddress, + } + } + + return scenario +} + +// ToProtoTemplate converts TemplateData to a proto ComputeInstanceTemplate +func (t *TemplateData) ToProtoTemplate() *ffv1.ComputeInstanceTemplate { + return &ffv1.ComputeInstanceTemplate{ + Id: t.ID, + Metadata: &sharedv1.Metadata{ + Name: t.Name, + }, + Title: t.Title, + Description: t.Description, + } +} + +// ToProtoInstance converts InstanceData to a proto ComputeInstance +func (i *InstanceData) ToProtoInstance() *ffv1.ComputeInstance { + return &ffv1.ComputeInstance{ + Id: i.ID, + Metadata: &sharedv1.Metadata{ + Name: i.Name, + }, + Spec: &ffv1.ComputeInstanceSpec{ + Template: i.Template, + }, + Status: &ffv1.ComputeInstanceStatus{ + State: i.State, + IpAddress: i.IPAddress, + }, + } +} diff --git a/internal/testing/mock_compute_instances_server.go b/internal/testing/mock_compute_instances_server.go new file mode 100644 index 0000000..1f01f5c --- /dev/null +++ b/internal/testing/mock_compute_instances_server.go @@ -0,0 +1,175 @@ +/* +Copyright (c) 2025 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package testing + +import ( + "context" + "fmt" + "sync" + + ffv1 "github.com/innabox/fulfillment-common/api/fulfillment/v1" + sharedv1 "github.com/innabox/fulfillment-common/api/shared/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// MockComputeInstancesServer is a mock implementation of the ComputeInstancesServer +type MockComputeInstancesServer struct { + ffv1.UnimplementedComputeInstancesServer + scenario *ComputeInstanceScenario + instances map[string]*ffv1.ComputeInstance + mu sync.RWMutex + nextID int +} + +// NewMockComputeInstancesServer creates a new mock compute instances server +func NewMockComputeInstancesServer(scenario *ComputeInstanceScenario) *MockComputeInstancesServer { + server := &MockComputeInstancesServer{ + scenario: scenario, + instances: make(map[string]*ffv1.ComputeInstance), + nextID: 1000, + } + + // Pre-populate with scenario instances + for _, instanceData := range scenario.Instances { + server.instances[instanceData.ID] = instanceData.ToProtoInstance() + } + + return server +} + +// Create creates a new compute instance +func (s *MockComputeInstancesServer) Create(ctx context.Context, request *ffv1.ComputeInstancesCreateRequest) (*ffv1.ComputeInstancesCreateResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + instance := request.GetObject() + if instance == nil { + return nil, status.Error(codes.InvalidArgument, "object is required") + } + + // Generate ID if not provided + if instance.Id == "" { + s.nextID++ + instance.Id = fmt.Sprintf("ci-test-%d", s.nextID) + } + + // Set state to PROGRESSING if not set + if instance.Status == nil { + instance.Status = &ffv1.ComputeInstanceStatus{} + } + if instance.Status.State == ffv1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED { + instance.Status.State = ffv1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PROGRESSING + } + + // Store the instance + s.instances[instance.Id] = instance + + return &ffv1.ComputeInstancesCreateResponse{Object: instance}, nil +} + +// Get retrieves a compute instance by ID +func (s *MockComputeInstancesServer) Get(ctx context.Context, request *ffv1.ComputeInstancesGetRequest) (*ffv1.ComputeInstancesGetResponse, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + instance, exists := s.instances[request.Id] + if !exists { + return nil, status.Errorf(codes.NotFound, "compute instance %q not found", request.Id) + } + + return &ffv1.ComputeInstancesGetResponse{Object: instance}, nil +} + +// List lists all compute instances +func (s *MockComputeInstancesServer) List(ctx context.Context, request *ffv1.ComputeInstancesListRequest) (*ffv1.ComputeInstancesListResponse, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + instances := make([]*ffv1.ComputeInstance, 0, len(s.instances)) + for _, instance := range s.instances { + instances = append(instances, instance) + } + + size := int32(len(instances)) + total := int32(len(instances)) + + return &ffv1.ComputeInstancesListResponse{ + Items: instances, + Size: &size, + Total: &total, + }, nil +} + +// Delete deletes a compute instance by ID +func (s *MockComputeInstancesServer) Delete(ctx context.Context, request *ffv1.ComputeInstancesDeleteRequest) (*ffv1.ComputeInstancesDeleteResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.instances[request.Id]; !exists { + return nil, status.Errorf(codes.NotFound, "compute instance %q not found", request.Id) + } + + delete(s.instances, request.Id) + + return &ffv1.ComputeInstancesDeleteResponse{}, nil +} + +// MockComputeInstanceTemplatesServer is a mock implementation of the ComputeInstanceTemplatesServer +type MockComputeInstanceTemplatesServer struct { + ffv1.UnimplementedComputeInstanceTemplatesServer + scenario *ComputeInstanceScenario +} + +// NewMockComputeInstanceTemplatesServer creates a new mock compute instance templates server +func NewMockComputeInstanceTemplatesServer(scenario *ComputeInstanceScenario) *MockComputeInstanceTemplatesServer { + return &MockComputeInstanceTemplatesServer{ + scenario: scenario, + } +} + +// Get retrieves a compute instance template by ID +func (s *MockComputeInstanceTemplatesServer) Get(ctx context.Context, request *ffv1.ComputeInstanceTemplatesGetRequest) (*ffv1.ComputeInstanceTemplatesGetResponse, error) { + for _, templateData := range s.scenario.Templates { + if templateData.ID == request.Id { + return &ffv1.ComputeInstanceTemplatesGetResponse{Object: templateData.ToProtoTemplate()}, nil + } + } + + return nil, status.Errorf(codes.NotFound, "compute instance template %q not found", request.Id) +} + +// List lists all compute instance templates +func (s *MockComputeInstanceTemplatesServer) List(ctx context.Context, request *ffv1.ComputeInstanceTemplatesListRequest) (*ffv1.ComputeInstanceTemplatesListResponse, error) { + templates := make([]*ffv1.ComputeInstanceTemplate, len(s.scenario.Templates)) + for i, templateData := range s.scenario.Templates { + templates[i] = templateData.ToProtoTemplate() + } + + size := int32(len(templates)) + total := int32(len(templates)) + + return &ffv1.ComputeInstanceTemplatesListResponse{ + Items: templates, + Size: &size, + Total: &total, + }, nil +} + +// NewMetadata creates a new metadata object with the given name +func NewMetadata(name string) *sharedv1.Metadata { + return &sharedv1.Metadata{ + Name: name, + } +}