Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion compute/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
cloud.google.com/go/storage v1.43.0
github.com/GoogleCloudPlatform/golang-samples v0.0.0-20240724083556-7f760db013b7
github.com/google/uuid v1.6.0
github.com/googleapis/gax-go/v2 v2.13.0
google.golang.org/api v0.193.0
google.golang.org/protobuf v1.34.2
)
Expand All @@ -23,7 +24,6 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
Expand Down
87 changes: 87 additions & 0 deletions compute/reservations/create_shared_reservation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2024 Google LLC
//
// 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
//
// https://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 snippets

// [START compute_reservation_create_shared]
import (
"context"
"fmt"
"io"

compute "cloud.google.com/go/compute/apiv1"
computepb "cloud.google.com/go/compute/apiv1/computepb"
gax "github.com/googleapis/gax-go/v2"
"google.golang.org/protobuf/proto"
)

type ClientInterface interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Is this used only for mocking the client? Is there a way to make a sample fully standalone (it should initialise the client), but still mock the client when running tests?

See my example in Python (different sample): GoogleCloudPlatform/python-docs-samples#12598

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's done this way to allow for either real or mocked client. I tried to do it in a mocking way without modifying the sample, but in Go it couldn't be done as in Python or Java...
Go doesn't support object replacement in runtime, so the typical solution for that is to make an abstraction (interface) that matches the same signature. I tried to cover the gap in comments to explain how it can be done by the developer, who will run this sample, but agree that it doesn't seem ideal.
Perhaps I am missing something - I am open to any suggestions on how to improve it

Close() error
Delete(context.Context, *computepb.DeleteReservationRequest, ...gax.CallOption) (*compute.Operation, error)
Insert(context.Context, *computepb.InsertReservationRequest, ...gax.CallOption) (*compute.Operation, error)
}

// Creates shared reservation from given template in particular zone
func createSharedReservation(w io.Writer, client ClientInterface, projectID, baseProjectId, zone, reservationName, sourceTemplate string) error {
// client, err := compute.NewReservationsRESTClient(ctx)
// projectID := "your_project_id". Destination of sharing.
// baseProjectId := "your_project_id2". Project where the reservation will be created.
// zone := "us-west3-a"
// reservationName := "your_reservation_name"
// sourceTemplate: existing template path. Following formats are allowed:
// - projects/{project_id}/global/instanceTemplates/{template_name}
// - projects/{project_id}/regions/{region}/instanceTemplates/{template_name}
// - https://www.googleapis.com/compute/v1/projects/{project_id}/global/instanceTemplates/instanceTemplate
// - https://www.googleapis.com/compute/v1/projects/{project_id}/regions/{region}/instanceTemplates/instanceTemplate

ctx := context.Background()

shareSettings := map[string]*computepb.ShareSettingsProjectConfig{
projectID: {ProjectId: proto.String(projectID)},
}

req := &computepb.InsertReservationRequest{
Project: baseProjectId,
ReservationResource: &computepb.Reservation{
Name: proto.String(reservationName),
Zone: proto.String(zone),
SpecificReservation: &computepb.AllocationSpecificSKUReservation{
Count: proto.Int64(2),
SourceInstanceTemplate: proto.String(sourceTemplate),
},
ShareSettings: &computepb.ShareSettings{
ProjectMap: shareSettings,
ShareType: proto.String("SPECIFIC_PROJECTS"),
},
},
Zone: zone,
}

op, err := client.Insert(ctx, req)
if err != nil {
return fmt.Errorf("unable to create reservation: %w", err)
}

if op != nil {
if err = op.Wait(ctx); err != nil {
return fmt.Errorf("unable to wait for the operation: %w", err)
}
}

fmt.Fprintf(w, "Reservation created\n")

return nil
}

// [END compute_reservation_create_shared]
39 changes: 39 additions & 0 deletions compute/reservations/mock_shared_project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2024 Google LLC
//
// 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
//
// https://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.

// Mock objects for shared project

package snippets

import (
"context"

compute "cloud.google.com/go/compute/apiv1"
computepb "cloud.google.com/go/compute/apiv1/computepb"
gax "github.com/googleapis/gax-go/v2"
)

type ReservationsClient struct{}

func (client ReservationsClient) Close() error {
return nil
}

func (client ReservationsClient) Insert(context.Context, *computepb.InsertReservationRequest, ...gax.CallOption) (*compute.Operation, error) {
return nil, nil
}

