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 }