From a2094fcce6cc43f667962d0dba0e5ae04fcb3ecb Mon Sep 17 00:00:00 2001 From: Carlos Panato Date: Sun, 14 Jun 2020 19:11:00 +0200 Subject: [PATCH] cloud/services: add bastion host service --- cloud/services/bastionhosts/bastionhosts.go | 161 ++++++++ .../bastionhosts/bastionhosts_test.go | 375 ++++++++++++++++++ cloud/services/bastionhosts/client.go | 86 ++++ .../mocks_bastionhosts/bastionhosts_mock.go | 94 +++++ .../bastionhosts/mocks_bastionhosts/doc.go | 20 + cloud/services/bastionhosts/service.go | 41 ++ 6 files changed, 777 insertions(+) create mode 100644 cloud/services/bastionhosts/bastionhosts.go create mode 100644 cloud/services/bastionhosts/bastionhosts_test.go create mode 100644 cloud/services/bastionhosts/client.go create mode 100644 cloud/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go create mode 100644 cloud/services/bastionhosts/mocks_bastionhosts/doc.go create mode 100644 cloud/services/bastionhosts/service.go diff --git a/cloud/services/bastionhosts/bastionhosts.go b/cloud/services/bastionhosts/bastionhosts.go new file mode 100644 index 00000000000..a6bece451b1 --- /dev/null +++ b/cloud/services/bastionhosts/bastionhosts.go @@ -0,0 +1,161 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 bastionhosts + +import ( + "context" + "fmt" + "strings" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" + "github.com/Azure/go-autorest/autorest/to" + "github.com/pkg/errors" + "k8s.io/klog" + + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha3" + azure "sigs.k8s.io/cluster-api-provider-azure/cloud" + "sigs.k8s.io/cluster-api-provider-azure/cloud/converters" +) + +// Spec specification for bastion host +type Spec struct { + Name string + SubnetName string + PublicIPName string + VnetName string +} + +// Reconcile gets/creates/updates a bastion host. +func (s *Service) Reconcile(ctx context.Context, spec interface{}) error { + bastionHostSpec, ok := spec.(*Spec) + if !ok { + return errors.New("invalid Bastion Host specification") + } + + klog.V(2).Infof("getting subnet %s", bastionHostSpec.SubnetName) + subnet, err := s.SubnetsClient.Get(ctx, s.Scope.ResourceGroup(), bastionHostSpec.VnetName, bastionHostSpec.SubnetName) + if err != nil { + return errors.Wrap(err, "failed to get subnet") + } + + klog.V(2).Infof("successfully got subnet %s", bastionHostSpec.SubnetName) + + klog.V(2).Infof("checking if public ip %s exist otherwise will try to create", bastionHostSpec.PublicIPName) + publicIP := network.PublicIPAddress{} + publicIP, err = s.PublicIPsClient.Get(ctx, s.Scope.ResourceGroup(), bastionHostSpec.PublicIPName) + if err != nil && azure.ResourceNotFound(err) { + iperr := s.createBastionPublicIP(ctx, bastionHostSpec.PublicIPName) + if iperr != nil { + return errors.Wrap(iperr, "failed to create bastion public IP") + } + var errPublicIP error + publicIP, errPublicIP = s.PublicIPsClient.Get(ctx, s.Scope.ResourceGroup(), bastionHostSpec.PublicIPName) + if errPublicIP != nil { + return errors.Wrap(errPublicIP, "failed to get public IP") + } + } else if err != nil { + return errors.Wrap(err, "failed to look for existing public IP") + } + klog.V(2).Infof("successfully got public ip %s", bastionHostSpec.PublicIPName) + + bastionHostName := bastionHostSpec.Name + klog.V(2).Infof("creating bastion host %s", bastionHostName) + + bastionHostIPConfigName := fmt.Sprintf("%s-%s", bastionHostSpec.Name, "bastionIP") + + err = s.Client.CreateOrUpdate( + ctx, + s.Scope.ResourceGroup(), + bastionHostName, + network.BastionHost{ + Name: to.StringPtr(bastionHostName), + Location: to.StringPtr(s.Scope.Location()), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: s.Scope.Name(), + Lifecycle: infrav1.ResourceLifecycleOwned, + Name: to.StringPtr(bastionHostName), + Role: to.StringPtr("Bastion"), + })), + BastionHostPropertiesFormat: &network.BastionHostPropertiesFormat{ + DNSName: to.StringPtr(fmt.Sprintf("%s-bastion", strings.ToLower(bastionHostName))), + IPConfigurations: &[]network.BastionHostIPConfiguration{ + { + Name: to.StringPtr(bastionHostIPConfigName), + BastionHostIPConfigurationPropertiesFormat: &network.BastionHostIPConfigurationPropertiesFormat{ + Subnet: &network.SubResource{ + ID: subnet.ID, + }, + PublicIPAddress: &network.SubResource{ + ID: publicIP.ID, + }, + PrivateIPAllocationMethod: network.Static, + }, + }, + }, + }, + }, + ) + if err != nil { + return errors.Wrap(err, "cannot create bastion host") + } + + klog.V(2).Infof("successfully created bastion host %s", bastionHostName) + return nil +} + +// Delete deletes the bastion host with the provided scope. +func (s *Service) Delete(ctx context.Context, spec interface{}) error { + bastionHostSpec, ok := spec.(*Spec) + if !ok { + return errors.New("invalid Bastion Host specification") + } + + klog.V(2).Infof("deleting Bastion Host %s", bastionHostSpec.Name) + err := s.Client.Delete(ctx, s.Scope.ResourceGroup(), bastionHostSpec.Name) + if err != nil && azure.ResourceNotFound(err) { + // already deleted + return nil + } + if err != nil { + return errors.Wrapf(err, "failed to delete Bastion Host %s in resource group %s", bastionHostSpec.Name, s.Scope.ResourceGroup()) + } + + klog.V(2).Infof("deleted Bastion Host %s", bastionHostSpec.Name) + return err +} + +func (s *Service) createBastionPublicIP(ctx context.Context, ipName string) error { + klog.V(2).Infof("creating bastion public IP %s", ipName) + + return s.PublicIPsClient.CreateOrUpdate( + ctx, + s.Scope.ResourceGroup(), + ipName, + network.PublicIPAddress{ + Sku: &network.PublicIPAddressSku{Name: network.PublicIPAddressSkuNameStandard}, + Name: to.StringPtr(ipName), + Location: to.StringPtr(s.Scope.Location()), + PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{ + PublicIPAddressVersion: network.IPv4, + PublicIPAllocationMethod: network.Static, + DNSSettings: &network.PublicIPAddressDNSSettings{ + DomainNameLabel: to.StringPtr(strings.ToLower(ipName)), + }, + }, + }, + ) +} diff --git a/cloud/services/bastionhosts/bastionhosts_test.go b/cloud/services/bastionhosts/bastionhosts_test.go new file mode 100644 index 00000000000..c7decb7ba24 --- /dev/null +++ b/cloud/services/bastionhosts/bastionhosts_test.go @@ -0,0 +1,375 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 bastionhosts + +import ( + "context" + "net/http" + "testing" + + . "github.com/onsi/gomega" + mock_bastionhosts "sigs.k8s.io/cluster-api-provider-azure/cloud/services/bastionhosts/mocks_bastionhosts" + "sigs.k8s.io/cluster-api-provider-azure/cloud/services/publicips/mock_publicips" + "sigs.k8s.io/cluster-api-provider-azure/cloud/services/subnets/mock_subnets" + + "github.com/Azure/go-autorest/autorest" + "github.com/golang/mock/gomock" + + network "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha3" + "sigs.k8s.io/cluster-api-provider-azure/cloud/scope" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func init() { + clusterv1.AddToScheme(scheme.Scheme) +} + +const ( + expectedInvalidSpec = "invalid Bastion Host specification" + subscriptionID = "123" +) + +func TestInvalidBastionHostSpec(t *testing.T) { + g := NewWithT(t) + + mockCtrl := gomock.NewController(t) + bastionHostMock := mock_bastionhosts.NewMockClient(mockCtrl) + + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, + } + + client := fake.NewFakeClient(cluster) + + clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ + AzureClients: scope.AzureClients{ + Authorizer: autorest.NullAuthorizer{}, + }, + Client: client, + Cluster: cluster, + AzureCluster: &infrav1.AzureCluster{ + Spec: infrav1.AzureClusterSpec{ + Location: "test-location", + ResourceGroup: "my-rg", + SubscriptionID: subscriptionID, + NetworkSpec: infrav1.NetworkSpec{ + Vnet: infrav1.VnetSpec{Name: "my-vnet", ResourceGroup: "my-rg"}, + }, + }, + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + + s := &Service{ + Scope: clusterScope, + Client: bastionHostMock, + } + + // Wrong Spec + wrongSpec := &network.PublicIPAddress{} + + err = s.Reconcile(context.TODO(), &wrongSpec) + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(expectedInvalidSpec)) + + err = s.Delete(context.TODO(), &wrongSpec) + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(expectedInvalidSpec)) +} + +func TestReconcileBastionHosts(t *testing.T) { + g := NewWithT(t) + + testcases := []struct { + name string + bastionHostSpec Spec + expectedError string + expect func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) + }{ + { + name: "get subnets fails", + bastionHostSpec: Spec{ + Name: "my-bastion", + VnetName: "my-vnet", + SubnetName: "my-subnet", + PublicIPName: "my-publicip", + }, + expectedError: "failed to get subnet: #: Internal Server Error: StatusCode=500", + expect: func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) { + mSubnet.Get(context.TODO(), "my-rg", "my-vnet", "my-subnet"). + Return(network.Subnet{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + }, + }, + { + name: "get publicip fails", + bastionHostSpec: Spec{ + Name: "my-bastion", + VnetName: "my-vnet", + SubnetName: "my-subnet", + PublicIPName: "my-publicip", + }, + expectedError: "failed to look for existing public IP: #: Internal Server Error: StatusCode=500", + expect: func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) { + gomock.InOrder( + mSubnet.Get(context.TODO(), "my-rg", "my-vnet", "my-subnet").Return(network.Subnet{}, nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))) + }, + }, + { + name: "create publicip fails", + bastionHostSpec: Spec{ + Name: "my-bastion", + VnetName: "my-vnet", + SubnetName: "my-subnet", + PublicIPName: "my-publicip", + }, + expectedError: "failed to create bastion public IP: #: Internal Server Error: StatusCode=500", + expect: func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) { + gomock.InOrder( + mSubnet.Get(context.TODO(), "my-rg", "my-vnet", "my-subnet").Return(network.Subnet{}, nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")), + mPublicIP.CreateOrUpdate(context.TODO(), "my-rg", "my-publicip", gomock.AssignableToTypeOf(network.PublicIPAddress{})).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))) + }, + }, + { + name: "fails to get a created publicip", + bastionHostSpec: Spec{ + Name: "my-bastion", + VnetName: "my-vnet", + SubnetName: "my-subnet", + PublicIPName: "my-publicip", + }, + expectedError: "failed to get public IP: #: Internal Server Error: StatusCode=500", + expect: func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) { + gomock.InOrder( + mSubnet.Get(context.TODO(), "my-rg", "my-vnet", "my-subnet").Return(network.Subnet{}, nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")), + mPublicIP.CreateOrUpdate(context.TODO(), "my-rg", "my-publicip", gomock.AssignableToTypeOf(network.PublicIPAddress{})).Return(nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))) + }, + }, + { + name: "bastion successfully created with created public ip", + bastionHostSpec: Spec{ + Name: "my-bastion", + VnetName: "my-vnet", + SubnetName: "my-subnet", + PublicIPName: "my-publicip", + }, + expectedError: "", + expect: func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) { + gomock.InOrder( + mSubnet.Get(context.TODO(), "my-rg", "my-vnet", "my-subnet").Return(network.Subnet{}, nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")), + mPublicIP.CreateOrUpdate(context.TODO(), "my-rg", "my-publicip", gomock.AssignableToTypeOf(network.PublicIPAddress{})).Return(nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, nil), + m.CreateOrUpdate(context.TODO(), "my-rg", "my-bastion", gomock.AssignableToTypeOf(network.BastionHost{}))) + }, + }, + { + name: "bastion successfully created", + bastionHostSpec: Spec{ + Name: "my-bastion", + VnetName: "my-vnet", + SubnetName: "my-subnet", + PublicIPName: "my-publicip", + }, + expectedError: "", + expect: func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) { + gomock.InOrder( + mSubnet.Get(context.TODO(), "my-rg", "my-vnet", "my-subnet").Return(network.Subnet{}, nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, nil), + m.CreateOrUpdate(context.TODO(), "my-rg", "my-bastion", gomock.AssignableToTypeOf(network.BastionHost{}))) + }, + }, + { + name: "fail to create a bastion", + bastionHostSpec: Spec{ + Name: "my-bastion", + VnetName: "my-vnet", + SubnetName: "my-subnet", + PublicIPName: "my-publicip", + }, + expectedError: "cannot create bastion host: #: Internal Server Error: StatusCode=500", + expect: func(m *mock_bastionhosts.MockClientMockRecorder, + mSubnet *mock_subnets.MockClientMockRecorder, + mPublicIP *mock_publicips.MockClientMockRecorder) { + gomock.InOrder( + mSubnet.Get(context.TODO(), "my-rg", "my-vnet", "my-subnet").Return(network.Subnet{}, nil), + mPublicIP.Get(context.TODO(), "my-rg", "my-publicip").Return(network.PublicIPAddress{}, nil), + m.CreateOrUpdate(context.TODO(), "my-rg", "my-bastion", gomock.AssignableToTypeOf(network.BastionHost{})).Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error"))) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + bastionHostMock := mock_bastionhosts.NewMockClient(mockCtrl) + subnetMock := mock_subnets.NewMockClient(mockCtrl) + publicIPsMock := mock_publicips.NewMockClient(mockCtrl) + + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, + } + client := fake.NewFakeClient(cluster) + + clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ + AzureClients: scope.AzureClients{ + Authorizer: autorest.NullAuthorizer{}, + }, + Client: client, + Cluster: cluster, + AzureCluster: &infrav1.AzureCluster{ + Spec: infrav1.AzureClusterSpec{ + Location: "test-location", + ResourceGroup: "my-rg", + SubscriptionID: subscriptionID, + }, + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + + tc.expect(bastionHostMock.EXPECT(), subnetMock.EXPECT(), + publicIPsMock.EXPECT()) + + s := &Service{ + Scope: clusterScope, + Client: bastionHostMock, + SubnetsClient: subnetMock, + PublicIPsClient: publicIPsMock, + } + + err = s.Reconcile(context.TODO(), &tc.bastionHostSpec) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +func TestDeleteBastionHost(t *testing.T) { + g := NewWithT(t) + + testcases := []struct { + name string + bastionHostSpec Spec + expectedError string + expect func(m *mock_bastionhosts.MockClientMockRecorder) + }{ + { + name: "successfully delete an existing bastion host", + bastionHostSpec: Spec{ + Name: "my-bastionhost", + }, + expectedError: "", + expect: func(m *mock_bastionhosts.MockClientMockRecorder) { + m.Delete(context.TODO(), "my-rg", "my-bastionhost") + }, + }, + { + name: "bastion host already deleted", + bastionHostSpec: Spec{ + Name: "my-bastionhost", + }, + expectedError: "", + expect: func(m *mock_bastionhosts.MockClientMockRecorder) { + m.Delete(context.TODO(), "my-rg", "my-bastionhost"). + Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) + }, + }, + { + name: "bastion host deletion fails", + bastionHostSpec: Spec{ + Name: "my-bastionhost", + }, + expectedError: "failed to delete Bastion Host my-bastionhost in resource group my-rg: #: Internal Server Error: StatusCode=500", + expect: func(m *mock_bastionhosts.MockClientMockRecorder) { + m.Delete(context.TODO(), "my-rg", "my-bastionhost"). + Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + bastionHostMock := mock_bastionhosts.NewMockClient(mockCtrl) + + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, + } + + client := fake.NewFakeClient(cluster) + + tc.expect(bastionHostMock.EXPECT()) + + clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ + AzureClients: scope.AzureClients{ + Authorizer: autorest.NullAuthorizer{}, + }, + Client: client, + Cluster: cluster, + AzureCluster: &infrav1.AzureCluster{ + Spec: infrav1.AzureClusterSpec{ + Location: "test-location", + ResourceGroup: "my-rg", + SubscriptionID: subscriptionID, + NetworkSpec: infrav1.NetworkSpec{ + Vnet: infrav1.VnetSpec{Name: "my-vnet", ResourceGroup: "my-rg"}, + }, + }, + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + + s := &Service{ + Scope: clusterScope, + Client: bastionHostMock, + } + + err = s.Delete(context.TODO(), &tc.bastionHostSpec) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/cloud/services/bastionhosts/client.go b/cloud/services/bastionhosts/client.go new file mode 100644 index 00000000000..2c68eccaea2 --- /dev/null +++ b/cloud/services/bastionhosts/client.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 bastionhosts + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" + "github.com/Azure/go-autorest/autorest" + azure "sigs.k8s.io/cluster-api-provider-azure/cloud" +) + +// Client wraps go-sdk +type Client interface { + Get(context.Context, string, string) (network.BastionHost, error) + CreateOrUpdate(context.Context, string, string, network.BastionHost) error + Delete(context.Context, string, string) error +} + +// AzureClient contains the Azure go-sdk Client +type AzureClient struct { + interfaces network.BastionHostsClient +} + +var _ Client = &AzureClient{} + +// NewClient creates a new bastion host client from subscription ID. +func NewClient(subscriptionID string, authorizer autorest.Authorizer) *AzureClient { + c := newBastionHostsClient(subscriptionID, authorizer) + return &AzureClient{c} +} + +// newBastionHostsClient creates a new bastion host client from subscription ID. +func newBastionHostsClient(subscriptionID string, authorizer autorest.Authorizer) network.BastionHostsClient { + BastionHostsClient := network.NewBastionHostsClient(subscriptionID) + BastionHostsClient.Authorizer = authorizer + BastionHostsClient.AddToUserAgent(azure.UserAgent()) + return BastionHostsClient +} + +// Get gets information about the specified bastion host. +func (ac *AzureClient) Get(ctx context.Context, resourceGroupName, bastionName string) (network.BastionHost, error) { + return ac.interfaces.Get(ctx, resourceGroupName, bastionName) +} + +// CreateOrUpdate creates or updates a bastion host. +func (ac *AzureClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, bastionName string, bastionHost network.BastionHost) error { + future, err := ac.interfaces.CreateOrUpdate(ctx, resourceGroupName, bastionName, bastionHost) + if err != nil { + return err + } + err = future.WaitForCompletionRef(ctx, ac.interfaces.Client) + if err != nil { + return err + } + _, err = future.Result(ac.interfaces) + return err +} + +// Delete deletes the specified network interface. +func (ac *AzureClient) Delete(ctx context.Context, resourceGroupName, bastionName string) error { + future, err := ac.interfaces.Delete(ctx, resourceGroupName, bastionName) + if err != nil { + return err + } + err = future.WaitForCompletionRef(ctx, ac.interfaces.Client) + if err != nil { + return err + } + _, err = future.Result(ac.interfaces) + return err +} diff --git a/cloud/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go b/cloud/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go new file mode 100644 index 00000000000..39cb931d47c --- /dev/null +++ b/cloud/services/bastionhosts/mocks_bastionhosts/bastionhosts_mock.go @@ -0,0 +1,94 @@ +/* +Copyright The Kubernetes Authors. + +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. +*/ + +// Code generated by MockGen. DO NOT EDIT. +// Source: ../client.go + +// Package mock_bastionhosts is a generated GoMock package. +package mock_bastionhosts + +import ( + context "context" + network "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockClient is a mock of Client interface +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Get mocks base method +func (m *MockClient) Get(arg0 context.Context, arg1, arg2 string) (network.BastionHost, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2) + ret0, _ := ret[0].(network.BastionHost) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get +func (mr *MockClientMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), arg0, arg1, arg2) +} + +// CreateOrUpdate mocks base method +func (m *MockClient) CreateOrUpdate(arg0 context.Context, arg1, arg2 string, arg3 network.BastionHost) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdate", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateOrUpdate indicates an expected call of CreateOrUpdate +func (mr *MockClientMockRecorder) CreateOrUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*MockClient)(nil).CreateOrUpdate), arg0, arg1, arg2, arg3) +} + +// Delete mocks base method +func (m *MockClient) Delete(arg0 context.Context, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete +func (mr *MockClientMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), arg0, arg1, arg2) +} diff --git a/cloud/services/bastionhosts/mocks_bastionhosts/doc.go b/cloud/services/bastionhosts/mocks_bastionhosts/doc.go new file mode 100644 index 00000000000..98a778ef8df --- /dev/null +++ b/cloud/services/bastionhosts/mocks_bastionhosts/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Run go generate to regenerate this mock. +//go:generate ../../../../hack/tools/bin/mockgen -destination bastionhosts_mock.go -package mock_bastionhosts -source ../client.go Client +//go:generate /usr/bin/env bash -c "cat ../../../../hack/boilerplate/boilerplate.generatego.txt bastionhosts_mock.go > _bastionhosts_mock.go && mv _bastionhosts_mock.go bastionhosts_mock.go" +package mock_bastionhosts //nolint diff --git a/cloud/services/bastionhosts/service.go b/cloud/services/bastionhosts/service.go new file mode 100644 index 00000000000..3df60a06b80 --- /dev/null +++ b/cloud/services/bastionhosts/service.go @@ -0,0 +1,41 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 bastionhosts + +import ( + "sigs.k8s.io/cluster-api-provider-azure/cloud/scope" + "sigs.k8s.io/cluster-api-provider-azure/cloud/services/publicips" + "sigs.k8s.io/cluster-api-provider-azure/cloud/services/subnets" +) + +// Service provides operations on azure resources +type Service struct { + Scope *scope.ClusterScope + Client + SubnetsClient subnets.Client + PublicIPsClient publicips.Client +} + +// NewService creates a new service. +func NewService(scope *scope.ClusterScope) *Service { + return &Service{ + Scope: scope, + Client: NewClient(scope.SubscriptionID, scope.Authorizer), + SubnetsClient: subnets.NewClient(scope.SubscriptionID, scope.Authorizer), + PublicIPsClient: publicips.NewClient(scope.SubscriptionID, scope.Authorizer), + } +}