From 709d49d5b92f7155d3c35fda27c3a06702a6b2fb Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Mon, 18 Sep 2023 09:27:11 -0600 Subject: [PATCH 01/12] update sep 18 neptune --- Makefile | 4 +- common/constants.go | 2 +- .../config/samples/neptune_v1_crd.yaml | 202 +++++++++++++++--- 3 files changed, 180 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index b1a7bb06..f971b293 100644 --- a/Makefile +++ b/Makefile @@ -230,9 +230,7 @@ release: kustomize rm -rf build mkdir build cd details/operator-sdk/config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) - cd details/operator-sdk && $(KUSTOMIZE) build config/default > $(BUILD_DIR)/only_astraconnector_operator.yaml - cat $(MAKEFILE_DIR)/details/operator-sdk/config/samples/neptune_v1_crd.yaml $(BUILD_DIR)/only_astraconnector_operator.yaml > $(BUILD_DIR)/astraconnector_operator.yaml - cp $(MAKEFILE_DIR)/details/operator-sdk/config/samples/astra_v1_astraconnector.yaml $(BUILD_DIR)/astra_v1_astraconnector.yaml + cd details/operator-sdk && $(KUSTOMIZE) build config/default > $(BUILD_DIR)/astra_v1_astraconnector.yaml .PHONY: generate-mocks generate-mocks: install-mockery diff --git a/common/constants.go b/common/constants.go index 3c221603..117a5ba3 100644 --- a/common/constants.go +++ b/common/constants.go @@ -48,7 +48,7 @@ const ( NeptuneClusterRoleName = "neptune-manager-role" NeptuneMetricServicePort = 8443 NeptuneMetricServiceProtocol = "TCP" - NeptuneDefaultImage = "controller:73846b5" + NeptuneDefaultImage = "controller:e056f69" AstraPrivateCloudType = "private" AstraPrivateCloudName = "private" diff --git a/details/operator-sdk/config/samples/neptune_v1_crd.yaml b/details/operator-sdk/config/samples/neptune_v1_crd.yaml index a3717c9c..5644ad92 100644 --- a/details/operator-sdk/config/samples/neptune_v1_crd.yaml +++ b/details/operator-sdk/config/samples/neptune_v1_crd.yaml @@ -78,14 +78,55 @@ spec: status: description: ApplicationStatus defines the observed state of Application properties: - collectionTimestamp: - description: CollectionTimestamp records the time a backup was completed. Collection time is recorded even on failed backups. Collection time is recorded before uploading the backup object. The server's time is used for CollectionTimestamps - format: date-time - nullable: true - type: string + conditions: + description: Each Condition contains details for one aspect of the current state of the Application. + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array protectionState: description: 'ProtectionState determines protection state of the app based upon different parameters The current logic for protectionState is as follows: Fully Protected: someScheduleIsActive && hasScheduleWithSuccessfulLastBackup Partially Protected: someScheduleIsActive || someSuccessfulBackupExists || someSuccessfulSnapshotExists Not Protected: default' type: string + required: + - conditions type: object type: object served: true @@ -106,10 +147,22 @@ spec: kind: AppMirrorRelationship listKind: AppMirrorRelationshipList plural: appmirrorrelationships + shortNames: + - amr singular: appmirrorrelationship scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.error + name: Error + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 schema: openAPIV3Schema: description: AppMirrorRelationship is the Schema for the appmirrorrelationships API @@ -125,12 +178,84 @@ spec: spec: description: AppMirrorRelationshipSpec defines the desired state of AppMirrorRelationship properties: - foo: - description: Foo is an example field of AppMirrorRelationship. Edit appmirrorrelationship_types.go to remove/update + applicationRef: + description: Name of the Application + type: string + desiredState: + description: DesiredState is the desired state of the AppMirrorRelationship + type: string + destinationAppVaultRef: + description: DestinationAppVaultRef is the name of the destination AppVault + type: string + sourceAppVaultRef: + description: SourceAppVaultRef is the name of the source AppVault + type: string + sourceSnapshotsPath: + description: SourceSnapshotPath is the name of the source app snapshots path type: string + required: + - applicationRef + - desiredState + - destinationAppVaultRef + - sourceAppVaultRef + - sourceSnapshotsPath type: object status: description: AppMirrorRelationshipStatus defines the observed state of AppMirrorRelationship + properties: + conditions: + description: Each Condition contains details for one aspect of the current state of the AppMirrorRelationship + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + error: + description: Error indicates the most recent error that has occurred. The error may not be permanent, so progress may continue after temporarily seeing an error. + type: string + state: + description: State indicates the current state of the AppMirrorRelationship + type: string + required: + - conditions + - state type: object type: object served: true @@ -151,10 +276,22 @@ spec: kind: AppMirrorUpdate listKind: AppMirrorUpdateList plural: appmirrorupdates + shortNames: + - amu singular: appmirrorupdate scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.error + name: Error + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 schema: openAPIV3Schema: description: AppMirrorUpdate is the Schema for the appmirrorupdates API @@ -445,6 +582,11 @@ spec: type: object status: description: AutoSupportBundleScheduleStatus defines the observed state of AutoSupportBundleSchedule + properties: + nextScheduledTimestamp: + description: NextScheduledTimestamp The time to run the next scheduled ASUP + format: date-time + type: string type: object type: object served: true @@ -1706,6 +1848,8 @@ spec: type: string secretRef: type: string + skipCertValidation: + type: boolean required: - dataSourceRef - pushEndpoint @@ -1878,6 +2022,9 @@ spec: - name type: object x-kubernetes-map-type: atomic + reclaimPolicy: + description: ReclaimPolicy defines what happens to the ResticSnapshot of a restic backup when the ResticVolumeBackup CR is deleted Valid options are Retain, Delete (default). + type: string resticEnv: description: Env vars to be provided to the restic CLI (including any required credentials) items: @@ -2028,6 +2175,9 @@ spec: error: description: Error indicates the most recent error that has occurred. The error may not be permanent, so progress may continue after temporarily seeing an error. type: string + resticDeleteJobName: + description: Name of the Job created to run Restic delete + type: string resticJobName: description: Name of the Job created to run Restic type: string @@ -2681,6 +2831,9 @@ spec: applicationRef: description: ApplicationRef is the reference to the Application being snapshotted. type: string + reclaimPolicy: + description: ReclaimPolicy defines what happens to the AppArchive of a snapshot when the snapshot CR is deleted Valid options are Retain, Delete (default). + type: string type: object status: description: SnapshotStatus defines the observed state of Snapshot @@ -3396,21 +3549,6 @@ rules: - update --- apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - creationTimestamp: null - name: neptune-resourcesummaryupload - namespace: neptune-system -rules: - - apiGroups: - - '*' - resources: - - secrets - verbs: - - get - - list ---- -apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null @@ -3504,6 +3642,14 @@ rules: verbs: - get - list + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch - apiGroups: - batch resources: @@ -3783,6 +3929,14 @@ rules: - get - list - watch + - apiGroups: + - trident.netapp.io + resources: + - tridentbackends + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole From 5a08657d91c795ad72d3803652545737b94737a3 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Mon, 18 Sep 2023 09:28:53 -0600 Subject: [PATCH 02/12] undo makefile --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index f971b293..4ffdde29 100644 --- a/Makefile +++ b/Makefile @@ -231,6 +231,8 @@ release: kustomize mkdir build cd details/operator-sdk/config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) cd details/operator-sdk && $(KUSTOMIZE) build config/default > $(BUILD_DIR)/astra_v1_astraconnector.yaml + cat $(MAKEFILE_DIR)/details/operator-sdk/config/samples/neptune_v1_crd.yaml $(BUILD_DIR)/only_astraconnector_operator.yaml > $(BUILD_DIR)/astraconnector_operator.yaml + cp $(MAKEFILE_DIR)/details/operator-sdk/config/samples/astra_v1_astraconnector.yaml $(BUILD_DIR)/astra_v1_astraconnector.yaml .PHONY: generate-mocks generate-mocks: install-mockery From 05975dfc8dd3c6b1b9e8c2ff2de2a61a9e4453a8 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Mon, 18 Sep 2023 09:29:45 -0600 Subject: [PATCH 03/12] undo makefile for reals --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4ffdde29..b1a7bb06 100644 --- a/Makefile +++ b/Makefile @@ -230,7 +230,7 @@ release: kustomize rm -rf build mkdir build cd details/operator-sdk/config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) - cd details/operator-sdk && $(KUSTOMIZE) build config/default > $(BUILD_DIR)/astra_v1_astraconnector.yaml + cd details/operator-sdk && $(KUSTOMIZE) build config/default > $(BUILD_DIR)/only_astraconnector_operator.yaml cat $(MAKEFILE_DIR)/details/operator-sdk/config/samples/neptune_v1_crd.yaml $(BUILD_DIR)/only_astraconnector_operator.yaml > $(BUILD_DIR)/astraconnector_operator.yaml cp $(MAKEFILE_DIR)/details/operator-sdk/config/samples/astra_v1_astraconnector.yaml $(BUILD_DIR)/astra_v1_astraconnector.yaml From 84597e13bd6d308c384308bffe791c792154fa83 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Mon, 18 Sep 2023 20:45:30 -0600 Subject: [PATCH 04/12] adding checks --- app/register/register.go | 127 ++++-------------- details/k8s/k8s_util.go | 25 ++++ .../api/v1/astraconnector_validator.go | 95 ++++++++++++- mocks/ClusterRegisterUtil.go | 21 --- mocks/K8sUtilInterface.go | 21 +++ 5 files changed, 168 insertions(+), 121 deletions(-) diff --git a/app/register/register.go b/app/register/register.go index 9cf97436..95887e5d 100644 --- a/app/register/register.go +++ b/app/register/register.go @@ -7,15 +7,13 @@ package register import ( "bytes" "context" - "crypto/tls" "encoding/base64" "encoding/json" "fmt" + "github.com/NetApp-Polaris/astra-connector-operator/util" "io" - "net" "net/http" "strconv" - "strings" "time" "github.com/go-logr/logr" @@ -35,41 +33,12 @@ const ( getClusterPollCount = 5 ) -// HTTPClient interface used for request and to facilitate testing -type HTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} - -// HeaderMap User specific details required for the http header -type HeaderMap struct { - AccountId string - Authorization string -} - -// DoRequest Makes http request with the given parameters -func DoRequest(ctx context.Context, client HTTPClient, method, url string, body io.Reader, headerMap HeaderMap) (*http.Response, error, context.CancelFunc) { - // Child context that can't exceed a deadline specified - childCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) // TODO : Update timeout here - - req, _ := http.NewRequestWithContext(childCtx, method, url, body) - - req.Header.Add("Content-Type", "application/json") - - if headerMap.Authorization != "" { - req.Header.Add("authorization", headerMap.Authorization) - } - - httpResponse, err := client.Do(req) - return httpResponse, err, cancel -} - type ClusterRegisterUtil interface { GetConnectorIDFromConfigMap(cmData map[string]string) (string, error) GetNatsSyncClientRegistrationURL() string GetNatsSyncClientUnregisterURL() string RegisterNatsSyncClient() (string, error) UnRegisterNatsSyncClient() error - GetAPITokenFromSecret(secretName string) (string, error) RegisterClusterWithAstra(astraConnectorId string) error CloudExists(astraHost, cloudID, apiToken string) bool ListClouds(astraHost, apiToken string) (*http.Response, error) @@ -90,13 +59,13 @@ type ClusterRegisterUtil interface { type clusterRegisterUtil struct { AstraConnector *v1.AstraConnector - Client HTTPClient + Client util.HTTPClient K8sClient client.Client Ctx context.Context Log logr.Logger } -func NewClusterRegisterUtil(astraConnector *v1.AstraConnector, client HTTPClient, k8sClient client.Client, log logr.Logger, ctx context.Context) ClusterRegisterUtil { +func NewClusterRegisterUtil(astraConnector *v1.AstraConnector, client util.HTTPClient, k8sClient client.Client, log logr.Logger, ctx context.Context) ClusterRegisterUtil { return &clusterRegisterUtil{ AstraConnector: astraConnector, Client: client, @@ -176,7 +145,7 @@ func (c clusterRegisterUtil) UnRegisterNatsSyncClient() error { return err } - response, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodPost, natsSyncClientUnregisterURL, bytes.NewBuffer(reqBodyBytes), HeaderMap{}) + response, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodPost, natsSyncClientUnregisterURL, bytes.NewBuffer(reqBodyBytes), util.HeaderMap{}) defer cancel() if err != nil { @@ -203,7 +172,7 @@ func (c clusterRegisterUtil) RegisterNatsSyncClient() (string, error) { return "", err } - response, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodPost, natsSyncClientRegisterURL, bytes.NewBuffer(reqBodyBytes), HeaderMap{}) + response, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodPost, natsSyncClientRegisterURL, bytes.NewBuffer(reqBodyBytes), util.HeaderMap{}) defer cancel() if err != nil { return "", err @@ -242,15 +211,6 @@ func GetAstraHostURL(astraConnector *v1.AstraConnector) string { return astraHost } -func (c clusterRegisterUtil) getAstraHostFromURL(astraHostURL string) (string, error) { - cloudBridgeURLSplit := strings.Split(astraHostURL, "://") - if len(cloudBridgeURLSplit) != 2 { - errStr := fmt.Sprintf("invalid cloudBridgeURL provided: %s, format - https://hostname", astraHostURL) - return "", errors.New(errStr) - } - return cloudBridgeURLSplit[1], nil -} - func (c clusterRegisterUtil) logHttpError(response *http.Response) { bodyBytes, err := io.ReadAll(response.Body) if err != nil { @@ -272,44 +232,11 @@ func (c clusterRegisterUtil) readResponseBody(response *http.Response) ([]byte, return bodyBytes, nil } -func (c clusterRegisterUtil) setHttpClient(disableTls bool, astraHost string) error { - if disableTls { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - c.Log.WithValues("disableTls", disableTls).Info("TLS Validation Disabled! Not for use in production!") - } - - if c.AstraConnector.Spec.NatsSyncClient.HostAliasIP != "" { - c.Log.WithValues("HostAliasIP", c.AstraConnector.Spec.NatsSyncClient.HostAliasIP).Info("Using the HostAlias IP") - cloudBridgeHost, err := c.getAstraHostFromURL(astraHost) - if err != nil { - return err - } - - dialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - } - - http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - if addr == cloudBridgeHost+":443" { - addr = c.AstraConnector.Spec.NatsSyncClient.HostAliasIP + ":443" - } - if addr == cloudBridgeHost+":80" { - addr = c.AstraConnector.Spec.NatsSyncClient.HostAliasIP + ":80" - } - return dialer.DialContext(ctx, network, addr) - } - } - - c.Client = &http.Client{} - return nil -} - func (c clusterRegisterUtil) CloudExists(astraHost, cloudID, apiToken string) bool { url := fmt.Sprintf("%s/accounts/%s/topology/v1/clouds/%s", astraHost, c.AstraConnector.Spec.Astra.AccountId, cloudID) - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - response, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + response, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) defer cancel() if err != nil { @@ -336,8 +263,8 @@ func (c clusterRegisterUtil) ListClouds(astraHost, apiToken string) (*http.Respo url := fmt.Sprintf("%s/accounts/%s/topology/v1/clouds", astraHost, c.AstraConnector.Spec.Astra.AccountId) c.Log.Info("Getting clouds") - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - response, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + response, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) defer cancel() if err != nil { @@ -429,8 +356,8 @@ func (c clusterRegisterUtil) CreateCloud(astraHost, cloudType, apiToken string) } c.Log.WithValues("cloudType", cloudType).Info("Creating cloud") - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - response, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodPost, url, bytes.NewBuffer(reqBodyBytes), headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + response, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodPost, url, bytes.NewBuffer(reqBodyBytes), headerMap) defer cancel() if err != nil { @@ -538,8 +465,8 @@ func (c clusterRegisterUtil) GetClusters(astraHost, cloudId, apiToken string) (G c.Log.Info("Getting Clusters") - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - clustersResp, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + clustersResp, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) defer cancel() if err != nil { @@ -585,8 +512,8 @@ func (c clusterRegisterUtil) GetCluster(astraHost, cloudId, clusterId, apiToken url := fmt.Sprintf("%s/accounts/%s/topology/v1/clouds/%s/clusters/%s", astraHost, c.AstraConnector.Spec.Astra.AccountId, cloudId, clusterId) var clustersRespJson Cluster - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - clustersResp, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + clustersResp, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) defer cancel() if err != nil { @@ -624,8 +551,8 @@ func (c clusterRegisterUtil) CreateCluster(astraHost, cloudId, astraConnectorId, } clustersBodyJson, _ := json.Marshal(clustersBody) - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - clustersResp, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodPost, url, bytes.NewBuffer(clustersBodyJson), headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + clustersResp, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodPost, url, bytes.NewBuffer(clustersBodyJson), headerMap) defer cancel() if err != nil { @@ -681,8 +608,8 @@ func (c clusterRegisterUtil) UpdateCluster(astraHost, cloudId, clusterId, astraC } clustersBodyJson, _ := json.Marshal(clustersBody) - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - clustersResp, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodPut, url, bytes.NewBuffer(clustersBodyJson), headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + clustersResp, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodPut, url, bytes.NewBuffer(clustersBodyJson), headerMap) defer cancel() if err != nil { @@ -740,8 +667,8 @@ func (c clusterRegisterUtil) GetStorageClass(astraHost, cloudId, clusterId, apiT c.Log.Info("Getting Storage Classes") - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - storageClassesResp, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + storageClassesResp, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodGet, url, nil, headerMap) defer cancel() if err != nil { @@ -803,8 +730,8 @@ func (c clusterRegisterUtil) UpdateManagedCluster(astraHost, clusterId, astraCon } manageClustersBodyJson, _ := json.Marshal(manageClustersBody) - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - manageClustersResp, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodPut, url, bytes.NewBuffer(manageClustersBodyJson), headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + manageClustersResp, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodPut, url, bytes.NewBuffer(manageClustersBodyJson), headerMap) defer cancel() if err != nil { @@ -833,8 +760,8 @@ func (c clusterRegisterUtil) CreateManagedCluster(astraHost, cloudId, clusterID, } manageClustersBodyJson, _ := json.Marshal(manageClustersBody) - headerMap := HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} - manageClustersResp, err, cancel := DoRequest(c.Ctx, c.Client, http.MethodPost, url, bytes.NewBuffer(manageClustersBodyJson), headerMap) + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + manageClustersResp, err, cancel := util.DoRequest(c.Ctx, c.Client, http.MethodPost, url, bytes.NewBuffer(manageClustersBodyJson), headerMap) defer cancel() if err != nil { @@ -981,10 +908,12 @@ func (c clusterRegisterUtil) RegisterClusterWithAstra(astraConnectorId string) e astraHost := GetAstraHostURL(c.AstraConnector) c.Log.WithValues("URL", astraHost).Info("Astra Host Info") - err := c.setHttpClient(c.AstraConnector.Spec.Astra.SkipTLSValidation, astraHost) + httpClient, err := util.SetHttpClient(c.AstraConnector.Spec.Astra.SkipTLSValidation, + astraHost, c.AstraConnector.Spec.NatsSyncClient.HostAliasIP, c.Log) if err != nil { return err } + c.Client = httpClient // Extract the apiToken from the secret provided in the CR Spec via "tokenRef" field // This is needed to make calls to the Astra diff --git a/details/k8s/k8s_util.go b/details/k8s/k8s_util.go index b9257ac9..2f6b9829 100644 --- a/details/k8s/k8s_util.go +++ b/details/k8s/k8s_util.go @@ -6,6 +6,8 @@ package k8s import ( "context" + coreV1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "github.com/go-logr/logr" "github.com/pkg/errors" @@ -25,6 +27,7 @@ type K8sUtil struct { type K8sUtilInterface interface { CreateOrUpdateResource(context.Context, client.Object, client.Object) error + GetAPITokenFromSecret(ctx context.Context, namespace, secretName string) (string, error) DeleteResource(context.Context, client.Object) error VersionGet() (string, error) } @@ -87,3 +90,25 @@ func (r *K8sUtil) VersionGet() (string, error) { r.log.V(3).Info("versionInfo", "versionInfo", versionInfo) return versionInfo.GitVersion, nil } + +// GetAPITokenFromSecret Gets Secret provided in the ACC Spec and returns api token string of the data in secret +func (r *K8sUtil) GetAPITokenFromSecret(ctx context.Context, namespace, secretName string) (string, error) { + secret := &coreV1.Secret{} + + err := r.Client.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, secret) + if err != nil { + r.log.WithValues("namespace", namespace, "secret", secretName).Error(err, "failed to get kubernetes secret") + return "", err + } + + // Extract the value of the 'apiToken' key from the secret + apiToken, ok := secret.Data["apiToken"] + if !ok { + r.log.WithValues("namespace", namespace, "secret", secretName).Error(err, "failed to extract apiToken key from secret") + return "", errors.New("failed to extract apiToken key from secret") + } + + // Convert the value to a string + apiTokenStr := string(apiToken) + return apiTokenStr, nil +} diff --git a/details/operator-sdk/api/v1/astraconnector_validator.go b/details/operator-sdk/api/v1/astraconnector_validator.go index 65d4edb1..f1fae15a 100644 --- a/details/operator-sdk/api/v1/astraconnector_validator.go +++ b/details/operator-sdk/api/v1/astraconnector_validator.go @@ -4,8 +4,15 @@ package v1 import ( "context" - + "fmt" + "github.com/NetApp-Polaris/astra-connector-operator/common" + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/client-go/kubernetes" + "net" + "net/http" + ctrl "sigs.k8s.io/controller-runtime" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "github.com/NetApp-Polaris/astra-connector-operator/util" @@ -21,6 +28,11 @@ func (ai *AstraConnector) ValidateCreateAstraConnector() field.ErrorList { allErrs = append(allErrs, err) } + if err := ai.ValidateTokenAndAccountID(); err != nil { + log.V(3).Info("error while creating AstraConnector Instance", "namespace", ai.Namespace, "err", err) + allErrs = append(allErrs, err) + } + return allErrs } @@ -39,3 +51,84 @@ func (ai *AstraConnector) ValidateNamespace() *field.Error { } return nil } + +// ValidateTokenAndAccountID Validates the token and AccoundID provided that AstraConnector should be deployed to. +func (ai *AstraConnector) ValidateTokenAndAccountID() *field.Error { + cloudBridgeJsonField := util.GetJSONFieldName(&ai.Spec, &ai.Spec.NatsSyncClient.CloudBridgeURL) + tokenRefBridgeJsonField := util.GetJSONFieldName(&ai.Spec, &ai.Spec.Astra.TokenRef) + accountJsonField := util.GetJSONFieldName(&ai.Spec, &ai.Spec.Astra.AccountId) + astraHost := getAstraHostURL(ai.Spec.NatsSyncClient.CloudBridgeURL) + accountId := ai.Spec.Astra.AccountId + + config, _ := ctrl.GetConfig() + clientset, _ := kubernetes.NewForConfig(config) + apiToken, err := getSecret(clientset, ai.Spec.Astra.TokenRef, ai.ObjectMeta.Namespace) + if err != nil { + log.Info("Check TokenRef, make sure Kubernetes secret was created.") + return field.NotFound(field.NewPath(tokenRefBridgeJsonField), ai.Name) + } + + httpClient, err := util.SetHttpClient(ai.Spec.Astra.SkipTLSValidation, + astraHost, ai.Spec.NatsSyncClient.HostAliasIP, log) + if err != nil { + log.Info(fmt.Sprintf("invalid cloudBridgeURL provided: %s, format - https://hostname", ai.Spec.NatsSyncClient.CloudBridgeURL)) + return field.Invalid(field.NewPath(cloudBridgeJsonField), ai.Name, "CloudBridgeURL invalid format") + } + + url := fmt.Sprintf("%s/accounts/%s", astraHost, accountId) + + headerMap := util.HeaderMap{Authorization: fmt.Sprintf("Bearer %s", apiToken)} + response, err, cancel := util.DoRequest(context.Background(), httpClient, http.MethodGet, url, nil, headerMap) + defer cancel() + + var dnsError *net.DNSError + if errors.As(err, &dnsError) { + log.Info("Please check CloudBridgeURL provided") + return field.Invalid(field.NewPath(cloudBridgeJsonField), ai.Name, "CloudBridgeURL not reachable") + } + + // We got a 200 from the GET Account Looks good! no errors + if response.StatusCode == 200 { + return nil + } + + // error handling below + if response.StatusCode == 401 { + log.Info("Please check token provided.. 401 Unauthorized response status code from Astra Control") + return field.Invalid(field.NewPath(tokenRefBridgeJsonField), ai.Name, "Unauthorized request with Token Provided") + } + + if response.StatusCode == 404 { + println("Please check account id provided.. 404 account not found in Astra Control") + return field.Invalid(field.NewPath(accountJsonField), ai.Name, "Account not found") + } + return nil +} + +func getAstraHostURL(cloudBridgeURL string) string { + var astraHost string + if cloudBridgeURL != "" { + astraHost = cloudBridgeURL + } else { + astraHost = common.NatsSyncClientDefaultCloudBridgeURL + } + return astraHost +} + +func getSecret(clientset *kubernetes.Clientset, secretName string, namespace string) (string, error) { + secret, err := clientset.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{}) + if err != nil { + log.WithValues("namespace", namespace, "secret", secretName).Error(err, "failed to get kubernetes secret") + return "", err + } + // Extract the value of the 'apiToken' key from the secret + apiToken, ok := secret.Data["apiToken"] + if !ok { + log.WithValues("namespace", namespace, "secret", secretName).Error(err, "failed to extract apiToken key from secret") + return "", errors.New("failed to extract apiToken key from secret") + } + + // Convert the value to a string + apiTokenStr := string(apiToken) + return apiTokenStr, nil +} diff --git a/mocks/ClusterRegisterUtil.go b/mocks/ClusterRegisterUtil.go index 4739d519..4b15c59e 100644 --- a/mocks/ClusterRegisterUtil.go +++ b/mocks/ClusterRegisterUtil.go @@ -128,27 +128,6 @@ func (_m *ClusterRegisterUtil) CreateOrUpdateManagedCluster(astraHost string, cl return r0, r1 } -// GetAPITokenFromSecret provides a mock function with given fields: secretName -func (_m *ClusterRegisterUtil) GetAPITokenFromSecret(secretName string) (string, error) { - ret := _m.Called(secretName) - - var r0 string - if rf, ok := ret.Get(0).(func(string) string); ok { - r0 = rf(secretName) - } else { - r0 = ret.Get(0).(string) - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(secretName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetCloudId provides a mock function with given fields: astraHost, cloudType, apiToken, retryTimeout func (_m *ClusterRegisterUtil) GetCloudId(astraHost string, cloudType string, apiToken string, retryTimeout ...time.Duration) (string, error) { _va := make([]interface{}, len(retryTimeout)) diff --git a/mocks/K8sUtilInterface.go b/mocks/K8sUtilInterface.go index 48d44121..14d9de1f 100644 --- a/mocks/K8sUtilInterface.go +++ b/mocks/K8sUtilInterface.go @@ -43,6 +43,27 @@ func (_m *K8sUtilInterface) DeleteResource(_a0 context.Context, _a1 client.Objec return r0 } +// GetAPITokenFromSecret provides a mock function with given fields: ctx, namespace, secretName +func (_m *K8sUtilInterface) GetAPITokenFromSecret(ctx context.Context, namespace string, secretName string) (string, error) { + ret := _m.Called(ctx, namespace, secretName) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, string, string) string); ok { + r0 = rf(ctx, namespace, secretName) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, namespace, secretName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // VersionGet provides a mock function with given fields: func (_m *K8sUtilInterface) VersionGet() (string, error) { ret := _m.Called() From cd79e2192c5f037ad950db6020f602fe804e303f Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Tue, 19 Sep 2023 10:06:40 -0600 Subject: [PATCH 05/12] adding required and token check --- .../operator-sdk/api/v1/astraconnector_types.go | 1 + .../api/v1/astraconnector_validator.go | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/details/operator-sdk/api/v1/astraconnector_types.go b/details/operator-sdk/api/v1/astraconnector_types.go index 45d6a371..9b6691ce 100644 --- a/details/operator-sdk/api/v1/astraconnector_types.go +++ b/details/operator-sdk/api/v1/astraconnector_types.go @@ -9,6 +9,7 @@ import ( ) type Astra struct { + // +kubebuilder:validation:Required AccountId string `json:"accountId"` // +kubebuilder:validation:Optional CloudId string `json:"cloudId"` diff --git a/details/operator-sdk/api/v1/astraconnector_validator.go b/details/operator-sdk/api/v1/astraconnector_validator.go index f1fae15a..54c70da8 100644 --- a/details/operator-sdk/api/v1/astraconnector_validator.go +++ b/details/operator-sdk/api/v1/astraconnector_validator.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "github.com/NetApp-Polaris/astra-connector-operator/common" + "github.com/google/uuid" "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" @@ -54,12 +55,19 @@ func (ai *AstraConnector) ValidateNamespace() *field.Error { // ValidateTokenAndAccountID Validates the token and AccoundID provided that AstraConnector should be deployed to. func (ai *AstraConnector) ValidateTokenAndAccountID() *field.Error { - cloudBridgeJsonField := util.GetJSONFieldName(&ai.Spec, &ai.Spec.NatsSyncClient.CloudBridgeURL) - tokenRefBridgeJsonField := util.GetJSONFieldName(&ai.Spec, &ai.Spec.Astra.TokenRef) - accountJsonField := util.GetJSONFieldName(&ai.Spec, &ai.Spec.Astra.AccountId) + cloudBridgeJsonField := util.GetJSONFieldName(&ai.Spec.NatsSyncClient, &ai.Spec.NatsSyncClient.CloudBridgeURL) + tokenRefBridgeJsonField := util.GetJSONFieldName(&ai.Spec.Astra, &ai.Spec.Astra.TokenRef) + accountJsonField := util.GetJSONFieldName(&ai.Spec.Astra, &ai.Spec.Astra.AccountId) astraHost := getAstraHostURL(ai.Spec.NatsSyncClient.CloudBridgeURL) accountId := ai.Spec.Astra.AccountId + // Account needs to be a valid UUID + _, err := uuid.Parse(accountId) + if err != nil { + println("Please check account id provided.. Token needs to be UUID") + return field.Invalid(field.NewPath(accountJsonField), ai.Name, "Account not valid") + } + config, _ := ctrl.GetConfig() clientset, _ := kubernetes.NewForConfig(config) apiToken, err := getSecret(clientset, ai.Spec.Astra.TokenRef, ai.ObjectMeta.Namespace) From 96c1811d38252163737100e1317fead48d0ce1e9 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Tue, 19 Sep 2023 10:19:33 -0600 Subject: [PATCH 06/12] reverting some changes --- .../config/samples/neptune_v1_crd.yaml | 202 +++--------------- 1 file changed, 24 insertions(+), 178 deletions(-) diff --git a/details/operator-sdk/config/samples/neptune_v1_crd.yaml b/details/operator-sdk/config/samples/neptune_v1_crd.yaml index 5644ad92..a3717c9c 100644 --- a/details/operator-sdk/config/samples/neptune_v1_crd.yaml +++ b/details/operator-sdk/config/samples/neptune_v1_crd.yaml @@ -78,55 +78,14 @@ spec: status: description: ApplicationStatus defines the observed state of Application properties: - conditions: - description: Each Condition contains details for one aspect of the current state of the Application. - items: - description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" - properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: message is a human readable message indicating details about the transition. This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array + collectionTimestamp: + description: CollectionTimestamp records the time a backup was completed. Collection time is recorded even on failed backups. Collection time is recorded before uploading the backup object. The server's time is used for CollectionTimestamps + format: date-time + nullable: true + type: string protectionState: description: 'ProtectionState determines protection state of the app based upon different parameters The current logic for protectionState is as follows: Fully Protected: someScheduleIsActive && hasScheduleWithSuccessfulLastBackup Partially Protected: someScheduleIsActive || someSuccessfulBackupExists || someSuccessfulSnapshotExists Not Protected: default' type: string - required: - - conditions type: object type: object served: true @@ -147,22 +106,10 @@ spec: kind: AppMirrorRelationship listKind: AppMirrorRelationshipList plural: appmirrorrelationships - shortNames: - - amr singular: appmirrorrelationship scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .status.state - name: State - type: string - - jsonPath: .status.error - name: Error - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 + - name: v1alpha1 schema: openAPIV3Schema: description: AppMirrorRelationship is the Schema for the appmirrorrelationships API @@ -178,84 +125,12 @@ spec: spec: description: AppMirrorRelationshipSpec defines the desired state of AppMirrorRelationship properties: - applicationRef: - description: Name of the Application - type: string - desiredState: - description: DesiredState is the desired state of the AppMirrorRelationship - type: string - destinationAppVaultRef: - description: DestinationAppVaultRef is the name of the destination AppVault - type: string - sourceAppVaultRef: - description: SourceAppVaultRef is the name of the source AppVault - type: string - sourceSnapshotsPath: - description: SourceSnapshotPath is the name of the source app snapshots path + foo: + description: Foo is an example field of AppMirrorRelationship. Edit appmirrorrelationship_types.go to remove/update type: string - required: - - applicationRef - - desiredState - - destinationAppVaultRef - - sourceAppVaultRef - - sourceSnapshotsPath type: object status: description: AppMirrorRelationshipStatus defines the observed state of AppMirrorRelationship - properties: - conditions: - description: Each Condition contains details for one aspect of the current state of the AppMirrorRelationship - items: - description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" - properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: message is a human readable message indicating details about the transition. This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - error: - description: Error indicates the most recent error that has occurred. The error may not be permanent, so progress may continue after temporarily seeing an error. - type: string - state: - description: State indicates the current state of the AppMirrorRelationship - type: string - required: - - conditions - - state type: object type: object served: true @@ -276,22 +151,10 @@ spec: kind: AppMirrorUpdate listKind: AppMirrorUpdateList plural: appmirrorupdates - shortNames: - - amu singular: appmirrorupdate scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .status.state - name: State - type: string - - jsonPath: .status.error - name: Error - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 + - name: v1alpha1 schema: openAPIV3Schema: description: AppMirrorUpdate is the Schema for the appmirrorupdates API @@ -582,11 +445,6 @@ spec: type: object status: description: AutoSupportBundleScheduleStatus defines the observed state of AutoSupportBundleSchedule - properties: - nextScheduledTimestamp: - description: NextScheduledTimestamp The time to run the next scheduled ASUP - format: date-time - type: string type: object type: object served: true @@ -1848,8 +1706,6 @@ spec: type: string secretRef: type: string - skipCertValidation: - type: boolean required: - dataSourceRef - pushEndpoint @@ -2022,9 +1878,6 @@ spec: - name type: object x-kubernetes-map-type: atomic - reclaimPolicy: - description: ReclaimPolicy defines what happens to the ResticSnapshot of a restic backup when the ResticVolumeBackup CR is deleted Valid options are Retain, Delete (default). - type: string resticEnv: description: Env vars to be provided to the restic CLI (including any required credentials) items: @@ -2175,9 +2028,6 @@ spec: error: description: Error indicates the most recent error that has occurred. The error may not be permanent, so progress may continue after temporarily seeing an error. type: string - resticDeleteJobName: - description: Name of the Job created to run Restic delete - type: string resticJobName: description: Name of the Job created to run Restic type: string @@ -2831,9 +2681,6 @@ spec: applicationRef: description: ApplicationRef is the reference to the Application being snapshotted. type: string - reclaimPolicy: - description: ReclaimPolicy defines what happens to the AppArchive of a snapshot when the snapshot CR is deleted Valid options are Retain, Delete (default). - type: string type: object status: description: SnapshotStatus defines the observed state of Snapshot @@ -3549,6 +3396,21 @@ rules: - update --- apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + creationTimestamp: null + name: neptune-resourcesummaryupload + namespace: neptune-system +rules: + - apiGroups: + - '*' + resources: + - secrets + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null @@ -3642,14 +3504,6 @@ rules: verbs: - get - list - - apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch - apiGroups: - batch resources: @@ -3929,14 +3783,6 @@ rules: - get - list - watch - - apiGroups: - - trident.netapp.io - resources: - - tridentbackends - verbs: - - get - - list - - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole From 4e8ce6ef9faff01c3726e5c27e67dfc594108fac Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Tue, 19 Sep 2023 10:42:02 -0600 Subject: [PATCH 07/12] more clean up --- app/register/register.go | 1 + details/k8s/k8s_util.go | 26 -------------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/app/register/register.go b/app/register/register.go index 95887e5d..a2f26b0b 100644 --- a/app/register/register.go +++ b/app/register/register.go @@ -39,6 +39,7 @@ type ClusterRegisterUtil interface { GetNatsSyncClientUnregisterURL() string RegisterNatsSyncClient() (string, error) UnRegisterNatsSyncClient() error + GetAPITokenFromSecret(secretName string) (string, error) RegisterClusterWithAstra(astraConnectorId string) error CloudExists(astraHost, cloudID, apiToken string) bool ListClouds(astraHost, apiToken string) (*http.Response, error) diff --git a/details/k8s/k8s_util.go b/details/k8s/k8s_util.go index 2f6b9829..2f4f6c50 100644 --- a/details/k8s/k8s_util.go +++ b/details/k8s/k8s_util.go @@ -6,9 +6,6 @@ package k8s import ( "context" - coreV1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "github.com/go-logr/logr" "github.com/pkg/errors" @@ -27,7 +24,6 @@ type K8sUtil struct { type K8sUtilInterface interface { CreateOrUpdateResource(context.Context, client.Object, client.Object) error - GetAPITokenFromSecret(ctx context.Context, namespace, secretName string) (string, error) DeleteResource(context.Context, client.Object) error VersionGet() (string, error) } @@ -90,25 +86,3 @@ func (r *K8sUtil) VersionGet() (string, error) { r.log.V(3).Info("versionInfo", "versionInfo", versionInfo) return versionInfo.GitVersion, nil } - -// GetAPITokenFromSecret Gets Secret provided in the ACC Spec and returns api token string of the data in secret -func (r *K8sUtil) GetAPITokenFromSecret(ctx context.Context, namespace, secretName string) (string, error) { - secret := &coreV1.Secret{} - - err := r.Client.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, secret) - if err != nil { - r.log.WithValues("namespace", namespace, "secret", secretName).Error(err, "failed to get kubernetes secret") - return "", err - } - - // Extract the value of the 'apiToken' key from the secret - apiToken, ok := secret.Data["apiToken"] - if !ok { - r.log.WithValues("namespace", namespace, "secret", secretName).Error(err, "failed to extract apiToken key from secret") - return "", errors.New("failed to extract apiToken key from secret") - } - - // Convert the value to a string - apiTokenStr := string(apiToken) - return apiTokenStr, nil -} From b9cd2ccda31afb0f4962389b2fd235b43fc2955f Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Tue, 19 Sep 2023 10:50:26 -0600 Subject: [PATCH 08/12] cleaning up mocks --- mocks/ClusterRegisterUtil.go | 21 +++++++++++++++++++++ mocks/K8sUtilInterface.go | 21 --------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/mocks/ClusterRegisterUtil.go b/mocks/ClusterRegisterUtil.go index 4b15c59e..4739d519 100644 --- a/mocks/ClusterRegisterUtil.go +++ b/mocks/ClusterRegisterUtil.go @@ -128,6 +128,27 @@ func (_m *ClusterRegisterUtil) CreateOrUpdateManagedCluster(astraHost string, cl return r0, r1 } +// GetAPITokenFromSecret provides a mock function with given fields: secretName +func (_m *ClusterRegisterUtil) GetAPITokenFromSecret(secretName string) (string, error) { + ret := _m.Called(secretName) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(secretName) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(secretName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetCloudId provides a mock function with given fields: astraHost, cloudType, apiToken, retryTimeout func (_m *ClusterRegisterUtil) GetCloudId(astraHost string, cloudType string, apiToken string, retryTimeout ...time.Duration) (string, error) { _va := make([]interface{}, len(retryTimeout)) diff --git a/mocks/K8sUtilInterface.go b/mocks/K8sUtilInterface.go index 14d9de1f..48d44121 100644 --- a/mocks/K8sUtilInterface.go +++ b/mocks/K8sUtilInterface.go @@ -43,27 +43,6 @@ func (_m *K8sUtilInterface) DeleteResource(_a0 context.Context, _a1 client.Objec return r0 } -// GetAPITokenFromSecret provides a mock function with given fields: ctx, namespace, secretName -func (_m *K8sUtilInterface) GetAPITokenFromSecret(ctx context.Context, namespace string, secretName string) (string, error) { - ret := _m.Called(ctx, namespace, secretName) - - var r0 string - if rf, ok := ret.Get(0).(func(context.Context, string, string) string); ok { - r0 = rf(ctx, namespace, secretName) - } else { - r0 = ret.Get(0).(string) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, namespace, secretName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // VersionGet provides a mock function with given fields: func (_m *K8sUtilInterface) VersionGet() (string, error) { ret := _m.Called() From f1326c31edfb7a586b846ea116a68a60a1462c58 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Tue, 19 Sep 2023 14:07:18 -0600 Subject: [PATCH 09/12] util --- util/http_util.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 util/http_util.go diff --git a/util/http_util.go b/util/http_util.go new file mode 100644 index 00000000..6ccd9916 --- /dev/null +++ b/util/http_util.go @@ -0,0 +1,83 @@ +package util + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/go-logr/logr" + "github.com/pkg/errors" + "io" + "net" + "net/http" + "strings" + "time" +) + +// HTTPClient interface used for request and to facilitate testing +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// HeaderMap User specific details required for the http header +type HeaderMap struct { + AccountId string + Authorization string +} + +// DoRequest Makes http request with the given parameters +func DoRequest(ctx context.Context, client HTTPClient, method, url string, body io.Reader, headerMap HeaderMap) (*http.Response, error, context.CancelFunc) { + // Child context that can't exceed a deadline specified + childCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) // TODO : Update timeout here + + req, _ := http.NewRequestWithContext(childCtx, method, url, body) + + req.Header.Add("Content-Type", "application/json") + + if headerMap.Authorization != "" { + req.Header.Add("authorization", headerMap.Authorization) + } + + httpResponse, err := client.Do(req) + return httpResponse, err, cancel +} + +func SetHttpClient(disableTls bool, astraHost, hostAliasIP string, log logr.Logger) (*http.Client, error) { + if disableTls { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + log.WithValues("disableTls", disableTls).Info("TLS Validation Disabled! Not for use in production!") + } + + if hostAliasIP != "" { + log.WithValues("HostAliasIP", hostAliasIP).Info("Using the HostAlias IP") + cloudBridgeHost, err := getAstraHostFromURL(astraHost) + if err != nil { + return &http.Client{}, err + } + + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + + http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + if addr == cloudBridgeHost+":443" { + addr = hostAliasIP + ":443" + } + if addr == cloudBridgeHost+":80" { + addr = hostAliasIP + ":80" + } + return dialer.DialContext(ctx, network, addr) + } + } + + return &http.Client{}, nil +} + +func getAstraHostFromURL(astraHostURL string) (string, error) { + cloudBridgeURLSplit := strings.Split(astraHostURL, "://") + if len(cloudBridgeURLSplit) != 2 { + errStr := fmt.Sprintf("invalid cloudBridgeURL provided: %s, format - https://hostname", astraHostURL) + return "", errors.New(errStr) + } + return cloudBridgeURLSplit[1], nil +} From 1e8d8622d3d5824f02bdded1834a9eaea03b28ad Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Tue, 19 Sep 2023 22:43:19 -0600 Subject: [PATCH 10/12] fixing unit test --- app/register/register.go | 3 +- .../api/v1/astraconnector_validator.go | 16 +++++----- .../api/v1/astraconnector_validator_test.go | 29 +++++++++++++------ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/app/register/register.go b/app/register/register.go index a2f26b0b..40308397 100644 --- a/app/register/register.go +++ b/app/register/register.go @@ -909,12 +909,11 @@ func (c clusterRegisterUtil) RegisterClusterWithAstra(astraConnectorId string) e astraHost := GetAstraHostURL(c.AstraConnector) c.Log.WithValues("URL", astraHost).Info("Astra Host Info") - httpClient, err := util.SetHttpClient(c.AstraConnector.Spec.Astra.SkipTLSValidation, + _, err := util.SetHttpClient(c.AstraConnector.Spec.Astra.SkipTLSValidation, astraHost, c.AstraConnector.Spec.NatsSyncClient.HostAliasIP, c.Log) if err != nil { return err } - c.Client = httpClient // Extract the apiToken from the secret provided in the CR Spec via "tokenRef" field // This is needed to make calls to the Astra diff --git a/details/operator-sdk/api/v1/astraconnector_validator.go b/details/operator-sdk/api/v1/astraconnector_validator.go index 54c70da8..e6098060 100644 --- a/details/operator-sdk/api/v1/astraconnector_validator.go +++ b/details/operator-sdk/api/v1/astraconnector_validator.go @@ -29,7 +29,7 @@ func (ai *AstraConnector) ValidateCreateAstraConnector() field.ErrorList { allErrs = append(allErrs, err) } - if err := ai.ValidateTokenAndAccountID(); err != nil { + if err := ai.ValidateTokenAndAccountID(nil); err != nil { log.V(3).Info("error while creating AstraConnector Instance", "namespace", ai.Namespace, "err", err) allErrs = append(allErrs, err) } @@ -54,7 +54,7 @@ func (ai *AstraConnector) ValidateNamespace() *field.Error { } // ValidateTokenAndAccountID Validates the token and AccoundID provided that AstraConnector should be deployed to. -func (ai *AstraConnector) ValidateTokenAndAccountID() *field.Error { +func (ai *AstraConnector) ValidateTokenAndAccountID(httpClient util.HTTPClient) *field.Error { cloudBridgeJsonField := util.GetJSONFieldName(&ai.Spec.NatsSyncClient, &ai.Spec.NatsSyncClient.CloudBridgeURL) tokenRefBridgeJsonField := util.GetJSONFieldName(&ai.Spec.Astra, &ai.Spec.Astra.TokenRef) accountJsonField := util.GetJSONFieldName(&ai.Spec.Astra, &ai.Spec.Astra.AccountId) @@ -76,11 +76,13 @@ func (ai *AstraConnector) ValidateTokenAndAccountID() *field.Error { return field.NotFound(field.NewPath(tokenRefBridgeJsonField), ai.Name) } - httpClient, err := util.SetHttpClient(ai.Spec.Astra.SkipTLSValidation, - astraHost, ai.Spec.NatsSyncClient.HostAliasIP, log) - if err != nil { - log.Info(fmt.Sprintf("invalid cloudBridgeURL provided: %s, format - https://hostname", ai.Spec.NatsSyncClient.CloudBridgeURL)) - return field.Invalid(field.NewPath(cloudBridgeJsonField), ai.Name, "CloudBridgeURL invalid format") + if httpClient == nil { + httpClient, err = util.SetHttpClient(ai.Spec.Astra.SkipTLSValidation, + astraHost, ai.Spec.NatsSyncClient.HostAliasIP, log) + if err != nil { + log.Info(fmt.Sprintf("invalid cloudBridgeURL provided: %s, format - https://hostname", ai.Spec.NatsSyncClient.CloudBridgeURL)) + return field.Invalid(field.NewPath(cloudBridgeJsonField), ai.Name, "CloudBridgeURL invalid format") + } } url := fmt.Sprintf("%s/accounts/%s", astraHost, accountId) diff --git a/details/operator-sdk/api/v1/astraconnector_validator_test.go b/details/operator-sdk/api/v1/astraconnector_validator_test.go index 66c17a7b..f50a1b53 100644 --- a/details/operator-sdk/api/v1/astraconnector_validator_test.go +++ b/details/operator-sdk/api/v1/astraconnector_validator_test.go @@ -13,12 +13,12 @@ import ( ) func TestAstraConnector_ValidateCreateAstraConnector(t *testing.T) { - ai := &v1.AstraConnector{} - err := ai.ValidateCreateAstraConnector() + ai := &v1.AstraConnector{Spec: v1.AstraConnectorSpec{Astra: v1.Astra{AccountId: "6587afff-7515-4c35-8e53-95545e427e31"}}} + err := ai.ValidateNamespace() // Validate that no error occurred - if len(err) != 0 { - t.Errorf("Expected no errors, but got %d", len(err)) + if err != nil { + t.Error("Expected no error, but got:", err) } } @@ -36,16 +36,27 @@ func TestAstraConnector_ValidateNamespace(t *testing.T) { ai := &v1.AstraConnector{} ai.ObjectMeta.Namespace = "default" - errors := ai.ValidateCreateAstraConnector() + err := ai.ValidateNamespace() // Validate that an error occurred - if errors == nil { + if err == nil { t.Error("Expected an error, but got nil") } // Validate the error message and field path expectedErrMsg := "default namespace not allowed" - assert.Equal(t, 1, len(errors)) - assert.Equal(t, expectedErrMsg, errors[0].Detail) - assert.Equal(t, "namespace", errors[0].Field) + assert.Error(t, err, expectedErrMsg) } + +//func TestAstraConnector_ValidateInputs(t *testing.T) { +// ai := &v1.AstraConnector{Spec: v1.AstraConnectorSpec{Astra: v1.Astra{AccountId: "6587afff-7515-4c35-8e53-95545e427e31"}}} +// mockHttpClient := &mocks.HTTPClient{} +// mockHttpClient.On() +// +// err := ai.ValidateTokenAndAccountID(mockHttpClient) +// +// // Validate that no error occurred +// if err != nil { +// t.Error("Expected no error, but got:", err) +// } +//} From be81687b3ebbf878c64cf1f47eab69527ddd3dbe Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Wed, 20 Sep 2023 09:17:43 -0600 Subject: [PATCH 11/12] WIP test --- .../api/v1/astraconnector_validator_test.go | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/details/operator-sdk/api/v1/astraconnector_validator_test.go b/details/operator-sdk/api/v1/astraconnector_validator_test.go index f50a1b53..367df42a 100644 --- a/details/operator-sdk/api/v1/astraconnector_validator_test.go +++ b/details/operator-sdk/api/v1/astraconnector_validator_test.go @@ -5,6 +5,9 @@ package v1_test import ( + "github.com/NetApp-Polaris/astra-connector-operator/mocks" + "github.com/stretchr/testify/mock" + "net/http" "testing" "github.com/stretchr/testify/assert" @@ -48,15 +51,15 @@ func TestAstraConnector_ValidateNamespace(t *testing.T) { assert.Error(t, err, expectedErrMsg) } -//func TestAstraConnector_ValidateInputs(t *testing.T) { -// ai := &v1.AstraConnector{Spec: v1.AstraConnectorSpec{Astra: v1.Astra{AccountId: "6587afff-7515-4c35-8e53-95545e427e31"}}} -// mockHttpClient := &mocks.HTTPClient{} -// mockHttpClient.On() -// -// err := ai.ValidateTokenAndAccountID(mockHttpClient) -// -// // Validate that no error occurred -// if err != nil { -// t.Error("Expected no error, but got:", err) -// } -//} +func TestAstraConnector_ValidateInputs(t *testing.T) { + ai := &v1.AstraConnector{Spec: v1.AstraConnectorSpec{Astra: v1.Astra{AccountId: "6587afff-7515-4c35-8e53-95545e427e31"}}} + mockHttpClient := &mocks.HTTPClient{} + mockHttpClient.On("Do", mock.Anything).Return(&http.Response{StatusCode: 200}, nil).Once() + + err := ai.ValidateTokenAndAccountID(mockHttpClient) + + // Validate that no error occurred + if err != nil { + t.Error("Expected no error, but got:", err) + } +} From 1e78f47ed33ee40f7a70d448f0028c5e56e6c1c9 Mon Sep 17 00:00:00 2001 From: Oscar Rodriguez Date: Wed, 15 Nov 2023 16:12:35 -0700 Subject: [PATCH 12/12] wip --- app/register/register.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/register/register.go b/app/register/register.go index c963e4b1..a83d6043 100644 --- a/app/register/register.go +++ b/app/register/register.go @@ -7,15 +7,12 @@ package register import ( "bytes" "context" - "crypto/tls" "encoding/base64" "encoding/json" "fmt" "io" - "net" "net/http" "strconv" - "strings" "time" "github.com/go-logr/logr" @@ -25,8 +22,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/NetApp-Polaris/astra-connector-operator/common" - "github.com/NetApp-Polaris/astra-connector-operator/util" v1 "github.com/NetApp-Polaris/astra-connector-operator/details/operator-sdk/api/v1" + "github.com/NetApp-Polaris/astra-connector-operator/util" ) const (