diff --git a/pkg/handlers/config.go b/pkg/handlers/config.go index 32bfa7c857..b0fbe376d1 100644 --- a/pkg/handlers/config.go +++ b/pkg/handlers/config.go @@ -611,7 +611,7 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, configGroups []kot return updateAppConfigResponse, err } - requiredItems, requiredItemsTitles := getMissingRequiredConfig(configGroups) + requiredItems, requiredItemsTitles := kotsadmconfig.GetMissingRequiredConfig(configGroups) // not having all the required items is only a failure for the version that the user intended to edit if len(requiredItems) > 0 && isPrimaryVersion { @@ -624,7 +624,7 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, configGroups []kot // so we don't need the complex logic in kots, we can just write if kotsKinds.ConfigValues != nil { values := kotsKinds.ConfigValues.Spec.Values - kotsKinds.ConfigValues.Spec.Values = updateAppConfigValues(values, configGroups) + kotsKinds.ConfigValues.Spec.Values = kotsadmconfig.UpdateAppConfigValues(values, configGroups) configValuesSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "ConfigValues") if err != nil { @@ -761,28 +761,6 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, configGroups []kot return updateAppConfigResponse, nil } -func getMissingRequiredConfig(configGroups []kotsv1beta1.ConfigGroup) ([]string, []string) { - requiredItems := make([]string, 0, 0) - requiredItemsTitles := make([]string, 0, 0) - for _, group := range configGroups { - if group.When == "false" { - continue - } - for _, item := range group.Items { - if kotsadmconfig.IsRequiredItem(item) && kotsadmconfig.IsUnsetItem(item) { - requiredItems = append(requiredItems, item.Name) - if item.Title != "" { - requiredItemsTitles = append(requiredItemsTitles, item.Title) - } else { - requiredItemsTitles = append(requiredItemsTitles, item.Name) - } - } - } - } - - return requiredItems, requiredItemsTitles -} - func updateAppConfigValues(values map[string]kotsv1beta1.ConfigValue, configGroups []kotsv1beta1.ConfigGroup) map[string]kotsv1beta1.ConfigValue { for _, group := range configGroups { for _, item := range group.Items { diff --git a/pkg/kotsadmconfig/config.go b/pkg/kotsadmconfig/config.go index e887f5cb19..3b335ffa38 100644 --- a/pkg/kotsadmconfig/config.go +++ b/pkg/kotsadmconfig/config.go @@ -2,10 +2,14 @@ package kotsadmconfig import ( "context" + "encoding/base64" + "fmt" "os" + "strconv" "github.com/pkg/errors" kotsconfig "github.com/replicatedhq/kots/pkg/config" + "github.com/replicatedhq/kots/pkg/crypto" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" @@ -13,6 +17,7 @@ import ( "github.com/replicatedhq/kots/pkg/template" "github.com/replicatedhq/kots/pkg/util" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/multitype" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -91,6 +96,76 @@ func NeedsConfiguration(appSlug string, sequence int64, isAirgap bool, kotsKinds return false, nil } +func GetMissingRequiredConfig(configGroups []kotsv1beta1.ConfigGroup) ([]string, []string) { + requiredItems := make([]string, 0, 0) + requiredItemsTitles := make([]string, 0, 0) + for _, group := range configGroups { + if group.When == "false" { + continue + } + for _, item := range group.Items { + if IsRequiredItem(item) && IsUnsetItem(item) { + requiredItems = append(requiredItems, item.Name) + if item.Title != "" { + requiredItemsTitles = append(requiredItemsTitles, item.Title) + } else { + requiredItemsTitles = append(requiredItemsTitles, item.Name) + } + } + } + } + + return requiredItems, requiredItemsTitles +} + +func UpdateAppConfigValues(values map[string]kotsv1beta1.ConfigValue, configGroups []kotsv1beta1.ConfigGroup) map[string]kotsv1beta1.ConfigValue { + for _, group := range configGroups { + for _, item := range group.Items { + if item.Type == "file" { + v := values[item.Name] + v.Filename = item.Filename + values[item.Name] = v + } + if item.Value.Type == multitype.Bool { + updatedValue := item.Value.BoolVal + v := values[item.Name] + v.Value = strconv.FormatBool(updatedValue) + values[item.Name] = v + } else if item.Value.Type == multitype.String { + updatedValue := item.Value.String() + if item.Type == "password" { + // encrypt using the key + // if the decryption succeeds, don't encrypt again + _, err := util.DecryptConfigValue(updatedValue) + if err != nil { + updatedValue = base64.StdEncoding.EncodeToString(crypto.Encrypt([]byte(updatedValue))) + } + } + + v := values[item.Name] + v.Value = updatedValue + values[item.Name] = v + } + for _, repeatableValues := range item.ValuesByGroup { + // clear out all variadic values for this group first + for name, value := range values { + if value.RepeatableItem == item.Name { + delete(values, name) + } + } + // add variadic groups back in declaratively + for itemName, valueItem := range repeatableValues { + v := values[itemName] + v.Value = fmt.Sprintf("%v", valueItem) + v.RepeatableItem = item.Name + values[itemName] = v + } + } + } + } + return values +} + func ReadConfigValuesFromInClusterSecret() (string, error) { log := logger.NewCLILogger(os.Stdout) diff --git a/pkg/upgradeservice/handlers/config.go b/pkg/upgradeservice/handlers/config.go index 0f2f6b3ad4..1c52e834a8 100644 --- a/pkg/upgradeservice/handlers/config.go +++ b/pkg/upgradeservice/handlers/config.go @@ -2,19 +2,28 @@ package handlers import ( "encoding/json" + "fmt" "net/http" + "os" "path/filepath" + "strings" "github.com/gorilla/mux" "github.com/pkg/errors" + downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" + apptypes "github.com/replicatedhq/kots/pkg/app/types" "github.com/replicatedhq/kots/pkg/config" kotsconfig "github.com/replicatedhq/kots/pkg/config" + "github.com/replicatedhq/kots/pkg/kotsadmconfig" configtypes "github.com/replicatedhq/kots/pkg/kotsadmconfig/types" configvalidation "github.com/replicatedhq/kots/pkg/kotsadmconfig/validation" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/render" + rendertypes "github.com/replicatedhq/kots/pkg/render/types" "github.com/replicatedhq/kots/pkg/template" + upgradepreflight "github.com/replicatedhq/kots/pkg/upgradeservice/preflight" "github.com/replicatedhq/kots/pkg/util" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" @@ -38,6 +47,19 @@ type LiveAppConfigResponse struct { ValidationErrors []configtypes.ConfigGroupValidationError `json:"validationErrors,omitempty"` } +type SaveAppConfigRequest struct { + Sequence int64 `json:"sequence"` + CreateNewVersion bool `json:"createNewVersion"` + ConfigGroups []kotsv1beta1.ConfigGroup `json:"configGroups"` +} + +type SaveAppConfigResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + RequiredItems []string `json:"requiredItems,omitempty"` + ValidationErrors []configtypes.ConfigGroupValidationError `json:"validationErrors,omitempty"` +} + func (h *Handler) CurrentAppConfig(w http.ResponseWriter, r *http.Request) { currentAppConfigResponse := CurrentAppConfigResponse{ Success: false, @@ -254,3 +276,121 @@ func configValuesFromConfigGroups(configGroups []kotsv1beta1.ConfigGroup) map[st return configValues } + +func (h *Handler) SaveAppConfig(w http.ResponseWriter, r *http.Request) { + saveAppConfigResponse := SaveAppConfigResponse{ + Success: false, + } + + params := GetContextParams(r) + appSlug := mux.Vars(r)["appSlug"] + + if params.AppSlug != appSlug { + saveAppConfigResponse.Error = "app slug does not match" + JSON(w, http.StatusForbidden, saveAppConfigResponse) + return + } + + saveAppConfigRequest := SaveAppConfigRequest{} + if err := json.NewDecoder(r.Body).Decode(&saveAppConfigRequest); err != nil { + logger.Error(err) + saveAppConfigResponse.Error = "failed to decode request body" + JSON(w, http.StatusBadRequest, saveAppConfigResponse) + return + } + + validationErrors, err := configvalidation.ValidateConfigSpec(kotsv1beta1.ConfigSpec{Groups: saveAppConfigRequest.ConfigGroups}) + if err != nil { + saveAppConfigResponse.Error = "failed to validate config spec." + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + if len(validationErrors) > 0 { + saveAppConfigResponse.Error = "invalid config values" + saveAppConfigResponse.ValidationErrors = validationErrors + logger.Errorf("%v, validation errors: %+v", saveAppConfigResponse.Error, validationErrors) + JSON(w, http.StatusBadRequest, saveAppConfigResponse) + return + } + + requiredItems, requiredItemsTitles := kotsadmconfig.GetMissingRequiredConfig(saveAppConfigRequest.ConfigGroups) + if len(requiredItems) > 0 { + saveAppConfigResponse.RequiredItems = requiredItems + saveAppConfigResponse.Error = fmt.Sprintf("The following fields are required: %s", strings.Join(requiredItemsTitles, ", ")) + logger.Errorf("%v, required items: %+v", saveAppConfigResponse.Error, requiredItems) + JSON(w, http.StatusBadRequest, saveAppConfigResponse) + return + } + + localRegistry := registrytypes.RegistrySettings{ + Hostname: params.RegistryEndpoint, + Username: params.RegistryUsername, + Password: params.RegistryPassword, + Namespace: params.RegistryNamespace, + IsReadOnly: params.RegistryIsReadOnly, + } + + app := &apptypes.App{ + ID: params.AppID, + Slug: params.AppSlug, + IsAirgap: params.AppIsAirgap, + IsGitOps: params.AppIsGitOps, + } + + if err := saveAppConfig(app, params.BaseArchive, params.NextSequence, saveAppConfigRequest.ConfigGroups, localRegistry, params.PreflightResultsDir); err != nil { + saveAppConfigResponse.Error = "failed to save app config" + logger.Error(errors.Wrap(err, saveAppConfigResponse.Error)) + JSON(w, http.StatusInternalServerError, saveAppConfigResponse) + return + } + + saveAppConfigResponse.Success = true + JSON(w, http.StatusOK, saveAppConfigResponse) +} + +func saveAppConfig(app *apptypes.App, archiveDir string, sequence int64, configGroups []kotsv1beta1.ConfigGroup, registrySettings registrytypes.RegistrySettings, preflightResultsDir string) error { + kotsKinds, err := kotsutil.LoadKotsKinds(archiveDir) + if err != nil { + return errors.Wrap(err, "failed to load kots kinds") + } + + if kotsKinds.ConfigValues == nil { + return errors.New("config values not found") + } + + values := kotsKinds.ConfigValues.Spec.Values + kotsKinds.ConfigValues.Spec.Values = kotsadmconfig.UpdateAppConfigValues(values, configGroups) + + configValuesSpec, err := kotsKinds.Marshal("kots.io", "v1beta1", "ConfigValues") + if err != nil { + return errors.Wrap(err, "failed to marshal config values") + } + + if err := os.WriteFile(filepath.Join(archiveDir, "upstream", "userdata", "config.yaml"), []byte(configValuesSpec), 0644); err != nil { + return errors.Wrap(err, "failed to write config values") + } + + err = render.RenderDir(rendertypes.RenderDirOptions{ + ArchiveDir: archiveDir, + App: app, + Downstreams: []downstreamtypes.Downstream{{Name: "this-cluster"}}, // TODO NOW: is this correct? + RegistrySettings: registrySettings, + Sequence: sequence, + }) + if err != nil { + cause := errors.Cause(err) + if _, ok := cause.(util.ActionableError); ok { + return err + } else { + return errors.Wrap(err, "failed to render archive directory") + } + } + + if err := upgradepreflight.Run(app, archiveDir, int64(sequence), registrySettings, false, preflightResultsDir); err != nil { + return errors.Wrap(err, "failed to run preflights") + } + + return nil +} diff --git a/pkg/upgradeservice/handlers/interface.go b/pkg/upgradeservice/handlers/interface.go index 7825aa6dff..8361cd9cd9 100644 --- a/pkg/upgradeservice/handlers/interface.go +++ b/pkg/upgradeservice/handlers/interface.go @@ -7,4 +7,5 @@ type UpgradeServiceHandler interface { CurrentAppConfig(w http.ResponseWriter, r *http.Request) LiveAppConfig(w http.ResponseWriter, r *http.Request) + SaveAppConfig(w http.ResponseWriter, r *http.Request) } diff --git a/pkg/upgradeservice/preflight/preflight.go b/pkg/upgradeservice/preflight/preflight.go new file mode 100644 index 0000000000..48f772f087 --- /dev/null +++ b/pkg/upgradeservice/preflight/preflight.go @@ -0,0 +1,370 @@ +package preflight + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" + "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" + preflightpkg "github.com/replicatedhq/kots/pkg/preflight" + "github.com/replicatedhq/kots/pkg/preflight/types" + "github.com/replicatedhq/kots/pkg/registry" + registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/render" + rendertypes "github.com/replicatedhq/kots/pkg/render/types" + "github.com/replicatedhq/kots/pkg/reporting" + "github.com/replicatedhq/kots/pkg/util" + troubleshootanalyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + troubleshootcollect "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/preflight" + troubleshootpreflight "github.com/replicatedhq/troubleshoot/pkg/preflight" +) + +func Run(app *apptypes.App, archiveDir string, sequence int64, registrySettings registrytypes.RegistrySettings, ignoreRBAC bool, resultsDir string) error { + kotsKinds, err := kotsutil.LoadKotsKinds(archiveDir) + if err != nil { + return errors.Wrap(err, "failed to load rendered kots kinds") + } + + tsKinds, err := kotsutil.LoadTSKindsFromPath(filepath.Join(archiveDir, "rendered")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to load troubleshoot kinds from path: %s", filepath.Join(archiveDir, "rendered"))) + } + + var preflight *troubleshootv1beta2.Preflight + runPreflights := false + if tsKinds.PreflightsV1Beta2 != nil { + + for _, v := range tsKinds.PreflightsV1Beta2 { + preflight = troubleshootpreflight.ConcatPreflightSpec(preflight, &v) + } + + injectDefaultPreflights(preflight, kotsKinds, registrySettings) + + numAnalyzers := 0 + for _, analyzer := range preflight.Spec.Analyzers { + exclude := troubleshootanalyze.GetExcludeFlag(analyzer).BoolOrDefaultFalse() + if !exclude { + numAnalyzers += 1 + } + } + runPreflights = numAnalyzers > 0 + } else if kotsKinds.Preflight != nil { + // render the preflight file + // we need to convert to bytes first, so that we can reuse the renderfile function + renderedMarshalledPreflights, err := kotsKinds.Marshal("troubleshoot.replicated.com", "v1beta1", "Preflight") + if err != nil { + return errors.Wrap(err, "failed to marshal rendered preflight") + } + + renderedPreflight, err := render.RenderFile(rendertypes.RenderFileOptions{ + KotsKinds: kotsKinds, + RegistrySettings: registrySettings, + AppSlug: app.Slug, + Sequence: sequence, + IsAirgap: app.IsAirgap, + Namespace: util.PodNamespace, + InputContent: []byte(renderedMarshalledPreflights), + }) + if err != nil { + return errors.Wrap(err, "failed to render preflights") + } + preflight, err = kotsutil.LoadPreflightFromContents(renderedPreflight) + if err != nil { + return errors.Wrap(err, "failed to load rendered preflight") + } + + injectDefaultPreflights(preflight, kotsKinds, registrySettings) + + numAnalyzers := 0 + for _, analyzer := range preflight.Spec.Analyzers { + exclude := troubleshootanalyze.GetExcludeFlag(analyzer).BoolOrDefaultFalse() + if !exclude { + numAnalyzers += 1 + } + } + runPreflights = numAnalyzers > 0 + } + + if !runPreflights { + logger.Debug("no analyzers found, not running preflights") + return nil + } + + var preflightErr error + defer func() { + if preflightErr != nil { + err := setPreflightResult(resultsDir, &types.PreflightResults{}, preflightErr) + if err != nil { + logger.Error(errors.Wrap(err, "failed to set preflight results")) + return + } + } + }() + + collectors, err := registry.UpdateCollectorSpecsWithRegistryData(preflight.Spec.Collectors, registrySettings, kotsKinds.Installation, kotsKinds.License, &kotsKinds.KotsApplication) + if err != nil { + preflightErr = errors.Wrap(err, "failed to rewrite images in preflight") + return preflightErr + } + preflight.Spec.Collectors = collectors + + go func() { + logger.Info("preflight checks beginning") + uploadPreflightResults, err := execute(resultsDir, preflight, ignoreRBAC) + if err != nil { + logger.Error(errors.Wrap(err, "failed to run preflight checks")) + return + } + + // Log the preflight results if there are any warnings or errors + // The app may not get installed so we need to see this info for debugging + if preflightpkg.GetPreflightState(uploadPreflightResults) != "pass" { + logger.Warnf("preflight checks completed with warnings or errors") + for _, result := range uploadPreflightResults.Results { + if result == nil { + continue + } + logger.Infof("preflight state=%s title=%q message=%q", preflightpkg.GetPreflightCheckState(result), result.Title, result.Message) + } + } else { + logger.Info("preflight checks completed") + } + + go func() { + err := reporting.GetReporter().SubmitAppInfo(app.ID) // send app and preflight info when preflights finish + if err != nil { + logger.Debugf("failed to submit app info: %v", err) + } + }() + }() + + return nil +} + +func injectDefaultPreflights(preflight *troubleshootv1beta2.Preflight, kotskinds *kotsutil.KotsKinds, registrySettings registrytypes.RegistrySettings) { + if !registrySettings.IsValid() || !registrySettings.IsReadOnly { + return + } + + // Get images from Installation.KnownImages, see UpdateCollectorSpecsWithRegistryData + images := []string{} + for _, image := range kotskinds.Installation.Spec.KnownImages { + images = append(images, image.Image) + } + + preflight.Spec.Collectors = append(preflight.Spec.Collectors, &troubleshootv1beta2.Collect{ + RegistryImages: &troubleshootv1beta2.RegistryImages{ + Images: images, + }, + }) + preflight.Spec.Analyzers = append(preflight.Spec.Analyzers, &troubleshootv1beta2.Analyze{ + RegistryImages: &troubleshootv1beta2.RegistryImagesAnalyze{ + AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ + CheckName: "Private Registry Images Available", + }, + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "missing > 0", + Message: "Application uses images that cannot be found in the private registry", + }, + }, + { + Warn: &troubleshootv1beta2.SingleOutcome{ + When: "errors > 0", + Message: "Availability of application images in the private registry could not be verified.", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "All images used by the application are present in the private registry", + }, + }, + }, + }, + }) +} + +func setPreflightResult(resultsDir string, preflightResults *types.PreflightResults, preflightRunError error) error { + if preflightRunError != nil { + if preflightResults.Errors == nil { + preflightResults.Errors = []*types.PreflightError{} + } + preflightResults.Errors = append(preflightResults.Errors, &types.PreflightError{ + Error: preflightRunError.Error(), + IsRBAC: false, + }) + } + + b, err := json.Marshal(preflightResults) + if err != nil { + return errors.Wrap(err, "failed to marshal preflight results") + } + + // write to preflight-results.json in the results dir + if err := os.WriteFile(filepath.Join(resultsDir, "preflight-results.json"), b, 0644); err != nil { + return errors.Wrap(err, "failed to write preflight results") + } + + return nil +} + +func setPreflightProgress(resultsDir string, b []byte) error { + if err := os.WriteFile(filepath.Join(resultsDir, "preflight-progress.json"), b, 0644); err != nil { + return errors.Wrap(err, "failed to write preflight progress") + } + + return nil +} + +// execute will execute the preflights using spec in preflightSpec. +// This spec should be rendered, no template functions remaining +func execute(resultsDir string, preflightSpec *troubleshootv1beta2.Preflight, ignorePermissionErrors bool) (*types.PreflightResults, error) { + logger.Info("executing preflight checks") + + progressChan := make(chan interface{}, 0) // non-zero buffer will result in missed messages + defer close(progressChan) + + var preflightRunError error + completeMx := sync.Mutex{} + isComplete := false + go func() { + for { + msg, ok := <-progressChan + if !ok { + return + } + + if err, ok := msg.(error); ok { + logger.Errorf("error while running preflights: %v", err) + } else { + switch m := msg.(type) { + case preflight.CollectProgress: + logger.Infof("preflight progress: %s", m.String()) + default: + logger.Infof("preflight progress: %+v", msg) + } + } + + progress, ok := msg.(preflight.CollectProgress) + if !ok { + continue + } + + // TODO: We need a nice title to display + progressBytes, err := json.Marshal(map[string]interface{}{ + "completedCount": progress.CompletedCount, + "totalCount": progress.TotalCount, + "currentName": progress.CurrentName, + "currentStatus": progress.CurrentStatus, + "updatedAt": time.Now().Format(time.RFC3339), + }) + if err != nil { + continue + } + + completeMx.Lock() + if !isComplete { + _ = setPreflightProgress(resultsDir, progressBytes) + } + completeMx.Unlock() + } + }() + + uploadPreflightResults := &types.PreflightResults{} + defer func() { + completeMx.Lock() + defer completeMx.Unlock() + + isComplete = true + if err := setPreflightResult(resultsDir, uploadPreflightResults, preflightRunError); err != nil { + logger.Error(errors.Wrap(err, "failed to set preflight results")) + return + } + }() + + restConfig, err := k8sutil.GetClusterConfig() + if err != nil { + preflightRunError = err + return nil, errors.Wrap(err, "failed to get cluster config") + } + + preflightSpec.Spec.Collectors = troubleshootcollect.DedupCollectors(preflightSpec.Spec.Collectors) + preflightSpec.Spec.Analyzers = troubleshootanalyze.DedupAnalyzers(preflightSpec.Spec.Analyzers) + + collectOpts := troubleshootpreflight.CollectOpts{ + Namespace: "", + IgnorePermissionErrors: ignorePermissionErrors, + ProgressChan: progressChan, + KubernetesRestConfig: restConfig, + } + + logger.Info("preflight collect phase") + collectResults, err := troubleshootpreflight.Collect(collectOpts, preflightSpec) + if err != nil && !isPermissionsError(err) { + preflightRunError = err + return nil, errors.Wrap(err, "failed to collect") + } + + clusterCollectResult, ok := collectResults.(troubleshootpreflight.ClusterCollectResult) + if !ok { + preflightRunError = errors.Errorf("unexpected result type: %T", collectResults) + return nil, preflightRunError + } + + if isPermissionsError(err) { + logger.Debug("skipping analyze due to RBAC errors") + rbacErrors := []*types.PreflightError{} + for _, collector := range clusterCollectResult.Collectors { + for _, e := range collector.GetRBACErrors() { + rbacErrors = append(rbacErrors, &types.PreflightError{ + Error: e.Error(), + IsRBAC: true, + }) + } + } + uploadPreflightResults.Errors = rbacErrors + } else { + logger.Info("preflight analyze phase") + analyzeResults := collectResults.Analyze() + + // the typescript api added some flair to this result + // so let's keep it for compatibility + // MORE TYPES! + results := []*troubleshootpreflight.UploadPreflightResult{} + for _, analyzeResult := range analyzeResults { + uploadPreflightResult := &troubleshootpreflight.UploadPreflightResult{ + Strict: analyzeResult.Strict, + IsFail: analyzeResult.IsFail, + IsWarn: analyzeResult.IsWarn, + IsPass: analyzeResult.IsPass, + Title: analyzeResult.Title, + Message: analyzeResult.Message, + URI: analyzeResult.URI, + } + + results = append(results, uploadPreflightResult) + } + uploadPreflightResults.Results = results + } + + return uploadPreflightResults, nil +} + +func isPermissionsError(err error) bool { + // TODO: make an error type in troubleshoot for this instead of hardcoding the message + if err == nil { + return false + } + return strings.Contains(err.Error(), "insufficient permissions to run all collectors") +} diff --git a/pkg/upgradeservice/server.go b/pkg/upgradeservice/server.go index a662c664a4..94dc967481 100644 --- a/pkg/upgradeservice/server.go +++ b/pkg/upgradeservice/server.go @@ -17,6 +17,12 @@ import ( func Serve(params types.UpgradeServiceParams) error { fmt.Printf("Starting KOTS Upgrade Service version %s on port %s\n", buildversion.Version(), params.Port) + preflightResultsDir, err := os.MkdirTemp("", "preflight-results") + if err != nil { + return errors.Wrap(err, "failed to create preflight results dir") + } + params.PreflightResultsDir = preflightResultsDir + if err := bootstrap(params); err != nil { return errors.Wrap(err, "failed to bootstrap") } diff --git a/pkg/upgradeservice/types/types.go b/pkg/upgradeservice/types/types.go index 96786d2506..72f4045a03 100644 --- a/pkg/upgradeservice/types/types.go +++ b/pkg/upgradeservice/types/types.go @@ -26,4 +26,6 @@ type UpgradeServiceParams struct { RegistryIsReadOnly bool `yaml:"registryIsReadOnly"` ReportingInfo *reportingtypes.ReportingInfo `yaml:"reportingInfo"` + + PreflightResultsDir string `yaml:"preflightResultsDir"` }