pkg/plan/track_response_builder.go (156 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package plan import ( "errors" "reflect" "strings" "time" "github.com/go-openapi/strfmt" "github.com/elastic/cloud-sdk-go/pkg/models" "github.com/elastic/cloud-sdk-go/pkg/util" ) const ( pendingPlanPath = "Info.PlanInfo.Pending" currentPlanPath = "Info.PlanInfo.Current" historyPlanPath = "Info.PlanInfo.History" planAttemptLog = "PlanAttemptLog" ) var ( // ErrPlanFinished is returned when a cluster has no plan PlanStepInfo ErrPlanFinished = errors.New("finished all the plan steps") errNoPendingPlan = errors.New("no pending plan") ) // buildTrackResponse takes a models.DeploymentResources and iterates over the // deployment's resources to obtain a TrackResponse from either the Pending, // Current or History fields from the resource's plan. It returns []TrackResponse // each of them being an individual update about a resource's plan. When // getCurrentPlan is set to true, the plan which is looked up is either the current // or the last plan in the resource's plan history in the case that the resource // does not have any current plan, which is a common case for resources which failed // to create properly. func buildTrackResponse(res *models.DeploymentResources, getCurrentPlan bool) []TrackResponse { var pending = make([]TrackResponse, 0) for _, info := range res.Elasticsearch { p, err := parseResourceInfo(info, util.Elasticsearch, getCurrentPlan) if err != nil { continue } pending = append(pending, p) } for _, info := range res.Kibana { p, err := parseResourceInfo(info, util.Kibana, getCurrentPlan) if err != nil { continue } pending = append(pending, p) } for _, info := range res.Apm { p, err := parseResourceInfo(info, util.Apm, getCurrentPlan) if err != nil { continue } pending = append(pending, p) } for _, info := range res.IntegrationsServer { p, err := parseResourceInfo(info, util.IntegrationsServer, getCurrentPlan) if err != nil { continue } pending = append(pending, p) } for _, info := range res.Appsearch { p, err := parseResourceInfo(info, util.Appsearch, getCurrentPlan) if err != nil { continue } pending = append(pending, p) } for _, info := range res.EnterpriseSearch { p, err := parseResourceInfo(info, util.EnterpriseSearch, getCurrentPlan) if err != nil { continue } pending = append(pending, p) } return pending } // parseResourceInfo takes in a <kind>ResourceInfo type along with the Kind to // be able to obtain the resource's plan using reflection, which is deferred to // getPlanStepInfo. This function builds the TrackResponse structure. func parseResourceInfo(info interface{}, kind string, getCurrentPlan bool) (TrackResponse, error) { stepLog, err := getPlanStepInfo(info, getCurrentPlan) if err != nil { return TrackResponse{}, err } var id, refID string if v := reflect.ValueOf(info); v.IsValid() { id, refID = stringPFieldValue(v, "ID"), stringPFieldValue(v, "RefID") } step, err := GetStepName(stepLog) if step == "" { return TrackResponse{}, ErrPlanFinished } return TrackResponse{ Kind: kind, ID: id, RefID: refID, Step: step, Err: err, FailureDetails: stepDetails(stepLog), Finished: step == planCompleted, Duration: getPlanDuration(stepLog), }, nil } // getPlanStepInfo takes in a resource's info and returns a slice of // ClusterPlanStepInfo, which is obtained by accessing the named fields via // reflection. The flow might be confusing but in a nutshell it tries to: // 1. Obtain the Pending plan step log. // 2. Obtain the Current plan step log when getCurrentPlan is true. // 3. (if getCurrentPlan == true and the Current plan is empty) obtains the // "Current" plan accessing the last item in the plan history slice. func getPlanStepInfo(workload interface{}, getCurrentPlan bool) ([]*models.ClusterPlanStepInfo, error) { var planName = pendingPlanPath if getCurrentPlan { planName = currentPlanPath } payloadValue := reflect.ValueOf(workload) if !payloadValue.IsValid() { return nil, errNoPendingPlan } // Get either the "Pending" or "Current" plan. plan := reflectElemFieldPath(payloadValue, planName) if !plan.IsValid() { return nil, errNoPendingPlan } // If the pending plan is nil and getCurrentPlan == false, return an error. var noPendingPlanAndWantPendingPlan = plan.IsNil() && !getCurrentPlan if noPendingPlanAndWantPendingPlan { return nil, errNoPendingPlan } // When either "Pending" or "Current" aren't nil, obtain the plan log. var planLog reflect.Value if !plan.IsNil() { planLog = plan.Elem().FieldByName(planAttemptLog) } // When either "Pending" or "Current" are nil, and the current plan needs // to be obtained as set by the "getCurrentPlan" bool. Another case is when // the planLog is empty, for whichever case, the latest plan in the plan // history trail is obtained. var currentPlanIsNilAndPlanLogIsNil = plan.IsNil() && getCurrentPlan || planLog.IsNil() if currentPlanIsNilAndPlanLogIsNil { if history := reflectElemFieldPath(payloadValue, historyPlanPath); history.Len() > 0 { var lastPlan = history.Index(history.Len() - 1) planLog = lastPlan.Elem().FieldByName(planAttemptLog) } } return getPlanLog(planLog) } func getPlanDuration(log []*models.ClusterPlanStepInfo) strfmt.Duration { for i := range log { return strfmt.Duration(time.Since(time.Time(*log[i].Started))) } return 0 } // stringPFieldValue obtains the value of a string pointer field. func stringPFieldValue(v reflect.Value, field string) string { if value := v.Elem().FieldByName(field); value.IsValid() { valueStrP := value.Interface().(*string) return *valueStrP } return "" } // reflectElemFieldPath obtains the reflect.Value at the end of the specified // path. Path is in the format of <Property>.<Property> as many times as // required to obtain the end field. func reflectElemFieldPath(v reflect.Value, p string) reflect.Value { properties := strings.Split(p, ".") if len(properties) == 1 { return v.Elem().FieldByName(p) } var fValue reflect.Value for i := range properties { field := properties[i] if fValue = v.Elem().FieldByName(field); fValue.IsValid() { return reflectElemFieldPath(fValue, strings.Join(properties[i+1:], ".")) } } return fValue } // getPlanLog gets the PlanStepInfo slice from PlanAttemptLog as reflect.Value. func getPlanLog(planLog reflect.Value) ([]*models.ClusterPlanStepInfo, error) { if !planLog.IsValid() { return nil, errNoPendingPlan } if log, ok := planLog.Interface().([]*models.ClusterPlanStepInfo); ok { return log, nil } return nil, errors.New("plan: failed casting PlanAttemptLog to []*models.ClusterPlanStepInfo") }