Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cmd/cli/command/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ func makeComposePsCmd() *cobra.Command {
return err
}

if err := cli.GetServices(cmd.Context(), projectName, provider, long); err != nil {
if err := cli.PrintServices(cmd.Context(), projectName, provider, long); err != nil {
if errNoServices := new(cli.ErrNoServices); !errors.As(err, errNoServices) {
return err
}
Expand Down
1 change: 0 additions & 1 deletion src/cmd/cli/command/globals.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ in the file conflict with existing shell environment variables. If conflicts are
found, it warns the user that the shell environment variable will take precedence.
*/
func checkEnvConflicts(stackName string) error {

path, err := filepath.Abs(filepath.Join(stacks.Directory, stackName))
if err != nil {
return err
Expand Down
1 change: 0 additions & 1 deletion src/cmd/cli/command/globals_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,6 @@ AWS_REGION="us-east-1"`,

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

prevTerm := term.DefaultTerm
var stdout, stderr bytes.Buffer
term.DefaultTerm = term.NewTerm(os.Stdin, &stdout, &stderr)
Expand Down
14 changes: 11 additions & 3 deletions src/pkg/agent/tools/default_tool_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/cli/compose"
"github.com/DefangLabs/defang/src/pkg/login"
"github.com/DefangLabs/defang/src/pkg/mcp/deployment_info"
"github.com/DefangLabs/defang/src/pkg/modes"
"github.com/DefangLabs/defang/src/pkg/term"
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
Expand Down Expand Up @@ -70,8 +69,17 @@ func (DefaultToolCLI) ConfigDelete(ctx context.Context, projectName string, prov
return cli.ConfigDelete(ctx, projectName, provider, name)
}

func (DefaultToolCLI) GetServices(ctx context.Context, projectName string, provider cliClient.Provider) ([]deployment_info.Service, error) {
return deployment_info.GetServices(ctx, projectName, provider)
func (DefaultToolCLI) GetServices(ctx context.Context, projectName string, provider cliClient.Provider) ([]*cli.Service, error) {
servicesResponse, err := cli.GetServices(ctx, projectName, provider)
if err != nil {
return nil, err
}

term.Debug("Checking service health...")
cli.UpdateServiceStates(ctx, servicesResponse.Services)

si, _, err := cli.GetServiceStatesAndEndpoints(servicesResponse.Services)
return si, err
}

func (DefaultToolCLI) PrintEstimate(mode modes.Mode, estimate *defangv1.EstimateResponse) string {
Expand Down
8 changes: 3 additions & 5 deletions src/pkg/agent/tools/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import (
"time"

"github.com/DefangLabs/defang/src/pkg/cli"
cliTypes "github.com/DefangLabs/defang/src/pkg/cli"
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/cli/compose"
"github.com/DefangLabs/defang/src/pkg/mcp/deployment_info"
"github.com/DefangLabs/defang/src/pkg/modes"
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
)
Expand All @@ -23,14 +21,14 @@ type CLIInterface interface {
Connect(ctx context.Context, cluster string) (*cliClient.GrpcClient, error)
CreatePlaygroundProvider(client *cliClient.GrpcClient) cliClient.Provider
GenerateAuthURL(authPort int) string
GetServices(ctx context.Context, projectName string, provider cliClient.Provider) ([]deployment_info.Service, error)
GetServices(ctx context.Context, projectName string, provider cliClient.Provider) ([]*cli.Service, error)
InteractiveLoginMCP(ctx context.Context, client *cliClient.GrpcClient, cluster string, mcpClient string) error
ListConfig(ctx context.Context, provider cliClient.Provider, projectName string) (*defangv1.Secrets, error)
LoadProject(ctx context.Context, loader cliClient.Loader) (*compose.Project, error)
LoadProjectNameWithFallback(ctx context.Context, loader cliClient.Loader, provider cliClient.Provider) (string, error)
NewProvider(ctx context.Context, providerId cliClient.ProviderID, client cliClient.FabricClient, stack string) cliClient.Provider
PrintEstimate(mode modes.Mode, estimate *defangv1.EstimateResponse) string
RunEstimate(ctx context.Context, project *compose.Project, client *cliClient.GrpcClient, provider cliClient.Provider, providerId cliClient.ProviderID, region string, mode modes.Mode) (*defangv1.EstimateResponse, error)
Tail(ctx context.Context, provider cliClient.Provider, projectName string, options cliTypes.TailOptions) error
TailAndMonitor(ctx context.Context, project *compose.Project, provider cliClient.Provider, waitTimeout time.Duration, options cliTypes.TailOptions) (cli.ServiceStates, error)
Tail(ctx context.Context, provider cliClient.Provider, projectName string, options cli.TailOptions) error
TailAndMonitor(ctx context.Context, project *compose.Project, provider cliClient.Provider, waitTimeout time.Duration, options cli.TailOptions) (cli.ServiceStates, error)
}
2 changes: 1 addition & 1 deletion src/pkg/agent/tools/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func HandleServicesTool(ctx context.Context, loader cliClient.ProjectLoader, cli
if err != nil {
var noServicesErr defangcli.ErrNoServices
if errors.As(err, &noServicesErr) {
return fmt.Sprintf("no services found for the specified project %q", projectName), nil
return noServicesErr.Error(), nil
}
if connect.CodeOf(err) == connect.CodeNotFound && strings.Contains(err.Error(), "is not deployed in Playground") {
return fmt.Sprintf("project %s is not deployed in Playground", projectName), nil
Expand Down
19 changes: 9 additions & 10 deletions src/pkg/agent/tools/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (
"testing"
"time"

"github.com/DefangLabs/defang/src/pkg/cli"
defangcli "github.com/DefangLabs/defang/src/pkg/cli"
"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/cli/compose"
"github.com/DefangLabs/defang/src/pkg/elicitations"
"github.com/DefangLabs/defang/src/pkg/mcp/deployment_info"
"github.com/DefangLabs/defang/src/pkg/modes"
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
"github.com/bufbuild/connect-go"
Expand All @@ -29,7 +29,7 @@ type MockCLI struct {
MockProjectName string

GetServicesError error
MockServices []deployment_info.Service
MockServices []*cli.Service
GetServicesCalled bool
GetServicesProject string
GetServicesProvider client.Provider
Expand All @@ -56,7 +56,7 @@ func (m *MockCLI) LoadProjectNameWithFallback(ctx context.Context, loader client
return "default-project", nil
}

func (m *MockCLI) GetServices(ctx context.Context, projectName string, provider client.Provider) ([]deployment_info.Service, error) {
func (m *MockCLI) GetServices(ctx context.Context, projectName string, provider client.Provider) ([]*cli.Service, error) {
m.GetServicesCalled = true
m.GetServicesProject = projectName
m.GetServicesProvider = provider
Expand Down Expand Up @@ -198,7 +198,7 @@ func TestHandleServicesToolWithMockCLI(t *testing.T) {
GetServicesError: defangcli.ErrNoServices{ProjectName: "test-project"},
},
expectedError: false, // Returns successful result with message
resultTextContains: "no services found for the specified project",
resultTextContains: "no services found in project",
expectedGetServices: true,
expectedProjectName: "test-project",
},
Expand Down Expand Up @@ -238,13 +238,12 @@ func TestHandleServicesToolWithMockCLI(t *testing.T) {
MockClient: &client.GrpcClient{},
MockProvider: &client.PlaygroundProvider{},
MockProjectName: "test-project",
MockServices: []deployment_info.Service{
MockServices: []*cli.Service{
{
Service: "test-service",
DeploymentId: "test-deployment",
PublicFqdn: "test.example.com",
PrivateFqdn: "test.internal",
Status: "running",
Service: "test-service",
Deployment: "test-deployment",
Fqdn: "test.example.com",
Status: "running",
},
},
},
Expand Down
4 changes: 1 addition & 3 deletions src/pkg/cli/client/byoc/aws/byoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,9 +491,7 @@ func (b *ByocAws) GetProjectUpdate(ctx context.Context, projectName string) (*de
}

s3Client := s3.NewFromConfig(cfg)
// Path to the state file, Defined at: https://github.com/DefangLabs/defang-mvp/blob/main/pulumi/cd/aws/byoc.ts#L104
pkg.Ensure(projectName != "", "ProjectName not set")
path := fmt.Sprintf("projects/%s/%s/project.pb", projectName, b.PulumiStack)
path := b.GetProjectUpdatePath(projectName)

term.Debug("Getting services from bucket:", bucketName, path)
getObjectOutput, err := s3Client.GetObject(ctx, &s3.GetObjectInput{
Expand Down
24 changes: 0 additions & 24 deletions src/pkg/cli/client/byoc/aws/domain_integration_test.go

This file was deleted.

6 changes: 6 additions & 0 deletions src/pkg/cli/client/byoc/baseclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,9 @@ func (b ByocBaseClient) GetPrivateFqdn(projectName string, fqn string) string {
safeFqn := dns.SafeLabel(fqn)
return fmt.Sprintf("%s.%s", safeFqn, GetPrivateDomain(projectName)) // TODO: consider merging this with ServicePrivateDNS
}

func (b ByocBaseClient) GetProjectUpdatePath(projectName string) string {
// Path to the state file, Defined at: https://github.com/DefangLabs/defang-mvp/blob/main/pulumi/cd/aws/byoc.ts#L104
pkg.Ensure(projectName != "", "ProjectName not set")
return fmt.Sprintf("projects/%s/%s/project.pb", projectName, b.PulumiStack)
}
2 changes: 1 addition & 1 deletion src/pkg/cli/client/byoc/do/byoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (b *ByocDo) GetProjectUpdate(ctx context.Context, projectName string) (*def
return nil, nil // no services yet
}

path := fmt.Sprintf("projects/%s/%s/project.pb", projectName, b.PulumiStack)
path := b.GetProjectUpdatePath(projectName)
getObjectOutput, err := s3client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucketName,
Key: &path,
Expand Down
3 changes: 1 addition & 2 deletions src/pkg/cli/client/byoc/gcp/byoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,8 +957,7 @@ func (b *ByocGcp) GetProjectUpdate(ctx context.Context, projectName string) (*de
return nil, errors.New("no defang cd bucket found")
}

// Path to the state file, Defined at: https://github.com/DefangLabs/defang-mvp/blob/main/pulumi/cd/byoc/aws/index.ts#L89
path := fmt.Sprintf("projects/%s/%s/project.pb", projectName, b.PulumiStack)
path := b.GetProjectUpdatePath(projectName)

// Current user might not have object viewer access to the bucket, use the upload service account to get the object
uploadSA := b.driver.GetServiceAccountEmail(DefangUploadServiceAccountName)
Expand Down
19 changes: 14 additions & 5 deletions src/pkg/cli/deploymentinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
)

type printService struct {
type Service struct {
Deployment string
Endpoint string
Service string
Expand All @@ -14,8 +14,8 @@ type printService struct {
Fqdn string
}

func PrintServiceStatesAndEndpoints(serviceInfos []*defangv1.ServiceInfo) error {
var serviceTableItems []*printService
func GetServiceStatesAndEndpoints(serviceInfos []*defangv1.ServiceInfo) ([]*Service, bool, error) {
var serviceTableItems []*Service

// showDomainNameColumn := false
showCertGenerateHint := false
Expand All @@ -38,7 +38,7 @@ func PrintServiceStatesAndEndpoints(serviceInfos []*defangv1.ServiceInfo) error
domainname = serviceInfo.PrivateFqdn
}

ps := &printService{
ps := &Service{
Deployment: serviceInfo.Etag,
Service: serviceInfo.Service.Name,
State: serviceInfo.State,
Expand All @@ -49,12 +49,21 @@ func PrintServiceStatesAndEndpoints(serviceInfos []*defangv1.ServiceInfo) error
serviceTableItems = append(serviceTableItems, ps)
}

return serviceTableItems, showCertGenerateHint, nil
}

func PrintServiceStatesAndEndpoints(serviceInfos []*defangv1.ServiceInfo) error {
services, showCertGenerateHint, err := GetServiceStatesAndEndpoints(serviceInfos)
if err != nil {
return err
}

attrs := []string{"Service", "Deployment", "State", "Fqdn", "Endpoint", "Status"}
// if showDomainNameColumn {
// attrs = append(attrs, "DomainName")
// }

err := term.Table(serviceTableItems, attrs...)
err = term.Table(services, attrs...)
if err != nil {
return err
}
Expand Down
79 changes: 46 additions & 33 deletions src/pkg/cli/getServices.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package cli

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
Expand All @@ -20,29 +22,34 @@ func (e ErrNoServices) Error() string {
if e.ProjectName == "" {
return "no services found"
}
return "no services found in project " + e.ProjectName
return fmt.Sprintf("no services found in project %q; check logs for deployment status", e.ProjectName)
}

func GetServices(ctx context.Context, projectName string, provider client.Provider, long bool) error {
func GetServices(ctx context.Context, projectName string, provider client.Provider) (*defangv1.GetServicesResponse, error) {
term.Debugf("Listing services in project %q", projectName)

servicesResponse, err := provider.GetServices(ctx, &defangv1.GetServicesRequest{Project: projectName})
if err != nil {
return err
return nil, err
}

numServices := len(servicesResponse.Services)
if numServices == 0 {
return ErrNoServices{ProjectName: projectName}
return nil, ErrNoServices{ProjectName: projectName}
}

return servicesResponse, nil
}

func PrintServices(ctx context.Context, projectName string, provider client.Provider, long bool) error {
servicesResponse, err := GetServices(ctx, projectName, provider)
if err != nil {
return err
}

term.Info("Checking service health...")
UpdateServiceStates(ctx, servicesResponse.Services)

return PrintServiceInfos(servicesResponse, long)
}

func PrintServiceInfos(servicesResponse *defangv1.GetServicesResponse, long bool) error {
if long {
return PrintObject("", servicesResponse)
}
Expand All @@ -58,32 +65,38 @@ func UpdateServiceStates(ctx context.Context, serviceInfos []*defangv1.ServiceIn

for _, serviceInfo := range serviceInfos {
for _, endpoint := range serviceInfo.Endpoints {
if !strings.Contains(endpoint, ":") {
wg.Add(1)
go func(serviceInfo *defangv1.ServiceInfo) {
defer wg.Done()
url := "https://" + endpoint + serviceInfo.HealthcheckPath
// Use the regular net/http package to make the request without retries
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
term.Errorf("failed to create healthcheck request for %q at %s: %s", serviceInfo.Service.Name, url, err.Error())
return
}
term.Debugf("[%s] checking health at %s", serviceInfo.Service.Name, url)
resp, err := http.DefaultClient.Do(req)
if err != nil {
term.Errorf("Healthcheck failed for %q at %s: %s", serviceInfo.Service.Name, url, err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
serviceInfo.State = defangv1.ServiceState_DEPLOYMENT_COMPLETED
term.Debugf("[%s] ✔ healthy", serviceInfo.Service.Name)
} else {
term.Debugf("[%s] ✘ unhealthy (%s)", serviceInfo.Service.Name, resp.Status)
}
}(serviceInfo)
if strings.Contains(endpoint, ":") {
// Skip endpoints with ports because they likely non-HTTP services
continue
}
wg.Add(1)
go func(serviceInfo *defangv1.ServiceInfo) {
defer wg.Done()
url, err := url.JoinPath("https://"+endpoint, serviceInfo.HealthcheckPath)
if err != nil {
term.Errorf("failed to construct healthcheck URL for %q at endpoint %s: %s", serviceInfo.Service.Name, endpoint, err.Error())
return
}
// Use the regular net/http package to make the request without retries
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
term.Errorf("failed to create healthcheck request for %q at %s: %s", serviceInfo.Service.Name, url, err.Error())
return
}
term.Debugf("[%s] checking health at %s", serviceInfo.Service.Name, url)
resp, err := http.DefaultClient.Do(req)
if err != nil {
term.Errorf("Healthcheck failed for %q at %s: %s", serviceInfo.Service.Name, url, err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
serviceInfo.State = defangv1.ServiceState_DEPLOYMENT_COMPLETED
term.Debugf("[%s] ✔ healthy", serviceInfo.Service.Name)
} else {
term.Debugf("[%s] ✘ unhealthy (%s)", serviceInfo.Service.Name, resp.Status)
}
}(serviceInfo)
}
}
wg.Wait()
Expand Down
Loading
Loading