From e80d08b6a20c1d35c0fd0f19f6f5e549eda87c8e Mon Sep 17 00:00:00 2001 From: Aviv Carmi Date: Mon, 23 Oct 2023 21:58:37 +0300 Subject: [PATCH 01/39] initial version --- .gitignore | 22 + README.md | 132 +- api.go | 317 + blueprint.go | 291 + build-ui.sh | 14 + component.go | 26 + docker/component.go | 367 + docker/config.go | 926 + docker/network.go | 134 + docker/waiter.go | 119 + exec.go | 75 + go.mod | 43 + go.sum | 123 + io.go | 138 + options.go | 21 + seed/mongo_seed_component.go | 110 + seed/mongo_seed_config.go | 21 + server.go | 42 + static_files.go | 547 + ui/.prettierrc | 7 + ui/build/asset-manifest.json | 15 + ui/build/favicon.ico | Bin 0 -> 3870 bytes ui/build/index.html | 1 + ui/build/static/css/main.a9a95614.css | 2 + ui/build/static/css/main.a9a95614.css.map | 1 + ui/build/static/js/787.3d3a0817.chunk.js | 2 + ui/build/static/js/787.3d3a0817.chunk.js.map | 1 + ui/build/static/js/main.2b9333d0.js | 3 + .../static/js/main.2b9333d0.js.LICENSE.txt | 104 + ui/build/static/js/main.2b9333d0.js.map | 1 + ui/package-lock.json | 30836 ++++++++++++++++ ui/package.json | 58 + ui/public/favicon.ico | Bin 0 -> 3870 bytes ui/public/index.html | 12 + ui/src/ApiLogDialog.css | 4 + ui/src/ApiLogDialog.tsx | 120 + ui/src/App.css | 39 + ui/src/App.tsx | 345 + ui/src/ComponentBox.css | 150 + ui/src/ComponentBox.tsx | 94 + ui/src/ComponentBoxHeader.tsx | 44 + ui/src/ComponentEnvVars.tsx | 116 + ui/src/ComponentInfo.tsx | 94 + ui/src/ComponentOutput.css | 68 + ui/src/ComponentOutput.tsx | 206 + ui/src/ComponentPanel.css | 46 + ui/src/ComponentPanel.tsx | 162 + ui/src/ComponentTitle.css | 26 + ui/src/ComponentTitle.tsx | 35 + ui/src/ComponentsBar.css | 53 + ui/src/ComponentsBar.tsx | 221 + ui/src/Loader.css | 62 + ui/src/Loader.tsx | 12 + ui/src/MainPanel.css | 8 + ui/src/MainPanel.tsx | 61 + ui/src/OfflinePanel.css | 14 + ui/src/OfflinePanel.tsx | 30 + ui/src/SplitButton.tsx | 96 + ui/src/SplitPanel.css | 35 + ui/src/SplitPanel.tsx | 186 + ui/src/api.ts | 211 + ui/src/index.css | 5 + ui/src/index.tsx | 15 + ui/src/prism.css | 116 + ui/src/react-app-env.d.ts | 1 + ui/src/reportWebVitals.ts | 15 + ui/src/setupTests.ts | 5 + ui/tsconfig.json | 26 + 68 files changed, 37231 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 api.go create mode 100644 blueprint.go create mode 100755 build-ui.sh create mode 100644 component.go create mode 100644 docker/component.go create mode 100644 docker/config.go create mode 100644 docker/network.go create mode 100644 docker/waiter.go create mode 100644 exec.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 io.go create mode 100644 options.go create mode 100644 seed/mongo_seed_component.go create mode 100644 seed/mongo_seed_config.go create mode 100644 server.go create mode 100644 static_files.go create mode 100644 ui/.prettierrc create mode 100644 ui/build/asset-manifest.json create mode 100644 ui/build/favicon.ico create mode 100644 ui/build/index.html create mode 100644 ui/build/static/css/main.a9a95614.css create mode 100644 ui/build/static/css/main.a9a95614.css.map create mode 100644 ui/build/static/js/787.3d3a0817.chunk.js create mode 100644 ui/build/static/js/787.3d3a0817.chunk.js.map create mode 100644 ui/build/static/js/main.2b9333d0.js create mode 100644 ui/build/static/js/main.2b9333d0.js.LICENSE.txt create mode 100644 ui/build/static/js/main.2b9333d0.js.map create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/public/favicon.ico create mode 100644 ui/public/index.html create mode 100644 ui/src/ApiLogDialog.css create mode 100644 ui/src/ApiLogDialog.tsx create mode 100644 ui/src/App.css create mode 100644 ui/src/App.tsx create mode 100644 ui/src/ComponentBox.css create mode 100644 ui/src/ComponentBox.tsx create mode 100644 ui/src/ComponentBoxHeader.tsx create mode 100644 ui/src/ComponentEnvVars.tsx create mode 100644 ui/src/ComponentInfo.tsx create mode 100644 ui/src/ComponentOutput.css create mode 100644 ui/src/ComponentOutput.tsx create mode 100644 ui/src/ComponentPanel.css create mode 100644 ui/src/ComponentPanel.tsx create mode 100644 ui/src/ComponentTitle.css create mode 100644 ui/src/ComponentTitle.tsx create mode 100644 ui/src/ComponentsBar.css create mode 100644 ui/src/ComponentsBar.tsx create mode 100644 ui/src/Loader.css create mode 100644 ui/src/Loader.tsx create mode 100644 ui/src/MainPanel.css create mode 100644 ui/src/MainPanel.tsx create mode 100644 ui/src/OfflinePanel.css create mode 100644 ui/src/OfflinePanel.tsx create mode 100644 ui/src/SplitButton.tsx create mode 100644 ui/src/SplitPanel.css create mode 100644 ui/src/SplitPanel.tsx create mode 100644 ui/src/api.ts create mode 100644 ui/src/index.css create mode 100644 ui/src/index.tsx create mode 100644 ui/src/prism.css create mode 100644 ui/src/react-app-env.d.ts create mode 100644 ui/src/reportWebVitals.ts create mode 100644 ui/src/setupTests.ts create mode 100644 ui/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5bb565 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +### ui + +# dependencies +/ui/node_modules +/ui/.pnp +/ui.pnp.js + +# testing +/ui/coverage +/ui/.idea + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.idea diff --git a/README.md b/README.md index ea45ed0..b21d82a 100644 --- a/README.md +++ b/README.md @@ -1 +1,131 @@ -# fengshui \ No newline at end of file +# Feng Shui + +Feng Shui is an integration system for development and continuous-integration environments. + +It's designed to allow switching between local development needs and automated environments seamlessly +and provide the best tooling to describe, provision, and monitor integrated components. + +A demo screen capture can be found [here](/demo.mov). + +--- + +## Motivation + +Any software that interacts with external components requires some solutions. In production environments you need +these two components to be able to interact in a safe and secure fashion. Obviously, there are countless solutions +for these needs such as cloud-managed products, container orchestration solutions such as Kubernetes, load balancers, +service discovery solutions, service mesh products, and so on. Feng Shui doesn't aim to help there. + +Next, if you want to run non-production automation such as integration testing during a CI/CD pipeline, you're going +to need to create an environment similar to production to be able to execute these tests. + +Lastly, development processes require a similar environment to function properly. + +Feng Shui aims to help with the last 2 use cases - development, and non-production automation. It aims to make them +completely similar to allow full reproducibility, on the one hand, and the best tooling for development needs, on the +other hand. + +## Alternatives + +#### So why not stick to what you use today? + +For starters, you might want to do that. Let's see when you actually need Feng Shui. +Here are the popular alternatives and how they compare with Feng Shui. + +##### Using Kubernetes for production, CI, and development + +This method has a huge advantage: you only have to describe your environment once. This means you maintain only +one description of your environments - using Kubernetes manifest files, but more importantly, the way your components +are deployed and provisioned in production is identical to the way they are in development in CI. + +Let's talk about some possible downsides: +* Local development is not always intuitive. While actively working on one or more components, there are some issues +to solve: + * If you fully containerize everything, like you normally do in Kubernetes: + * You need to solve how you debug running containers. Attaching a remote debugging session is not always easy. + * How do you rebuild container images each time you perform a code change. This process can take several minutes +every time you perform any code change? + * How do you manage and override image tag values in your original manifest files? Does it mean you maintain +separate manifest files for production and dev purposes? + * Can you provide hot reloading or similar tools in environments where this is desired? + * If you choose to avoid developing components in containers, and simply run them outside the cluster: + * How easy it is to configure and run a component outside the cluster? + * Can components running outside the cluster communicate with components running inside it? This needs to be solved +specifically for development purposes. + * Can components running inside the cluster communicate with components running outside of it? This requires +a different, probably more complex solution. +* What about non-containerized steps? By that, I'm not referring to actual production components that are not +containerized. I'm talking about steps that do not exist in production at all. For instance, creating seed data +that must exist in a database for other components to boot successfully. This step usually involves writing some code +or using some automation to create initial data. For each such requirement, you can either find a solution +that prevents writing custom code or containerizing your code. Either way, you add complexity and time. +* What about an environment that keeps some components outside Kubernetes in production? For instance, some companies +do not run their databases inside a Kubernetes cluster. This also means you have to maintain manifest files +specifically for dev and CI, and the environments are not identical to production. + +##### Using docker-compose for CI and development + +If you're not using container orchestration tools like Kubernetes in production, but you need some integration +between several components, this will probably be your first choice. + +However, it does have all the possible downsides of Kubernetes mentioned above, on top of some other ones: +* You manage your docker-compose manifest files specifically for dev and CI. This means you have a duplicate to +maintain, but also, your dev and CI envs can potentially be different from production. +* Managing dependencies between services is not always easy - if one service needs to be fully operational before +another one starts, it can be a bit tricky. + +##### Using a remote staging/dev environment + +Some cases have developers use a remote environment for dev and testing purposes. +It can either be achieved using tools such as Kubernetes, or even simply connecting to remote components. + +These solutions are a very good fit for use cases that require running a lot of components. I.e., if you need 50 +components up and running to run your tests, running it all locally is not feasible. +However, they can have downsides or complexities: +* You need internet connectivity. It sounds quite funny because you have a connection everywhere these days, right? +But think about the times that your internet goes down, and you can at least keep on debugging your if statement. +Now you can't. Think about all the times that the speed goes down, this directly affects your ability to run and debug +your local code. +* What if something breaks? Connecting to remote components every time you want to do any kind of local development +simply add issues that are more complex to understand and debug. You might need to debug your debugging sessions. +* Is this environment shared? If so, this is obviously bad. Tests can suddenly stop passing because someone made +a change that had unintended consequences. +* If this environment is not shared, how much does it cost to have an entire duplicate of the production stack for each +engineer in the organization? + +##### Using testcontainers or a similar library to write container management code for CI and development + +This option is quite close to Feng Shui. These tools allow you to write custom code to describe your environment, +so you have full control over what you can do. As with most other options, you must manage your test env separately from +production since you don't use testcontainers in production. This means you have to maintain 2 copies, but also, +production env can defer from your test env. + +In addition, testcontainers have 2 more downsides: +* You can only write in Java or Go. +* testcontainers bring a LOT of dependencies. + +##### Can Feng Shui meet my needs? + +Feng Shui is quite close to testcontainers. It allows you to either write Go, or use config files to describe your env. +It can be used as a Go library, or as a CLI tool directly without actually writing code. + +Feng Shui is designed around running seamlessly inside and outside docker containers, to allow simple debugging of +components you currently work on, while running all the rest in containers, connecting everything fluently, and +providing you with the best tooling to manage and monitor the entire environment. + +At this point, you will have to manage Feng Shui files/code separately from your production environment, but we do want +to add support to read directly from Helm and Kustomize files to allow maintaining only one copy of production env. + +One last thing to consider, since Feng Shui runs everything locally, you can't run too many components on your machine. +As mentioned earlier, if you need 50 components up and running to run your tests, running it all locally is not +feasible. You will have to use multiple remote machines for that. If this is your use case, an interesting company +that does it well is [Raftt](https://www.raftt.io/) and I suggest reading more about what they do. + +## How To Build + +To locally work on the UI, cd into the `ui` dir and run react dev server using `npm start`. +To build static UI files: +- cd into the `ui` dir and run `npm run build` +- run `go-bindata -o static_files.go -pkg fengshui -prefix ui/build -fs ui/build/...` + +To install `go-bindata`, run `go install github.com/go-bindata/go-bindata/v3/go-bindata@v3.1.3` diff --git a/api.go b/api.go new file mode 100644 index 0000000..d0f1d8e --- /dev/null +++ b/api.go @@ -0,0 +1,317 @@ +package fengshui + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/gorilla/mux" + "io" + "net/http" + "strings" + "time" +) + +const ( + contentType = "Content-Type" + applicationJSON = "application/json" + accessControl = "Access-Control-Allow-Origin" + accessControlValue = "*" + accessControlAllowHeaders = "Access-Control-Allow-Headers" + accessControlAllowHeadersValue = "Content-Type, Origin, Accept, token" + accessControlAllowMethods = "Access-Control-Allow-Methods" + accessControlAllowMethodsValue = "GET,POST,PUT,DELETE,OPTIONS" + invalidContentType = "invalid content type" + failedToReadBody = "failed to read body" +) + +func registerRoutes(router *mux.Router, blueprint *Blueprint) { + apiRoute(router, http.MethodGet, "/status", getStatusHandler{blueprint: blueprint}) + apiRoute(router, http.MethodPost, "/start_component", postStartHandler{blueprint: blueprint}) + apiRoute(router, http.MethodPost, "/stop_component", postStopHandler{blueprint: blueprint}) + apiRoute(router, http.MethodPost, "/apply", postApplyHandler{blueprint: blueprint}) + apiRoute(router, http.MethodPost, "/stop_all", postStopAllHandler{blueprint: blueprint}) + apiRoute(router, http.MethodGet, "/output", getOutputHandler{blueprint: blueprint}) + router.PathPrefix("/").Handler(newWebHandler()) +} + +type getStatusHandler struct { + blueprint *Blueprint +} + +type GetStatusResponse struct { + ID string `json:"id"` + Components [][]GetStatusResponseComponent `json:"components"` +} + +type GetStatusResponseComponent struct { + ID string `json:"id"` + Type string `json:"type"` + Status ComponentStatus `json:"status"` + Info any `json:"info"` + EnvVars map[string]string `json:"env_vars"` +} + +func (g getStatusHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + status, err := g.blueprint.Status(request.Context()) + if err != nil { + apiError(g.blueprint, writer, err.Error(), http.StatusInternalServerError) + return + } + + apiSuccess(g.blueprint, writer, status, http.StatusOK) +} + +type postApplyHandler struct { + blueprint *Blueprint +} + +type postApplyRequest struct { + EnabledComponentIDs []string `json:"enabled_component_ids"` +} + +var x = 0 + +func (p postApplyHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + body := postApplyRequest{} + if !apiParse(p.blueprint, writer, request, &body) { + return + } + + err := p.blueprint.Apply(request.Context(), body.EnabledComponentIDs) + if err != nil { + apiError(p.blueprint, writer, err.Error(), http.StatusInternalServerError) + return + } + + apiSuccess(p.blueprint, writer, nil, http.StatusOK) +} + +type postStopAllHandler struct { + blueprint *Blueprint +} + +type postStopAllRequest struct { + Cleanup bool `json:"cleanup"` +} + +func (p postStopAllHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + body := postStopAllRequest{} + if !apiParse(p.blueprint, writer, request, &body) { + return + } + + err := p.blueprint.StopAll(request.Context()) + if err != nil { + apiError(p.blueprint, writer, err.Error(), http.StatusInternalServerError) + return + } + + if body.Cleanup { + err = p.blueprint.Cleanup(request.Context()) + if err != nil { + apiError(p.blueprint, writer, err.Error(), http.StatusInternalServerError) + return + } + } + + apiSuccess(p.blueprint, writer, nil, http.StatusOK) +} + +type postStartHandler struct { + blueprint *Blueprint +} + +type postStartRequest struct { + ComponentID string `json:"component_id"` +} + +func (p postStartHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + body := postStartRequest{} + if !apiParse(p.blueprint, writer, request, &body) { + return + } + + err := p.blueprint.StartComponent(request.Context(), body.ComponentID) + if err != nil { + apiError(p.blueprint, writer, err.Error(), http.StatusInternalServerError) + return + } + + apiSuccess(p.blueprint, writer, nil, http.StatusOK) +} + +type postStopHandler struct { + blueprint *Blueprint +} + +type postStopRequest struct { + ComponentID string `json:"component_id"` +} + +func (p postStopHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + body := postStopRequest{} + if !apiParse(p.blueprint, writer, request, &body) { + return + } + + err := p.blueprint.StopComponent(request.Context(), body.ComponentID) + if err != nil { + apiError(p.blueprint, writer, err.Error(), http.StatusInternalServerError) + return + } + + apiSuccess(p.blueprint, writer, nil, http.StatusOK) +} + +type getOutputHandler struct { + blueprint *Blueprint +} + +func (g getOutputHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + writer.Header().Set(accessControl, accessControlValue) + reader := g.blueprint.Output() + + ch := reader.Chan() + + for { + select { + case data := <-ch: + _, err := writer.Write(data) + if err != nil { + if !errors.Is(err, context.Canceled) { + g.blueprint.logger(LogLevelError, fmt.Sprintf("could not write output stream response: %v", err)) + } + continue + } + if f, ok := writer.(http.Flusher); ok { + f.Flush() + } + case <-request.Context().Done(): + err := reader.Close() + if err != nil { + if !errors.Is(err, context.Canceled) { + g.blueprint.logger(LogLevelError, fmt.Sprintf("could not close output reader: %v", err)) + } + } + return + } + } +} + +func apiParse(b *Blueprint, writer http.ResponseWriter, request *http.Request, target any) bool { + if strings.ToLower(request.Header.Get(contentType)) != applicationJSON { + apiError(b, writer, invalidContentType, http.StatusBadRequest) + return false + } + + defer func() { + _ = request.Body.Close() + }() + body, err := io.ReadAll(request.Body) + if err != nil { + apiError(b, writer, failedToReadBody, http.StatusInternalServerError) + return false + } + + err = json.Unmarshal(body, target) + if err != nil { + apiError(b, writer, err.Error(), http.StatusInternalServerError) + return false + } + + return true +} + +type apiErrorResponse struct { + Error string `json:"error"` +} + +func apiError(b *Blueprint, writer http.ResponseWriter, error string, status int) { + if status >= 500 && !strings.Contains(error, "context canceled") { + b.logger(LogLevelError, fmt.Sprintf("failed to serve request with status %d: %s", status, error)) + } + writer.Header().Set(accessControl, accessControlValue) + writer.Header().Set(contentType, applicationJSON) + writer.WriteHeader(status) + + response := apiErrorResponse{Error: error} + data, err := json.Marshal(response) + if err != nil { + b.logger(LogLevelError, fmt.Sprintf("could not marshal fail response: %v", err)) + return + } + + _, err = writer.Write(data) + if err != nil { + if !errors.Is(err, context.Canceled) { + b.logger(LogLevelError, fmt.Sprintf("could not write fail response: %v", err)) + } + } +} + +func apiSuccess(b *Blueprint, writer http.ResponseWriter, body any, status int) { + var data []byte + if body == nil { + data = []byte(`{}`) + } else { + var err error + data, err = json.Marshal(body) + if err != nil { + apiError( + b, + writer, + fmt.Sprintf("could not marshal response body: %s", err.Error()), + http.StatusInternalServerError, + ) + return + } + } + + writer.Header().Set(accessControl, accessControlValue) + writer.Header().Set(contentType, applicationJSON) + writer.WriteHeader(status) + _, err := writer.Write(data) + if err != nil { + b.logger(LogLevelError, fmt.Sprintf("could not write successful response: %v", err)) + } +} + +func apiRoute(router *mux.Router, method, path string, handler http.Handler) { + router.Methods(method).Path(path).Handler(handler) + router.Methods(http.MethodOptions).Path(path).HandlerFunc(optionsHandler) +} + +func optionsHandler(writer http.ResponseWriter, _ *http.Request) { + writer.Header().Set(accessControl, accessControlValue) + writer.Header().Set(accessControlAllowHeaders, accessControlAllowHeadersValue) + writer.Header().Set(accessControlAllowMethods, accessControlAllowMethodsValue) + writer.WriteHeader(http.StatusOK) +} + +const indexFilePath = "index.html" + +type webHandler struct { + fileServer http.Handler +} + +func newWebHandler() *webHandler { + return &webHandler{ + fileServer: http.FileServer(AssetFile()), + } +} + +func (h webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + _, err := Asset(path) + if err != nil { + data, _ := Asset(indexFilePath) + http.ServeContent(w, r, indexFilePath, time.Time{}, bytes.NewReader(data)) + } else { + h.fileServer.ServeHTTP(w, r) + } +} diff --git a/blueprint.go b/blueprint.go new file mode 100644 index 0000000..8013289 --- /dev/null +++ b/blueprint.go @@ -0,0 +1,291 @@ +package fengshui + +import ( + "context" + "fmt" + "golang.org/x/sync/errgroup" + "strings" +) + +type Blueprint struct { + id string + components [][]Component + componentsByID map[string]Component + outputManager *outputManager + logger Logger +} + +func NewBlueprint(id string, components [][]Component, options ...Option) (*Blueprint, error) { + if id == "" { + id = "generic_blueprint" + } + id = strings.ReplaceAll(id, " ", "_") + + om := newOutputManager() + componentsByID := make(map[string]Component) + for _, concurrentComponents := range components { + for _, component := range concurrentComponents { + componentID := component.ID() + if componentID == "" { + return nil, ErrInvalidComponentID{msg: "component id may not be empty"} + } + if strings.Contains(componentID, "|") || strings.Contains(componentID, " ") { + return nil, ErrInvalidComponentID{id: componentID, msg: "component id may not contain '|' or ' '"} + } + + _, exists := componentsByID[componentID] + if exists { + return nil, ErrInvalidComponentID{id: componentID, msg: "duplicate component id"} + } + + err := component.SetOutputWriter(context.Background(), om.writer(component.ID())) + if err != nil { + return nil, err + } + + componentsByID[componentID] = component + } + } + + b := &Blueprint{id: id, components: components, componentsByID: componentsByID, outputManager: om} + for _, option := range options { + option(b) + } + if b.logger == nil { + b.logger = func(LogLevel, string) {} + } + + return b, nil +} + +func (b *Blueprint) Components() []Component { + result := make([]Component, 0, len(b.componentsByID)) + for _, component := range b.componentsByID { + result = append(result, component) + } + return result +} + +func (b *Blueprint) Apply(ctx context.Context, enabledComponentIDs []string) error { + b.logger(LogLevelInfo, "applying state") + enabledComponents := make(map[string]struct{}, len(enabledComponentIDs)) + for _, id := range enabledComponentIDs { + enabledComponents[id] = struct{}{} + } + return b.apply(ctx, enabledComponents) +} + +func (b *Blueprint) StartAll(ctx context.Context) error { + b.logger(LogLevelInfo, "starting all") + all := make(map[string]struct{}, len(b.componentsByID)) + for id := range b.componentsByID { + all[id] = struct{}{} + } + return b.apply(ctx, all) +} + +func (b *Blueprint) StopAll(ctx context.Context) error { + b.logger(LogLevelInfo, "stopping all") + for i := len(b.components) - 1; i >= 0; i-- { + concurrentComponents := b.components[i] + g, ctx := errgroup.WithContext(ctx) + for _, component := range concurrentComponents { + component := component + g.Go(func() error { + b.logger(LogLevelInfo, fmt.Sprintf("stopping %s", component.ID())) + err := component.Stop(ctx) + if err != nil { + return fmt.Errorf("could not stop %s: %w", component.ID(), err) + } + + return nil + }) + } + err := g.Wait() + if err != nil { + return err + } + } + + return nil +} + +func (b *Blueprint) StartComponent(ctx context.Context, componentID string) error { + component, err := b.componentByID(componentID) + if err != nil { + return err + } + + status, err := component.Status(ctx) + if err != nil { + return err + } + + if status == ComponentStatusRunning { + return nil + } + + b.logger(LogLevelInfo, fmt.Sprintf("preparing %s", componentID)) + err = component.Prepare(ctx) + if err != nil { + return err + } + + b.logger(LogLevelInfo, fmt.Sprintf("starting %s", componentID)) + return component.Start(ctx) +} + +func (b *Blueprint) StopComponent(ctx context.Context, componentID string) error { + component, err := b.componentByID(componentID) + if err != nil { + return err + } + + b.logger(LogLevelInfo, fmt.Sprintf("stopping %s", componentID)) + return component.Stop(ctx) +} + +func (b *Blueprint) Status(ctx context.Context) (GetStatusResponse, error) { + result := GetStatusResponse{ID: b.id, Components: make([][]GetStatusResponseComponent, len(b.components))} + for i, concurrentComponents := range b.components { + components := make([]GetStatusResponseComponent, len(concurrentComponents)) + for j, component := range concurrentComponents { + status, err := component.Status(ctx) + if err != nil { + return GetStatusResponse{}, fmt.Errorf("could not get status for %s: %w", component.ID(), err) + } + components[j] = GetStatusResponseComponent{ + ID: component.ID(), + Type: component.Type(), + Status: status, + Info: component.Config(), + EnvVars: component.EnvVars(), + } + } + result.Components[i] = components + } + return result, nil +} + +func (b *Blueprint) Output() *Reader { + return b.outputManager.reader() +} + +func (b *Blueprint) Cleanup(ctx context.Context) error { + b.logger(LogLevelInfo, "cleaning up") + g, ctx := errgroup.WithContext(ctx) + for _, concurrentComponents := range b.components { + for _, component := range concurrentComponents { + component := component + g.Go(func() error { + b.logger(LogLevelInfo, fmt.Sprintf("cleaning up %s", component.ID())) + err := component.Cleanup(ctx) + if err != nil { + return fmt.Errorf("could not cleanup %s: %w", component.ID(), err) + } + + return nil + }) + } + } + return g.Wait() +} + +func (b *Blueprint) apply(ctx context.Context, enabledComponentIDs map[string]struct{}) error { + err := b.prepare(ctx, enabledComponentIDs) + if err != nil { + return err + } + + for _, concurrentComponents := range b.components { + g, ctx := errgroup.WithContext(ctx) + for _, component := range concurrentComponents { + component := component + _, ok := enabledComponentIDs[component.ID()] + if ok { + g.Go(func() error { + status, err := component.Status(ctx) + if err != nil { + return fmt.Errorf("could not get status for %s: %w", component.ID(), err) + } + + if status != ComponentStatusStopped { + return nil + } + + b.logger(LogLevelInfo, fmt.Sprintf("starting %s", component.ID())) + err = component.Start(ctx) + if err != nil { + return fmt.Errorf("could not start %s: %w", component.ID(), err) + } + + return nil + }) + } else { + g.Go(func() error { + b.logger(LogLevelInfo, fmt.Sprintf("stopping %s", component.ID())) + err := component.Stop(ctx) + if err != nil { + return fmt.Errorf("could not stop %s: %w", component.ID(), err) + } + + return nil + }) + } + } + err := g.Wait() + if err != nil { + return err + } + } + return nil +} + +func (b *Blueprint) prepare(ctx context.Context, enabledComponentIDs map[string]struct{}) error { + g, ctx := errgroup.WithContext(ctx) + for _, concurrentComponents := range b.components { + for _, component := range concurrentComponents { + _, ok := enabledComponentIDs[component.ID()] + if !ok { + continue + } + component := component + g.Go(func() error { + status, err := component.Status(ctx) + if err != nil { + return fmt.Errorf("could not get status for %s: %w", component.ID(), err) + } + + if status == ComponentStatusRunning { + return nil + } + + b.logger(LogLevelInfo, fmt.Sprintf("preparing up %s", component.ID())) + err = component.Prepare(ctx) + if err != nil { + return fmt.Errorf("could not prepare %s: %w", component.ID(), err) + } + + return nil + }) + } + } + return g.Wait() +} + +func (b *Blueprint) componentByID(componentID string) (Component, error) { + component := b.componentsByID[componentID] + if component == nil { + return nil, ErrInvalidComponentID{id: componentID, msg: "not found"} + } + return component, nil +} + +type ErrInvalidComponentID struct { + id string + msg string +} + +func (e ErrInvalidComponentID) Error() string { + return fmt.Sprintf("component id '%s' is invalid: %s", e.id, e.msg) +} diff --git a/build-ui.sh b/build-ui.sh new file mode 100755 index 0000000..f86d428 --- /dev/null +++ b/build-ui.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +if which go-bindata >/dev/null; then + echo "building..." +else + echo "cannot find go-bindata. to install it, run 'go install github.com/go-bindata/go-bindata/v3/go-bindata@v3.1.3'" + exit 1 +fi + +npm --prefix ui run build + +go-bindata -o static_files.go -pkg fengshui -prefix ui/build -fs ui/build/... diff --git a/component.go b/component.go new file mode 100644 index 0000000..69ac42e --- /dev/null +++ b/component.go @@ -0,0 +1,26 @@ +package fengshui + +import ( + "context" +) + +type Component interface { + ID() string + Type() string + SetOutputWriter(ctx context.Context, writer *Writer) error + Prepare(ctx context.Context) error + Start(ctx context.Context) error + Stop(ctx context.Context) error + Cleanup(ctx context.Context) error + Status(ctx context.Context) (ComponentStatus, error) + Config() any + EnvVars() map[string]string +} + +type ComponentStatus string + +const ( + ComponentStatusStopped ComponentStatus = "stopped" + ComponentStatusRunning ComponentStatus = "running" + ComponentStatusFinished ComponentStatus = "finished" +) diff --git a/docker/component.go b/docker/component.go new file mode 100644 index 0000000..9c111b4 --- /dev/null +++ b/docker/component.go @@ -0,0 +1,367 @@ +package docker + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "github.com/docker/docker/errdefs" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/stdcopy" + "github.com/perimeterx/fengshui" + "math" + "strings" + "sync" + "time" +) + +const ComponentType = "docker component" + +type Component struct { + Host string + Writer *fengshui.Writer + + lock sync.Mutex + blueprintID string + cli *client.Client + config Config + networkMode NetworkMode + runConfig *runConfig + latestLogMessage time.Time +} + +func NewComponent( + cli *client.Client, + blueprintID string, + networkMode NetworkMode, + config Config, +) (*Component, error) { + host, err := validateNetworkMode(networkMode, config) + if err != nil { + return nil, err + } + + runConf, err := config.validate(networkMode, blueprintID) + if err != nil { + return nil, err + } + + return &Component{ + cli: cli, + config: config, + blueprintID: blueprintID, + networkMode: networkMode, + runConfig: runConf, + Host: host, + }, nil +} + +func (c *Component) ID() string { + return c.config.Name +} + +func (c *Component) Type() string { + return ComponentType +} + +func (c *Component) SetOutputWriter(ctx context.Context, writer *fengshui.Writer) error { + c.Writer = writer + + cont, err := c.findContainer(ctx) + if err != nil { + return err + } + if cont == nil { + return nil + } + + err = c.followLogs(cont.ID) + if err != nil { + return err + } + + return nil +} + +func (c *Component) Prepare(ctx context.Context) error { + c.lock.Lock() + defer c.lock.Unlock() + + err := createNetwork(ctx, c) + if err != nil { + return err + } + + if c.config.ImagePullOptions != nil && c.config.ImagePullOptions.Disabled { + c.Writer.WriteString(fmt.Sprintf("image pull disabled")) + return nil + } + + opts, err := c.config.imagePullOptions() + if err != nil { + return err + } + + reader, err := c.cli.ImagePull(ctx, c.config.Image, opts) + if err != nil { + return err + } + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + bytes := scanner.Bytes() + msg := jsonmessage.JSONMessage{} + err = json.Unmarshal(bytes, &msg) + if err != nil { + return fmt.Errorf("failed to parse image pull output: %w", err) + } + + if msg.Progress == nil || msg.Progress.Total == 0 { + if msg.ID == "" { + c.Writer.WriteString(msg.Status) + } else { + c.Writer.WriteString(fmt.Sprintf( + "%s %s", + c.Writer.Color.Cyan(msg.ID), + msg.Status, + )) + } + } else { + c.Writer.WriteString(fmt.Sprintf( + "%s %s %d%%", + c.Writer.Color.Cyan(msg.ID), + msg.Status, + int(math.Ceil(float64(msg.Progress.Current)/float64(msg.Progress.Total)*100)), + )) + } + } + + return reader.Close() +} + +func (c *Component) Start(ctx context.Context) error { + c.lock.Lock() + defer c.lock.Unlock() + + var id string + res, err := c.cli.ContainerCreate( + ctx, + c.runConfig.containerConfig, + c.runConfig.hostConfig, + c.runConfig.networkingConfig, + c.runConfig.platformConfig, + c.ContainerName(), + ) + if err == nil { + id = res.ID + } else if !errdefs.IsConflict(err) { + return err + } else { + cont, err := c.findContainer(ctx) + if err != nil { + return err + } + + id = cont.ID + } + + err = c.cli.ContainerStart(ctx, id, types.ContainerStartOptions{}) + if err != nil { + return err + } + + err = c.followLogs(id) + if err != nil { + return err + } + + for _, waiter := range c.runConfig.waiters { + err = waiter(c.cli, id) + if err != nil { + return err + } + } + + return nil +} + +func (c *Component) Stop(ctx context.Context) error { + c.lock.Lock() + defer c.lock.Unlock() + + cont, err := c.findContainer(ctx) + if err != nil { + return err + } + + if cont == nil { + return nil + } + + return c.cli.ContainerRemove(ctx, cont.ID, types.ContainerRemoveOptions{Force: true}) +} + +func (c *Component) Cleanup(ctx context.Context) error { + c.lock.Lock() + defer c.lock.Unlock() + + if c.config.ImagePullOptions != nil && c.config.ImagePullOptions.Disabled { + c.Writer.WriteString(fmt.Sprintf("image remove disabled")) + return nil + } + + _, err := c.cli.ImageRemove(ctx, c.config.Image, types.ImageRemoveOptions{}) + if errdefs.IsNotFound(err) { + return nil + } + + err = deleteNetwork(ctx, c) + if err != nil { + return err + } + return err +} + +func (c *Component) Status(ctx context.Context) (fengshui.ComponentStatus, error) { + cont, err := c.findContainer(ctx) + if err != nil { + return "", err + } + + if cont != nil && cont.State == "running" { + return fengshui.ComponentStatusRunning, nil + } + + return fengshui.ComponentStatusStopped, nil +} + +func (c *Component) Config() any { + return c.config +} + +func (c *Component) EnvVars() map[string]string { + return c.config.Env +} + +func (c *Component) Exec(ctx context.Context, cmd []string) (int, error) { + cont, err := c.findContainer(ctx) + if err != nil { + return 0, err + } + + c.Writer.WriteString(c.Writer.Color.Cyan(fmt.Sprintf("executing: %s", strings.Join(cmd, " ")))) + response, err := c.cli.ContainerExecCreate(ctx, cont.ID, types.ExecConfig{ + Cmd: cmd, + Detach: false, + AttachStdout: true, + AttachStderr: true, + }) + if err != nil { + return 0, err + } + + hijack, err := c.cli.ContainerExecAttach(ctx, response.ID, types.ExecStartCheck{}) + if err != nil { + return 0, err + } + + scanner := bufio.NewScanner(hijack.Reader) + for scanner.Scan() { + c.Writer.WriteString(c.Writer.Color.Cyan(fmt.Sprintf("exec output: %s", scanner.Text()))) + } + + hijack.Close() + + execResp, err := c.cli.ContainerExecInspect(ctx, response.ID) + if err != nil { + return 0, err + } + + c.Writer.WriteString(c.Writer.Color.Cyan(fmt.Sprintf("exit code: %d", execResp.ExitCode))) + return execResp.ExitCode, nil +} + +func (c *Component) ContainerName() string { + return fmt.Sprintf("%s_%s", c.blueprintID, c.config.Name) +} + +func (c *Component) findContainer(ctx context.Context) (*types.Container, error) { + name := c.ContainerName() + containers, err := c.cli.ContainerList(ctx, types.ContainerListOptions{ + All: true, + Filters: filters.NewArgs(filters.Arg("name", name)), + }) + if err != nil { + return nil, err + } + + for _, co := range containers { + if len(co.Names) > 0 && co.Names[0][1:] == name { + return &co, nil + } + } + + return nil, nil +} + +func (c *Component) followLogs(id string) error { + containerReader, err := c.cli.ContainerLogs(context.Background(), id, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Timestamps: true, + Follow: true, + }) + if err != nil { + return err + } + + go func() { + scanner := bufio.NewScanner(containerReader) + for scanner.Scan() { + bytes := scanner.Bytes() + var text string + var t time.Time + if len(bytes) > 8 { + t, text = extractMessageTime(string(bytes[8:])) + stream := stdcopy.StdType(bytes[0]) + if stream == stdcopy.Stderr { + // stderr + text = c.Writer.Color.Red(text) + } else if stream == stdcopy.Systemerr { + // docker system error, restart consume logs + _ = containerReader.Close() + err = c.followLogs(id) + if err != nil { + msg := fmt.Sprintf("failed to consume container logs: %v", err) + c.Writer.WriteString(c.Writer.Color.Red(msg)) + } + return + } + } else { + t, text = extractMessageTime(string(bytes)) + } + if t.Before(c.latestLogMessage) { + continue + } + c.latestLogMessage = t + c.Writer.WriteStringWithTime(t, text) + } + + _ = containerReader.Close() + }() + return nil +} + +func extractMessageTime(message string) (time.Time, string) { + pos := strings.Index(message, " ") + if pos > -1 { + t, err := time.Parse(time.RFC3339Nano, message[:pos]) + if err == nil { + return t, message[pos+1:] + } + } + return time.Now(), message +} diff --git a/docker/config.go b/docker/config.go new file mode 100644 index 0000000..5efff21 --- /dev/null +++ b/docker/config.go @@ -0,0 +1,926 @@ +package docker + +import ( + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/blkiodev" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/strslice" + "github.com/docker/go-units" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "gopkg.in/yaml.v3" + "os" + "time" +) + +// Config - Docker Component configuration +type Config struct { + // Name - the name of the container, and the ID of the blueprint component + // Name cannot be empty + Name string `json:"name" yaml:"name"` + + // Env - environment variables for the container + Env map[string]string `json:"env,omitempty" yaml:"env,omitempty"` + + // Ports - list of ports to expose + // we don't map internal ports to a different external ports since it won't be consistent + // for open network situations used in local development powered by docker network mode "host" + Ports []Port `json:"ports,omitempty" yaml:"ports,omitempty"` + + // Waiters - list of waiters. A waiter is a function responsible for waiting for healthy status + // of the container before finishing the container start process + Waiters []Waiter `json:"waiters,omitempty" yaml:"waiters,omitempty"` + + // ImagePullOptions - options for pulling the container image + ImagePullOptions *ImagePullOptions `json:"image_pull_options,omitempty" yaml:"image_pull_options,omitempty"` + + // Hostname - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L71 + Hostname string `json:"hostname,omitempty" yaml:"hostname,omitempty"` + + // Domainname - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L72 + Domainname string `json:"domainname,omitempty" yaml:"domainname,omitempty"` + + // User - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L73 + User string `json:"user,omitempty" yaml:"user,omitempty"` + + // AttachStdin - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L74 + AttachStdin bool `json:"attach_stdin,omitempty" yaml:"attach_stdin,omitempty"` + + // AttachStdout - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L75 + AttachStdout bool `json:"attach_stdout,omitempty" yaml:"attach_stdout,omitempty"` + + // AttachStderr - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L76 + AttachStderr bool `json:"attach_stderr,omitempty" yaml:"attach_stderr,omitempty"` + + // Tty - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L78 + Tty bool `json:"tty,omitempty" yaml:"tty,omitempty"` + + // OpenStdin - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L79 + OpenStdin bool `json:"open_stdin,omitempty" yaml:"open_stdin,omitempty"` + + // StdinOnce - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L80 + StdinOnce bool `json:"stdin_once,omitempty" yaml:"stdin_once,omitempty"` + + // Cmd - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L82 + Cmd StrSlice `json:"cmd,omitempty" yaml:"cmd,omitempty"` + + // Healthcheck - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L83 + Healthcheck *Healthcheck `json:"healthcheck,omitempty" yaml:"healthcheck,omitempty"` + + // ArgsEscaped - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L84 + ArgsEscaped bool `json:"args_escaped,omitempty" yaml:"args_escaped,omitempty"` + + // Image - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L85 + // Image cannot be empty + Image string `json:"image,omitempty" yaml:"image,omitempty"` + + // Volumes - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L86 + Volumes map[string]struct{} `json:"volumes,omitempty" yaml:"volumes,omitempty"` + + // WorkingDir - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L87 + WorkingDir string `json:"working_dir,omitempty" yaml:"working_dir,omitempty"` + + // Entrypoint - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L88 + Entrypoint StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"` + + // NetworkDisabled - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L89 + NetworkDisabled bool `json:"network_disabled,omitempty" yaml:"network_disabled,omitempty"` + + // MacAddress - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L90 + MacAddress string `json:"mac_address,omitempty" yaml:"mac_address,omitempty"` + + // OnBuild - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L91 + OnBuild []string `json:"on_build,omitempty" yaml:"on_build,omitempty"` + + // Labels - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L92 + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + + // StopSignal - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L93 + StopSignal string `json:"stop_signal,omitempty" yaml:"stop_signal,omitempty"` + + // StopTimeout - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L94 + StopTimeout *int `json:"stop_timeout,omitempty" yaml:"stop_timeout,omitempty"` + + // Shell - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L95 + Shell StrSlice `json:"shell,omitempty" yaml:"shell,omitempty"` + + // Binds - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L381 + Binds []string `json:"binds,omitempty" yaml:"binds,omitempty"` + + // ContainerIDFile - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L382 + ContainerIDFile string `json:"container_id_file,omitempty" yaml:"container_id_file,omitempty"` + + // LogConfig - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L383 + LogConfig *LogConfig `json:"log_config,omitempty" yaml:"log_config,omitempty"` + + // RestartPolicy - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L386 + RestartPolicy *RestartPolicy `json:"restart_policy,omitempty" yaml:"restart_policy,omitempty"` + + // VolumeDriver - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L388 + VolumeDriver string `json:"volume_driver,omitempty" yaml:"volume_driver,omitempty"` + + // VolumesFrom - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L389 + VolumesFrom []string `json:"volumes_from,omitempty" yaml:"volumes_from,omitempty"` + + // ConsoleSize - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L390 + ConsoleSize []uint `json:"console_size,omitempty" yaml:"console_size,omitempty"` + + // Annotations - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L391 + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + + // CapAdd - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L394 + CapAdd StrSlice `json:"cap_add,omitempty" yaml:"cap_add,omitempty"` + + // CapDrop - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L395 + CapDrop StrSlice `json:"cap_drop,omitempty" yaml:"cap_drop,omitempty"` + + // CgroupnsMode - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L396 + CgroupnsMode container.CgroupnsMode `json:"cgroupns_mode,omitempty" yaml:"cgroupns_mode,omitempty"` + + // DNS - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L397 + DNS []string `json:"dns,omitempty" yaml:"dns,omitempty"` + + // DNSOptions - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L398 + DNSOptions []string `json:"dns_options,omitempty" yaml:"dns_options,omitempty"` + + // DNSSearch - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L399 + DNSSearch []string `json:"dns_search,omitempty" yaml:"dns_search,omitempty"` + + // ExtraHosts - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L400 + ExtraHosts []string `json:"extra_hosts,omitempty" yaml:"extra_hosts,omitempty"` + + // GroupAdd - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L401 + GroupAdd []string `json:"group_add,omitempty" yaml:"group_add,omitempty"` + + // IpcMode - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L402 + IpcMode container.IpcMode `json:"ipc_mode,omitempty" yaml:"ipc_mode,omitempty"` + + // Cgroup - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L403 + Cgroup container.CgroupSpec `json:"cgroup,omitempty" yaml:"cgroup,omitempty"` + + // Links - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L404 + Links []string `json:"links,omitempty" yaml:"links,omitempty"` + + // OomScoreAdj - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L405 + OomScoreAdj int `json:"oom_score_adj,omitempty" yaml:"oom_score_adj,omitempty"` + + // PidMode - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L406 + PidMode container.PidMode `json:"pid_mode,omitempty" yaml:"pid_mode,omitempty"` + + // Privileged - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L407 + Privileged bool `json:"privileged,omitempty" yaml:"privileged,omitempty"` + + // PublishAllPorts - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L408 + PublishAllPorts bool `json:"publish_all_ports,omitempty" yaml:"publish_all_ports,omitempty"` + + // ReadonlyRootfs - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L409 + ReadonlyRootfs bool `json:"readonly_rootfs,omitempty" yaml:"readonly_rootfs,omitempty"` + + // SecurityOpt - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L410 + SecurityOpt []string `json:"security_opt,omitempty" yaml:"security_opt,omitempty"` + + // StorageOpt - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L411 + StorageOpt map[string]string `json:"storage_opt,omitempty" yaml:"storage_opt,omitempty"` + + // Tmpfs - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L412 + Tmpfs map[string]string `json:"tmpfs,omitempty" yaml:"tmpfs,omitempty"` + + // UTSMode - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L413 + UTSMode container.UTSMode `json:"uts_mode,omitempty" yaml:"uts_mode,omitempty"` + + // UsernsMode - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L414 + UsernsMode container.UsernsMode `json:"userns_mode,omitempty" yaml:"userns_mode,omitempty"` + + // ShmSize - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L415 + ShmSize int64 `json:"shm_size,omitempty" yaml:"shm_size,omitempty"` + + // Sysctls - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L416 + Sysctls map[string]string `json:"sysctls,omitempty" yaml:"sysctls,omitempty"` + + // Runtime - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L417 + Runtime string `json:"runtime,omitempty" yaml:"runtime,omitempty"` + + // Isolation - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L420 + Isolation container.Isolation `json:"isolation,omitempty" yaml:"isolation,omitempty"` + + // Resources - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L423 + Resources *Resources `json:"resources,omitempty" yaml:"resources,omitempty"` + + // Mounts - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L426 + Mounts []Mount `json:"mounts,omitempty" yaml:"mounts,omitempty"` + + // MaskedPaths - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L429 + MaskedPaths []string `json:"masked_paths,omitempty" yaml:"masked_paths,omitempty"` + + // ReadonlyPaths - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L432 + ReadonlyPaths []string `json:"readonly_paths,omitempty" yaml:"readonly_paths,omitempty"` + + // Init - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L435 + Init *bool `json:"init,omitempty" yaml:"init,omitempty"` + + // PlatformConfig describes the platform which the image in the manifest runs on + PlatformConfig *PlatformConfig `json:"platform_config,omitempty" yaml:"platform_config,omitempty"` +} + +type Port struct { + // Port to expose + Port string `json:"port,omitempty" yaml:"port,omitempty"` + + // Protocol to use - "tcp"/"udp". can be left empty, the default value in this case will be "tcp" + Protocol string `json:"protocol,omitempty" yaml:"protocol,omitempty"` +} + +type WaiterType string + +const ( + // WaiterTypeString - waits for a log line to contain a string value + WaiterTypeString WaiterType = "string" + + // WaiterTypeRegex - waits for a log line to match a regex + WaiterTypeRegex WaiterType = "regex" + + // WaiterTypeDuration - waits for a certain amount of time + WaiterTypeDuration WaiterType = "duration" +) + +type Waiter struct { + // Type - WaiterType type to use + Type WaiterType `json:"type,omitempty" yaml:"type,omitempty"` + + // String - only for Type == "string" + // the string value to wait for + String string `json:"string,omitempty" yaml:"string,omitempty"` + + // Regex - only for Type == "regex" + // the regex value to wait for + // compiled to a go regexp.Regexp using re2 syntax + Regex string `json:"regex,omitempty" yaml:"regex,omitempty"` + + // Duration - only for Type == "duration" + // the duration to wait + // parsed as a go duration using time.ParseDuration + Duration string `json:"duration,omitempty" yaml:"duration,omitempty"` +} + +type ImagePullOptions struct { + // Disabled allow disabling image pull/remove + // this is useful when the image already exists on the machine and we want it to remain after cleanup + Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"` + + // All - used for https://github.com/moby/moby/blob/v24.0.6/api/types/client.go#L279 + All bool `json:"all,omitempty" yaml:"all,omitempty"` + + // RegistryAuth - used for https://github.com/moby/moby/blob/v24.0.6/api/types/client.go#L280 + // only available when RegistryAuthFunc is not set + RegistryAuth string `json:"registry_auth,omitempty" yaml:"registry_auth,omitempty"` + + // PrivilegeFunc - used for https://github.com/moby/moby/blob/v24.0.6/api/types/client.go#L281 + // available only via code, not available in config files + PrivilegeFunc types.RequestPrivilegeFunc `json:"-" yaml:"-"` + + // Platform - used for https://github.com/moby/moby/blob/v24.0.6/api/types/client.go#L282 + Platform string `json:"platform,omitempty" yaml:"platform,omitempty"` + + // a lazy load function for the RegistryAuth + // available only via code, not available in config files + // used when loading the auth file take a long time, and you want to avoid loading it when it's not needed + RegistryAuthFunc func() (string, error) `json:"-" yaml:"-"` +} + +type Healthcheck struct { + // Test - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L44 + Test []string `json:"test,omitempty" yaml:"test,omitempty"` + + // Interval - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L47 + Interval time.Duration `json:"interval,omitempty" yaml:"interval,omitempty"` + + // Timeout - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L48 + Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` + + // StartPeriod - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L49 + StartPeriod time.Duration `json:"start_period,omitempty" yaml:"start_period,omitempty"` + + // Retries - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/config.go#L53 + Retries int `json:"retries,omitempty" yaml:"retries,omitempty"` +} + +type LogConfig struct { + // Type - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L321 + Type string `json:"type,omitempty" yaml:"type,omitempty"` + + // Config - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L322 + Config map[string]string `json:"config,omitempty" yaml:"config,omitempty"` +} + +type RestartPolicy struct { + // Name - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L274 + Name string + + // MaximumRetryCount - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L275 + MaximumRetryCount int +} + +type Resources struct { + // CPUShares - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L328 + CPUShares int64 `json:"cpu_shares,omitempty" yaml:"cpu_shares,omitempty"` + + // Memory - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L329 + Memory int64 `json:"memory,omitempty" yaml:"memory,omitempty"` + + // NanoCPUs - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L330 + NanoCPUs int64 `json:"nano_cp_us,omitempty" yaml:"nano_cp_us,omitempty"` + + // CgroupParent - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L333 + CgroupParent string `json:"cgroup_parent,omitempty" yaml:"cgroup_parent,omitempty"` + + // BlkioWeight - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L334 + BlkioWeight uint16 `json:"blkio_weight,omitempty" yaml:"blkio_weight,omitempty"` + + // BlkioWeightDevice - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L335 + BlkioWeightDevice []WeightDevice `json:"blkio_weight_device,omitempty" yaml:"blkio_weight_device,omitempty"` + + // BlkioDeviceReadBps - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L336 + BlkioDeviceReadBps []ThrottleDevice `json:"blkio_device_read_bps,omitempty" yaml:"blkio_device_read_bps,omitempty"` + + // BlkioDeviceWriteBps - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L337 + BlkioDeviceWriteBps []ThrottleDevice `json:"blkio_device_write_bps,omitempty" yaml:"blkio_device_write_bps,omitempty"` + + // BlkioDeviceReadIOps - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L338 + BlkioDeviceReadIOps []ThrottleDevice `json:"blkio_device_read_i_ops,omitempty" yaml:"blkio_device_read_i_ops,omitempty"` + + // BlkioDeviceWriteIOps - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L339 + BlkioDeviceWriteIOps []ThrottleDevice `json:"blkio_device_write_i_ops,omitempty" yaml:"blkio_device_write_i_ops,omitempty"` + + // CPUPeriod - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L340 + CPUPeriod int64 `json:"cpu_period,omitempty" yaml:"cpu_period,omitempty"` + + // CPUQuota - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L341 + CPUQuota int64 `json:"cpu_quota,omitempty" yaml:"cpu_quota,omitempty"` + + // CPURealtimePeriod - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L342 + CPURealtimePeriod int64 `json:"cpu_realtime_period,omitempty" yaml:"cpu_realtime_period,omitempty"` + + // CPURealtimeRuntime - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L343 + CPURealtimeRuntime int64 `json:"cpu_realtime_runtime,omitempty" yaml:"cpu_realtime_runtime,omitempty"` + + // CpusetCpus - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L344 + CpusetCpus string `json:"cpuset_cpus,omitempty" yaml:"cpuset_cpus,omitempty"` + + // CpusetMems - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L345 + CpusetMems string `json:"cpuset_mems,omitempty" yaml:"cpuset_mems,omitempty"` + + // Devices - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L346 + Devices []DeviceMapping `json:"devices,omitempty" yaml:"devices,omitempty"` + + // DeviceCgroupRules - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L347 + DeviceCgroupRules []string `json:"device_cgroup_rules,omitempty" yaml:"device_cgroup_rules,omitempty"` + + // DeviceRequests - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L348 + DeviceRequests []DeviceRequest `json:"device_requests,omitempty" yaml:"device_requests,omitempty"` + + // KernelMemoryTCP - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L353 + KernelMemoryTCP int64 `json:"kernel_memory_tcp,omitempty" yaml:"kernel_memory_tcp,omitempty"` + + // MemoryReservation - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L354 + MemoryReservation int64 `json:"memory_reservation,omitempty" yaml:"memory_reservation,omitempty"` + + // MemorySwap - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L355 + MemorySwap int64 `json:"memory_swap,omitempty" yaml:"memory_swap,omitempty"` + + // MemorySwappiness - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L356 + MemorySwappiness *int64 `json:"memory_swappiness,omitempty" yaml:"memory_swappiness,omitempty"` + + // OomKillDisable - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L357 + OomKillDisable *bool `json:"oom_kill_disable,omitempty" yaml:"oom_kill_disable,omitempty"` + + // PidsLimit - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L358 + PidsLimit *int64 `json:"pids_limit,omitempty" yaml:"pids_limit,omitempty"` + + // Ulimits - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L359 + Ulimits []Ulimit `json:"ulimits,omitempty" yaml:"ulimits,omitempty"` + + // CPUCount - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L362 + CPUCount int64 `json:"cpu_count,omitempty" yaml:"cpu_count,omitempty"` + + // CPUPercent - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L363 + CPUPercent int64 `json:"cpu_percent,omitempty" yaml:"cpu_percent,omitempty"` + + // IOMaximumIOps - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L364 + IOMaximumIOps uint64 `json:"io_maximum_i_ops,omitempty" yaml:"io_maximum_i_ops,omitempty"` + + // IOMaximumBandwidth - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L365 + IOMaximumBandwidth uint64 `json:"io_maximum_bandwidth,omitempty" yaml:"io_maximum_bandwidth,omitempty"` +} + +type WeightDevice struct { + // Path - used for https://github.com/moby/moby/blob/v24.0.6/api/types/blkiodev/blkio.go#L7 + Path string `json:"path,omitempty" yaml:"path,omitempty"` + + // Weight - used for https://github.com/moby/moby/blob/v24.0.6/api/types/blkiodev/blkio.go#L8 + Weight uint16 `json:"weight,omitempty" yaml:"weight,omitempty"` +} + +type ThrottleDevice struct { + // Path - used for https://github.com/moby/moby/blob/v24.0.6/api/types/blkiodev/blkio.go#L17 + Path string `json:"path,omitempty" yaml:"path,omitempty"` + + // Rate - used for https://github.com/moby/moby/blob/v24.0.6/api/types/blkiodev/blkio.go#L18 + Rate uint64 `json:"rate,omitempty" yaml:"rate,omitempty"` +} + +type DeviceMapping struct { + // PathOnHost - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L267 + PathOnHost string `json:"path_on_host,omitempty" yaml:"path_on_host,omitempty"` + + // PathInContainer - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L268 + PathInContainer string `json:"path_in_container,omitempty" yaml:"path_in_container,omitempty"` + + // CgroupPermissions - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L269 + CgroupPermissions string `json:"cgroup_permissions,omitempty" yaml:"cgroup_permissions,omitempty"` +} + +type DeviceRequest struct { + // Driver - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L258 + Driver string `json:"driver,omitempty" yaml:"driver,omitempty"` + + // Count - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L259 + Count int `json:"count,omitempty" yaml:"count,omitempty"` + + // DeviceIDs - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L260 + DeviceIDs []string `json:"device_ids,omitempty" yaml:"device_ids,omitempty"` + + // Capabilities - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L261 + Capabilities [][]string `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` + + // Options - used for https://github.com/moby/moby/blob/v24.0.6/api/types/container/hostconfig.go#L262 + Options map[string]string `json:"options,omitempty" yaml:"options,omitempty"` +} + +type Ulimit struct { + // Name - used for https://github.com/docker/go-units/blob/master/ulimit.go#L11 + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Hard - used for https://github.com/docker/go-units/blob/master/ulimit.go#L12 + Hard int64 `json:"hard,omitempty" yaml:"hard,omitempty"` + + // Soft - used for https://github.com/docker/go-units/blob/master/ulimit.go#L13 + Soft int64 `json:"soft,omitempty" yaml:"soft,omitempty"` +} + +type Mount struct { + // Type - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L26 + Type mount.Type `json:"type,omitempty" yaml:"type,omitempty"` + + // Source - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L30 + Source string `json:"source,omitempty" yaml:"source,omitempty"` + + // Target - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L31 + Target string `json:"target,omitempty" yaml:"target,omitempty"` + + // ReadOnly - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L32 + ReadOnly bool `json:"read_only,omitempty" yaml:"read_only,omitempty"` + + // Consistency - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L33 + Consistency mount.Consistency `json:"consistency,omitempty" yaml:"consistency,omitempty"` + + // BindOptions - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L35 + BindOptions *BindOptions `json:"bind_options,omitempty" yaml:"bind_options,omitempty"` + + // VolumeOptions - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L36 + VolumeOptions *VolumeOptions `json:"volume_options,omitempty" yaml:"volume_options,omitempty"` + + // TmpfsOptions - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L37 + TmpfsOptions *TmpfsOptions `json:"tmpfs_options,omitempty" yaml:"tmpfs_options,omitempty"` +} + +type BindOptions struct { + // Propagation - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L85 + Propagation mount.Propagation `json:"propagation,omitempty" yaml:"propagation,omitempty"` + + // NonRecursive - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L86 + NonRecursive bool `json:"non_recursive,omitempty" yaml:"non_recursive,omitempty"` + + // CreateMountpoint - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L87 + CreateMountpoint bool `json:"create_mountpoint,omitempty" yaml:"create_mountpoint,omitempty"` +} + +type VolumeOptions struct { + // NoCopy - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L92 + NoCopy bool `json:"no_copy,omitempty" yaml:"no_copy,omitempty"` + + // Labels - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L93 + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + + // DriverConfig - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L94 + DriverConfig *Driver `json:"driver_config,omitempty" yaml:"driver_config,omitempty"` +} + +type Driver struct { + // Name - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L99 + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Options - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L100 + Options map[string]string `json:"options,omitempty" yaml:"options,omitempty"` +} + +type TmpfsOptions struct { + // SizeBytes - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L113 + SizeBytes int64 `json:"size_bytes,omitempty" yaml:"size_bytes,omitempty"` + + // Mode - used for https://github.com/moby/moby/blob/v24.0.6/api/types/mount/mount.go#L115 + Mode os.FileMode `json:"mode,omitempty" yaml:"mode,omitempty"` +} + +type PlatformConfig struct { + // Architecture - used for https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/specs-go/v1/descriptor.go#L56 + Architecture string `json:"architecture,omitempty" yaml:"architecture,omitempty"` + + // OS - used for https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/specs-go/v1/descriptor.go#L56 + OS string `json:"os,omitempty" yaml:"os,omitempty"` + + // OSVersion - used for https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/specs-go/v1/descriptor.go#L56 + OSVersion string `json:"os_version,omitempty" yaml:"os_version,omitempty"` + + // OSFeatures - used for https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/specs-go/v1/descriptor.go#L56 + OSFeatures []string `json:"os_features,omitempty" yaml:"os_features,omitempty"` + + // Variant - used for https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/specs-go/v1/descriptor.go#L56 + Variant string `json:"variant,omitempty" yaml:"variant,omitempty"` +} + +type StrSlice strslice.StrSlice + +func (s *StrSlice) UnmarshalYAML(value *yaml.Node) error { + var p []string + err := value.Decode(&p) + if err != nil { + var str string + err = value.Decode(&str) + if err != nil { + return err + } + + p = append(p, str) + } + + *s = p + return nil +} + +type runConfig struct { + containerConfig *container.Config + hostConfig *container.HostConfig + networkingConfig *network.NetworkingConfig + platformConfig *ocispec.Platform + networkCreate types.NetworkCreate + waiters []waiterFunc +} + +func (c Config) validate(networkMode NetworkMode, blueprintID string) (*runConfig, error) { + if c.Name == "" { + return nil, ErrInvalidConfig{Property: "name", Msg: "cannot be empty"} + } + + if c.Image == "" { + return nil, ErrInvalidConfig{Property: "image", Msg: "cannot be empty"} + } + + if l := len(c.ConsoleSize); l != 0 && l != 2 { + return nil, ErrInvalidConfig{Property: "console_size", Msg: "must have exactly two elements"} + } + + waiters := make([]waiterFunc, len(c.Waiters)) + for i, waiter := range c.Waiters { + f, err := validateWaiter(waiter) + if err != nil { + return nil, err + } + + waiters[i] = f + } + + result := &runConfig{ + containerConfig: c.containerConfig(), + hostConfig: c.hostConfig(), + platformConfig: c.PlatformConfig.build(), + waiters: waiters, + } + configureNetwork(networkMode, c, blueprintID, result) + + return result, nil +} + +func (c Config) imagePullOptions() (types.ImagePullOptions, error) { + result := types.ImagePullOptions{} + + if c.ImagePullOptions != nil { + var auth string + if c.ImagePullOptions.RegistryAuthFunc != nil { + var err error + auth, err = c.ImagePullOptions.RegistryAuthFunc() + if err != nil { + return types.ImagePullOptions{}, err + } + } else { + auth = c.ImagePullOptions.RegistryAuth + } + + result.All = c.ImagePullOptions.All + result.RegistryAuth = auth + result.PrivilegeFunc = c.ImagePullOptions.PrivilegeFunc + result.Platform = c.ImagePullOptions.Platform + } + + return result, nil +} + +func (c Config) containerConfig() *container.Config { + env := make([]string, 0, len(c.Env)) + for key, value := range c.Env { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + + return &container.Config{ + Hostname: c.Hostname, + Domainname: c.Domainname, + User: c.User, + AttachStdin: c.AttachStdin, + AttachStdout: c.AttachStdout, + AttachStderr: c.AttachStderr, + ExposedPorts: nil, + Tty: c.Tty, + OpenStdin: c.OpenStdin, + StdinOnce: c.StdinOnce, + Env: env, + Cmd: strslice.StrSlice(c.Cmd), + Healthcheck: c.Healthcheck.build(), + ArgsEscaped: c.ArgsEscaped, + Image: c.Image, + Volumes: c.Volumes, + WorkingDir: c.WorkingDir, + Entrypoint: strslice.StrSlice(c.Entrypoint), + NetworkDisabled: c.NetworkDisabled, + MacAddress: c.MacAddress, + OnBuild: c.OnBuild, + Labels: c.Labels, + StopSignal: c.StopSignal, + StopTimeout: c.StopTimeout, + Shell: strslice.StrSlice(c.Shell), + } +} + +func (c *Healthcheck) build() *container.HealthConfig { + if c == nil { + return nil + } + + return &container.HealthConfig{ + Test: c.Test, + Interval: c.Interval, + Timeout: c.Timeout, + StartPeriod: c.StartPeriod, + Retries: c.Retries, + } +} + +func (c Config) hostConfig() *container.HostConfig { + var consoleSize [2]uint + if len(c.ConsoleSize) > 0 { + consoleSize = [2]uint{c.ConsoleSize[0], c.ConsoleSize[1]} + } + return &container.HostConfig{ + Binds: c.Binds, + ContainerIDFile: c.ContainerIDFile, + LogConfig: c.LogConfig.build(), + RestartPolicy: c.RestartPolicy.build(), + AutoRemove: true, + VolumeDriver: c.VolumeDriver, + VolumesFrom: c.VolumesFrom, + ConsoleSize: consoleSize, + Annotations: c.Annotations, + CapAdd: strslice.StrSlice(c.CapAdd), + CapDrop: strslice.StrSlice(c.CapDrop), + CgroupnsMode: c.CgroupnsMode, + DNS: c.DNS, + DNSOptions: c.DNSOptions, + DNSSearch: c.DNSSearch, + ExtraHosts: c.ExtraHosts, + GroupAdd: c.GroupAdd, + IpcMode: c.IpcMode, + Cgroup: c.Cgroup, + Links: c.Links, + OomScoreAdj: c.OomScoreAdj, + PidMode: c.PidMode, + Privileged: c.Privileged, + PublishAllPorts: c.PublishAllPorts, + ReadonlyRootfs: c.ReadonlyRootfs, + SecurityOpt: c.SecurityOpt, + StorageOpt: c.StorageOpt, + Tmpfs: c.Tmpfs, + UTSMode: c.UTSMode, + UsernsMode: c.UsernsMode, + ShmSize: c.ShmSize, + Sysctls: c.Sysctls, + Runtime: c.Runtime, + Isolation: c.Isolation, + Resources: c.Resources.build(), + Mounts: mapSlice(c.Mounts, Mount.build), + MaskedPaths: c.MaskedPaths, + ReadonlyPaths: c.ReadonlyPaths, + Init: c.Init, + } +} + +func (c *LogConfig) build() container.LogConfig { + if c == nil { + return container.LogConfig{} + } + + return container.LogConfig{ + Type: c.Type, + Config: c.Config, + } +} + +func (c *RestartPolicy) build() container.RestartPolicy { + if c == nil { + return container.RestartPolicy{} + } + + return container.RestartPolicy{ + Name: c.Name, + MaximumRetryCount: c.MaximumRetryCount, + } +} + +func (c *Resources) build() container.Resources { + if c == nil { + return container.Resources{} + } + + return container.Resources{ + CPUShares: c.CPUShares, + Memory: c.Memory, + NanoCPUs: c.NanoCPUs, + CgroupParent: c.CgroupParent, + BlkioWeight: c.BlkioWeight, + BlkioWeightDevice: mapSlice(c.BlkioWeightDevice, WeightDevice.build), + BlkioDeviceReadBps: mapSlice(c.BlkioDeviceReadBps, ThrottleDevice.build), + BlkioDeviceWriteBps: mapSlice(c.BlkioDeviceWriteBps, ThrottleDevice.build), + BlkioDeviceReadIOps: mapSlice(c.BlkioDeviceReadIOps, ThrottleDevice.build), + BlkioDeviceWriteIOps: mapSlice(c.BlkioDeviceWriteIOps, ThrottleDevice.build), + CPUPeriod: c.CPUPeriod, + CPUQuota: c.CPUQuota, + CPURealtimePeriod: c.CPURealtimePeriod, + CPURealtimeRuntime: c.CPURealtimeRuntime, + CpusetCpus: c.CpusetCpus, + CpusetMems: c.CpusetMems, + Devices: mapSlice(c.Devices, DeviceMapping.build), + DeviceCgroupRules: c.DeviceCgroupRules, + DeviceRequests: mapSlice(c.DeviceRequests, DeviceRequest.build), + KernelMemoryTCP: c.KernelMemoryTCP, + MemoryReservation: c.MemoryReservation, + MemorySwap: c.MemorySwap, + MemorySwappiness: c.MemorySwappiness, + OomKillDisable: c.OomKillDisable, + PidsLimit: c.PidsLimit, + Ulimits: mapSlice(c.Ulimits, Ulimit.build), + CPUCount: c.CPUCount, + CPUPercent: c.CPUPercent, + IOMaximumIOps: c.IOMaximumIOps, + IOMaximumBandwidth: c.IOMaximumBandwidth, + } +} + +func (c WeightDevice) build() *blkiodev.WeightDevice { + return &blkiodev.WeightDevice{ + Path: c.Path, + Weight: c.Weight, + } +} + +func (c ThrottleDevice) build() *blkiodev.ThrottleDevice { + return &blkiodev.ThrottleDevice{ + Path: c.Path, + Rate: c.Rate, + } +} + +func (c DeviceMapping) build() container.DeviceMapping { + return container.DeviceMapping{ + PathOnHost: c.PathOnHost, + PathInContainer: c.PathInContainer, + CgroupPermissions: c.CgroupPermissions, + } +} + +func (c DeviceRequest) build() container.DeviceRequest { + return container.DeviceRequest{ + Driver: c.Driver, + Count: c.Count, + DeviceIDs: c.DeviceIDs, + Capabilities: c.Capabilities, + Options: c.Options, + } +} + +func (c Ulimit) build() *units.Ulimit { + return &units.Ulimit{ + Name: c.Name, + Hard: c.Hard, + Soft: c.Soft, + } +} + +func (c Mount) build() mount.Mount { + return mount.Mount{ + Type: c.Type, + Source: c.Source, + Target: c.Target, + ReadOnly: c.ReadOnly, + Consistency: c.Consistency, + BindOptions: c.BindOptions.build(), + VolumeOptions: c.VolumeOptions.build(), + TmpfsOptions: c.TmpfsOptions.build(), + } +} + +func (c *BindOptions) build() *mount.BindOptions { + if c == nil { + return nil + } + + return &mount.BindOptions{ + Propagation: c.Propagation, + NonRecursive: c.NonRecursive, + CreateMountpoint: c.CreateMountpoint, + } +} + +func (c *VolumeOptions) build() *mount.VolumeOptions { + if c == nil { + return nil + } + + return &mount.VolumeOptions{ + NoCopy: c.NoCopy, + Labels: c.Labels, + DriverConfig: c.DriverConfig.build(), + } +} + +func (c *Driver) build() *mount.Driver { + if c == nil { + return nil + } + + return &mount.Driver{ + Name: c.Name, + Options: c.Options, + } +} + +func (c *TmpfsOptions) build() *mount.TmpfsOptions { + if c == nil { + return nil + } + + return &mount.TmpfsOptions{ + SizeBytes: c.SizeBytes, + Mode: c.Mode, + } +} + +func (c *PlatformConfig) build() *ocispec.Platform { + if c == nil { + return nil + } + + return &ocispec.Platform{ + Architecture: c.Architecture, + OS: c.OS, + OSVersion: c.OSVersion, + OSFeatures: c.OSFeatures, + Variant: c.Variant, + } +} + +func mapSlice[T1, T2 any](slice []T1, mapper func(T1) T2) []T2 { + result := make([]T2, len(slice)) + for i, m := range slice { + result[i] = mapper(m) + } + + return result +} + +type ErrInvalidConfig struct { + Property string + Msg string +} + +func (e ErrInvalidConfig) Error() string { + return fmt.Sprintf("invalid docker config - property %s: %s", e.Property, e.Msg) +} diff --git a/docker/network.go b/docker/network.go new file mode 100644 index 0000000..0c932cf --- /dev/null +++ b/docker/network.go @@ -0,0 +1,134 @@ +package docker + +import ( + "context" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "github.com/docker/go-connections/nat" + "runtime" + "strings" + "sync" +) + +type NetworkMode string + +const ( + NetworkModeClosed NetworkMode = "closed" + NetworkModeOpen NetworkMode = "open" +) + +func ParseNetworkMode(value string) (NetworkMode, error) { + switch strings.ToLower(value) { + case "closed": + return NetworkModeClosed, nil + case "open", "": + return NetworkModeOpen, nil + } + return "", ErrInvalidNetworkMode{v: value} +} + +func validateNetworkMode(networkMode NetworkMode, config Config) (host string, err error) { + if networkMode == NetworkModeClosed { + return config.Name, nil + } + if networkMode == NetworkModeOpen { + if runtime.GOOS == "linux" { + return "localhost", nil + } + return "host.docker.internal", nil + } + return "", ErrInvalidNetworkMode{v: string(networkMode)} +} + +func configureNetwork(networkMode NetworkMode, config Config, blueprintID string, runConfig *runConfig) { + if networkMode == NetworkModeClosed { + configureClosedNetwork(blueprintID, runConfig) + } else if networkMode == NetworkModeOpen { + if runtime.GOOS == "linux" { + configureOpenLinuxNetwork(blueprintID, runConfig) + } else { + configureOpenNetwork(config, blueprintID, runConfig) + } + } +} + +func configureClosedNetwork(blueprintID string, runConfig *runConfig) { + runConfig.networkCreate = types.NetworkCreate{ + CheckDuplicate: true, + Driver: "bridge", + } + runConfig.hostConfig.NetworkMode = "bridge" + runConfig.networkingConfig = &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{blueprintID: {NetworkID: blueprintID}}, + } +} + +func configureOpenLinuxNetwork(blueprintID string, runConfig *runConfig) { + runConfig.networkCreate = types.NetworkCreate{ + CheckDuplicate: true, + Driver: "host", + } + runConfig.hostConfig.NetworkMode = "host" + runConfig.networkingConfig = &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{blueprintID: {NetworkID: blueprintID}}, + } +} + +func configureOpenNetwork(config Config, blueprintID string, runConfig *runConfig) { + runConfig.networkCreate = types.NetworkCreate{ + CheckDuplicate: true, + Driver: "bridge", + } + runConfig.hostConfig.NetworkMode = "bridge" + runConfig.networkingConfig = &network.NetworkingConfig{ + EndpointsConfig: map[string]*network.EndpointSettings{blueprintID: {NetworkID: blueprintID}}, + } + runConfig.containerConfig.ExposedPorts = nat.PortSet{} + runConfig.hostConfig.PortBindings = nat.PortMap{} + for _, port := range config.Ports { + protocol := port.Protocol + if protocol == "" { + protocol = "tcp" + } + p := nat.Port(fmt.Sprintf("%s/%s", port.Port, protocol)) + runConfig.containerConfig.ExposedPorts[p] = struct{}{} + runConfig.hostConfig.PortBindings[p] = append(runConfig.hostConfig.PortBindings[p], nat.PortBinding{ + HostPort: port.Port, + }) + } +} + +var networkLock sync.Mutex + +func createNetwork(ctx context.Context, c *Component) error { + networkLock.Lock() + defer networkLock.Unlock() + _, err := c.cli.NetworkCreate(ctx, c.blueprintID, c.runConfig.networkCreate) + if err != nil && !strings.Contains(err.Error(), "already exists") { + return err + } + + return nil +} + +func deleteNetwork(ctx context.Context, c *Component) error { + networkLock.Lock() + defer networkLock.Unlock() + err := c.cli.NetworkRemove(ctx, c.blueprintID) + if err != nil && + !strings.Contains(err.Error(), "has active endpoints") && + !strings.Contains(err.Error(), "not found") { + return err + } + + return nil +} + +type ErrInvalidNetworkMode struct { + v string +} + +func (e ErrInvalidNetworkMode) Error() string { + return fmt.Sprintf("invalid network mode %s", e.v) +} diff --git a/docker/waiter.go b/docker/waiter.go new file mode 100644 index 0000000..d3036eb --- /dev/null +++ b/docker/waiter.go @@ -0,0 +1,119 @@ +package docker + +import ( + "bufio" + "context" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "regexp" + "strings" + "time" +) + +func WaitForLog(s string) Waiter { + return Waiter{ + Type: WaiterTypeString, + String: s, + } +} + +func WaitForLogRegex(regexp string) Waiter { + return Waiter{ + Type: WaiterTypeRegex, + Regex: regexp, + } +} + +func WaitForDuration(duration string) Waiter { + return Waiter{ + Type: WaiterTypeDuration, + Duration: duration, + } +} + +type waiterFunc func(cli *client.Client, containerID string) error + +func validateWaiter(w Waiter) (waiterFunc, error) { + switch w.Type { + case WaiterTypeString: + return func(cli *client.Client, containerID string) error { + containerReader, err := cli.ContainerLogs(context.Background(), containerID, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Timestamps: true, + Follow: true, + }) + if err != nil { + return err + } + + scanner := bufio.NewScanner(containerReader) + for scanner.Scan() { + if strings.Contains(scanner.Text(), w.String) { + return nil + } + } + + _ = containerReader.Close() + + return ErrContainerStopped{without: fmt.Sprintf("reaching log '%s'", w.String)} + }, nil + case WaiterTypeRegex: + re, err := regexp.Compile(w.Regex) + if err != nil { + return nil, err + } + + return func(cli *client.Client, containerID string) error { + containerReader, err := cli.ContainerLogs(context.Background(), containerID, types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Timestamps: true, + Follow: true, + }) + if err != nil { + return err + } + + scanner := bufio.NewScanner(containerReader) + for scanner.Scan() { + if re.MatchString(scanner.Text()) { + return nil + } + } + + _ = containerReader.Close() + + return ErrContainerStopped{without: fmt.Sprintf("reaching log regex '%s'", w.String)} + }, nil + case WaiterTypeDuration: + d, err := time.ParseDuration(w.Duration) + if err != nil { + return nil, err + } + + return func(cli *client.Client, containerID string) error { + time.Sleep(d) + return nil + }, nil + } + + return nil, ErrInvalidWaiterType{Type: w.Type} +} + +type ErrInvalidWaiterType struct { + Type WaiterType +} + +func (e ErrInvalidWaiterType) Error() string { + return fmt.Sprintf("invalid waiter type %s", e.Type) +} + +type ErrContainerStopped struct { + without string +} + +func (e ErrContainerStopped) Error() string { + return fmt.Sprintf("container stopped without %s", e.without) +} diff --git a/exec.go b/exec.go new file mode 100644 index 0000000..2241a19 --- /dev/null +++ b/exec.go @@ -0,0 +1,75 @@ +package fengshui + +import ( + "context" + "fmt" + "os/exec" + "runtime" + "time" +) + +type ExecutionMode string + +const ( + ExecutionModeStart ExecutionMode = "start" + ExecutionModeStop ExecutionMode = "stop" + ExecutionModeDaemon ExecutionMode = "daemon" +) + +func ParseExecutionMode(value string) (ExecutionMode, error) { + switch value { + case "start": + return ExecutionModeStart, nil + case "stop": + return ExecutionModeStop, nil + case "daemon", "": + return ExecutionModeDaemon, nil + } + return "", ErrInvalidExecutionMode{v: value} +} + +func Execute(server *Server, executionMode ExecutionMode) error { + switch executionMode { + case ExecutionModeStart: + return server.blueprint.StartAll(context.Background()) + case ExecutionModeStop: + err := server.blueprint.StopAll(context.Background()) + if err != nil { + return err + } + + return server.blueprint.Cleanup(context.Background()) + case ExecutionModeDaemon: + go func() { + time.Sleep(time.Second * 2) + err := openBrowser("http://localhost" + server.addr) + if err != nil { + server.errHandler(fmt.Sprintf("could not open browser window: %s", err.Error())) + } + }() + + return server.Start() + } + return ErrInvalidExecutionMode{v: string(executionMode)} +} + +func openBrowser(url string) error { + switch runtime.GOOS { + case "linux": + return exec.Command("xdg-open", url).Start() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + return exec.Command("open", url).Start() + default: + return fmt.Errorf("unsupported platform") + } +} + +type ErrInvalidExecutionMode struct { + v string +} + +func (e ErrInvalidExecutionMode) Error() string { + return fmt.Sprintf("invalid execution mode %s", e.v) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1cfbdd8 --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module github.com/perimeterx/fengshui + +go 1.20 + +require ( + github.com/docker/docker v24.0.6+incompatible + github.com/docker/go-connections v0.4.0 + github.com/docker/go-units v0.5.0 + github.com/gorilla/mux v1.8.0 + github.com/opencontainers/image-spec v1.0.2 + go.mongodb.org/mongo-driver v1.12.1 + golang.org/x/sync v0.3.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.6.0 // indirect + gotest.tools/v3 v3.5.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ed62afb --- /dev/null +++ b/go.sum @@ -0,0 +1,123 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= +github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/io.go b/io.go new file mode 100644 index 0000000..6fb1b90 --- /dev/null +++ b/io.go @@ -0,0 +1,138 @@ +package fengshui + +import ( + "fmt" + "strings" + "sync" + "time" +) + +const ( + chanBufferSize = 100 + timeFormat = "2006-01-02T15:04:05.000000000Z07:00" +) + +type outputManager struct { + lock sync.Mutex + messages [][]byte + readers []*Reader +} + +func newOutputManager() *outputManager { + return &outputManager{} +} + +func (o *outputManager) write(t time.Time, component, message string) { + data := []byte(fmt.Sprintf("%s