diff --git a/helm/trident-operator/templates/tridentconfigurator.yaml b/helm/trident-operator/templates/tridentconfigurator.yaml index 4a3be6d7d..8496eb5e7 100644 --- a/helm/trident-operator/templates/tridentconfigurator.yaml +++ b/helm/trident-operator/templates/tridentconfigurator.yaml @@ -33,3 +33,79 @@ spec: {{- end }} {{- end }} +--- + +{{- if .Values.ontapConfigurator.enabled }} +{{- $includeCR := false }} +{{- range .Values.ontapConfigurator.svms }} + {{- if or (has "nfs" .protocols) (has "smb" .protocols) }} + {{- $includeCR = true }} + {{- end }} +{{- end }} + +{{- if $includeCR }} +apiVersion: trident.netapp.io/v1 +kind: TridentConfigurator +metadata: + name: netapp-nas-backend-configurator +spec: + storageDriverName: ontap-nas + svms: + {{- range .Values.ontapConfigurator.svms }} + {{- if or (has "nfs" .protocols) (has "smb" .protocols) }} + - fsxnID: {{ .fsxnID | quote }} + protocols: + {{- $filteredProtocols := list }} + {{- range .protocols }} + {{- if or (eq . "nfs") (eq . "smb") }} + {{- $filteredProtocols = append $filteredProtocols . }} + {{- end }} + {{- end }} + {{- range $filteredProtocols }} + - {{ . | quote }} + {{- end }} + svmName: {{ .svmName | quote }} + authType: {{ .authType | default "awsarn" | quote }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} + + +--- + +{{- if .Values.ontapConfigurator.enabled }} +{{- $includeCR := false }} +{{- range .Values.ontapConfigurator.svms }} + {{- if or (has "iscsi" .protocols) (has "nvme" .protocols) }} + {{- $includeCR = true }} + {{- end }} +{{- end }} + +{{- if $includeCR }} +apiVersion: trident.netapp.io/v1 +kind: TridentConfigurator +metadata: + name: netapp-san-backend-configurator +spec: + storageDriverName: ontap-san + svms: + {{- range .Values.ontapConfigurator.svms }} + {{- if or (has "iscsi" .protocols) (has "nvme" .protocols) }} + - fsxnID: {{ .fsxnID | quote }} + protocols: + {{- $filteredProtocols := list }} + {{- range .protocols }} + {{- if or (eq . "iscsi") (eq . "nvme") }} + {{- $filteredProtocols = append $filteredProtocols . }} + {{- end }} + {{- end }} + {{- range $filteredProtocols }} + - {{ . | quote }} + {{- end }} + svmName: {{ .svmName | quote }} + authType: {{ .authType | default "awsarn" | quote }} + {{- end }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/trident-operator/values.yaml b/helm/trident-operator/values.yaml index a2e7c1d80..cd2d162ec 100644 --- a/helm/trident-operator/values.yaml +++ b/helm/trident-operator/values.yaml @@ -177,3 +177,12 @@ anfConfigurator: netappAccounts: [] resourceGroups: [] customerEncryptionKeys: {} + +# Auto generated ONTAP backend related fields consumed by the configurator controller. +ontapConfigurator: + enabled: false + svms: + - fsxnID: '' + svmName: '' + protocols: [] + authType: '' diff --git a/mocks/mock_storage_drivers/mock_ontap/mock_awsapi.go b/mocks/mock_storage_drivers/mock_ontap/mock_awsapi.go index 2eacd6bc1..842ddaeaf 100644 --- a/mocks/mock_storage_drivers/mock_ontap/mock_awsapi.go +++ b/mocks/mock_storage_drivers/mock_ontap/mock_awsapi.go @@ -23,6 +23,7 @@ import ( type MockAWSAPI struct { ctrl *gomock.Controller recorder *MockAWSAPIMockRecorder + isgomock struct{} } // MockAWSAPIMockRecorder is the mock recorder for MockAWSAPI. @@ -42,218 +43,248 @@ func (m *MockAWSAPI) EXPECT() *MockAWSAPIMockRecorder { return m.recorder } +// CreateSVM mocks base method. +func (m *MockAWSAPI) CreateSVM(ctx context.Context, request *awsapi.SVMCreateRequest) (*awsapi.SVM, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSVM", ctx, request) + ret0, _ := ret[0].(*awsapi.SVM) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSVM indicates an expected call of CreateSVM. +func (mr *MockAWSAPIMockRecorder) CreateSVM(ctx, request any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSVM", reflect.TypeOf((*MockAWSAPI)(nil).CreateSVM), ctx, request) +} + +// CreateSecret mocks base method. +func (m *MockAWSAPI) CreateSecret(ctx context.Context, request *awsapi.SecretCreateRequest) (*awsapi.Secret, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSecret", ctx, request) + ret0, _ := ret[0].(*awsapi.Secret) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSecret indicates an expected call of CreateSecret. +func (mr *MockAWSAPIMockRecorder) CreateSecret(ctx, request any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAWSAPI)(nil).CreateSecret), ctx, request) +} + // CreateVolume mocks base method. -func (m *MockAWSAPI) CreateVolume(arg0 context.Context, arg1 *awsapi.VolumeCreateRequest) (*awsapi.Volume, error) { +func (m *MockAWSAPI) CreateVolume(ctx context.Context, request *awsapi.VolumeCreateRequest) (*awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateVolume", arg0, arg1) + ret := m.ctrl.Call(m, "CreateVolume", ctx, request) ret0, _ := ret[0].(*awsapi.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateVolume indicates an expected call of CreateVolume. -func (mr *MockAWSAPIMockRecorder) CreateVolume(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) CreateVolume(ctx, request any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVolume", reflect.TypeOf((*MockAWSAPI)(nil).CreateVolume), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVolume", reflect.TypeOf((*MockAWSAPI)(nil).CreateVolume), ctx, request) } // DeleteVolume mocks base method. -func (m *MockAWSAPI) DeleteVolume(arg0 context.Context, arg1 *awsapi.Volume) error { +func (m *MockAWSAPI) DeleteVolume(ctx context.Context, volume *awsapi.Volume) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteVolume", arg0, arg1) + ret := m.ctrl.Call(m, "DeleteVolume", ctx, volume) ret0, _ := ret[0].(error) return ret0 } // DeleteVolume indicates an expected call of DeleteVolume. -func (mr *MockAWSAPIMockRecorder) DeleteVolume(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) DeleteVolume(ctx, volume any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVolume", reflect.TypeOf((*MockAWSAPI)(nil).DeleteVolume), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVolume", reflect.TypeOf((*MockAWSAPI)(nil).DeleteVolume), ctx, volume) } // GetFilesystemByID mocks base method. -func (m *MockAWSAPI) GetFilesystemByID(arg0 context.Context, arg1 string) (*awsapi.Filesystem, error) { +func (m *MockAWSAPI) GetFilesystemByID(ctx context.Context, ID string) (*awsapi.Filesystem, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFilesystemByID", arg0, arg1) + ret := m.ctrl.Call(m, "GetFilesystemByID", ctx, ID) ret0, _ := ret[0].(*awsapi.Filesystem) ret1, _ := ret[1].(error) return ret0, ret1 } // GetFilesystemByID indicates an expected call of GetFilesystemByID. -func (mr *MockAWSAPIMockRecorder) GetFilesystemByID(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetFilesystemByID(ctx, ID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilesystemByID", reflect.TypeOf((*MockAWSAPI)(nil).GetFilesystemByID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilesystemByID", reflect.TypeOf((*MockAWSAPI)(nil).GetFilesystemByID), ctx, ID) } // GetFilesystems mocks base method. -func (m *MockAWSAPI) GetFilesystems(arg0 context.Context) (*[]*awsapi.Filesystem, error) { +func (m *MockAWSAPI) GetFilesystems(ctx context.Context) (*[]*awsapi.Filesystem, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFilesystems", arg0) + ret := m.ctrl.Call(m, "GetFilesystems", ctx) ret0, _ := ret[0].(*[]*awsapi.Filesystem) ret1, _ := ret[1].(error) return ret0, ret1 } // GetFilesystems indicates an expected call of GetFilesystems. -func (mr *MockAWSAPIMockRecorder) GetFilesystems(arg0 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetFilesystems(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilesystems", reflect.TypeOf((*MockAWSAPI)(nil).GetFilesystems), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilesystems", reflect.TypeOf((*MockAWSAPI)(nil).GetFilesystems), ctx) } // GetSVMByID mocks base method. -func (m *MockAWSAPI) GetSVMByID(arg0 context.Context, arg1 string) (*awsapi.SVM, error) { +func (m *MockAWSAPI) GetSVMByID(ctx context.Context, ID string) (*awsapi.SVM, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSVMByID", arg0, arg1) + ret := m.ctrl.Call(m, "GetSVMByID", ctx, ID) ret0, _ := ret[0].(*awsapi.SVM) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSVMByID indicates an expected call of GetSVMByID. -func (mr *MockAWSAPIMockRecorder) GetSVMByID(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetSVMByID(ctx, ID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSVMByID", reflect.TypeOf((*MockAWSAPI)(nil).GetSVMByID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSVMByID", reflect.TypeOf((*MockAWSAPI)(nil).GetSVMByID), ctx, ID) } // GetSVMs mocks base method. -func (m *MockAWSAPI) GetSVMs(arg0 context.Context) (*[]*awsapi.SVM, error) { +func (m *MockAWSAPI) GetSVMs(ctx context.Context) (*[]*awsapi.SVM, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSVMs", arg0) + ret := m.ctrl.Call(m, "GetSVMs", ctx) ret0, _ := ret[0].(*[]*awsapi.SVM) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSVMs indicates an expected call of GetSVMs. -func (mr *MockAWSAPIMockRecorder) GetSVMs(arg0 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetSVMs(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSVMs", reflect.TypeOf((*MockAWSAPI)(nil).GetSVMs), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSVMs", reflect.TypeOf((*MockAWSAPI)(nil).GetSVMs), ctx) } // GetSecret mocks base method. -func (m *MockAWSAPI) GetSecret(arg0 context.Context, arg1 string) (map[string]string, error) { +func (m *MockAWSAPI) GetSecret(ctx context.Context, secretARN string) (*awsapi.Secret, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSecret", arg0, arg1) - ret0, _ := ret[0].(map[string]string) + ret := m.ctrl.Call(m, "GetSecret", ctx, secretARN) + ret0, _ := ret[0].(*awsapi.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // GetSecret indicates an expected call of GetSecret. -func (mr *MockAWSAPIMockRecorder) GetSecret(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetSecret(ctx, secretARN any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecret", reflect.TypeOf((*MockAWSAPI)(nil).GetSecret), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecret", reflect.TypeOf((*MockAWSAPI)(nil).GetSecret), ctx, secretARN) } // GetVolume mocks base method. -func (m *MockAWSAPI) GetVolume(arg0 context.Context, arg1 *storage.VolumeConfig) (*awsapi.Volume, error) { +func (m *MockAWSAPI) GetVolume(ctx context.Context, volConfig *storage.VolumeConfig) (*awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVolume", arg0, arg1) + ret := m.ctrl.Call(m, "GetVolume", ctx, volConfig) ret0, _ := ret[0].(*awsapi.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVolume indicates an expected call of GetVolume. -func (mr *MockAWSAPIMockRecorder) GetVolume(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetVolume(ctx, volConfig any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolume", reflect.TypeOf((*MockAWSAPI)(nil).GetVolume), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolume", reflect.TypeOf((*MockAWSAPI)(nil).GetVolume), ctx, volConfig) } // GetVolumeByARN mocks base method. -func (m *MockAWSAPI) GetVolumeByARN(arg0 context.Context, arg1 string) (*awsapi.Volume, error) { +func (m *MockAWSAPI) GetVolumeByARN(ctx context.Context, volumeARN string) (*awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVolumeByARN", arg0, arg1) + ret := m.ctrl.Call(m, "GetVolumeByARN", ctx, volumeARN) ret0, _ := ret[0].(*awsapi.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVolumeByARN indicates an expected call of GetVolumeByARN. -func (mr *MockAWSAPIMockRecorder) GetVolumeByARN(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetVolumeByARN(ctx, volumeARN any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeByARN", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumeByARN), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeByARN", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumeByARN), ctx, volumeARN) } // GetVolumeByID mocks base method. -func (m *MockAWSAPI) GetVolumeByID(arg0 context.Context, arg1 string) (*awsapi.Volume, error) { +func (m *MockAWSAPI) GetVolumeByID(ctx context.Context, volumeID string) (*awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVolumeByID", arg0, arg1) + ret := m.ctrl.Call(m, "GetVolumeByID", ctx, volumeID) ret0, _ := ret[0].(*awsapi.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVolumeByID indicates an expected call of GetVolumeByID. -func (mr *MockAWSAPIMockRecorder) GetVolumeByID(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetVolumeByID(ctx, volumeID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeByID", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumeByID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeByID", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumeByID), ctx, volumeID) } // GetVolumeByName mocks base method. -func (m *MockAWSAPI) GetVolumeByName(arg0 context.Context, arg1 string) (*awsapi.Volume, error) { +func (m *MockAWSAPI) GetVolumeByName(ctx context.Context, name string) (*awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVolumeByName", arg0, arg1) + ret := m.ctrl.Call(m, "GetVolumeByName", ctx, name) ret0, _ := ret[0].(*awsapi.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVolumeByName indicates an expected call of GetVolumeByName. -func (mr *MockAWSAPIMockRecorder) GetVolumeByName(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetVolumeByName(ctx, name any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeByName", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumeByName), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeByName", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumeByName), ctx, name) } // GetVolumes mocks base method. -func (m *MockAWSAPI) GetVolumes(arg0 context.Context) (*[]*awsapi.Volume, error) { +func (m *MockAWSAPI) GetVolumes(ctx context.Context) (*[]*awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVolumes", arg0) + ret := m.ctrl.Call(m, "GetVolumes", ctx) ret0, _ := ret[0].(*[]*awsapi.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVolumes indicates an expected call of GetVolumes. -func (mr *MockAWSAPIMockRecorder) GetVolumes(arg0 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) GetVolumes(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumes", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumes), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumes", reflect.TypeOf((*MockAWSAPI)(nil).GetVolumes), ctx) } // RelabelVolume mocks base method. -func (m *MockAWSAPI) RelabelVolume(arg0 context.Context, arg1 *awsapi.Volume, arg2 map[string]string) error { +func (m *MockAWSAPI) RelabelVolume(ctx context.Context, volume *awsapi.Volume, labels map[string]string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RelabelVolume", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "RelabelVolume", ctx, volume, labels) ret0, _ := ret[0].(error) return ret0 } // RelabelVolume indicates an expected call of RelabelVolume. -func (mr *MockAWSAPIMockRecorder) RelabelVolume(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) RelabelVolume(ctx, volume, labels any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RelabelVolume", reflect.TypeOf((*MockAWSAPI)(nil).RelabelVolume), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RelabelVolume", reflect.TypeOf((*MockAWSAPI)(nil).RelabelVolume), ctx, volume, labels) } // ResizeVolume mocks base method. -func (m *MockAWSAPI) ResizeVolume(arg0 context.Context, arg1 *awsapi.Volume, arg2 uint64) (*awsapi.Volume, error) { +func (m *MockAWSAPI) ResizeVolume(ctx context.Context, volume *awsapi.Volume, newSizeBytes uint64) (*awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ResizeVolume", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "ResizeVolume", ctx, volume, newSizeBytes) ret0, _ := ret[0].(*awsapi.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // ResizeVolume indicates an expected call of ResizeVolume. -func (mr *MockAWSAPIMockRecorder) ResizeVolume(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) ResizeVolume(ctx, volume, newSizeBytes any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResizeVolume", reflect.TypeOf((*MockAWSAPI)(nil).ResizeVolume), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResizeVolume", reflect.TypeOf((*MockAWSAPI)(nil).ResizeVolume), ctx, volume, newSizeBytes) } // VolumeExists mocks base method. -func (m *MockAWSAPI) VolumeExists(arg0 context.Context, arg1 *storage.VolumeConfig) (bool, *awsapi.Volume, error) { +func (m *MockAWSAPI) VolumeExists(ctx context.Context, volConfig *storage.VolumeConfig) (bool, *awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VolumeExists", arg0, arg1) + ret := m.ctrl.Call(m, "VolumeExists", ctx, volConfig) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(*awsapi.Volume) ret2, _ := ret[2].(error) @@ -261,15 +292,15 @@ func (m *MockAWSAPI) VolumeExists(arg0 context.Context, arg1 *storage.VolumeConf } // VolumeExists indicates an expected call of VolumeExists. -func (mr *MockAWSAPIMockRecorder) VolumeExists(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) VolumeExists(ctx, volConfig any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExists", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExists), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExists", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExists), ctx, volConfig) } // VolumeExistsByARN mocks base method. -func (m *MockAWSAPI) VolumeExistsByARN(arg0 context.Context, arg1 string) (bool, *awsapi.Volume, error) { +func (m *MockAWSAPI) VolumeExistsByARN(ctx context.Context, volumeARN string) (bool, *awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VolumeExistsByARN", arg0, arg1) + ret := m.ctrl.Call(m, "VolumeExistsByARN", ctx, volumeARN) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(*awsapi.Volume) ret2, _ := ret[2].(error) @@ -277,15 +308,15 @@ func (m *MockAWSAPI) VolumeExistsByARN(arg0 context.Context, arg1 string) (bool, } // VolumeExistsByARN indicates an expected call of VolumeExistsByARN. -func (mr *MockAWSAPIMockRecorder) VolumeExistsByARN(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) VolumeExistsByARN(ctx, volumeARN any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExistsByARN", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExistsByARN), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExistsByARN", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExistsByARN), ctx, volumeARN) } // VolumeExistsByID mocks base method. -func (m *MockAWSAPI) VolumeExistsByID(arg0 context.Context, arg1 string) (bool, *awsapi.Volume, error) { +func (m *MockAWSAPI) VolumeExistsByID(ctx context.Context, volumeID string) (bool, *awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VolumeExistsByID", arg0, arg1) + ret := m.ctrl.Call(m, "VolumeExistsByID", ctx, volumeID) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(*awsapi.Volume) ret2, _ := ret[2].(error) @@ -293,15 +324,15 @@ func (m *MockAWSAPI) VolumeExistsByID(arg0 context.Context, arg1 string) (bool, } // VolumeExistsByID indicates an expected call of VolumeExistsByID. -func (mr *MockAWSAPIMockRecorder) VolumeExistsByID(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) VolumeExistsByID(ctx, volumeID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExistsByID", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExistsByID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExistsByID", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExistsByID), ctx, volumeID) } // VolumeExistsByName mocks base method. -func (m *MockAWSAPI) VolumeExistsByName(arg0 context.Context, arg1 string) (bool, *awsapi.Volume, error) { +func (m *MockAWSAPI) VolumeExistsByName(ctx context.Context, name string) (bool, *awsapi.Volume, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VolumeExistsByName", arg0, arg1) + ret := m.ctrl.Call(m, "VolumeExistsByName", ctx, name) ret0, _ := ret[0].(bool) ret1, _ := ret[1].(*awsapi.Volume) ret2, _ := ret[2].(error) @@ -309,22 +340,22 @@ func (m *MockAWSAPI) VolumeExistsByName(arg0 context.Context, arg1 string) (bool } // VolumeExistsByName indicates an expected call of VolumeExistsByName. -func (mr *MockAWSAPIMockRecorder) VolumeExistsByName(arg0, arg1 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) VolumeExistsByName(ctx, name any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExistsByName", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExistsByName), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeExistsByName", reflect.TypeOf((*MockAWSAPI)(nil).VolumeExistsByName), ctx, name) } // WaitForVolumeStates mocks base method. -func (m *MockAWSAPI) WaitForVolumeStates(arg0 context.Context, arg1 *awsapi.Volume, arg2, arg3 []string, arg4 time.Duration) (string, error) { +func (m *MockAWSAPI) WaitForVolumeStates(ctx context.Context, volume *awsapi.Volume, desiredStates, abortStates []string, maxElapsedTime time.Duration) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "WaitForVolumeStates", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "WaitForVolumeStates", ctx, volume, desiredStates, abortStates, maxElapsedTime) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // WaitForVolumeStates indicates an expected call of WaitForVolumeStates. -func (mr *MockAWSAPIMockRecorder) WaitForVolumeStates(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +func (mr *MockAWSAPIMockRecorder) WaitForVolumeStates(ctx, volume, desiredStates, abortStates, maxElapsedTime any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForVolumeStates", reflect.TypeOf((*MockAWSAPI)(nil).WaitForVolumeStates), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForVolumeStates", reflect.TypeOf((*MockAWSAPI)(nil).WaitForVolumeStates), ctx, volume, desiredStates, abortStates, maxElapsedTime) } diff --git a/operator/controllers/configurator/controller.go b/operator/controllers/configurator/controller.go index 57008f3b2..9d810f26f 100644 --- a/operator/controllers/configurator/controller.go +++ b/operator/controllers/configurator/controller.go @@ -393,6 +393,24 @@ func (c *Controller) reconcile(keyItem string) error { Log().Error("Failed to process ANF backend: ", err) return err } + case config.OntapNASStorageDriverName, config.OntapSANStorageDriverName: + isAwsFsxn, err := tconfCR.IsAwsFsxnTconf() + if err != nil { + Log().Error("Failed to check if tconf is for AWS FSxN: ", err) + return err + } + if isAwsFsxn { + Log().Debugf("Tconf indicates auto-backend config is for AWS FSxN") + fsxn, err := storage_drivers.NewFsxnInstance(torcCR, tconfCR, c.Clients) + if err != nil { + Log().Info("Failed to create FsxN backend instance: ", err) + return err + } + if err := c.ProcessBackend(fsxn, tconfCR); err != nil { + Log().Error("Failed to process FsxN backend: ", err) + return err + } + } default: return fmt.Errorf("backend not supported") } diff --git a/operator/controllers/configurator/storage_drivers/fsx.go b/operator/controllers/configurator/storage_drivers/fsx.go new file mode 100644 index 000000000..d67a7668f --- /dev/null +++ b/operator/controllers/configurator/storage_drivers/fsx.go @@ -0,0 +1,301 @@ +// Copyright 2024 NetApp, Inc. All Rights Reserved. + +package storage_drivers + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/cenkalti/backoff/v4" + + k8sclient "github.com/netapp/trident/cli/k8s_client" + "github.com/netapp/trident/config" + . "github.com/netapp/trident/logging" + confClients "github.com/netapp/trident/operator/controllers/configurator/clients" + operatorV1 "github.com/netapp/trident/operator/crd/apis/netapp/v1" + sa "github.com/netapp/trident/storage_attribute" + "github.com/netapp/trident/storage_drivers/ontap/awsapi" + "github.com/netapp/trident/utils" +) + +const ( + SvmStateCreated = "CREATED" + AWSRegion = "AWS_REGION" + + TridentSecretPattern = "trident-%s" + SvmNamePattern = "trident-%s" + StorageClassNamePattern = "trident-%s-%s" + BackendNamePattern = "trident-%s-%s" + VsAdmin = "vsadmin" + Description = "Trident secret for FsxN for ONTAP" + + // Tags for the secret + FileSystemId = "file-system-id" +) + +type AWS struct { + AwsConfig + ConfClient confClients.ConfiguratorClientInterface + AwsClient *awsapi.Client + ManagementLif string + TBCNamePrefix string + TridentNamespace string +} + +type AwsConfig struct { + StorageDriverName string `json:"storageDriverName"` + SVMs []SVM `json:"svms"` +} + +type SVM struct { + FsxnID string `json:"fsxnID"` + Protocols []string `json:"protocols"` + AuthType string `json:"authType"` + SvmName string `json:"svmName"` + SecretARNName string `json:"secretARNName"` + ManagementLIF string `json:"managementLIF"` +} + +// NewFsxnInstance creates a new instance of the AWS struct and populates it with the provided CRs and client +func NewFsxnInstance( + torcCR *operatorV1.TridentOrchestrator, configuratorCR *operatorV1.TridentConfigurator, + client confClients.ConfiguratorClientInterface, +) (*AWS, error) { + if torcCR == nil { + return nil, fmt.Errorf("empty torc CR") + } + + if configuratorCR == nil { + return nil, fmt.Errorf("empty AWSFsxN configurator CR") + } + + if client == nil { + return nil, fmt.Errorf("invalid client") + } + + awsConfig := AwsConfig{} + if err := json.Unmarshal(configuratorCR.Spec.Raw, &awsConfig); err != nil { + Log().Errorf("Error occured while unmarshalling configurator CR: %v", err) + return nil, err + } + return &AWS{ + AwsConfig: awsConfig, + ConfClient: client, + TBCNamePrefix: configuratorCR.Name, + TridentNamespace: torcCR.Spec.Namespace, + }, nil +} + +// Validate validates the AWS configuration and prepares each FSxN instance for the auto-backend configuration. +func (aws *AWS) Validate() error { + var ( + awsRegion string + apiKey string + secretKey string + ) + awsRegion = os.Getenv(AWSRegion) + if awsRegion == "" { + return fmt.Errorf("%s is not set", AWSRegion) + } + // If key and secret are set, then add to the config + apiKey = os.Getenv("AWS_ACCESS_KEY_ID") + secretKey = os.Getenv("AWS_SECRET_ACCESS_KEY") + + api, err := awsapi.NewClient(context.Background(), awsapi.ClientConfig{ + APIRegion: awsRegion, + APIKey: apiKey, + SecretKey: secretKey, + }) + if err != nil { + return fmt.Errorf("error occurred while creating AWS client: %w", err) + } + + aws.AwsClient = api + + for key, fsxnInstance := range aws.SVMs { + if err := aws.processFsxnInstance(context.Background(), key, fsxnInstance); err != nil { + return err + } + } + + return nil +} + +// processFsxnInstance processes the auto-backend configuration for the FSxN instance. +// It creates the SVM if it does not exist and creates the secret for the SVM. +func (aws *AWS) processFsxnInstance(ctx context.Context, key int, svm SVM) error { + var ( + svmName string + secretName string + secretARN string + svmExists bool + ) + svmName = svm.SvmName + if svmName == "" { + svmName = fmt.Sprintf(SvmNamePattern, svm.FsxnID) + } + _, err := aws.AwsClient.GetFilesystemByID(ctx, svm.FsxnID) + if err != nil { + return fmt.Errorf("error occurred while getting fsxn id: %v : %v", svm.FsxnID, err) + } + Log().Debugf("Filesystem ID: %s exists", svm.FsxnID) + secretName = fmt.Sprintf(TridentSecretPattern, strings.TrimPrefix(svmName, "trident-")) + // Get the secret if it already exists or create a new one + secret, _ := aws.AwsClient.GetSecret(ctx, secretName) + if secret != nil { + Log().Debugf("Secret %s already exists, reusing the same for auto-backend config.", secretName) + secretARN = secret.SecretARN + } else { + Log().Debugf("Creating secret %s for auto-backend config.", secretName) + resSecret, err := aws.AwsClient.CreateSecret(ctx, &awsapi.SecretCreateRequest{ + Name: secretName, + Description: Description, + SecretData: map[string]string{ + "username": VsAdmin, + "password": utils.GenerateRandomPassword(ctx, 10, true, true, true, true), + }, + Tags: map[string]string{ + FileSystemId: svm.FsxnID, + }, + }) + if err != nil { + return fmt.Errorf("error occurred while creating secret: %w ", err) + } + secretARN = resSecret.SecretARN + } + + aws.SVMs[key].SecretARNName = secretARN + aws.AwsClient.SetClientFsConfig(svm.FsxnID) + + svmList, err := aws.AwsClient.GetSVMs(ctx) + if err != nil { + return fmt.Errorf("error occurred while getting SVMs: %w", err) + } + for _, storageVirtualMachine := range *svmList { + if storageVirtualMachine.Name == svmName { + // SVM already exists against the filesystem. Reuse the same for auto-backend config. + Log().Debugf("SVM already exists: %v for fsxnId: %v", storageVirtualMachine.Name, + storageVirtualMachine.FilesystemID) + aws.SVMs[key].SvmName = storageVirtualMachine.Name + aws.SVMs[key].ManagementLIF = storageVirtualMachine.MgtEndpoint.IPAddresses[0] + svmExists = true + break + } + } + if !svmExists { + // Create SVM if it does not exist against the filesystem + svmCreateRequest := &awsapi.SVMCreateRequest{ + Name: svmName, + SecretARN: secretARN, + } + + _, err = aws.AwsClient.CreateSVM(ctx, svmCreateRequest) + if err != nil { + return fmt.Errorf("error occurred while creating SVM: %w", err) + } + svmStatus := func() error { + svm, err := aws.AwsClient.GetSVMs(ctx) + if err != nil { + return fmt.Errorf("error occurred while getting SVM: %w", err) + } + + for _, svm := range *svm { + if svm.Name == svmCreateRequest.Name && svm.State == SvmStateCreated { + // SVM is in running state now and ready for auto-backend config + Log().Infof("SVM %v is in running state", svm.Name) + aws.SVMs[key].SvmName = svm.Name + aws.SVMs[key].ManagementLIF = svm.MgtEndpoint.IPAddresses[0] + return nil + } + } + + return fmt.Errorf("SVM is still not in running state") + } + checkSvmCreatedNotify := func(err error, duration time.Duration) { + Log().WithFields(LogFields{ + "svmName": svmName, + "err": err, + }).Debug("Svm not yet created, waiting.") + } + checkSVMStatusBackoff := backoff.NewExponentialBackOff() + checkSVMStatusBackoff.InitialInterval = 1 * time.Second + checkSVMStatusBackoff.RandomizationFactor = 0.1 + checkSVMStatusBackoff.Multiplier = 1.414 + checkSVMStatusBackoff.MaxInterval = 10 * time.Second + checkSVMStatusBackoff.MaxElapsedTime = 5 * time.Minute + + // Retry to check the SVM status until it is in running state + if err := backoff.RetryNotify(svmStatus, checkSVMStatusBackoff, checkSvmCreatedNotify); err != nil { + Log().Errorf("Svm not in running state after %3.2f minutes", checkSVMStatusBackoff.MaxElapsedTime.Minutes()) + return err + } + } + return nil +} + +// Create creates the Trident backend against the storage drivers +func (aws *AWS) Create() ([]string, error) { + var ( + backendList []string + backendName string + ) + for _, svm := range aws.SVMs { + for _, protocol := range svm.Protocols { + backendName = getFsxnBackendName(svm.FsxnID, protocol) + backendYAML := getFsxnTBCYaml(svm, aws.TridentNamespace, backendName, protocol) + if err := aws.ConfClient.CreateOrPatchObject(confClients.OBackend, backendName, + aws.TridentNamespace, backendYAML); err != nil { + return nil, fmt.Errorf("error creating or patching object: %w", err) + } + backendList = append(backendList, backendName) + } + } + return backendList, nil +} + +// CreateStorageClass creates a storage class for the storage driver +func (aws *AWS) CreateStorageClass() error { + var driver string + for _, svm := range aws.SVMs { + for _, protocol := range svm.Protocols { + name := getFsxnStorageClassName(svm.FsxnID, protocol) + if protocol == sa.NFS { + driver = config.OntapNASStorageDriverName + } else if protocol == sa.ISCSI { + driver = config.OntapSANStorageDriverName + } + storageClassYaml := getFsxnStorageClassYaml(name, driver) + if err := aws.ConfClient.CreateOrPatchObject(confClients.OStorageClass, name, + aws.TridentNamespace, storageClassYaml); err != nil { + return fmt.Errorf("error creating or patching object: %w", err) + } + } + } + return nil +} + +// CreateSnapshotClass creates a snapshot class for the storage driver +func (aws *AWS) CreateSnapshotClass() error { + fsxnSnapClassYAML := GetVolumeSnapshotClassYAML(NetAppSnapshotClassName) + return aws.ConfClient.CreateOrPatchObject(confClients.OSnapshotClass, NetAppSnapshotClassName, + "", fsxnSnapClassYAML) +} + +// GetCloudProvider returns the cloud provider for the storage driver +func (aws *AWS) GetCloudProvider() string { + return k8sclient.CloudProviderAWS +} + +// getFsxnTBCYaml returns the FsxN Trident backend config name +func getFsxnBackendName(fsxnId, protocolType string) string { + return fmt.Sprintf(BackendNamePattern, fsxnId, protocolType) +} + +// getFsxnStorageClassName returns the storage class name for the FSxN backend +func getFsxnStorageClassName(fsxnId, protocolType string) string { + return fmt.Sprintf(StorageClassNamePattern, fsxnId, protocolType) +} diff --git a/operator/controllers/configurator/storage_drivers/yaml_factory.go b/operator/controllers/configurator/storage_drivers/yaml_factory.go index b495d8b98..38f32a857 100644 --- a/operator/controllers/configurator/storage_drivers/yaml_factory.go +++ b/operator/controllers/configurator/storage_drivers/yaml_factory.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/netapp/trident/config" sa "github.com/netapp/trident/storage_attribute" "github.com/netapp/trident/utils" @@ -157,6 +158,7 @@ func constructAADSecret(namespace string) string { return aadSecret } +// GetVolumeSnapshotClassYAML returns the VolumeSnapshotClass YAML func GetVolumeSnapshotClassYAML(name string) string { vscYAML := volumeSnapshotClassTemplate @@ -165,6 +167,7 @@ func GetVolumeSnapshotClassYAML(name string) string { return vscYAML } +// volumeSnapshotClassTemplate is a template for the VolumeSnapshotClass YAML const volumeSnapshotClassTemplate = `--- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass @@ -173,3 +176,63 @@ metadata: driver: csi.trident.netapp.io deletionPolicy: Delete ` + +// getFsxnTBCYaml returns the FsxN Trident backend config YAML +func getFsxnTBCYaml(svm SVM, tridentNamespace, backendName, protocolType string) string { + tbcYaml := FsxnTBCYaml + tbcYaml = strings.ReplaceAll(tbcYaml, "{TBC_NAME}", backendName) + tbcYaml = strings.ReplaceAll(tbcYaml, "{NAMESPACE}", tridentNamespace) + tbcYaml = strings.ReplaceAll(tbcYaml, "{MANAGEMENT_LIF}", svm.ManagementLIF) + tbcYaml = strings.ReplaceAll(tbcYaml, "{FILE_SYSTEM_ID}", svm.FsxnID) + tbcYaml = strings.ReplaceAll(tbcYaml, "{SVM_NAME}", svm.SvmName) + tbcYaml = strings.ReplaceAll(tbcYaml, "{AWS_ARN}", svm.SecretARNName) + if protocolType == sa.NFS { + tbcYaml = strings.ReplaceAll(tbcYaml, "{DRIVER_TYPE}", config.OntapNASStorageDriverName) + tbcYaml = strings.ReplaceAll(tbcYaml, "{NAS_TYPE}", strings.Join([]string{"nasType:", protocolType}, " ")) + } else if protocolType == sa.ISCSI { + tbcYaml = strings.ReplaceAll(tbcYaml, "{DRIVER_TYPE}", config.OntapSANStorageDriverName) + tbcYaml = strings.ReplaceAll(tbcYaml, "{NAS_TYPE}", "") + } + return tbcYaml +} + +// FsxnTBCYaml is a template for the FsxN san driver Trident backend config YAML +const FsxnTBCYaml = `--- +apiVersion: trident.netapp.io/v1 +kind: TridentBackendConfig +metadata: + name: {TBC_NAME} + namespace: {NAMESPACE} +spec: + version: 1 + storageDriverName: {DRIVER_TYPE} + {NAS_TYPE} + managementLIF: {MANAGEMENT_LIF} + aws: + fsxFileSystemID: {FILE_SYSTEM_ID} + svm: {SVM_NAME} + credentials: + name: {AWS_ARN} + type: awsarn +` + +// getFsxnStorageClassYaml returns the Fsxn storage class YAML +func getFsxnStorageClassYaml(name, backendType string) string { + scYaml := fsxnStorageClassTemplate + scYaml = strings.ReplaceAll(scYaml, "{NAME}", name) + scYaml = strings.ReplaceAll(scYaml, "{BACKEND_TYPE}", backendType) + return scYaml +} + +// fsxnStorageClassTemplate is a template for the Fsxn storage class YAML +const fsxnStorageClassTemplate = `--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {NAME} +provisioner: csi.trident.netapp.io +parameters: + backendType: {BACKEND_TYPE} +volumeBindingMode: Immediate +allowVolumeExpansion: true +` diff --git a/operator/crd/apis/netapp/v1/tridentconfigurator.go b/operator/crd/apis/netapp/v1/tridentconfigurator.go index 5002c4fe2..980f25189 100644 --- a/operator/crd/apis/netapp/v1/tridentconfigurator.go +++ b/operator/crd/apis/netapp/v1/tridentconfigurator.go @@ -31,6 +31,7 @@ const ( Done TConfPhase = "Done" StorageDriverName = "storageDriverName" + FsxnID = "fsxnID" ) func (tc *TridentConfigurator) GetStorageDriverName() (string, error) { @@ -56,3 +57,19 @@ func (tc *TridentConfigurator) Validate() error { } return nil } + +func (tc *TridentConfigurator) IsAwsFsxnTconf() (bool, error) { + var tConfSpec map[string]interface{} + if err := json.Unmarshal(tc.Spec.Raw, &tConfSpec); err != nil { + return false, err + } + svms, _ := tConfSpec["svms"].([]interface{}) + for _, svm := range svms { + svmMap, _ := svm.(map[string]interface{}) + fsxnId, ok := svmMap[FsxnID].(string) + if ok && fsxnId != "" { + return true, nil + } + } + return false, nil +} diff --git a/storage_drivers/ontap/aws_common.go b/storage_drivers/ontap/aws_common.go index 9a6a55d3a..877b966c3 100644 --- a/storage_drivers/ontap/aws_common.go +++ b/storage_drivers/ontap/aws_common.go @@ -18,7 +18,8 @@ import ( // SetSvmCredentials Pull SVM credentials out of AWS secret store and enter them into the config. func SetSvmCredentials(ctx context.Context, secretARN string, api awsapi.AWSAPI, config *drivers.OntapStorageDriverConfig) (err error) { - secretMap, secretErr := api.GetSecret(ctx, secretARN) + secret, secretErr := api.GetSecret(ctx, secretARN) + secretMap := secret.SecretMap if secretErr != nil { return fmt.Errorf("could not retrieve credentials from AWS Secrets Manager; %w", secretErr) } diff --git a/storage_drivers/ontap/aws_common_test.go b/storage_drivers/ontap/aws_common_test.go index ad05adffe..e246c321c 100644 --- a/storage_drivers/ontap/aws_common_test.go +++ b/storage_drivers/ontap/aws_common_test.go @@ -275,17 +275,23 @@ func TestSvmCredentials(t *testing.T) { config.CommonStorageDriverConfig.DebugTraceFlags["method"] = true config.StorageDriverName = "ontap-nas" tests := []struct { - name string - secretMap map[string]string - error string + name string + secret *awsapi.Secret + error string }{ - {"The username key is missing", map[string]string{}, "ontap-nas driver must include username in the secret referenced by Credentials"}, - {"The username key is missing", map[string]string{"username": "username"}, "ontap-nas driver must include password in the secret referenced by Credentials"}, - {"The username key is missing", map[string]string{"username": "username", "password": "password"}, ""}, + {"Both username and password key is missing", &awsapi.Secret{ + SecretMap: map[string]string{}, + }, "ontap-nas driver must include username in the secret referenced by Credentials"}, + {"The password key is missing", &awsapi.Secret{ + SecretMap: map[string]string{"username": "username"}, + }, "ontap-nas driver must include password in the secret referenced by Credentials"}, + {"Both username and password key is present", &awsapi.Secret{ + SecretMap: map[string]string{"username": "username", "password": "password"}, + }, ""}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - mockAWSAPI.EXPECT().GetSecret(ctx, secretArn).Return(test.secretMap, nil) + mockAWSAPI.EXPECT().GetSecret(ctx, secretArn).Return(test.secret, nil) err := SetSvmCredentials(ctx, secretArn, mockAWSAPI, config) if test.error == "" { assert.NoError(t, err, nil) diff --git a/storage_drivers/ontap/awsapi/aws.go b/storage_drivers/ontap/awsapi/aws.go index bc5e6a26c..8219e9ac4 100644 --- a/storage_drivers/ontap/awsapi/aws.go +++ b/storage_drivers/ontap/awsapi/aws.go @@ -18,6 +18,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/fsx" fsxtypes "github.com/aws/aws-sdk-go-v2/service/fsx/types" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + secretmanagertypes "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" "github.com/cenkalti/backoff/v4" . "github.com/netapp/trident/logging" @@ -112,7 +113,54 @@ func NewClient(ctx context.Context, config ClientConfig) (*Client, error) { return client, nil } -func (d *Client) GetSecret(ctx context.Context, secretARN string) (map[string]string, error) { +func (d *Client) SetClientFsConfig(fileSystemId string) { + d.config.FilesystemID = fileSystemId +} + +func (d *Client) CreateSecret(ctx context.Context, request *SecretCreateRequest) (*Secret, error) { + logFields := LogFields{ + "API": "CreateSecret", + "SecretName": request.Name, + "Description": request.Description, + } + + secretBytes, err := json.Marshal(request.SecretData) + if err != nil { + Logc(ctx).WithFields(logFields).WithError(err).Error("Could not marshal secret data.") + return nil, fmt.Errorf("error marshaling secret data; %w", err) + } + + // Add tags to the secret request if they are provided + tags := make([]secretmanagertypes.Tag, 0) + for k, v := range request.Tags { + tags = append(tags, secretmanagertypes.Tag{ + Key: utils.Ptr(k), + Value: utils.Ptr(v), + }) + } + + input := &secretsmanager.CreateSecretInput{ + Name: utils.Ptr(request.Name), + Description: utils.Ptr(request.Description), + SecretString: utils.Ptr(string(secretBytes)), + Tags: tags, + } + + createSecretOutput, err := d.secretsClient.CreateSecret(ctx, input) + if err != nil { + Logc(ctx).WithFields(logFields).WithError(err).Error("Could not create secret.") + return nil, fmt.Errorf("error creating secret; %w", err) + } + + logFields["requestID"], _ = middleware.GetRequestIDMetadata(createSecretOutput.ResultMetadata) + Logc(ctx).WithFields(logFields).Info("Secret created.") + + return &Secret{ + SecretARN: DerefString(createSecretOutput.ARN), + }, nil +} + +func (d *Client) GetSecret(ctx context.Context, secretARN string) (*Secret, error) { input := &secretsmanager.GetSecretValueInput{ SecretId: utils.Ptr(secretARN), VersionStage: utils.Ptr("AWSCURRENT"), @@ -122,13 +170,20 @@ func (d *Client) GetSecret(ctx context.Context, secretARN string) (map[string]st if err != nil { return nil, err } - + secret := &Secret{ + FSxObject: FSxObject{ + ARN: DerefString(secretData.ARN), + ID: DerefString(secretData.VersionId), + Name: DerefString(secretData.Name), + }, + } var secretMap map[string]string if err = json.Unmarshal([]byte(DerefString(secretData.SecretString)), &secretMap); err != nil { return nil, err } - - return secretMap, nil + secret.SecretMap = secretMap + secret.SecretARN = *secretData.ARN + return secret, nil } // ParseVolumeARN parses the AWS-style ARN for a volume. @@ -254,6 +309,41 @@ func (d *Client) getFilesystemFromFSxFilesystem(f fsxtypes.FileSystem) *Filesyst return filesystem } +func (d *Client) CreateSVM(ctx context.Context, request *SVMCreateRequest) (*SVM, error) { + logFields := LogFields{ + "API": "CreateStorageVirtualMachine", + "svmName": request.Name, + "filesystem": d.config.FilesystemID, + } + + secret, err := d.GetSecret(ctx, request.SecretARN) + if err != nil { + logFields["requestID"] = GetRequestIDFromError(err) + Logc(ctx).WithFields(logFields).WithError(err).Error("Could not get secret.") + return nil, fmt.Errorf("error getting secret; %w", err) + } + input := &fsx.CreateStorageVirtualMachineInput{ + FileSystemId: utils.Ptr(d.config.FilesystemID), + Name: utils.Ptr(request.Name), + SvmAdminPassword: utils.Ptr(secret.SecretMap["password"]), + } + + output, err := d.fsxClient.CreateStorageVirtualMachine(ctx, input) + if err != nil { + logFields["requestID"] = GetRequestIDFromError(err) + Logc(ctx).WithFields(logFields).WithError(err).Error("Could not create SVM.") + return nil, fmt.Errorf("error creating SVM; %w", err) + } + + newSVM := d.getSVMFromFSxSVM(*output.StorageVirtualMachine) + + logFields["requestID"], _ = middleware.GetRequestIDMetadata(output.ResultMetadata) + logFields["svmID"] = newSVM.ID + Logc(ctx).WithFields(logFields).Info("SVM create request issued.") + + return newSVM, nil +} + func (d *Client) GetSVMs(ctx context.Context) (*[]*SVM, error) { logFields := LogFields{"API": "DescribeStorageVirtualMachinesPaginator.NextPage"} diff --git a/storage_drivers/ontap/awsapi/aws_structs.go b/storage_drivers/ontap/awsapi/aws_structs.go index b06260753..f5442cf5e 100644 --- a/storage_drivers/ontap/awsapi/aws_structs.go +++ b/storage_drivers/ontap/awsapi/aws_structs.go @@ -21,6 +21,12 @@ type FSxObject struct { Name string `json:"name"` } +type Secret struct { + FSxObject + SecretARN string `json:"secretARN"` + SecretMap map[string]string `json:"secretMap"` +} + type Filesystem struct { FSxObject Created time.Time `json:"created"` @@ -77,3 +83,18 @@ type VolumeCreateRequest struct { BackupID string `json:"backupId,omitempty"` SnapshotID string `json:"snapshotId,omitempty"` } + +type SecretCreateRequest struct { + Name string `json:"Name"` + Description string `json:"Description"` + SecretData map[string]string `json:"SecretData"` + Tags map[string]string `json:"Tags"` +} + +type SVMCreateRequest struct { + ClientRequestToken string `json:"ClientRequestToken"` + SecretARN string `json:"SecretARN"` + Name string `json:"Name"` + RootVolumeSecurityStyle string `json:"RootVolumeSecurityStyle"` + SvmAdminPassword string `json:"SvmAdminPassword"` +} diff --git a/storage_drivers/ontap/awsapi/types.go b/storage_drivers/ontap/awsapi/types.go index 3bd4dc39e..0682fd141 100644 --- a/storage_drivers/ontap/awsapi/types.go +++ b/storage_drivers/ontap/awsapi/types.go @@ -12,11 +12,13 @@ import ( ) type AWSAPI interface { - GetSecret(ctx context.Context, secretARN string) (map[string]string, error) + CreateSecret(ctx context.Context, request *SecretCreateRequest) (*Secret, error) + GetSecret(ctx context.Context, secretARN string) (*Secret, error) GetFilesystems(ctx context.Context) (*[]*Filesystem, error) GetFilesystemByID(ctx context.Context, ID string) (*Filesystem, error) + CreateSVM(ctx context.Context, request *SVMCreateRequest) (*SVM, error) GetSVMs(ctx context.Context) (*[]*SVM, error) GetSVMByID(ctx context.Context, ID string) (*SVM, error) diff --git a/utils/utils.go b/utils/utils.go index a65d7cc58..6afa134bc 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" "math" + "math/big" "net" "net/http" "net/url" @@ -1088,3 +1089,56 @@ func parseIntInRange(s string, min, max int64) (int64, error) { } return i, nil } + +func GenerateRandomPassword(ctx context.Context, length int, lowerChar, upperChar, digitChar, specialChar bool) string { + const ( + lowerChars = "abcdefghijklmnopqrstuvwxyz" + upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + digitChars = "0123456789" + specialChars = "!@#$%^&*()-_+=<>?{}[]|\\~`" + ) + var allChars string + if lowerChar { + allChars += lowerChars + } + if upperChar { + allChars += upperChars + } + if digitChar { + allChars += digitChars + } + if specialChar { + allChars += specialChars + } + + if len(allChars) == 0 { + Logc(ctx).Warn("No character sets selected, using default character set (lowercase letters, uppercase letters, digits)") + allChars = lowerChars + upperChars + digitChars + } + + for { + passwordBytes := make([]byte, length) + hasLetter, hasDigit := false, false + + for i := 0; i < length; i++ { + randomIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(allChars)))) + if err != nil { + Logc(ctx).Warn("Failed to generate random number for password, setting default password") + return "" + } + char := allChars[randomIndex.Int64()] + passwordBytes[i] = char + if strings.ContainsRune(lowerChars+upperChars, rune(char)) { + hasLetter = true + } + if strings.ContainsRune(digitChars, rune(char)) { + hasDigit = true + } + } + + password := string(passwordBytes) + if hasLetter && hasDigit && !strings.Contains(password, "admin") { + return password + } + } +} diff --git a/utils/utils_test.go b/utils/utils_test.go index 08e83a3ad..f30f13d54 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -1634,3 +1634,71 @@ func TestParseIntInRange(t *testing.T) { }) } } + +func TestGenerateRandomPassword(t *testing.T) { + tests := []struct { + name string + length int + lowerChar bool + upperChar bool + digitChar bool + specialChar bool + expectError bool + }{ + { + name: "all character sets", + length: 12, + lowerChar: true, + upperChar: true, + digitChar: true, + specialChar: true, + expectError: false, + }, + { + name: "lowerChar and digit", + length: 8, + lowerChar: true, + upperChar: false, + digitChar: true, + specialChar: false, + expectError: false, + }, + { + name: "uppercase and digit", + length: 8, + lowerChar: false, + upperChar: true, + digitChar: true, + specialChar: false, + expectError: false, + }, + { + name: "lowerchar, upperChar and digit", + length: 8, + lowerChar: true, + upperChar: true, + digitChar: true, + specialChar: false, + expectError: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + password := GenerateRandomPassword(ctx, test.length, test.lowerChar, test.upperChar, test.digitChar, test.specialChar) + if test.expectError { + assert.Empty(t, password) + } else { + assert.NotEmpty(t, password) + assert.Equal(t, test.length, len(password)) + if test.lowerChar || test.upperChar { + assert.Regexp(t, `[a-zA-Z]`, password) + } + if test.digitChar { + assert.Regexp(t, `[0-9]`, password) + } + } + }) + } +}