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 }