-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add agent sync #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add agent sync #146
Changes from 1 commit
e21bad4
a4e6090
c7a0f73
afb1c2e
2c32bde
3e26e72
27853e2
6c1a0eb
69f7b2b
8a2c1b0
c50e224
a180044
3016ff0
614c48a
65c273a
6e27bea
4c994d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package docker | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "time" | ||
|
|
||
| composetypes "github.com/compose-spec/compose-go/v2/types" | ||
| "github.com/docker/compose/v5/pkg/api" | ||
| ) | ||
|
|
||
| const ( | ||
| composeFileName = "compose.yaml" | ||
| deployWaitTimeout = 2 * time.Minute | ||
| ) | ||
|
|
||
| type DeployRequest struct { | ||
| ApplicationID string | ||
| ApplicationName string | ||
| ComposeFile string | ||
| } | ||
|
|
||
| var loadProject = func(ctx context.Context, composeService api.Compose, options api.ProjectLoadOptions) (*composetypes.Project, error) { | ||
| return composeService.LoadProject(ctx, options) | ||
| } | ||
|
|
||
| var upProject = func(ctx context.Context, composeService api.Compose, project *composetypes.Project, options api.UpOptions) error { | ||
| return composeService.Up(ctx, project, options) | ||
| } | ||
|
|
||
| func (c *Client) Deploy(ctx context.Context, req DeployRequest) error { | ||
| if c.compose == nil { | ||
| return errors.New("docker compose service is not initialized") | ||
| } | ||
| if c.deploymentsDir == "" { | ||
| return errors.New("deployments directory is not configured") | ||
| } | ||
| if !c.Ready() { | ||
| return errors.New("docker daemon is not ready") | ||
| } | ||
|
|
||
| if req.ComposeFile == "" { | ||
| return errors.New("compose file is empty") | ||
| } | ||
|
|
||
| applicationDir := filepath.Join(c.deploymentsDir, req.ApplicationName) | ||
| composePath := filepath.Join(applicationDir, composeFileName) | ||
|
|
||
| if err := os.MkdirAll(applicationDir, 0o755); err != nil { | ||
|
Check failure on line 53 in backend/internal/agent/docker/deploy.go
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| return fmt.Errorf("create deployment directory: %w", err) | ||
|
maximbigler marked this conversation as resolved.
|
||
| } | ||
| if err := os.WriteFile(composePath, []byte(req.ComposeFile), 0o600); err != nil { | ||
Check failureCode scanning / CodeQL Uncontrolled data used in path expression High
This path depends on a
user-provided value Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| return fmt.Errorf("write compose file: %w", err) | ||
| } | ||
|
|
||
| project, err := loadProject(ctx, c.compose, api.ProjectLoadOptions{ | ||
| ProjectName: "orca-" + strings.ToLower(req.ApplicationName), | ||
|
maximbigler marked this conversation as resolved.
Outdated
|
||
| ConfigPaths: []string{composePath}, | ||
| WorkingDir: applicationDir, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("load compose project: %w", err) | ||
| } | ||
|
|
||
| if err := upProject(ctx, c.compose, project, api.UpOptions{ | ||
| Create: api.CreateOptions{ | ||
| RemoveOrphans: true, | ||
| Recreate: api.RecreateDiverged, | ||
| RecreateDependencies: api.RecreateDiverged, | ||
| }, | ||
| Start: api.StartOptions{ | ||
| Wait: true, | ||
| WaitTimeout: deployWaitTimeout, | ||
| }, | ||
| }); err != nil { | ||
| return fmt.Errorf("compose up: %w", err) | ||
| } | ||
|
|
||
| c.log.Info(). | ||
| Str("application_id", req.ApplicationID). | ||
| Str("application_name", req.ApplicationName). | ||
| Str("compose_path", composePath). | ||
| Msg("deployment completed") | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package docker | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| composetypes "github.com/compose-spec/compose-go/v2/types" | ||
| "github.com/docker/compose/v5/pkg/api" | ||
| ) | ||
|
|
||
| func TestDeploy_WritesComposeFileAndRunsComposeUp(t *testing.T) { | ||
| c := newTestClient(t) | ||
| c.deploymentsDir = t.TempDir() | ||
|
|
||
| originalLoadProject := loadProject | ||
| originalUpProject := upProject | ||
| t.Cleanup(func() { | ||
| loadProject = originalLoadProject | ||
| upProject = originalUpProject | ||
| }) | ||
|
|
||
| var gotLoadOptions api.ProjectLoadOptions | ||
| loadProject = func(_ context.Context, _ api.Compose, options api.ProjectLoadOptions) (*composetypes.Project, error) { | ||
| gotLoadOptions = options | ||
| return &composetypes.Project{Name: options.ProjectName}, nil | ||
| } | ||
|
|
||
| var gotUpOptions api.UpOptions | ||
| upProject = func(_ context.Context, _ api.Compose, project *composetypes.Project, options api.UpOptions) error { | ||
| if project.Name != "orca-billing" { | ||
| t.Fatalf("expected project name %q, got %q", "orca-billing", project.Name) | ||
| } | ||
| gotUpOptions = options | ||
| return nil | ||
| } | ||
|
|
||
| req := DeployRequest{ | ||
| ApplicationID: "app-123", | ||
| ApplicationName: "billing", | ||
| ComposeFile: "services:\n app:\n image: ghcr.io/orcacd/app:1.0.0\n", | ||
| } | ||
|
|
||
| if err := c.Deploy(t.Context(), req); err != nil { | ||
| t.Fatalf("Deploy: %v", err) | ||
| } | ||
|
|
||
| composePath := filepath.Join(c.deploymentsDir, req.ApplicationName, composeFileName) | ||
| content, err := os.ReadFile(composePath) | ||
| if err != nil { | ||
| t.Fatalf("ReadFile: %v", err) | ||
| } | ||
| if string(content) != req.ComposeFile { | ||
| t.Fatalf("expected compose file to be written to deployment volume") | ||
| } | ||
|
|
||
| if gotLoadOptions.ProjectName != "orca-billing" { | ||
| t.Fatalf("expected load project name %q, got %q", "orca-billing", gotLoadOptions.ProjectName) | ||
| } | ||
| if len(gotLoadOptions.ConfigPaths) != 1 || gotLoadOptions.ConfigPaths[0] != composePath { | ||
| t.Fatalf("unexpected config paths: %#v", gotLoadOptions.ConfigPaths) | ||
| } | ||
| if gotLoadOptions.WorkingDir != filepath.Join(c.deploymentsDir, req.ApplicationName) { | ||
| t.Fatalf("expected working dir %q, got %q", filepath.Join(c.deploymentsDir, req.ApplicationName), gotLoadOptions.WorkingDir) | ||
| } | ||
|
|
||
| if !gotUpOptions.Start.Wait { | ||
| t.Fatal("expected compose up to wait for services to become ready") | ||
| } | ||
| if gotUpOptions.Start.WaitTimeout != deployWaitTimeout { | ||
| t.Fatalf("expected wait timeout %s, got %s", deployWaitTimeout, gotUpOptions.Start.WaitTimeout) | ||
| } | ||
| if !gotUpOptions.Create.RemoveOrphans { | ||
| t.Fatal("expected compose up to remove orphaned containers") | ||
| } | ||
| if gotUpOptions.Create.Recreate != api.RecreateDiverged { | ||
| t.Fatalf("expected recreate strategy %q, got %q", api.RecreateDiverged, gotUpOptions.Create.Recreate) | ||
| } | ||
| } | ||
|
|
||
| func TestDeploy_RejectsUnsafeApplicationID(t *testing.T) { | ||
| c := newTestClient(t) | ||
| c.deploymentsDir = t.TempDir() | ||
|
|
||
| err := c.Deploy(t.Context(), DeployRequest{ | ||
| ApplicationID: "bad/id", | ||
| ComposeFile: "services: {}\n", | ||
| }) | ||
| if err == nil { | ||
| t.Fatal("expected deploy to reject unsafe application ids") | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.