-
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
Open
maximbigler
wants to merge
17
commits into
main
Choose a base branch
from
feature/add-agent-sync
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: Add agent sync #146
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e21bad4
feat: Add agent sync
maximbigler a4e6090
fix: Folder Permission
maximbigler c7a0f73
fix: Falls positive in deploy_test.go
maximbigler afb1c2e
fix: Check for unsafe application path
maximbigler 2c32bde
feat: Add managed by label
maximbigler 3e26e72
fix: Deploying button in applications details
maximbigler 27853e2
feat: Remove custom deployments dir
maximbigler 6c1a0eb
docs: Add todo for websocket send logic in agent
maximbigler 69f7b2b
feat: Move DeploymentInProgress into deployer
maximbigler 8a2c1b0
fix: Path injection in deploy
maximbigler c50e224
fix: Golangci-lint error
maximbigler a180044
feat: Change the application status sync
maximbigler 3016ff0
feat: Split websocket and deployment state
maximbigler 614c48a
fix: go backend tests
maximbigler 65c273a
Fix: hub.go formatting
maximbigler 6e27bea
Merge branch 'main' into feature/add-agent-sync
maximbigler 4c994d3
feat: Add test for application deploy
maximbigler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| package docker | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/OrcaCD/orca-cd/internal/shared/utils" | ||
| 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") | ||
| } | ||
|
|
||
| // Validate application name to prevent path traversal | ||
| if err := utils.DoesNotLookLikeFilePath(req.ApplicationName); err != nil { | ||
| return fmt.Errorf("invalid application name: %w", err) | ||
| } | ||
|
|
||
| applicationDir := filepath.Join(c.deploymentsDir, req.ApplicationName) | ||
| composePath := filepath.Join(applicationDir, composeFileName) | ||
|
|
||
| // Verify the final path stays within the deployments directory | ||
| if err := utils.IsPathWithinBase(c.deploymentsDir, composePath); err != nil { | ||
| return fmt.Errorf("invalid compose file path: %w", err) | ||
| } | ||
|
|
||
| if err := os.MkdirAll(applicationDir, 0o750); err != nil { | ||
|
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 { | ||
|
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: strings.ToLower(req.ApplicationName), | ||
| ConfigPaths: []string{composePath}, | ||
| WorkingDir: applicationDir, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("load compose project: %w", err) | ||
| } | ||
|
|
||
| // Add OrcaCD managed label to all services | ||
| for _, service := range project.Services { | ||
| if service.Labels == nil { | ||
| service.Labels = make(map[string]string) | ||
| } | ||
| service.Labels["managed_by"] = "orca-cd" | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| 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 != "billing" { | ||
| t.Fatalf("expected project name %q, got %q", "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) | ||
| //nolint:gosec // composePath is built from t.TempDir() and a fixed test application id | ||
| 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 != "billing" { | ||
| t.Fatalf("expected load project name %q, got %q", "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_RejectsUnsafeApplicationName(t *testing.T) { | ||
| c := newTestClient(t) | ||
| c.deploymentsDir = t.TempDir() | ||
|
|
||
| err := c.Deploy(t.Context(), DeployRequest{ | ||
| ApplicationID: "019e1ce8-7938-71b8-be55-4b184f307a2d", | ||
| ApplicationName: "../bad", | ||
| ComposeFile: "services: {}\n", | ||
| }) | ||
| if err == nil { | ||
| t.Fatal("expected deploy to reject unsafe application names") | ||
| } | ||
| err2 := c.Deploy(t.Context(), DeployRequest{ | ||
| ApplicationID: "019e1ce8-7938-71b8-be55-4b184f307a2d", | ||
| ApplicationName: "test/orcacd-docs", | ||
| ComposeFile: "services: {}\n", | ||
| }) | ||
| if err2 == nil { | ||
| t.Fatal("expected deploy to reject unsafe application names") | ||
| } | ||
| err3 := c.Deploy(t.Context(), DeployRequest{ | ||
| ApplicationID: "019e1ce8-7938-71b8-be55-4b184f307a2d", | ||
| ApplicationName: "bad/../name", | ||
| ComposeFile: "services: {}\n", | ||
| }) | ||
| if err3 == nil { | ||
| t.Fatal("expected deploy to reject unsafe application names") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.