Skip to content

Commit

Permalink
check: Unify app installation check
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
mike-sul committed Dec 11, 2024
1 parent c45d8ea commit 363ff36
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 75 deletions.
60 changes: 16 additions & 44 deletions cmd/composectl/cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}
}
Expand Down Expand Up @@ -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
Expand Down
46 changes: 26 additions & 20 deletions cmd/composectl/cmd/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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))
Expand All @@ -62,8 +66,8 @@ func init() {
}

var psCmd = &cobra.Command{
Use: "ps",
Short: "ps <ref> [<ref>]",
Use: "ps [<ref>]...",
Short: "ps [<ref>]...",
Long: ``,
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
Expand All @@ -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
}

Expand Down
7 changes: 4 additions & 3 deletions pkg/compose/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 15 additions & 8 deletions pkg/compose/v1/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,27 +246,34 @@ 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))
if err != nil {
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) {
Expand Down

0 comments on commit 363ff36

Please sign in to comment.