diff --git a/api/cloud-resources/v1beta1/gcpvpcpeering_types.go b/api/cloud-resources/v1beta1/gcpvpcpeering_types.go index 8d9d92bd5..f6f0ec82a 100644 --- a/api/cloud-resources/v1beta1/gcpvpcpeering_types.go +++ b/api/cloud-resources/v1beta1/gcpvpcpeering_types.go @@ -9,10 +9,18 @@ import ( // Important: Run "make" to regenerate code after modifying this file type GcpVpcPeeringSpec struct { - ImportCustomRoutes bool `json:"importCustomRoutes,omitempty"` - PeeringName string `json:"peeringName,omitempty"` - RemoteVpc string `json:"remoteVpc,omitempty"` - RemoteProject string `json:"remoteProject,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule=(self == oldSelf), message="ImportCustomRoutes is immutable." + ImportCustomRoutes bool `json:"importCustomRoutes,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule=(self == oldSelf), message="PeeringName is immutable." + PeeringName string `json:"peeringName,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule=(self == oldSelf), message="RemoteVpc is immutable." + RemoteVpc string `json:"remoteVpc,omitempty"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule=(self == oldSelf), message="RemoteNetwork is immutable." + RemoteProject string `json:"remoteProject,omitempty"` } type GcpVpcPeeringStatus struct { diff --git a/config/crd/bases/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml b/config/crd/bases/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml index 037bbd542..68ea5a314 100644 --- a/config/crd/bases/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml +++ b/config/crd/bases/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml @@ -34,12 +34,24 @@ spec: properties: importCustomRoutes: type: boolean + x-kubernetes-validations: + - message: ImportCustomRoutes is immutable. + rule: (self == oldSelf) peeringName: type: string + x-kubernetes-validations: + - message: PeeringName is immutable. + rule: (self == oldSelf) remoteProject: type: string + x-kubernetes-validations: + - message: RemoteNetwork is immutable. + rule: (self == oldSelf) remoteVpc: type: string + x-kubernetes-validations: + - message: RemoteVpc is immutable. + rule: (self == oldSelf) type: object status: properties: diff --git a/config/dist/skr/crd/bases/providers/gcp/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml b/config/dist/skr/crd/bases/providers/gcp/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml index 037bbd542..d01cf82e4 100644 --- a/config/dist/skr/crd/bases/providers/gcp/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml +++ b/config/dist/skr/crd/bases/providers/gcp/cloud-resources.kyma-project.io_gcpvpcpeerings.yaml @@ -34,12 +34,24 @@ spec: properties: importCustomRoutes: type: boolean + x-kubernetes-validations: + - message: ImportCustomRoutes is immutable. + rule: (self == oldSelf) peeringName: type: string + x-kubernetes-validations: + - message: PeeringName is immutable. + rule: (self == oldSelf) remoteProject: type: string + x-kubernetes-validations: + - message: RemoteProject is immutable. + rule: (self == oldSelf) remoteVpc: type: string + x-kubernetes-validations: + - message: RemoteVpc is immutable. + rule: (self == oldSelf) type: object status: properties: diff --git a/internal/controller/cloud-control/vpcpeering_gcp_test.go b/internal/controller/cloud-control/vpcpeering_gcp_test.go index ade68f307..4a7315ccc 100644 --- a/internal/controller/cloud-control/vpcpeering_gcp_test.go +++ b/internal/controller/cloud-control/vpcpeering_gcp_test.go @@ -4,9 +4,9 @@ import ( cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" scopePkg "github.com/kyma-project/cloud-manager/pkg/kcp/scope" . "github.com/kyma-project/cloud-manager/pkg/testinfra/dsl" + "github.com/kyma-project/cloud-manager/pkg/util" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "time" ) var _ = Describe("Feature: KCP VpcPeering", func() { @@ -63,7 +63,7 @@ var _ = Describe("Feature: KCP VpcPeering", func() { }) By("Then VpcPeering does not exist", func() { - Eventually(IsDeleted, 5*time.Second). + Eventually(IsDeleted, 5*util.Timing.T1000ms()). WithArguments(infra.Ctx(), infra.KCP().Client(), vpcpeering). Should(Succeed(), "expected VpcPeering does not to exist (being deleted), but it still exists") }) diff --git a/pkg/kcp/provider/gcp/mock/vpcPeeringStore.go b/pkg/kcp/provider/gcp/mock/vpcPeeringStore.go index 8a23af851..0c060b016 100644 --- a/pkg/kcp/provider/gcp/mock/vpcPeeringStore.go +++ b/pkg/kcp/provider/gcp/mock/vpcPeeringStore.go @@ -4,7 +4,7 @@ import ( compute "cloud.google.com/go/compute/apiv1" pb "cloud.google.com/go/compute/apiv1/computepb" "context" - "github.com/elliotchance/pie/v2" + "fmt" "k8s.io/utils/ptr" "sync" ) @@ -14,41 +14,103 @@ type vpcPeeringEntry struct { } type vpcPeeringStore struct { m sync.Mutex - items []*vpcPeeringEntry + items map[string]*vpcPeeringEntry } -func (s *vpcPeeringStore) CreateVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, importCustomRoutes *bool, kymaProject *string, kymaVpc *string) (*pb.NetworkPeering, error) { +func getFullNetworkUrl(project, vpc string) string { + return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/networks/%s", project, vpc) +} + +func (s *vpcPeeringStore) CreateRemoteVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, importCustomRoutes *bool, kymaProject *string, kymaVpc *string) (*compute.Operation, error) { s.m.Lock() defer s.m.Unlock() + remoteNetwork := getFullNetworkUrl(*remoteProject, *remoteVpc) + kymaNetwork := getFullNetworkUrl(*kymaProject, *kymaVpc) + + _, peeringExists := s.items[remoteNetwork] + if peeringExists { + return new(compute.Operation), nil + } + + state := pb.NetworkPeering_ACTIVE.String() item := &vpcPeeringEntry{ peering: &pb.NetworkPeering{ Name: name, - Network: remoteVpc, + Network: &kymaNetwork, ImportCustomRoutes: importCustomRoutes, ExchangeSubnetRoutes: ptr.To(true), }, } + item.peering.State = &state + s.items[remoteNetwork] = item + + return new(compute.Operation), nil +} + +func (s *vpcPeeringStore) CreateKymaVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, importCustomRoutes *bool, kymaProject *string, kymaVpc *string) (*compute.Operation, error) { + s.m.Lock() + defer s.m.Unlock() - s.items = append(s.items, item) + remoteNetwork := getFullNetworkUrl(*remoteProject, *remoteVpc) + kymaNetwork := getFullNetworkUrl(*kymaProject, *kymaVpc) - return item.peering, nil + _, peeringExists := s.items[kymaNetwork] + if peeringExists { + return new(compute.Operation), nil + } + + state := pb.NetworkPeering_ACTIVE.String() + + item := &vpcPeeringEntry{ + peering: &pb.NetworkPeering{ + Name: name, + Network: &remoteNetwork, + ImportCustomRoutes: importCustomRoutes, + ExchangeSubnetRoutes: ptr.To(true), + }, + } + item.peering.State = &state + + s.items[kymaNetwork] = item + + return new(compute.Operation), nil } -func (s *vpcPeeringStore) DeleteVpcPeering(ctx context.Context, name *string, kymaProject *string, kymaVpc *string) (*compute.Operation, error) { +func (s *vpcPeeringStore) CheckRemoteNetworkTags(context context.Context, remoteVpc *string, remoteProject *string, desiredTag string) (bool, error) { + s.m.Lock() + defer s.m.Unlock() + + return true, nil +} + +func (s *vpcPeeringStore) GetVpcPeering(ctx context.Context, name *string, project *string, vpc *string) (*pb.NetworkPeering, error) { s.m.Lock() defer s.m.Unlock() - s.items = pie.Filter(s.items, func(vpe *vpcPeeringEntry) bool { - return !(vpe.peering.Name == name && *vpe.peering.Network == "https://www.googleapis.com/compute/v1/projects/"+*kymaProject+"/global/networks/"+*kymaVpc) - }) - return nil, nil + + if s.items == nil { + s.items = make(map[string]*vpcPeeringEntry) + } + + network := getFullNetworkUrl(*project, *vpc) + + _, peeringExists := s.items[network] + if !peeringExists { + return nil, nil + } + + return s.items[network].peering, nil } -func (s *vpcPeeringStore) DescribeVpcPeeringConnections(ctx context.Context) ([]*pb.NetworkPeering, error) { +func (s *vpcPeeringStore) DeleteVpcPeering(ctx context.Context, name *string, kymaProject *string, kymaVpc *string) (*compute.Operation, error) { s.m.Lock() defer s.m.Unlock() - return pie.Map(s.items, func(e *vpcPeeringEntry) *pb.NetworkPeering { - return e.peering - }), nil + kymaNetwork := getFullNetworkUrl(*kymaProject, *kymaVpc) + + if s.items[kymaNetwork] == nil { + return nil, nil + } + s.items[kymaNetwork] = nil + return new(compute.Operation), nil } diff --git a/pkg/kcp/provider/gcp/vpcpeering/addFinalizer.go b/pkg/kcp/provider/gcp/vpcpeering/addFinalizer.go deleted file mode 100644 index 5a9499056..000000000 --- a/pkg/kcp/provider/gcp/vpcpeering/addFinalizer.go +++ /dev/null @@ -1,30 +0,0 @@ -package vpcpeering - -import ( - "context" - cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" - "github.com/kyma-project/cloud-manager/pkg/composed" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -func addFinalizer(ctx context.Context, state composed.State) (error, context.Context) { - // Object is being deleted, don't add finalizer - if composed.MarkedForDeletionPredicate(ctx, state) { - return nil, nil - } - - // If finalizer already present, don't add it again. - if controllerutil.ContainsFinalizer(state.Obj(), cloudcontrolv1beta1.FinalizerName) { - return nil, nil - } - - //Add finalizer - controllerutil.AddFinalizer(state.Obj(), cloudcontrolv1beta1.FinalizerName) - - if err := state.UpdateObj(ctx); err != nil { - return composed.LogErrorAndReturn(err, "Error adding Finalizer", composed.StopWithRequeue, ctx) - } - - // Requeue to reload the object - return composed.StopWithRequeue, nil -} diff --git a/pkg/kcp/provider/gcp/vpcpeering/client/cloudComputeClient.go b/pkg/kcp/provider/gcp/vpcpeering/client/vpcPeeringClient.go similarity index 59% rename from pkg/kcp/provider/gcp/vpcpeering/client/cloudComputeClient.go rename to pkg/kcp/provider/gcp/vpcpeering/client/vpcPeeringClient.go index b521c431d..d68ec03b5 100644 --- a/pkg/kcp/provider/gcp/vpcpeering/client/cloudComputeClient.go +++ b/pkg/kcp/provider/gcp/vpcpeering/client/vpcPeeringClient.go @@ -1,19 +1,3 @@ -package client - -import ( - compute "cloud.google.com/go/compute/apiv1" - pb "cloud.google.com/go/compute/apiv1/computepb" - resourcemanager "cloud.google.com/go/resourcemanager/apiv3" - "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" - "context" - "fmt" - "github.com/kyma-project/cloud-manager/pkg/common/abstractions" - "github.com/kyma-project/cloud-manager/pkg/kcp/provider/gcp/cloudclient" - "google.golang.org/api/option" - "k8s.io/utils/ptr" - "strings" -) - /* required GCP permissions ========================= @@ -30,6 +14,24 @@ required GCP permissions compute.networks.ListEffectiveTags => https://cloud.google.com/resource-manager/reference/rest/v3/tagKeys/get */ +package client + +import ( + compute "cloud.google.com/go/compute/apiv1" + pb "cloud.google.com/go/compute/apiv1/computepb" + resourcemanager "cloud.google.com/go/resourcemanager/apiv3" + "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" + "context" + "fmt" + "github.com/elliotchance/pie/v2" + "github.com/kyma-project/cloud-manager/pkg/common/abstractions" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/kcp/provider/gcp/cloudclient" + "google.golang.org/api/option" + "k8s.io/utils/ptr" + "strings" +) + func createGcpNetworksClient(ctx context.Context) (*compute.NetworksClient, error) { c, err := compute.NewNetworksRESTClient(ctx, option.WithCredentialsFile(abstractions.NewOSEnvironment().Get("GCP_SA_JSON_KEY_PATH"))) if err != nil { @@ -48,90 +50,63 @@ type networkClient struct { } type VpcPeeringClient interface { - CreateVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, importCustomRoutes *bool, kymaProject *string, kymaVpc *string) (*pb.NetworkPeering, error) DeleteVpcPeering(ctx context.Context, name *string, kymaProject *string, kymaVpc *string) (*compute.Operation, error) + GetVpcPeering(ctx context.Context, name *string, project *string, vpc *string) (*pb.NetworkPeering, error) + CreateRemoteVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, importCustomRoutes *bool, kymaProject *string, kymaVpc *string) (*compute.Operation, error) + CreateKymaVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, importCustomRoutes *bool, kymaProject *string, kymaVpc *string) (*compute.Operation, error) + CheckRemoteNetworkTags(context context.Context, remoteVpc *string, remoteProject *string, desiredTag string) (bool, error) } -func (c *networkClient) CreateVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, importCustomRoutes *bool, kymaProject *string, kymaVpc *string) (*pb.NetworkPeering, error) { - - kymaNetwork := getFullNetworkUrl(*kymaProject, *kymaVpc) - remoteNetwork := getFullNetworkUrl(*remoteProject, *remoteVpc) - +func CreateVpcPeeringRequest(ctx context.Context, name *string, sourceVpc *string, sourceProject *string, importCustomRoutes *bool, exportCustomRoutes *bool, destinationProject *string, destinationVpc *string) (*compute.Operation, error) { gcpNetworkClient, err := createGcpNetworksClient(ctx) - if err != nil { return nil, err } defer gcpNetworkClient.Close() + destinationNetworkUrl := getFullNetworkUrl(*destinationProject, *destinationVpc) - //NetworkPeering will only be created if the remote vpc has a tag with the kyma shoot name - remoteNetworkInfo, err := gcpNetworkClient.Get(ctx, &pb.GetNetworkRequest{Network: *remoteVpc, Project: *remoteProject}) - if err != nil { - return nil, err - } - - isRemoteNetworkTagged, err := c.CheckRemoteNetworkTags(ctx, remoteNetworkInfo, *kymaVpc) - - if !isRemoteNetworkTagged || (err != nil && err.Error() == "no more items in iterator") { - return nil, fmt.Errorf("remote network " + *remoteVpc + " is not tagged with the kyma shoot name " + *kymaVpc) - } else if err != nil { - return nil, err - } - - //peering from kyma to remote vpc - peeringRequestFromKyma := &pb.AddPeeringNetworkRequest{ - Network: *kymaVpc, - Project: *kymaProject, + vpcPeeringRequest := &pb.AddPeeringNetworkRequest{ + Network: *sourceVpc, + Project: *sourceProject, NetworksAddPeeringRequestResource: &pb.NetworksAddPeeringRequest{ NetworkPeering: &pb.NetworkPeering{ Name: name, - Network: &remoteNetwork, - ImportCustomRoutes: importCustomRoutes, + Network: &destinationNetworkUrl, + ExportCustomRoutes: exportCustomRoutes, ExchangeSubnetRoutes: ptr.To(true), + ImportCustomRoutes: importCustomRoutes, }, }, } - _, err = gcpNetworkClient.AddPeering(ctx, peeringRequestFromKyma) + operation, err := gcpNetworkClient.AddPeering(ctx, vpcPeeringRequest) if err != nil { return nil, err } + return operation, nil - var networkPeering *pb.NetworkPeering - net, err := gcpNetworkClient.Get(ctx, &pb.GetNetworkRequest{Network: *kymaVpc, Project: *kymaProject}) - nps := net.GetPeerings() - for _, np := range nps { - if *np.Network == remoteNetwork { - networkPeering = np - break - } - } +} +func (c *networkClient) CreateRemoteVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, customRoutes *bool, kymaProject *string, kymaVpc *string) (*compute.Operation, error) { //peering from remote vpc to kyma //by default exportCustomRoutes is false but if the remote vpc wants kyma to import custom routes, the peering needs to export them :) exportCustomRoutes := false - if *importCustomRoutes { + importCustomRoutes := false + if *customRoutes { exportCustomRoutes = true } - peeringRequestFromRemote := &pb.AddPeeringNetworkRequest{ - Network: *remoteVpc, - Project: *remoteProject, - NetworksAddPeeringRequestResource: &pb.NetworksAddPeeringRequest{ - NetworkPeering: &pb.NetworkPeering{ - Name: name, - Network: &kymaNetwork, - ExportCustomRoutes: &exportCustomRoutes, - ExchangeSubnetRoutes: ptr.To(true), - }, - }, - } + return CreateVpcPeeringRequest(ctx, name, remoteVpc, remoteProject, &importCustomRoutes, &exportCustomRoutes, kymaProject, kymaVpc) +} - _, err = gcpNetworkClient.AddPeering(ctx, peeringRequestFromRemote) - if err != nil { - return networkPeering, err +func (c *networkClient) CreateKymaVpcPeering(ctx context.Context, name *string, remoteVpc *string, remoteProject *string, customRoutes *bool, kymaProject *string, kymaVpc *string) (*compute.Operation, error) { + //peering from kyma to remote vpc + //Kyma will not export custom routes to the remote vpc, but if the remote vpc is exporting them we need to import them + exportCustomRoutes := false + importCustomRoutes := false + if *customRoutes { + importCustomRoutes = true } - - return networkPeering, nil + return CreateVpcPeeringRequest(ctx, name, kymaVpc, kymaProject, &importCustomRoutes, &exportCustomRoutes, remoteProject, remoteVpc) } func (c *networkClient) DeleteVpcPeering(ctx context.Context, name *string, kymaProject *string, kymaVpc *string) (*compute.Operation, error) { @@ -151,11 +126,44 @@ func (c *networkClient) DeleteVpcPeering(ctx context.Context, name *string, kyma return deleteVpcPeeringOperation, nil } +func (c *networkClient) GetVpcPeering(ctx context.Context, name *string, project *string, vpc *string) (*pb.NetworkPeering, error) { + gcpNetworkClient, err := createGcpNetworksClient(ctx) + if err != nil { + return nil, err + } + defer gcpNetworkClient.Close() + network, err := gcpNetworkClient.Get(ctx, &pb.GetNetworkRequest{Network: *vpc, Project: *project}) + if err != nil { + return nil, err + } + peerings := pie.Filter(network.GetPeerings(), func(peering *pb.NetworkPeering) bool { return peering.GetName() == *name }) + + if len(peerings) == 0 { + logger := composed.LoggerFromCtx(ctx) + logger.Info("Vpc Peering not found") + return nil, nil + } + return peerings[0], nil +} + func getFullNetworkUrl(project, vpc string) string { return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/networks/%s", project, vpc) } -func (c *networkClient) CheckRemoteNetworkTags(context context.Context, remoteNetwork *pb.Network, desiredTag string) (bool, error) { +func (c *networkClient) CheckRemoteNetworkTags(context context.Context, remoteVpc *string, remoteProject *string, desiredTag string) (bool, error) { + + gcpNetworkClient, err := createGcpNetworksClient(context) + if err != nil { + return false, err + } + defer gcpNetworkClient.Close() + + //NetworkPeering will only be created if the remote vpc has a tag with the kyma shoot name + remoteNetwork, err := gcpNetworkClient.Get(context, &pb.GetNetworkRequest{Network: *remoteVpc, Project: *remoteProject}) + if err != nil { + return false, err + } + //Unfortunately get networks doesn't return the tags, so we need to use the resource manager tag bindings client tbc, err := resourcemanager.NewTagBindingsClient(context, option.WithCredentialsFile(abstractions.NewOSEnvironment().Get("GCP_SA_JSON_KEY_PATH"))) if err != nil { @@ -168,6 +176,9 @@ func (c *networkClient) CheckRemoteNetworkTags(context context.Context, remoteNe for { tag, err := tagIterator.Next() if err != nil { + if err.Error() == "no more items in iterator" { + return false, nil + } return false, err } //since we are not sure where the user is going to put the tag under, let's check if the tag key contains the desired tag diff --git a/pkg/kcp/provider/gcp/vpcpeering/createKymaVpcPeering.go b/pkg/kcp/provider/gcp/vpcpeering/createKymaVpcPeering.go new file mode 100644 index 000000000..8f7694bc5 --- /dev/null +++ b/pkg/kcp/provider/gcp/vpcpeering/createKymaVpcPeering.go @@ -0,0 +1,93 @@ +/* +required GCP permissions +========================= + - The service account used to create the VPC peering connection needs the following permissions: + ** Creates the VPC peering connection + compute.networks.addPeering => https://cloud.google.com/compute/docs/reference/rest/v1/networks/addPeering + ** Removes the VPC peering connection + compute.networks.removePeering => https://cloud.google.com/compute/docs/reference/rest/v1/networks/removePeering + ** Gets the network (VPCs) in order to retrieve the peerings + compute.networks.get => https://cloud.google.com/compute/docs/reference/rest/v1/networks/get +*/ + +package vpcpeering + +import ( + "context" + "fmt" + cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func createKymaVpcPeering(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if state.kymaVpcPeering != nil { + return nil, nil + } + + gcpScope := state.Scope().Spec.Scope.Gcp + project := gcpScope.Project + vpc := gcpScope.VpcNetwork + + //First we need to check if the remote VPC is tagged with the shoot name. + isVpcTagged, err := state.client.CheckRemoteNetworkTags(ctx, state.remoteVpc, state.remoteProject, state.Scope().Spec.Scope.Gcp.VpcNetwork) + if err != nil { + logger.Error(err, "Error creating GCP Kyma VPC Peering while checking remote network tags") + return err, ctx + } + + if !isVpcTagged { + logger.Error(err, "Remote network "+*state.remoteVpc+" is not tagged with the kyma shoot name "+state.Scope().Spec.Scope.Gcp.VpcNetwork) + return composed.UpdateStatus(state.ObjAsVpcPeering()). + SetExclusiveConditions(metav1.Condition{ + Type: cloudcontrolv1beta1.ConditionTypeError, + Status: "True", + Reason: cloudcontrolv1beta1.ReasonFailedCreatingVpcPeeringConnection, + Message: fmt.Sprintf("Error creating VpcPeering, remote VPC does not have a tag with the key: %s", state.Scope().Spec.Scope.Gcp.VpcNetwork), + }). + ErrorLogMessage("Error creating Remote VpcPeering"). + FailedError(composed.StopWithRequeue). + SuccessError(composed.StopWithRequeueDelay(5*util.Timing.T60000ms())). + Run(ctx, state) + } + + _, err = state.client.CreateKymaVpcPeering( + ctx, + state.peeringName, + state.remoteVpc, + state.remoteProject, + state.importCustomRoutes, + &project, + &vpc) + + if err != nil { + return composed.UpdateStatus(state.ObjAsVpcPeering()). + SetExclusiveConditions(metav1.Condition{ + Type: cloudcontrolv1beta1.ConditionTypeError, + Status: "True", + Reason: cloudcontrolv1beta1.ReasonFailedCreatingVpcPeeringConnection, + Message: fmt.Sprintf("Error creating Remote VpcPeering %s", err), + }). + ErrorLogMessage("Error creating Remote VpcPeering"). + FailedError(composed.StopWithRequeue). + SuccessError(composed.StopWithRequeueDelay(util.Timing.T60000ms())). + Run(ctx, state) + } + logger.Info("Kyma VPC Peering Connection created") + return composed.StopWithRequeueDelay(3 * util.Timing.T10000ms()), ctx +} + +func remoteVpcTagChallenge(ctx context.Context, state *State) (bool, error) { + isVpcTagged, err := state.client.CheckRemoteNetworkTags(ctx, state.remoteVpc, state.remoteProject, state.Scope().Spec.Scope.Gcp.VpcNetwork) + if isVpcTagged == false || (err != nil && err.Error() == "no more items in iterator") { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} diff --git a/pkg/kcp/provider/gcp/vpcpeering/createRemoteVpcPeering.go b/pkg/kcp/provider/gcp/vpcpeering/createRemoteVpcPeering.go new file mode 100644 index 000000000..e7c25410f --- /dev/null +++ b/pkg/kcp/provider/gcp/vpcpeering/createRemoteVpcPeering.go @@ -0,0 +1,73 @@ +/* +required GCP permissions +========================= + - The service account used to create the VPC peering connection needs the following permissions: + ** Creates the VPC peering connection + compute.networks.addPeering => https://cloud.google.com/compute/docs/reference/rest/v1/networks/addPeering + ** Gets the network (VPCs) in order to retrieve the peerings + compute.networks.get => https://cloud.google.com/compute/docs/reference/rest/v1/networks/get + ** Fetches the remote network tags + compute.networks.ListEffectiveTags => https://cloud.google.com/resource-manager/reference/rest/v3/tagKeys/get +*/ + +package vpcpeering + +import ( + "context" + "fmt" + cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "regexp" +) + +func createRemoteVpcPeering(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if state.remoteVpcPeering != nil { + return nil, nil + } + + gcpScope := state.Scope().Spec.Scope.Gcp + project := gcpScope.Project + vpc := gcpScope.VpcNetwork + + _, err := state.client.CreateRemoteVpcPeering( + ctx, + state.peeringName, + state.remoteVpc, + state.remoteProject, + state.importCustomRoutes, + &project, + &vpc) + + if err != nil { + message := fmt.Sprintf("Error creating Remote VpcPeering %s", err) + // If we already have a peering with the same network and project, we need to let the user know that the peering already exists + // and he might need to either delete the existing peering or use the same name for the new peering. This is required since we don't + // delete any objects on the user project. + matchesExistingPeering, regexError := regexp.Match("There is already a peering (.*) with the same network. Select another network.", []byte(err.Error())) + if regexError != nil { + return err, nil + } + if matchesExistingPeering { + message = fmt.Sprintf("Error creating Remote VpcPeering: %s Please check the VPC peerings on your project.", err) + } + + return composed.UpdateStatus(state.ObjAsVpcPeering()). + SetExclusiveConditions(metav1.Condition{ + Type: cloudcontrolv1beta1.ConditionTypeError, + Status: "True", + Reason: cloudcontrolv1beta1.ReasonFailedCreatingVpcPeeringConnection, + Message: message, + }). + ErrorLogMessage("Error creating Remote VpcPeering"). + FailedError(composed.StopWithRequeue). + SuccessError(composed.StopWithRequeueDelay(util.Timing.T60000ms())). + Run(ctx, state) + } + logger.Info("Remote VPC Peering Connection created") + return composed.StopWithRequeueDelay(3 * util.Timing.T10000ms()), ctx +} diff --git a/pkg/kcp/provider/gcp/vpcpeering/createVpcPeering.go b/pkg/kcp/provider/gcp/vpcpeering/createVpcPeering.go deleted file mode 100644 index cc712826b..000000000 --- a/pkg/kcp/provider/gcp/vpcpeering/createVpcPeering.go +++ /dev/null @@ -1,75 +0,0 @@ -package vpcpeering - -import ( - "context" - "fmt" - cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" - "github.com/kyma-project/cloud-manager/pkg/composed" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "time" -) - -func createVpcPeeringConnection(ctx context.Context, st composed.State) (error, context.Context) { - state := st.(*State) - logger := composed.LoggerFromCtx(ctx) - - if state.vpcPeeringConnection != nil { - return nil, nil - } - - gcpScope := state.Scope().Spec.Scope.Gcp - project := gcpScope.Project - vpc := gcpScope.VpcNetwork - - con, err := state.client.CreateVpcPeering( - ctx, - state.peeringName, - state.remoteVpc, - state.remoteProject, - state.importCustomRoutes, - &project, - &vpc) - - if err != nil { - logger.Error(err, "Error creating VPC Peering") - - if err.Error() == "remote network "+*state.remoteVpc+" is not tagged with the kyma shoot name "+vpc { - return composed.UpdateStatus(state.ObjAsVpcPeering()). - SetExclusiveConditions(metav1.Condition{ - Type: cloudcontrolv1beta1.ConditionTypeError, - Status: "True", - Reason: cloudcontrolv1beta1.ReasonFailedLoadingRemoteVpcNetwork, //I believe we should change it for something like ReasonRemoteNetworkNotTagged - Message: fmt.Sprintf("Remote network %s is not tagged with the kyma shoot name %s", *state.remoteVpc, vpc), - }). - ErrorLogMessage("Remote network is not tagged with the kyma shoot name"). - FailedError(composed.StopWithRequeue). - SuccessError(composed.StopWithRequeueDelay(time.Minute)). - Run(ctx, state) - } - - return composed.UpdateStatus(state.ObjAsVpcPeering()). - SetExclusiveConditions(metav1.Condition{ - Type: cloudcontrolv1beta1.ConditionTypeError, - Status: "True", - Reason: cloudcontrolv1beta1.ReasonFailedCreatingVpcPeeringConnection, - Message: fmt.Sprintf("Failed creating VpcPeerings %s", err), - }). - ErrorLogMessage("Error updating VpcPeering status due to failed creating vpc peering connection"). - FailedError(composed.StopWithRequeue). - SuccessError(composed.StopWithRequeueDelay(time.Minute)). - Run(ctx, state) - } - - ctx = composed.LoggerIntoCtx(ctx, logger) - - logger.Info("GCP VPC Peering Connection created") - - state.vpcPeeringConnection = con - - err = state.UpdateObjStatus(ctx) - - if err != nil { - return composed.LogErrorAndReturn(err, "Error updating VPC Peering status", composed.StopWithRequeue, ctx) - } - return nil, ctx -} diff --git a/pkg/kcp/provider/gcp/vpcpeering/deleteVpcPeering.go b/pkg/kcp/provider/gcp/vpcpeering/deleteVpcPeering.go index c90ea995c..4dd0165bb 100644 --- a/pkg/kcp/provider/gcp/vpcpeering/deleteVpcPeering.go +++ b/pkg/kcp/provider/gcp/vpcpeering/deleteVpcPeering.go @@ -10,6 +10,11 @@ func deleteVpcPeering(ctx context.Context, st composed.State) (error, context.Co obj := state.ObjAsVpcPeering() logger := composed.LoggerFromCtx(ctx) + if state.kymaVpcPeering == nil { + logger.Info("VPC Peering is not loaded") + return nil, ctx + } + logger.Info("Deleting GCP VPC Peering " + obj.Spec.VpcPeering.Gcp.PeeringName) _, err := state.client.DeleteVpcPeering( @@ -23,5 +28,5 @@ func deleteVpcPeering(ctx context.Context, st composed.State) (error, context.Co return err, ctx } - return nil, nil + return nil, ctx } diff --git a/pkg/kcp/provider/gcp/vpcpeering/loadKymaVpcPeering.go b/pkg/kcp/provider/gcp/vpcpeering/loadKymaVpcPeering.go new file mode 100644 index 000000000..17262e674 --- /dev/null +++ b/pkg/kcp/provider/gcp/vpcpeering/loadKymaVpcPeering.go @@ -0,0 +1,45 @@ +package vpcpeering + +import ( + "context" + "fmt" + "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func loadKymaVpcPeering(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if state.kymaVpcPeering != nil { + return nil, ctx + } + + logger.Info("Loading VPC Peering") + + kymaVpcPeering, err := state.client.GetVpcPeering(ctx, state.peeringName, &state.Scope().Spec.Scope.Gcp.Project, &state.Scope().Spec.Scope.Gcp.VpcNetwork) + if err != nil { + logger.Error(err, "Error loading Kyma Vpc Peering") + meta.SetStatusCondition(state.ObjAsVpcPeering().Conditions(), metav1.Condition{ + Type: v1beta1.ConditionTypeError, + Status: "True", + Reason: v1beta1.ReasonFailedCreatingVpcPeeringConnection, + Message: fmt.Sprintf("Error loading Kyma Vpc Peering: %s", err), + }) + err = state.UpdateObjStatus(ctx) + if err != nil { + return composed.LogErrorAndReturn(err, + "Error updating status since it was not possible to load the Kyma Vpc Peering", + composed.StopWithRequeueDelay((util.Timing.T10000ms())), + ctx, + ) + } + return composed.StopWithRequeueDelay(util.Timing.T60000ms()), nil + } + + state.kymaVpcPeering = kymaVpcPeering + return nil, ctx +} diff --git a/pkg/kcp/provider/gcp/vpcpeering/loadRemoteVpcPeering.go b/pkg/kcp/provider/gcp/vpcpeering/loadRemoteVpcPeering.go new file mode 100644 index 000000000..358985137 --- /dev/null +++ b/pkg/kcp/provider/gcp/vpcpeering/loadRemoteVpcPeering.go @@ -0,0 +1,45 @@ +package vpcpeering + +import ( + "context" + "fmt" + "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func loadRemoteVpcPeering(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if state.remoteVpcPeering != nil { + return nil, ctx + } + + logger.Info("Loading Remote VPC Peering") + + remoteVpcPeering, err := state.client.GetVpcPeering(ctx, state.peeringName, state.remoteProject, state.remoteVpc) + if err != nil { + logger.Error(err, "Error loading Remote VpcPeering") + meta.SetStatusCondition(state.ObjAsVpcPeering().Conditions(), metav1.Condition{ + Type: v1beta1.ConditionTypeError, + Status: "True", + Reason: v1beta1.ReasonFailedCreatingVpcPeeringConnection, + Message: fmt.Sprintf("Error loading Remote Vpc Peering: %s", err), + }) + err = state.UpdateObjStatus(ctx) + if err != nil { + return composed.LogErrorAndReturn(err, + "Error updating status since it was not possible to load the remote Vpc Peering", + composed.StopWithRequeueDelay((util.Timing.T10000ms())), + ctx, + ) + } + return composed.StopWithRequeueDelay(util.Timing.T60000ms()), nil + } + + state.remoteVpcPeering = remoteVpcPeering + return nil, ctx +} diff --git a/pkg/kcp/provider/gcp/vpcpeering/new.go b/pkg/kcp/provider/gcp/vpcpeering/new.go index 061c098d8..ad9746ad1 100644 --- a/pkg/kcp/provider/gcp/vpcpeering/new.go +++ b/pkg/kcp/provider/gcp/vpcpeering/new.go @@ -3,6 +3,7 @@ package vpcpeering import ( "context" "fmt" + "github.com/kyma-project/cloud-manager/pkg/common/actions" "github.com/kyma-project/cloud-manager/pkg/composed" "github.com/kyma-project/cloud-manager/pkg/kcp/vpcpeering/types" ) @@ -20,25 +21,26 @@ func New(stateFactory StateFactory) composed.Action { return composed.ComposeActions( "gcpVpcPeering", - composed.BuildSwitchAction( - "gcpVpcPeering-switch", - // default action - composed.ComposeActions("gcpVpcPeering-non-delete", - addFinalizer, - createVpcPeeringConnection, - updateSuccessStatus, - composed.StopAndForgetAction, + actions.AddFinalizer, + loadRemoteVpcPeering, + loadKymaVpcPeering, + composed.IfElse(composed.Not(composed.MarkedForDeletionPredicate), + composed.ComposeActions( + "gcpVpcPeering-create", + createRemoteVpcPeering, + waitRemoteVpcPeeringAvailable, + createKymaVpcPeering, + waitVpcPeeringActive, + updateStatus, ), - composed.NewCase( - composed.MarkedForDeletionPredicate, - composed.ComposeActions( - "gcpVpcPeering-delete", - removeReadyCondition, - deleteVpcPeering, - removeFinalizer, - ), + composed.ComposeActions( + "gcpVpcPeering-delete", + removeReadyCondition, + deleteVpcPeering, + waitKymaVpcPeeringDeletion, + actions.RemoveFinalizer, ), - ), // switch + ), composed.StopAndForgetAction, )(ctx, state) } diff --git a/pkg/kcp/provider/gcp/vpcpeering/removeFinalizer.go b/pkg/kcp/provider/gcp/vpcpeering/removeFinalizer.go deleted file mode 100644 index 0d8cbdfae..000000000 --- a/pkg/kcp/provider/gcp/vpcpeering/removeFinalizer.go +++ /dev/null @@ -1,27 +0,0 @@ -package vpcpeering - -import ( - "context" - cloudcontrolv1beta1 "github.com/kyma-project/cloud-manager/api/cloud-control/v1beta1" - "github.com/kyma-project/cloud-manager/pkg/composed" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -func removeFinalizer(ctx context.Context, st composed.State) (error, context.Context) { - state := st.(*State) - logger := composed.LoggerFromCtx(ctx) - - isUpdated := controllerutil.RemoveFinalizer(state.ObjAsVpcPeering(), cloudcontrolv1beta1.FinalizerName) - if !isUpdated { - return nil, nil - } - - logger.Info("Removing finalizer") - - err := state.UpdateObj(ctx) - if err != nil { - return composed.LogErrorAndReturn(err, "Error updating KCP VpcPeering after finalizer removed", composed.StopWithRequeue, ctx) - } - - return composed.StopAndForget, nil -} diff --git a/pkg/kcp/provider/gcp/vpcpeering/state.go b/pkg/kcp/provider/gcp/vpcpeering/state.go index 744423c21..b3f3f0db9 100644 --- a/pkg/kcp/provider/gcp/vpcpeering/state.go +++ b/pkg/kcp/provider/gcp/vpcpeering/state.go @@ -1,7 +1,7 @@ package vpcpeering import ( - computepb "cloud.google.com/go/compute/apiv1/computepb" + pb "cloud.google.com/go/compute/apiv1/computepb" "context" "github.com/go-logr/logr" "github.com/kyma-project/cloud-manager/pkg/common/abstractions" @@ -19,11 +19,14 @@ type State struct { //gcp config gcpConfig *gcpclient.GcpConfig - peeringName *string - vpcPeeringConnection *computepb.NetworkPeering - remoteVpc *string - remoteProject *string - importCustomRoutes *bool + peeringName *string + remoteVpc *string + remoteProject *string + importCustomRoutes *bool + + //Peerings on both sides + remoteVpcPeering *pb.NetworkPeering + kymaVpcPeering *pb.NetworkPeering } type StateFactory interface { diff --git a/pkg/kcp/provider/gcp/vpcpeering/updateStatus.go b/pkg/kcp/provider/gcp/vpcpeering/updateStatus.go index 0ec990c7d..23e5b9c50 100644 --- a/pkg/kcp/provider/gcp/vpcpeering/updateStatus.go +++ b/pkg/kcp/provider/gcp/vpcpeering/updateStatus.go @@ -8,14 +8,22 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func updateSuccessStatus(ctx context.Context, st composed.State) (error, context.Context) { +func updateStatus(ctx context.Context, st composed.State) (error, context.Context) { state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + logger.Info("GCP VPC Peering Update Status") + + if composed.MarkedForDeletionPredicate(ctx, state) { + logger.Info("GCP VPC Peering is marked for deletion") + return nil, ctx + } if meta.IsStatusConditionTrue( *state.ObjAsVpcPeering().Conditions(), cloudcontrol1beta1.ConditionTypeReady, ) { - return nil, nil + return nil, ctx } return composed.UpdateStatus(state.ObjAsVpcPeering()). @@ -23,7 +31,7 @@ func updateSuccessStatus(ctx context.Context, st composed.State) (error, context Type: cloudcontrol1beta1.ConditionTypeReady, Status: "True", Reason: cloudcontrol1beta1.ReasonReady, - Message: "Additional VpcPeerings(s) are provisioned", + Message: "VpcPeering :" + *state.peeringName + " is provisioned", }). ErrorLogMessage("Error updating VpcPeering success status after setting Ready condition"). SuccessLogMsg("KPC VpcPeering is ready"). diff --git a/pkg/kcp/provider/gcp/vpcpeering/waitKymaVpcPeeringDeletion.go b/pkg/kcp/provider/gcp/vpcpeering/waitKymaVpcPeeringDeletion.go new file mode 100644 index 000000000..cd978e5e9 --- /dev/null +++ b/pkg/kcp/provider/gcp/vpcpeering/waitKymaVpcPeeringDeletion.go @@ -0,0 +1,19 @@ +package vpcpeering + +import ( + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" +) + +func waitKymaVpcPeeringDeletion(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if state.kymaVpcPeering != nil { + logger.Info("GCP Kyma VPC Peering is not deleted yet, re-queueing with delay") + return composed.StopWithRequeueDelay(util.Timing.T10000ms()), ctx + } + + return nil, ctx +} diff --git a/pkg/kcp/provider/gcp/vpcpeering/waitRemoteVpcPeeringAvailable.go b/pkg/kcp/provider/gcp/vpcpeering/waitRemoteVpcPeeringAvailable.go new file mode 100644 index 000000000..778ae337f --- /dev/null +++ b/pkg/kcp/provider/gcp/vpcpeering/waitRemoteVpcPeeringAvailable.go @@ -0,0 +1,20 @@ +package vpcpeering + +import ( + pb "cloud.google.com/go/compute/apiv1/computepb" + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" +) + +func waitRemoteVpcPeeringAvailable(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if *state.remoteVpcPeering.State != pb.NetworkPeering_INACTIVE.String() && *state.remoteVpcPeering.State != pb.NetworkPeering_ACTIVE.String() { + logger.Info("GCP Remote VPC Peering is not ready yet, re-queueing with delay") + return composed.StopWithRequeueDelay(util.Timing.T10000ms()), ctx + } + + return nil, ctx +} diff --git a/pkg/kcp/provider/gcp/vpcpeering/waitVpcPeeringActive.go b/pkg/kcp/provider/gcp/vpcpeering/waitVpcPeeringActive.go new file mode 100644 index 000000000..35acb961b --- /dev/null +++ b/pkg/kcp/provider/gcp/vpcpeering/waitVpcPeeringActive.go @@ -0,0 +1,20 @@ +package vpcpeering + +import ( + pb "cloud.google.com/go/compute/apiv1/computepb" + "context" + "github.com/kyma-project/cloud-manager/pkg/composed" + "github.com/kyma-project/cloud-manager/pkg/util" +) + +func waitVpcPeeringActive(ctx context.Context, st composed.State) (error, context.Context) { + state := st.(*State) + logger := composed.LoggerFromCtx(ctx) + + if state.kymaVpcPeering.GetState() != pb.NetworkPeering_ACTIVE.String() && state.remoteVpcPeering.GetState() != pb.NetworkPeering_ACTIVE.String() { + logger.Info("GCP VPC Peering is not ready yet, re-queueing with delay") + return composed.StopWithRequeueDelay(util.Timing.T10000ms()), ctx + } + + return nil, ctx +}