github.com/defang-io/defang/src@v0.0.0-20240505002154-bdf411911834/pkg/cli/client/grpc.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"github.com/bufbuild/connect-go"
    15  	compose "github.com/compose-spec/compose-go/v2/types"
    16  	"github.com/defang-io/defang/src/pkg/auth"
    17  	"github.com/defang-io/defang/src/pkg/term"
    18  	"github.com/defang-io/defang/src/pkg/types"
    19  	defangv1 "github.com/defang-io/defang/src/protos/io/defang/v1"
    20  	"github.com/defang-io/defang/src/protos/io/defang/v1/defangv1connect"
    21  	"github.com/google/uuid"
    22  	"google.golang.org/protobuf/types/known/emptypb"
    23  )
    24  
    25  type GrpcClient struct {
    26  	anonID string
    27  	client defangv1connect.FabricControllerClient
    28  
    29  	tenantID types.TenantID
    30  	Loader   ProjectLoader
    31  }
    32  
    33  func NewGrpcClient(host, accessToken string, tenantID types.TenantID, loader ProjectLoader) *GrpcClient {
    34  	baseUrl := "http://"
    35  	if strings.HasSuffix(host, ":443") {
    36  		baseUrl = "https://"
    37  	}
    38  	baseUrl += host
    39  	// Debug(" - Connecting to", baseUrl)
    40  	fabricClient := defangv1connect.NewFabricControllerClient(http.DefaultClient, baseUrl, connect.WithGRPC(), connect.WithInterceptors(auth.NewAuthInterceptor(accessToken)))
    41  
    42  	state := State{AnonID: uuid.NewString()}
    43  
    44  	// Restore anonID from config file
    45  	statePath := filepath.Join(StateDir, "state.json")
    46  	if bytes, err := os.ReadFile(statePath); err == nil {
    47  		json.Unmarshal(bytes, &state)
    48  	} else { // could be not found or path error
    49  		if bytes, err := json.MarshalIndent(state, "", "  "); err == nil {
    50  			os.MkdirAll(StateDir, 0700)
    51  			os.WriteFile(statePath, bytes, 0644)
    52  		}
    53  	}
    54  
    55  	return &GrpcClient{client: fabricClient, anonID: state.AnonID, tenantID: tenantID, Loader: loader}
    56  }
    57  
    58  func getMsg[T any](resp *connect.Response[T], err error) (*T, error) {
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	return resp.Msg, nil
    63  }
    64  
    65  func (g GrpcClient) LoadProject() (*compose.Project, error) {
    66  	return g.Loader.LoadWithDefaultProjectName(string(g.tenantID))
    67  }
    68  
    69  func (g GrpcClient) GetVersions(ctx context.Context) (*defangv1.Version, error) {
    70  	return getMsg(g.client.GetVersion(ctx, &connect.Request[emptypb.Empty]{}))
    71  }
    72  
    73  func (g GrpcClient) Token(ctx context.Context, req *defangv1.TokenRequest) (*defangv1.TokenResponse, error) {
    74  	req.AnonId = g.anonID
    75  	return getMsg(g.client.Token(ctx, &connect.Request[defangv1.TokenRequest]{Msg: req}))
    76  }
    77  
    78  func (g GrpcClient) RevokeToken(ctx context.Context) error {
    79  	_, err := g.client.RevokeToken(ctx, &connect.Request[emptypb.Empty]{})
    80  	return err
    81  }
    82  
    83  func (g GrpcClient) Update(ctx context.Context, req *defangv1.Service) (*defangv1.ServiceInfo, error) {
    84  	return getMsg(g.client.Update(ctx, &connect.Request[defangv1.Service]{Msg: req}))
    85  }
    86  
    87  func (g GrpcClient) Deploy(ctx context.Context, req *defangv1.DeployRequest) (*defangv1.DeployResponse, error) {
    88  	// TODO: remove this when playground supports BYOD
    89  	for _, service := range req.Services {
    90  		if service.Domainname != "" {
    91  			term.Warnf("Defang provider does not support the domainname field for now, service: %v, domain: %v", service.Name, service.Domainname)
    92  		}
    93  	}
    94  	return getMsg(g.client.Deploy(ctx, &connect.Request[defangv1.DeployRequest]{Msg: req}))
    95  }
    96  
    97  func (g GrpcClient) Get(ctx context.Context, req *defangv1.ServiceID) (*defangv1.ServiceInfo, error) {
    98  	return getMsg(g.client.Get(ctx, &connect.Request[defangv1.ServiceID]{Msg: req}))
    99  }
   100  
   101  func (g GrpcClient) Delete(ctx context.Context, req *defangv1.DeleteRequest) (*defangv1.DeleteResponse, error) {
   102  	return getMsg(g.client.Delete(ctx, &connect.Request[defangv1.DeleteRequest]{Msg: req}))
   103  }
   104  
   105  func (g GrpcClient) Publish(ctx context.Context, req *defangv1.PublishRequest) error {
   106  	_, err := g.client.Publish(ctx, &connect.Request[defangv1.PublishRequest]{Msg: req})
   107  	return err
   108  }
   109  
   110  func (g GrpcClient) GetServices(ctx context.Context) (*defangv1.ListServicesResponse, error) {
   111  	return getMsg(g.client.GetServices(ctx, &connect.Request[emptypb.Empty]{}))
   112  }
   113  
   114  func (g GrpcClient) GenerateFiles(ctx context.Context, req *defangv1.GenerateFilesRequest) (*defangv1.GenerateFilesResponse, error) {
   115  	return getMsg(g.client.GenerateFiles(ctx, &connect.Request[defangv1.GenerateFilesRequest]{Msg: req}))
   116  }
   117  
   118  func (g GrpcClient) PutConfig(ctx context.Context, req *defangv1.SecretValue) error {
   119  	_, err := g.client.PutSecret(ctx, &connect.Request[defangv1.SecretValue]{Msg: req})
   120  	return err
   121  }
   122  
   123  func (g GrpcClient) DeleteConfig(ctx context.Context, req *defangv1.Secrets) error {
   124  	// _, err := g.client.DeleteSecrets(ctx, &connect.Request[v1.Secrets]{Msg: req}); TODO: implement this in the server
   125  	var errs []error
   126  	for _, name := range req.Names {
   127  		_, err := g.client.PutSecret(ctx, &connect.Request[defangv1.SecretValue]{Msg: &defangv1.SecretValue{Name: name}})
   128  		errs = append(errs, err)
   129  	}
   130  	return errors.Join(errs...)
   131  }
   132  
   133  func (g GrpcClient) ListConfig(ctx context.Context) (*defangv1.Secrets, error) {
   134  	return getMsg(g.client.ListSecrets(ctx, &connect.Request[emptypb.Empty]{}))
   135  }
   136  
   137  func (g GrpcClient) CreateUploadURL(ctx context.Context, req *defangv1.UploadURLRequest) (*defangv1.UploadURLResponse, error) {
   138  	return getMsg(g.client.CreateUploadURL(ctx, &connect.Request[defangv1.UploadURLRequest]{Msg: req}))
   139  }
   140  
   141  func (g GrpcClient) WhoAmI(ctx context.Context) (*defangv1.WhoAmIResponse, error) {
   142  	return getMsg(g.client.WhoAmI(ctx, &connect.Request[emptypb.Empty]{}))
   143  }
   144  
   145  func (g GrpcClient) DelegateSubdomainZone(ctx context.Context, req *defangv1.DelegateSubdomainZoneRequest) (*defangv1.DelegateSubdomainZoneResponse, error) {
   146  	return getMsg(g.client.DelegateSubdomainZone(ctx, &connect.Request[defangv1.DelegateSubdomainZoneRequest]{Msg: req}))
   147  }
   148  
   149  func (g GrpcClient) DeleteSubdomainZone(ctx context.Context) error {
   150  	_, err := getMsg(g.client.DeleteSubdomainZone(ctx, &connect.Request[emptypb.Empty]{}))
   151  	return err
   152  }
   153  
   154  func (g GrpcClient) GetDelegateSubdomainZone(ctx context.Context) (*defangv1.DelegateSubdomainZoneResponse, error) {
   155  	return getMsg(g.client.GetDelegateSubdomainZone(ctx, &connect.Request[emptypb.Empty]{}))
   156  }
   157  
   158  func (g *GrpcClient) Tail(ctx context.Context, req *defangv1.TailRequest) (ServerStream[defangv1.TailResponse], error) {
   159  	return g.client.Tail(ctx, &connect.Request[defangv1.TailRequest]{Msg: req})
   160  }
   161  
   162  func (g *GrpcClient) BootstrapCommand(ctx context.Context, command string) (ETag, error) {
   163  	return "", errors.New("the bootstrap command is not valid for the Defang provider")
   164  }
   165  
   166  func (g *GrpcClient) AgreeToS(ctx context.Context) error {
   167  	_, err := g.client.SignEULA(ctx, &connect.Request[emptypb.Empty]{})
   168  	return err
   169  }
   170  
   171  func (g *GrpcClient) Track(event string, properties ...Property) error {
   172  	// Convert map[string]any to map[string]string
   173  	var props map[string]string
   174  	if len(properties) > 0 {
   175  		props = make(map[string]string, len(properties))
   176  		for _, p := range properties {
   177  			props[p.Name] = fmt.Sprint(p.Value)
   178  		}
   179  	}
   180  	_, err := g.client.Track(context.Background(), &connect.Request[defangv1.TrackRequest]{Msg: &defangv1.TrackRequest{
   181  		AnonId:     g.anonID,
   182  		Event:      event,
   183  		Properties: props,
   184  		Os:         runtime.GOOS,
   185  		Arch:       runtime.GOARCH,
   186  	}})
   187  	return err
   188  }
   189  
   190  func (g *GrpcClient) CheckLoginAndToS(ctx context.Context) error {
   191  	_, err := g.client.CheckToS(ctx, &connect.Request[emptypb.Empty]{})
   192  	return err
   193  }
   194  
   195  func (g *GrpcClient) Destroy(ctx context.Context) (ETag, error) {
   196  	// Get all the services in the project and delete them all at once
   197  	project, err := g.GetServices(ctx)
   198  	if err != nil {
   199  		return "", err
   200  	}
   201  	if len(project.Services) == 0 {
   202  		return "", errors.New("no services found")
   203  	}
   204  	var names []string
   205  	for _, service := range project.Services {
   206  		names = append(names, service.Service.Name)
   207  	}
   208  	resp, err := g.Delete(ctx, &defangv1.DeleteRequest{Names: names})
   209  	if err != nil {
   210  		return "", err
   211  	}
   212  	return resp.Etag, nil
   213  }
   214  
   215  func (g *GrpcClient) TearDown(ctx context.Context) error {
   216  	return errors.New("the teardown command is not valid for the Defang provider")
   217  }
   218  
   219  func (g *GrpcClient) BootstrapList(context.Context) error {
   220  	return errors.New("the list command is not valid for the Defang provider")
   221  }
   222  
   223  func (g *GrpcClient) Restart(ctx context.Context, names ...string) (ETag, error) {
   224  	// For now, we'll just get the service info and pass it back to Deploy as-is.
   225  	services := make([]*defangv1.Service, 0, len(names))
   226  	for _, name := range names {
   227  		serviceInfo, err := g.Get(ctx, &defangv1.ServiceID{Name: name})
   228  		if err != nil {
   229  			return "", err
   230  		}
   231  		services = append(services, serviceInfo.Service)
   232  	}
   233  
   234  	dr, err := g.Deploy(ctx, &defangv1.DeployRequest{Services: services})
   235  	if err != nil {
   236  		return "", err
   237  	}
   238  	return dr.Etag, nil
   239  }
   240  
   241  func (g GrpcClient) ServiceDNS(name string) string {
   242  	whoami, _ := g.WhoAmI(context.TODO())
   243  	return whoami.Tenant + "-" + name
   244  }