generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: use buildengine for deploy cmd (#1002)
Not fully tested yet, but starting to flesh out the subscription flow for build -> deploy ```sh ftl deploy ../ftl-examples/online-boutique/backend/services examples/go info:time: Building module info:ad: Building module info:currency: Building module info:cart: Building module info:cart: Deploying module info:ad: Deploying module info:time: Deploying module info:currency: Deploying module info:shipping: Building module info:productcatalog: Building module info:payment: Building module info:echo: Building module info:shipping: Deploying module info:payment: Deploying module info:echo: Deploying module info:productcatalog: Deploying module info:recommendation: Building module info:checkout: Building module info:recommendation: Deploying module info:checkout: Deploying module ``` --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Loading branch information
1 parent
8dc0d59
commit c995fa4
Showing
9 changed files
with
286 additions
and
279 deletions.
There are no files selected for viewing
This file contains 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 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,229 @@ | ||
package buildengine | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"time" | ||
|
||
"connectrpc.com/connect" | ||
"golang.org/x/exp/maps" | ||
"google.golang.org/protobuf/proto" | ||
"google.golang.org/protobuf/types/known/timestamppb" | ||
|
||
ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" | ||
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" | ||
schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema" | ||
"github.com/TBD54566975/ftl/common/moduleconfig" | ||
"github.com/TBD54566975/ftl/internal/log" | ||
"github.com/TBD54566975/ftl/internal/sha256" | ||
"github.com/TBD54566975/ftl/internal/slices" | ||
) | ||
|
||
type deploymentArtefact struct { | ||
*ftlv1.DeploymentArtefact | ||
localPath string | ||
} | ||
|
||
// Deploy a module to the FTL controller with the given number of replicas. Optionally wait for the deployment to become ready. | ||
func Deploy(ctx context.Context, module Module, replicas int32, waitForDeployOnline bool, client ftlv1connect.ControllerServiceClient) error { | ||
logger := log.FromContext(ctx).Scope(module.Module) | ||
ctx = log.ContextWithLogger(ctx, logger) | ||
logger.Infof("Deploying module") | ||
|
||
deployDir := module.AbsDeployDir() | ||
files, err := findFiles(deployDir, module.Deploy) | ||
if err != nil { | ||
logger.Errorf(err, "failed to find files in %s", deployDir) | ||
return err | ||
} | ||
|
||
filesByHash, err := hashFiles(deployDir, files) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
gadResp, err := client.GetArtefactDiffs(ctx, connect.NewRequest(&ftlv1.GetArtefactDiffsRequest{ClientDigests: maps.Keys(filesByHash)})) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
moduleSchema, err := loadProtoSchema(deployDir, module.ModuleConfig, replicas) | ||
if err != nil { | ||
return fmt.Errorf("failed to load protobuf schema from %q: %w", module.ModuleConfig.Schema, err) | ||
} | ||
|
||
logger.Debugf("Uploading %d/%d files", len(gadResp.Msg.MissingDigests), len(files)) | ||
for _, missing := range gadResp.Msg.MissingDigests { | ||
file := filesByHash[missing] | ||
content, err := os.ReadFile(file.localPath) | ||
if err != nil { | ||
return err | ||
} | ||
logger.Tracef("Uploading %s", relToCWD(file.localPath)) | ||
resp, err := client.UploadArtefact(ctx, connect.NewRequest(&ftlv1.UploadArtefactRequest{ | ||
Content: content, | ||
})) | ||
if err != nil { | ||
return err | ||
} | ||
logger.Debugf("Uploaded %s as %s:%s", relToCWD(file.localPath), sha256.FromBytes(resp.Msg.Digest), file.Path) | ||
} | ||
|
||
resp, err := client.CreateDeployment(ctx, connect.NewRequest(&ftlv1.CreateDeploymentRequest{ | ||
Schema: moduleSchema, | ||
Artefacts: slices.Map(maps.Values(filesByHash), func(a deploymentArtefact) *ftlv1.DeploymentArtefact { | ||
return a.DeploymentArtefact | ||
}), | ||
})) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
_, err = client.ReplaceDeploy(ctx, connect.NewRequest(&ftlv1.ReplaceDeployRequest{DeploymentName: resp.Msg.GetDeploymentName(), MinReplicas: replicas})) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if waitForDeployOnline { | ||
logger.Debugf("Waiting for deployment %s to become ready", resp.Msg.DeploymentName) | ||
err = checkReadiness(ctx, client, resp.Msg.DeploymentName, replicas) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func loadProtoSchema(deployDir string, config moduleconfig.ModuleConfig, replicas int32) (*schemapb.Module, error) { | ||
schema := filepath.Join(deployDir, config.Schema) | ||
content, err := os.ReadFile(schema) | ||
if err != nil { | ||
return nil, err | ||
} | ||
module := &schemapb.Module{} | ||
err = proto.Unmarshal(content, module) | ||
if err != nil { | ||
return nil, err | ||
} | ||
module.Runtime = &schemapb.ModuleRuntime{ | ||
CreateTime: timestamppb.Now(), | ||
Language: config.Language, | ||
MinReplicas: replicas, | ||
} | ||
return module, nil | ||
} | ||
|
||
func findFiles(base string, files []string) ([]string, error) { | ||
var out []string | ||
for _, file := range files { | ||
file = filepath.Join(base, file) | ||
info, err := os.Stat(file) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if info.IsDir() { | ||
dirFiles, err := findFilesInDir(file) | ||
if err != nil { | ||
return nil, err | ||
} | ||
out = append(out, dirFiles...) | ||
} else { | ||
out = append(out, file) | ||
} | ||
} | ||
return out, nil | ||
} | ||
|
||
func findFilesInDir(dir string) ([]string, error) { | ||
var out []string | ||
return out, filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
if info.IsDir() { | ||
return nil | ||
} | ||
out = append(out, path) | ||
return nil | ||
}) | ||
} | ||
|
||
func hashFiles(base string, files []string) (filesByHash map[string]deploymentArtefact, err error) { | ||
filesByHash = map[string]deploymentArtefact{} | ||
for _, file := range files { | ||
r, err := os.Open(file) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer r.Close() //nolint:gosec | ||
hash, err := sha256.SumReader(r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
info, err := r.Stat() | ||
if err != nil { | ||
return nil, err | ||
} | ||
isExecutable := info.Mode()&0111 != 0 | ||
path, err := filepath.Rel(base, file) | ||
if err != nil { | ||
return nil, err | ||
} | ||
filesByHash[hash.String()] = deploymentArtefact{ | ||
DeploymentArtefact: &ftlv1.DeploymentArtefact{ | ||
Digest: hash.String(), | ||
Path: path, | ||
Executable: isExecutable, | ||
}, | ||
localPath: file, | ||
} | ||
} | ||
return filesByHash, nil | ||
} | ||
|
||
func relToCWD(path string) string { | ||
cwd, err := os.Getwd() | ||
if err != nil { | ||
panic(err) | ||
} | ||
rel, err := filepath.Rel(cwd, path) | ||
if err != nil { | ||
return path | ||
} | ||
return rel | ||
} | ||
|
||
func checkReadiness(ctx context.Context, client ftlv1connect.ControllerServiceClient, deploymentName string, replicas int32) error { | ||
ticker := time.NewTicker(time.Second) | ||
defer ticker.Stop() | ||
|
||
for { | ||
select { | ||
case <-ticker.C: | ||
status, err := client.Status(ctx, connect.NewRequest(&ftlv1.StatusRequest{ | ||
AllDeployments: true, | ||
})) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var found bool | ||
for _, deployment := range status.Msg.Deployments { | ||
if deployment.Key == deploymentName { | ||
found = true | ||
if deployment.Replicas >= replicas { | ||
return nil | ||
} | ||
} | ||
} | ||
if !found { | ||
return fmt.Errorf("deployment %s not found: %v", deploymentName, status.Msg.Deployments) | ||
} | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
} | ||
} | ||
} |
This file contains 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 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.