From 363ff36121c1b5eaa7a019479d8b3fe654f1d421 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Mon, 9 Dec 2024 17:29:43 +0100 Subject: [PATCH] check: Unify app installation check Use the same function to check whether app in installed for both the "check" and "ps" commands. This check includes: 1. Verification if all app images are present in the docker store. 2. Verification if an app bundle is properly installed in the compose/project directory (all bundle files present and their hashes match the expected hashes). Also, a new flag "--install" was added to the "ps" command to turn on/off the installation check after checking if app is running. By default this check is enabled. Signed-off-by: Mike Sul --- cmd/composectl/cmd/check.go | 60 ++++++++++--------------------------- cmd/composectl/cmd/ps.go | 46 +++++++++++++++------------- pkg/compose/app.go | 7 +++-- pkg/compose/v1/app.go | 23 +++++++++----- 4 files changed, 61 insertions(+), 75 deletions(-) diff --git a/cmd/composectl/cmd/check.go b/cmd/composectl/cmd/check.go index 16b41ae..b63eefa 100644 --- a/cmd/composectl/cmd/check.go +++ b/cmd/composectl/cmd/check.go @@ -13,7 +13,6 @@ import ( v1 "github.com/foundriesio/composeapp/pkg/compose/v1" "github.com/opencontainers/go-digest" "github.com/spf13/cobra" - "io/fs" "os" "path" ) @@ -50,11 +49,9 @@ type ( } appInstallCheckResult struct { - AppName string `json:"app_name"` - MissingImages []string `json:"missing_images"` - MissingComposeFiles []string `json:"missing_compose_files"` - InvalidComposeFiles []string `json:"invalid_compose_files"` - ErrorComposeFiles []string `json:"error_compose_files"` + AppName string `json:"app_name"` + MissingImages []string `json:"missing_images"` + BundleErrors compose.AppBundleErrs `json:"bundle_errors"` } InstallCheckResult map[string]*appInstallCheckResult @@ -127,25 +124,20 @@ func checkAppsCmd(cmd *cobra.Command, args []string, opts *checkOptions) { cr.print() if opts.CheckInstall { for appRef, r := range ir { - if len(r.InvalidComposeFiles) > 0 || len(r.MissingComposeFiles) > 0 || len(r.MissingImages) > 0 || len(r.ErrorComposeFiles) > 0 { + if len(r.MissingImages) > 0 || len(r.BundleErrors) > 0 { fmt.Printf("%s is not installed (%s)\n", r.AppName, appRef) - for _, issue := range []struct { - issueType string - issueValues []string - }{ - {"missing images", r.MissingImages}, - {"missing compose files", r.MissingComposeFiles}, - {"invalid compose files", r.InvalidComposeFiles}, - {"error compose files", r.ErrorComposeFiles}, - } { - if len(issue.issueValues) == 0 { - continue - } - fmt.Printf("\t%s:\n", issue.issueType) - for _, val := range issue.issueValues { + if len(r.MissingImages) > 0 { + fmt.Println("\tmissing images:") + for _, val := range r.MissingImages { fmt.Println("\t\t" + val) } } + if len(r.BundleErrors) > 0 { + fmt.Println("\tapp bundle errors:") + for f, e := range r.BundleErrors { + fmt.Printf("\t\t%s:\t%s\n", f, e) + } + } } } } @@ -313,30 +305,10 @@ func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string if err != nil { return nil, err } - var missingComposeFiles []string - var invalidComposeFiles []string - var errComposeFiles []string - for filePath, checkErr := range errMap { - switch checkErr.(type) { - case *compose.ErrBlobDigestMismatch, *compose.ErrBlobSizeMismatch, *compose.ErrBlobSizeLimitExceed: - { - invalidComposeFiles = append(invalidComposeFiles, filePath) - } - case *fs.PathError: - { - missingComposeFiles = append(missingComposeFiles, filePath) - } - default: - errComposeFiles = append(errComposeFiles, fmt.Sprintf("%s: %s", filePath, checkErr.Error())) - } - } - checkResult[appRef] = &appInstallCheckResult{ - AppName: app.Name(), - MissingImages: missingImages, - InvalidComposeFiles: invalidComposeFiles, - MissingComposeFiles: missingComposeFiles, - ErrorComposeFiles: errComposeFiles, + AppName: app.Name(), + MissingImages: missingImages, + BundleErrors: errMap, } } return checkResult, nil diff --git a/cmd/composectl/cmd/ps.go b/cmd/composectl/cmd/ps.go index 20e6dda..74bbbc6 100644 --- a/cmd/composectl/cmd/ps.go +++ b/cmd/composectl/cmd/ps.go @@ -28,11 +28,13 @@ type ( Health string `json:"health,omitempty"` } App struct { - URI string `json:"uri"` - Name string `json:"name"` - State string `json:"state"` - Services []*Service `json:"services"` - InStore bool `json:"in_store"` + URI string `json:"uri"` + Name string `json:"name"` + State string `json:"state"` + Services []*Service `json:"services"` + InStore bool `json:"in_store"` + BundleErrors compose.AppBundleErrs `json:"bundle_errors"` + MissingImages []string `json:"missing_images"` } ServiceStatus struct { URI string `json:"uri"` @@ -44,13 +46,15 @@ type ( AppStatus []ServiceStatus psOptions struct { - Format string + Format string + CheckInstall bool } ) func init() { opts := psOptions{} psCmd.Flags().StringVar(&opts.Format, "format", "table", "Format the output. Values: [table | json]") + psCmd.Flags().BoolVar(&opts.CheckInstall, "install", true, "Also check if app is installed") psCmd.Run = func(cmd *cobra.Command, args []string) { if opts.Format != "table" && opts.Format != "json" { DieNotNil(fmt.Errorf("invalid value of `--format` option: %s", opts.Format)) @@ -62,8 +66,8 @@ func init() { } var psCmd = &cobra.Command{ - Use: "ps", - Short: "ps []", + Use: "ps []...", + Short: "ps []...", Long: ``, } @@ -72,13 +76,13 @@ func psApps(cmd *cobra.Command, args []string, opts *psOptions) { if len(args) == 0 { printAppStatuses(runningApps, opts.Format) } else { - appStatuses := getAppsStatus(cmd.Context(), args, runningApps) + appStatuses := getAppsStatus(cmd.Context(), args, runningApps, opts.CheckInstall) printAppStatuses(appStatuses, opts.Format) } } -func getAppsStatus(ctx context.Context, appRefs []string, runningApps map[string]*App) map[string]*App { +func getAppsStatus(ctx context.Context, appRefs []string, runningApps map[string]*App, checkInstall bool) map[string]*App { storeBlobProvider := compose.NewStoreBlobProvider(path.Join(config.StoreRoot, "blobs", "sha256")) apps := map[string]compose.App{} for _, appRef := range appRefs { @@ -173,16 +177,7 @@ func getAppsStatus(ctx context.Context, appRefs []string, runningApps map[string appState = "not running" } } - if appState == "running" { - errMap, err := app.CheckComposeInstallation(ctx, storeBlobProvider, path.Join(config.ComposeRoot, app.Name())) - if err == nil { - if len(errMap) > 0 { - appState = "running invalid bundle" - } - } else { - fmt.Printf("failed to check whether app bundle is installed") - } - } + appStatuses[appRef] = &App{ URI: appRef, Name: app.Name(), @@ -192,6 +187,17 @@ func getAppsStatus(ctx context.Context, appRefs []string, runningApps map[string } } + if checkInstall { + checkInstallResult, err := checkIfInstalled(ctx, appRefs, config.StoreRoot, config.DockerHost) + DieNotNil(err) + for app, ir := range checkInstallResult { + appStatuses[app].BundleErrors = ir.BundleErrors + appStatuses[app].MissingImages = ir.MissingImages + if appStatuses[app].State == "running" && len(ir.BundleErrors) > 0 { + appStatuses[app].State = "running with an invalid app bundle" + } + } + } return appStatuses } diff --git a/pkg/compose/app.go b/pkg/compose/app.go index ef338c1..44bd1e1 100644 --- a/pkg/compose/app.go +++ b/pkg/compose/app.go @@ -20,15 +20,16 @@ type ( Digest digest.Digest } - AppTree TreeNode - App interface { + AppBundleErrs map[string]string + AppTree TreeNode + App interface { Name() string Ref() *AppRef HasLayersMeta(arch string) bool GetBlobRuntimeSize(desc *ocispec.Descriptor, arch string, blockSize int64) int64 GetComposeRoot() *TreeNode GetCompose(ctx context.Context, provider BlobProvider) (*composetypes.Project, error) - CheckComposeInstallation(ctx context.Context, provider BlobProvider, installationRootDir string) (map[string]error, error) + CheckComposeInstallation(ctx context.Context, provider BlobProvider, installationRootDir string) (AppBundleErrs, error) } AppLoader interface { LoadAppTree(context.Context, BlobProvider, platforms.MatchComparer, string) (App, *AppTree, error) diff --git a/pkg/compose/v1/app.go b/pkg/compose/v1/app.go index 8fc2d40..76021a2 100644 --- a/pkg/compose/v1/app.go +++ b/pkg/compose/v1/app.go @@ -246,16 +246,20 @@ func (a *appCtx) GetLayersMetadataDescriptor() (*ocispec.Descriptor, error) { return &desc, nil } -func (a *appCtx) CheckComposeInstallation(ctx context.Context, provider compose.BlobProvider, installationRootDir string) (map[string]error, error) { - appIndex, err := a.getAppBundleIndex(ctx, provider) - if err != nil && err != ErrAppIndexNotFound { - return nil, err +func (a *appCtx) CheckComposeInstallation(ctx context.Context, provider compose.BlobProvider, installationRootDir string) (bundleErrs compose.AppBundleErrs, err error) { + appIndex, errBundleIndx := a.getAppBundleIndex(ctx, provider) + if errBundleIndx != nil { + if errBundleIndx != ErrAppIndexNotFound { + return nil, errBundleIndx + } else { + return nil, nil + } } - errMap := map[string]error{} + bundleErrMap := compose.AppBundleErrs{} for filePath, fileDigest := range appIndex { f, err := os.Open(path.Join(installationRootDir, filePath)) if os.IsNotExist(err) { - errMap[filePath] = err + bundleErrMap[filePath] = err.Error() continue } r, err := compose.NewSecureReadCloser(f, compose.WithExpectedDigest(fileDigest), compose.WithReadLimit(AppBundleFileMaxSize)) @@ -263,10 +267,13 @@ func (a *appCtx) CheckComposeInstallation(ctx context.Context, provider compose. return nil, err } if _, err := io.ReadAll(r); err != nil { - errMap[filePath] = err + bundleErrMap[filePath] = err.Error() } } - return errMap, nil + if len(bundleErrMap) > 0 { + bundleErrs = bundleErrMap + } + return } func (a *appCtx) getAppBundleIndex(ctx context.Context, blobProvider compose.BlobProvider) (map[string]digest.Digest, error) {