get.porter.sh/porter@v1.3.0/pkg/storage/plugins/mongodb_docker/store.go (about)

     1  package mongodb_docker
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os/exec"
     8  	"runtime"
     9  	"strings"
    10  	"time"
    11  
    12  	"get.porter.sh/porter/pkg/portercontext"
    13  	"get.porter.sh/porter/pkg/storage/plugins"
    14  	"get.porter.sh/porter/pkg/storage/plugins/mongodb"
    15  	"get.porter.sh/porter/pkg/tracing"
    16  	"go.mongodb.org/mongo-driver/bson"
    17  )
    18  
    19  var _ plugins.StorageProtocol = &Store{}
    20  
    21  // Store is a storage plugin for porter suitable for running on machines
    22  // that have not configured proper storage, i.e. a mongo database.
    23  // It runs mongodb in a docker container and stores its data in a docker volume.
    24  type Store struct {
    25  	*mongodb.Store
    26  	context *portercontext.Context
    27  
    28  	config PluginConfig
    29  }
    30  
    31  func NewStore(c *portercontext.Context, cfg PluginConfig) *Store {
    32  	s := &Store{
    33  		context: c,
    34  		config:  cfg,
    35  	}
    36  
    37  	// This is extra insurance that the db connection is closed
    38  	runtime.SetFinalizer(s, func(s *Store) {
    39  		s.Close()
    40  	})
    41  
    42  	return s
    43  }
    44  
    45  // Connect initializes the plugin for use.
    46  // The plugin itself is responsible for ensuring it was called.
    47  // Close is called automatically when the plugin is used by Porter.
    48  func (s *Store) Connect(ctx context.Context) error {
    49  	if s.Store != nil {
    50  		return nil
    51  	}
    52  
    53  	// Run mongo in a container storing its data in a volume
    54  	container := "porter-mongodb-docker-plugin"
    55  	dataVol := container + "-data"
    56  
    57  	conn, err := EnsureMongoIsRunning(ctx, s.context, container, s.config.Port, dataVol, s.config.Database, s.config.Timeout)
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	s.Store = conn
    63  	return nil
    64  }
    65  
    66  func (s *Store) Close() error {
    67  	if s.Store == nil {
    68  		return nil
    69  	}
    70  
    71  	err := s.Store.Close()
    72  	s.Store = nil
    73  	return err
    74  }
    75  
    76  // EnsureIndex makes sure that the specified index exists as specified.
    77  // If it does exist with a different definition, the index is recreated.
    78  func (s *Store) EnsureIndex(ctx context.Context, opts plugins.EnsureIndexOptions) error {
    79  	if err := s.Connect(ctx); err != nil {
    80  		return err
    81  	}
    82  
    83  	return s.Store.EnsureIndex(ctx, opts)
    84  }
    85  
    86  func (s *Store) Aggregate(ctx context.Context, opts plugins.AggregateOptions) ([]bson.Raw, error) {
    87  	if err := s.Connect(ctx); err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	return s.Store.Aggregate(ctx, opts)
    92  }
    93  
    94  func (s *Store) Count(ctx context.Context, opts plugins.CountOptions) (int64, error) {
    95  	if err := s.Connect(ctx); err != nil {
    96  		return 0, err
    97  	}
    98  
    99  	return s.Store.Count(ctx, opts)
   100  }
   101  
   102  func (s *Store) Find(ctx context.Context, opts plugins.FindOptions) ([]bson.Raw, error) {
   103  	if err := s.Connect(ctx); err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return s.Store.Find(ctx, opts)
   108  }
   109  
   110  func (s *Store) Insert(ctx context.Context, opts plugins.InsertOptions) error {
   111  	if err := s.Connect(ctx); err != nil {
   112  		return err
   113  	}
   114  
   115  	return s.Store.Insert(ctx, opts)
   116  }
   117  
   118  func (s *Store) Patch(ctx context.Context, opts plugins.PatchOptions) error {
   119  	if err := s.Connect(ctx); err != nil {
   120  		return err
   121  	}
   122  
   123  	return s.Store.Patch(ctx, opts)
   124  }
   125  
   126  func (s *Store) Remove(ctx context.Context, opts plugins.RemoveOptions) error {
   127  	if err := s.Connect(ctx); err != nil {
   128  		return err
   129  	}
   130  
   131  	return s.Store.Remove(ctx, opts)
   132  }
   133  
   134  func (s *Store) Update(ctx context.Context, opts plugins.UpdateOptions) error {
   135  	if err := s.Connect(ctx); err != nil {
   136  		return err
   137  	}
   138  
   139  	return s.Store.Update(ctx, opts)
   140  }
   141  
   142  func EnsureMongoIsRunning(ctx context.Context, c *portercontext.Context, container string, port string, dataVol string, dbName string, timeoutSeconds int) (*mongodb.Store, error) {
   143  	ctx, span := tracing.StartSpan(ctx)
   144  	defer span.EndSpan()
   145  
   146  	if err := checkDockerAvailability(ctx); err != nil {
   147  		return nil, span.Error(errors.New("Docker is not available"))
   148  	}
   149  
   150  	if dataVol != "" {
   151  		err := exec.Command("docker", "volume", "inspect", dataVol).Run()
   152  		if err != nil {
   153  			span.Debugf("creating a data volume, %s, for the mongodb plugin", dataVol)
   154  
   155  			err = exec.Command("docker", "volume", "create", dataVol).Run()
   156  			if err != nil {
   157  				if exitErr, ok := err.(*exec.ExitError); ok {
   158  					err = fmt.Errorf("%s", string(exitErr.Stderr))
   159  				}
   160  				return nil, span.Error(fmt.Errorf("error creating %s docker volume: %w", dataVol, err))
   161  			}
   162  		}
   163  	}
   164  
   165  	mongoImg := "mongo:8.0-noble"
   166  
   167  	// TODO(carolynvs): run this using the docker library
   168  	startMongo := func() error {
   169  		span.Debugf("pulling %s", mongoImg)
   170  
   171  		err := exec.Command("docker", "pull", mongoImg).Run()
   172  		if err != nil {
   173  			if exitErr, ok := err.(*exec.ExitError); ok {
   174  				err = fmt.Errorf("%s", string(exitErr.Stderr))
   175  			}
   176  			return span.Error(fmt.Errorf("error pulling %s: %w", mongoImg, err))
   177  		}
   178  
   179  		span.Debugf("running a mongo server in a container on port %s", port)
   180  
   181  		args := []string{"run", "--name", container, "-p=" + port + ":27017", "-d",
   182  			"--health-cmd", "echo 'db.runCommand(\"ping\").ok' | mongosh localhost:27017/admin --quiet",
   183  			"--health-interval", "30s",
   184  			"--health-retries", "3",
   185  			"--health-start-period", "10s",
   186  			"--health-start-interval", "0.5s",
   187  		}
   188  		if dataVol != "" {
   189  			args = append(args, "--mount", "source="+dataVol+",destination=/data/db")
   190  		}
   191  		args = append(args, mongoImg)
   192  		mongoC := exec.Command("docker", args...)
   193  		err = mongoC.Start()
   194  		if err != nil {
   195  			if exitErr, ok := err.(*exec.ExitError); ok {
   196  				err = fmt.Errorf("%s", string(exitErr.Stderr))
   197  			}
   198  			return span.Error(fmt.Errorf("error running a mongo container for the mongodb-docker plugin: %w", err))
   199  		}
   200  		return nil
   201  	}
   202  	containerStatus, err := exec.Command("docker", "container", "inspect", container).Output()
   203  	if err != nil {
   204  		if exitErr, ok := err.(*exec.ExitError); ok && strings.Contains(strings.ToLower(string(exitErr.Stderr)), "no such") { // Container doesn't exist
   205  			if err = startMongo(); err != nil {
   206  				return nil, err
   207  			}
   208  		} else {
   209  			if exitErr, ok := err.(*exec.ExitError); ok {
   210  				err = fmt.Errorf("%s", string(exitErr.Stderr))
   211  			}
   212  			return nil, span.Error(fmt.Errorf("error inspecting container %s: %w", container, err))
   213  		}
   214  	} else if !strings.Contains(string(containerStatus), `"Status": "running"`) { // Container is stopped
   215  		err = exec.Command("docker", "rm", "-f", container).Run()
   216  		if err != nil {
   217  			if exitErr, ok := err.(*exec.ExitError); ok {
   218  				err = fmt.Errorf("%s", string(exitErr.Stderr))
   219  			}
   220  			return nil, span.Error(fmt.Errorf("error cleaning up stopped container %s: %w", container, err))
   221  		}
   222  
   223  		if err = startMongo(); err != nil {
   224  			return nil, span.Error(err)
   225  		}
   226  	} else if !strings.Contains(string(containerStatus), mongoImg) {
   227  		err = span.Errorf("this version of Porter requires %s. Please upgrade the MongoDB data format as described in https://porter.sh/docs/operations/upgrade-mongo-data-format/.", mongoImg)
   228  		return nil, err
   229  	}
   230  
   231  	// wait until the mongo daemon is ready
   232  	span.Debug("waiting for the mongo service to be ready")
   233  
   234  	mongoPluginCfg := mongodb.PluginConfig{
   235  		URL:     fmt.Sprintf("mongodb://localhost:%s/%s?connect=direct", port, dbName),
   236  		Timeout: timeoutSeconds,
   237  	}
   238  	timeout, cancel := context.WithTimeout(ctx, 10*time.Second)
   239  	tick := time.NewTicker(50 * time.Millisecond)
   240  	defer tick.Stop()
   241  	defer cancel()
   242  	for {
   243  		select {
   244  		case <-timeout.Done():
   245  			return nil, span.Error(errors.New("timeout waiting for local mongodb daemon to be ready"))
   246  		case <-tick.C:
   247  			containerStatus, err := exec.Command("docker", "inspect", "--format", "{{lower .State.Health.Status }}", container).Output()
   248  			if err != nil {
   249  				continue
   250  			}
   251  			containerHealth := strings.TrimSpace(string(containerStatus))
   252  			span.Debugf("MongoDB container status: [%s]", containerHealth)
   253  			if strings.EqualFold(containerHealth, "healthy") {
   254  				conn := mongodb.NewStore(c, mongoPluginCfg)
   255  				err = conn.Connect(ctx)
   256  				if err == nil {
   257  					return conn, nil
   258  				}
   259  			} else if strings.EqualFold(containerHealth, "unhealthy") {
   260  				if checkMongoVersionError(container) {
   261  					return nil, span.Errorf("this version of Porter requires %s. Please upgrade the MongoDB data format as described in https://porter.sh/docs/operations/upgrade-mongo-data-format/.", mongoImg)
   262  				}
   263  			} else {
   264  				continue
   265  			}
   266  		}
   267  	}
   268  
   269  }
   270  
   271  func checkMongoVersionError(container string) bool {
   272  	containerLogs, err := exec.Command("docker", "logs", container).Output()
   273  	if err == nil && strings.Contains(string(containerLogs), "This version of MongoDB is too recent to start up on the existing data files") {
   274  		return true
   275  	}
   276  	return false
   277  }
   278  
   279  func checkDockerAvailability(ctx context.Context) error {
   280  	_, err := exec.Command("docker", "info").Output()
   281  	return err
   282  }