func (client ReservationsClient) Delete(context.Context, *computepb.DeleteReservationRequest, ...gax.CallOption) (*compute.Operation, error) {
return nil, nil
}
142 changes: 97 additions & 45 deletions compute/reservations/reservations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,49 +108,101 @@ func TestReservations(t *testing.T) {

var buf bytes.Buffer

err := createTemplate(tc.ProjectID, templateName)
if err != nil {
t.Errorf("createTemplate got err: %v", err)
}
defer deleteTemplate(tc.ProjectID, templateName)

sourceTemplate, err := getTemplate(tc.ProjectID, templateName)
if err != nil {
t.Errorf("getTemplate got err: %v", err)
}

want := "Reservation created"
if err := createReservation(&buf, tc.ProjectID, zone, reservationName, *sourceTemplate.SelfLink); err != nil {
t.Errorf("createReservation got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("createReservation got %s, want %s", got, want)
}
buf.Reset()

want = fmt.Sprintf("Reservation: %s", reservationName)
if err := getReservation(&buf, tc.ProjectID, zone, reservationName); err != nil {
t.Errorf("getReservation got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("getReservation got %s, want %s", got, want)
}
buf.Reset()

want = fmt.Sprintf("- %s %d", reservationName, 2)
if err := listReservations(&buf, tc.ProjectID, zone); err != nil {
t.Errorf("listReservations got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("listReservations got %s, want %s", got, want)
}
buf.Reset()

want = "Reservation deleted"
if err := deleteReservation(&buf, tc.ProjectID, zone, reservationName); err != nil {
t.Errorf("deleteReservation got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("deleteReservation got %s, want %s", got, want)
}
t.Run("Reservation CRUD", func(t *testing.T) {
err := createTemplate(tc.ProjectID, templateName)
if err != nil {
t.Errorf("createTemplate got err: %v", err)
}
defer deleteTemplate(tc.ProjectID, templateName)

sourceTemplate, err := getTemplate(tc.ProjectID, templateName)
if err != nil {
t.Errorf("getTemplate got err: %v", err)
}

want := "Reservation created"
if err := createReservation(&buf, tc.ProjectID, zone, reservationName, *sourceTemplate.SelfLink); err != nil {
t.Errorf("createReservation got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("createReservation got %s, want %s", got, want)
}
buf.Reset()

want = fmt.Sprintf("Reservation: %s", reservationName)
if err := getReservation(&buf, tc.ProjectID, zone, reservationName); err != nil {
t.Errorf("getReservation got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("getReservation got %s, want %s", got, want)
}
buf.Reset()

want = fmt.Sprintf("- %s %d", reservationName, 2)
if err := listReservations(&buf, tc.ProjectID, zone); err != nil {
t.Errorf("listReservations got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("listReservations got %s, want %s", got, want)
}
buf.Reset()

want = "Reservation deleted"
if err := deleteReservation(&buf, tc.ProjectID, zone, reservationName); err != nil {
t.Errorf("deleteReservation got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("deleteReservation got %s, want %s", got, want)
}
})

t.Run("Shared reservation CRUD", func(t *testing.T) {
baseProjectID := tc.ProjectID
// This test require 2 projects, therefore one of them is mocked.
// If you want to make a real test, please adjust projectID accordingly and uncomment reservationsClient creation.
// Make sure that base project has proper permissions to share reservations.
// See: https://cloud.google.com/compute/docs/instances/reservations-shared#shared_reservation_constraint
destinationProjectID := "some-project"
err := createTemplate(baseProjectID, templateName)
if err != nil {
t.Errorf("createTemplate got err: %v", err)
}
defer deleteTemplate(baseProjectID, templateName)

sourceTemplate, err := getTemplate(baseProjectID, templateName)
if err != nil {
t.Errorf("getTemplate got err: %v", err)
}

want := "Reservation created"

ctx := context.Background()

// Uncomment line below if you want to run the test without mocks
// reservationsClient, err := compute.NewReservationsRESTClient(ctx)
reservationsClient := ReservationsClient{}
if err != nil {
t.Errorf("Couldn't create reservationsClient, err: %v", err)
}
defer reservationsClient.Close()

if err := createSharedReservation(&buf, reservationsClient, destinationProjectID, baseProjectID, zone, reservationName, *sourceTemplate.SelfLink); err != nil {
t.Errorf("createSharedReservation got err: %v", err)
}
if got := buf.String(); !strings.Contains(got, want) {
t.Errorf("createSharedReservation got %s, want %s", got, want)
}
buf.Reset()

req := &computepb.DeleteReservationRequest{
Project: baseProjectID,
Reservation: reservationName,
Zone: zone,
}

_, err = reservationsClient.Delete(ctx, req)
if err != nil {
t.Errorf("unable to delete reservation: %v", err)
}
})
}
Loading