From 85a39366d894bfac78891d1cae22267680f41188 Mon Sep 17 00:00:00 2001 From: Jamie White Date: Thu, 22 Jun 2023 15:14:42 +0200 Subject: [PATCH 1/2] Automatically register apps when project has server-stored waypoint.hcl Co-authored-by: Izaak Lauer <8404559+izaaklauer@users.noreply.github.com> --- .changelog/4819.txt | 3 + pkg/server/singleprocess/service_project.go | 67 ++++++++++++++++++- .../handlertest/test_service_project.go | 51 ++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 .changelog/4819.txt diff --git a/.changelog/4819.txt b/.changelog/4819.txt new file mode 100644 index 00000000000..93a35482b0a --- /dev/null +++ b/.changelog/4819.txt @@ -0,0 +1,3 @@ +```release-note:improvement +server: Perform project init without requiring a runner if waypoint.hcl is stored serverside +``` diff --git a/pkg/server/singleprocess/service_project.go b/pkg/server/singleprocess/service_project.go index 24ecb1f5fc6..06e06d46e5e 100644 --- a/pkg/server/singleprocess/service_project.go +++ b/pkg/server/singleprocess/service_project.go @@ -5,8 +5,11 @@ package singleprocess import ( "context" + "fmt" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" empty "google.golang.org/protobuf/types/known/emptypb" @@ -34,7 +37,13 @@ func (s *Service) UpsertProject( ) } - if projectNeedsRemoteInit(result) { + if hasUsableWaypointHCL(result) { + proj, err := s.serverSideProjectInit(ctx, result) + if err != nil { + return nil, err // already externalized + } + result = proj + } else if projectNeedsRemoteInit(result) { // The project is connected to a data source but doesn’t use // automatic polling, so let’s queue some remote init operations // to ensure the application list is populated. @@ -266,3 +275,59 @@ func projectNeedsRemoteInit(project *pb.Project) bool { return true } + +// hasUsableWaypointHCL verifies that a project has waypoint.hcl contents +// of type HCL +func hasUsableWaypointHCL(project *pb.Project) bool { + return len(project.WaypointHcl) > 0 && project.WaypointHclFormat == pb.Hcl_HCL +} + +// serverSideProjectInit initializes a project that directly contains a waypoint.hcl. +// "init" currently consists of getting the list of apps on a project, and upserting each one. +// If the waypoint.hcl is not on the project but is in VCS, you must enqueue an init job +// rather than attempting a serverside init. +// Returns externalized errors +func (s *Service) serverSideProjectInit(ctx context.Context, project *pb.Project) (*pb.Project, error) { + if !hasUsableWaypointHCL(project) { + return nil, fmt.Errorf("cannot init a project without a stored waypoint.hcl with hcl type contents serverside") + } + file, _ := hclsyntax.ParseConfig(project.WaypointHcl, "", hcl.Pos{}) + content, _ := file.Body.Content(&hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + {Type: "app", LabelNames: []string{"name"}}, + }, + }) + projRef := &pb.Ref_Project{Project: project.Name} + + for _, b := range content.Blocks.ByType()["app"] { + name := b.Labels[0] + _, err := s.UpsertApplication(ctx, &pb.UpsertApplicationRequest{ + Project: projRef, + Name: name, + }) + if err != nil { + return nil, hcerr.Externalize( + hclog.FromContext(ctx), + err, + "failed to register app %q while creating project %q", + name, project.GetName(), + ) + } + } + + // Reload the project to populate the newly-added apps + resp, err := s.GetProject(ctx, &pb.GetProjectRequest{ + Project: projRef, + }) + if err != nil { + return nil, hcerr.Externalize( + hclog.FromContext(ctx), + err, + "failed to reload project %q", + project.GetName(), + ) + } + project = resp.Project + + return project, nil +} diff --git a/pkg/serverhandler/handlertest/test_service_project.go b/pkg/serverhandler/handlertest/test_service_project.go index f648a1e434f..33ab338ef69 100644 --- a/pkg/serverhandler/handlertest/test_service_project.go +++ b/pkg/serverhandler/handlertest/test_service_project.go @@ -19,6 +19,7 @@ func init() { TestServiceProject_GetApplication, TestServiceProject_UpsertApplication, TestServiceProject_InvalidName, + TestServiceProject_AutoPopulateApps, } } @@ -275,3 +276,53 @@ func TestServiceProject_InvalidName(t *testing.T, factory Factory) { }) require.Error(err) } + +func TestServiceProject_AutoPopulateApps(t *testing.T, factory Factory) { + ctx := context.Background() + require := require.New(t) + client, _ := factory(t) + + project := ptypes.TestProject(t, &pb.Project{ + WaypointHcl: []byte(` + project = "test" + + variable "vartest" { + type = string + default = "" + } + + app "website" { + build { + use "docker" {} + } + deploy { + use "kubernetes" {} + } + release { + use "kubernetes" {} + } + } + + app "api" { + build { + use "docker" {} + } + deploy { + use "kubernetes" {} + } + release { + use "kubernetes" {} + } + } + `), + }) + + resp, err := client.UpsertProject(ctx, &pb.UpsertProjectRequest{ + Project: project, + }) + require.NoError(err) + require.NotNil(resp) + require.Len(resp.Project.Applications, 2) + require.Equal("website", resp.Project.Applications[0].Name) + require.Equal("api", resp.Project.Applications[1].Name) +} From 75faea0e23f03876ceca03c4a34c4b79f0e01696 Mon Sep 17 00:00:00 2001 From: Jamie White Date: Thu, 22 Jun 2023 23:19:34 +0200 Subject: [PATCH 2/2] Use state functions rather than public RPCs to update apps and project Co-authored-by: Brian Cain --- pkg/server/singleprocess/service_project.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/server/singleprocess/service_project.go b/pkg/server/singleprocess/service_project.go index 06e06d46e5e..0df5ce94917 100644 --- a/pkg/server/singleprocess/service_project.go +++ b/pkg/server/singleprocess/service_project.go @@ -301,7 +301,7 @@ func (s *Service) serverSideProjectInit(ctx context.Context, project *pb.Project for _, b := range content.Blocks.ByType()["app"] { name := b.Labels[0] - _, err := s.UpsertApplication(ctx, &pb.UpsertApplicationRequest{ + _, err := s.state(ctx).AppPut(ctx, &pb.Application{ Project: projRef, Name: name, }) @@ -316,9 +316,7 @@ func (s *Service) serverSideProjectInit(ctx context.Context, project *pb.Project } // Reload the project to populate the newly-added apps - resp, err := s.GetProject(ctx, &pb.GetProjectRequest{ - Project: projRef, - }) + result, err := s.state(ctx).ProjectGet(ctx, projRef) if err != nil { return nil, hcerr.Externalize( hclog.FromContext(ctx), @@ -327,7 +325,6 @@ func (s *Service) serverSideProjectInit(ctx context.Context, project *pb.Project project.GetName(), ) } - project = resp.Project - return project, nil + return result, nil }