sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/vmextensions/vmextensions_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vmextensions
    18  
    19  import (
    20  	"context"
    21  	"io"
    22  	"net/http"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/pkg/errors"
    29  	"go.uber.org/mock/gomock"
    30  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    31  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    32  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/async/mock_async"
    33  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/vmextensions/mock_vmextensions"
    34  	gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock"
    35  	"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    36  )
    37  
    38  var (
    39  	extensionSpec1 = VMExtensionSpec{
    40  		ExtensionSpec: azure.ExtensionSpec{
    41  			Name:      "my-extension-1",
    42  			VMName:    "my-vm",
    43  			Publisher: "some-publisher",
    44  			Version:   "1.0",
    45  		},
    46  		ResourceGroup: "my-rg",
    47  		Location:      "test-location",
    48  	}
    49  
    50  	extensionSpec2 = VMExtensionSpec{
    51  		ExtensionSpec: azure.ExtensionSpec{
    52  			Name:      "my-extension-2",
    53  			VMName:    "my-vm",
    54  			Publisher: "other-publisher",
    55  			Version:   "2.0",
    56  		},
    57  		ResourceGroup: "my-rg",
    58  		Location:      "test-location",
    59  	}
    60  
    61  	notDoneError          = azure.NewOperationNotDoneError(&infrav1.Future{})
    62  	extensionNotDoneError = errors.Wrapf(notDoneError, "extension is still in provisioning state. This likely means that bootstrapping has not yet completed on the VM")
    63  )
    64  
    65  func extensionFailedError() error {
    66  	return errors.Wrapf(internalError(), "extension state failed. This likely means the Kubernetes node bootstrapping process failed or timed out. Check VM boot diagnostics logs to learn more")
    67  }
    68  
    69  func internalError() *azcore.ResponseError {
    70  	return &azcore.ResponseError{
    71  		RawResponse: &http.Response{
    72  			Body:       io.NopCloser(strings.NewReader("#: Internal Server Error: StatusCode=500")),
    73  			StatusCode: http.StatusInternalServerError,
    74  		},
    75  	}
    76  }
    77  
    78  func TestReconcileVMExtension(t *testing.T) {
    79  	testcases := []struct {
    80  		name          string
    81  		expectedError string
    82  		expect        func(s *mock_vmextensions.MockVMExtensionScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder)
    83  	}{
    84  		{
    85  			name:          "extension is in succeeded state",
    86  			expectedError: "",
    87  			expect: func(s *mock_vmextensions.MockVMExtensionScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
    88  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
    89  				s.VMExtensionSpecs().Return([]azure.ResourceSpecGetter{&extensionSpec1})
    90  				r.CreateOrUpdateResource(gomockinternal.AContext(), &extensionSpec1, serviceName).Return(nil, nil)
    91  				s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, nil)
    92  			},
    93  		},
    94  		{
    95  			name:          "extension is in failed state",
    96  			expectedError: extensionFailedError().Error(),
    97  			expect: func(s *mock_vmextensions.MockVMExtensionScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
    98  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
    99  				s.VMExtensionSpecs().Return([]azure.ResourceSpecGetter{&extensionSpec1})
   100  				r.CreateOrUpdateResource(gomockinternal.AContext(), &extensionSpec1, serviceName).Return(nil, internalError())
   101  				s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, gomockinternal.ErrStrEq(extensionFailedError().Error()))
   102  			},
   103  		},
   104  		{
   105  			name:          "extension is still creating",
   106  			expectedError: extensionNotDoneError.Error(),
   107  			expect: func(s *mock_vmextensions.MockVMExtensionScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   108  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   109  				s.VMExtensionSpecs().Return([]azure.ResourceSpecGetter{&extensionSpec1})
   110  				r.CreateOrUpdateResource(gomockinternal.AContext(), &extensionSpec1, serviceName).Return(nil, notDoneError)
   111  				s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, gomockinternal.ErrStrEq(extensionNotDoneError.Error()))
   112  			},
   113  		},
   114  		{
   115  			name:          "reconcile multiple extensions",
   116  			expectedError: "",
   117  			expect: func(s *mock_vmextensions.MockVMExtensionScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   118  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   119  				s.VMExtensionSpecs().Return([]azure.ResourceSpecGetter{&extensionSpec1, &extensionSpec2})
   120  				r.CreateOrUpdateResource(gomockinternal.AContext(), &extensionSpec1, serviceName).Return(nil, nil)
   121  				r.CreateOrUpdateResource(gomockinternal.AContext(), &extensionSpec2, serviceName).Return(nil, nil)
   122  				s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, nil)
   123  			},
   124  		},
   125  		{
   126  			name:          "error creating the first extension",
   127  			expectedError: extensionFailedError().Error(),
   128  			expect: func(s *mock_vmextensions.MockVMExtensionScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) {
   129  				s.DefaultedAzureServiceReconcileTimeout().Return(reconciler.DefaultAzureServiceReconcileTimeout)
   130  				s.VMExtensionSpecs().Return([]azure.ResourceSpecGetter{&extensionSpec1, &extensionSpec2})
   131  				r.CreateOrUpdateResource(gomockinternal.AContext(), &extensionSpec1, serviceName).Return(nil, internalError())
   132  				r.CreateOrUpdateResource(gomockinternal.AContext(), &extensionSpec2, serviceName).Return(nil, nil)
   133  				s.UpdatePutStatus(infrav1.BootstrapSucceededCondition, serviceName, gomockinternal.ErrStrEq(extensionFailedError().Error()))
   134  			},
   135  		},
   136  	}
   137  	for _, tc := range testcases {
   138  		tc := tc
   139  		t.Run(tc.name, func(t *testing.T) {
   140  			g := NewWithT(t)
   141  
   142  			t.Parallel()
   143  			mockCtrl := gomock.NewController(t)
   144  			defer mockCtrl.Finish()
   145  			scopeMock := mock_vmextensions.NewMockVMExtensionScope(mockCtrl)
   146  			asyncMock := mock_async.NewMockReconciler(mockCtrl)
   147  
   148  			tc.expect(scopeMock.EXPECT(), asyncMock.EXPECT())
   149  
   150  			s := &Service{
   151  				Scope:      scopeMock,
   152  				Reconciler: asyncMock,
   153  			}
   154  
   155  			err := s.Reconcile(context.TODO())
   156  			if tc.expectedError != "" {
   157  				g.Expect(err).To(HaveOccurred())
   158  				g.Expect(err).To(MatchError(tc.expectedError))
   159  			} else {
   160  				g.Expect(err).NotTo(HaveOccurred())
   161  			}
   162  		})
   163  	}
   164  }