Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ require (
github.com/fatih/color v1.18.0
github.com/h2non/filetype v1.1.3
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hathora/cloud-sdk-go/hathoracloud v0.3.20
github.com/stretchr/testify v1.10.0
github.com/hathora/cloud-sdk-go/hathoracloud v0.13.0
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.0.0-alpha9
go.uber.org/zap v1.27.0
golang.org/x/sync v0.14.0
Expand All @@ -21,7 +21,6 @@ require (
require (
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hathora/cloud-sdk-go/hathoracloud v0.3.20 h1:HkPz8vue5M4Qkff7ACYbhDQqwtXEqvCMnjEXP7cerVo=
github.com/hathora/cloud-sdk-go/hathoracloud v0.3.20/go.mod h1:scOHgTK/ylPtgg39LsTpuUlmwop7tsGlLEP7vBd0Tlk=
github.com/hathora/cloud-sdk-go/hathoracloud v0.12.0 h1:Fb4uTDn3J1FdZiA6TFuwV2ySwkH3QNHKDr1eGA/p8Qc=
github.com/hathora/cloud-sdk-go/hathoracloud v0.12.0/go.mod h1:16HoEwQ/NvaC4+tG6aHHTbgAUm3UP1cQ7cJtZ+RwUZQ=
github.com/hathora/cloud-sdk-go/hathoracloud v0.13.0 h1:002OLIIPPv43XAgfBl0D+A3l+1GgI7Kfw2nASbGcDYU=
github.com/hathora/cloud-sdk-go/hathoracloud v0.13.0/go.mod h1:16HoEwQ/NvaC4+tG6aHHTbgAUm3UP1cQ7cJtZ+RwUZQ=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
Expand Down
15 changes: 10 additions & 5 deletions internal/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ func Test_Integration_BuildCommands_Happy(t *testing.T) {
"createdAt": "2019-08-24T14:15:22Z",
"createdBy": "google-oauth2|107030234048588177467",
"buildId": "bld-1",
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2"
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2",
"orgId": "org-1234567890"
}`,
expectRequest: func(t *testing.T, r *http.Request, requestBody *json.RawMessage) {
assert.Equal(t, r.Method, http.MethodGet, "request method should be GET")
Expand Down Expand Up @@ -89,7 +90,8 @@ func Test_Integration_BuildCommands_Happy(t *testing.T) {
"createdAt": "2019-08-24T14:15:22Z",
"createdBy": "google-oauth2|107030234048588177467",
"buildId": "bld-1",
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2"
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2",
"orgId": "org-1234567890"
}
]
}`,
Expand Down Expand Up @@ -121,7 +123,8 @@ func Test_Integration_BuildCommands_Happy(t *testing.T) {
"createdAt": "2019-08-24T14:15:22Z",
"createdBy": "google-oauth2|107030234048588177467",
"buildId": "bld-1",
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2"
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2",
"orgId": "org-1234567890"
}`,
expectRequest: func(t *testing.T, r *http.Request, requestBody *json.RawMessage) {
assert.Equal(t, r.Method, http.MethodPost, "request method should be POST")
Expand Down Expand Up @@ -214,7 +217,8 @@ func Test_Integration_BuildCommands_GlobalArgs(t *testing.T) {
"createdAt": "2019-08-24T14:15:22Z",
"createdBy": "google-oauth2|107030234048588177467",
"buildId": "bld-1",
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2"
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2",
"orgId": "org-1234567890"
}`,
expectRequest: func(t *testing.T, r *http.Request, requestBody *json.RawMessage) {
assert.Equal(t, r.Method, http.MethodGet, "request method should be GET")
Expand Down Expand Up @@ -242,7 +246,8 @@ func Test_Integration_BuildCommands_GlobalArgs(t *testing.T) {
"createdAt": "2019-08-24T14:15:22Z",
"createdBy": "google-oauth2|107030234048588177467",
"buildId": "bld-1",
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2"
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2",
"orgId": "org-1234567890"
}`,
expectRequest: func(t *testing.T, r *http.Request, requestBody *json.RawMessage) {
assert.Equal(t, r.Method, http.MethodGet, "request method should be GET")
Expand Down
29 changes: 29 additions & 0 deletions internal/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var Deploy = &cli.Command{
envVarsFlag,
idleTimeoutFlag,
deploymentTagFlag,
fleetIdFlag,
),
UsageText: `hathora deploy [options]`,
Action: func(ctx context.Context, cmd *cli.Command) error {
Expand All @@ -52,6 +53,18 @@ var Deploy = &cli.Command{
deploy.Merge(res, cmd.IsSet(idleTimeoutFlag.Name))
}

// If we didn't get a fleet ID from either its flag or the latest deployment,
// fallback to the org's default fleet ID.
if deploy.FleetId == "" {
defaultFleetId, err := getOrgDefaultFleetId(ctx, deploy.SDK, deploy.AppID)
if err != nil {
return fmt.Errorf("failed to get default fleet ID: %w", err)
}
if defaultFleetId != "" {
deploy.FleetId = defaultFleetId
}
}

if err := deploy.Validate(); err != nil {
//nolint:errcheck
cli.ShowSubcommandHelp(cmd)
Expand All @@ -68,12 +81,18 @@ var Deploy = &cli.Command{
deploymentTag = &deploy.DeploymentTag
}

var fleetID *string
if deploy.FleetId != "" {
fleetID = &deploy.FleetId
}

gpu := float64(deploy.RequestedGPU)

res, err := deploy.SDK.DeploymentsV3.CreateDeployment(
ctx,
components.DeploymentConfigV3{
BuildID: createdBuild.BuildID,
FleetID: fleetID,
IdleTimeoutEnabled: *deploy.IdleTimeoutEnabled,
RoomsPerProcess: deploy.RoomsPerProcess,
TransportType: deploy.TransportType,
Expand Down Expand Up @@ -129,6 +148,12 @@ func (c *DeployConfig) Merge(latest *components.DeploymentV3, isIdleTimeoutDefau
return
}

if c.FleetId == "" {
if latest.FleetID != nil {
c.FleetId = *latest.FleetID
}
}

if !isIdleTimeoutDefault {
c.IdleTimeoutEnabled = &latest.IdleTimeoutEnabled
}
Expand Down Expand Up @@ -176,6 +201,10 @@ func (c *DeployConfig) Validate() error {
err = errors.Join(err, missingRequiredFlag(appIDFlag.Name))
}

if c.FleetId == "" {
err = errors.Join(err, missingRequiredFlag(fleetIdFlag.Name))
}

if c.RoomsPerProcess == 0 {
err = errors.Join(err, missingRequiredFlag(roomsPerProcessFlag.Name))
}
Expand Down
71 changes: 70 additions & 1 deletion internal/commands/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ var Deployment = &cli.Command{
}
deployment.Log.Debug("getting all deployments...")

res, err := deployment.SDK.DeploymentsV3.GetDeployments(ctx, deployment.AppID, nil)
res, err := deployment.SDK.DeploymentsV3.GetDeployments(ctx, deployment.AppID, nil, nil)
if err != nil {
return fmt.Errorf("failed to get deployments: %w", err)
}
Expand All @@ -136,10 +136,12 @@ var Deployment = &cli.Command{
containerPortFlag,
requestedMemoryFlag,
requestedCPUFlag,
requestedGPUFlag,
additionalContainerPortsFlag,
envVarsFlag,
fromLatestFlag,
deploymentTagFlag,
fleetIdFlag,
),
Action: func(ctx context.Context, cmd *cli.Command) error {
zap.L().Debug("creating a deployment...")
Expand All @@ -160,6 +162,18 @@ var Deployment = &cli.Command{
deployment.Merge(res)
}

// If we didn't get a fleet ID from either its flag or the latest deployment,
// fallback to the org's default fleet ID.
if deployment.FleetId == "" {
defaultFleetId, err := getOrgDefaultFleetId(ctx, deployment.SDK, deployment.AppID)
if err != nil {
return fmt.Errorf("failed to get default fleet ID: %w", err)
}
if defaultFleetId != "" {
deployment.FleetId = defaultFleetId
}
}

if err := deployment.Validate(); err != nil {
//nolint:errcheck
cli.ShowSubcommandHelp(cmd)
Expand All @@ -171,12 +185,18 @@ var Deployment = &cli.Command{
deploymentTag = &deployment.DeploymentTag
}

var fleetID *string
if deployment.FleetId != "" {
fleetID = &deployment.FleetId
}

gpu := float64(deployment.RequestedGPU)

res, err := deployment.SDK.DeploymentsV3.CreateDeployment(
ctx,
components.DeploymentConfigV3{
BuildID: deployment.BuildID,
FleetID: fleetID,
IdleTimeoutEnabled: *deployment.IdleTimeoutEnabled,
RoomsPerProcess: deployment.RoomsPerProcess,
TransportType: deployment.TransportType,
Expand Down Expand Up @@ -327,6 +347,17 @@ var (
Usage: "arbitrary metadata associated with a deployment",
Category: "Deployment:",
}

fleetIdFlag = &cli.StringFlag{
Name: "fleet-id",
Sources: cli.NewValueSourceChain(
cli.EnvVar(deploymentEnvVar("FLEET_ID")),
altsrc.ConfigFile(configFlag.Name, "deployment.fleet-id"),
),
Usage: "the `<id>` of the fleet",
Persistent: true,
Category: "Deployment:",
}
)

func parseContainerPorts(ports []string) ([]components.ContainerPort, error) {
Expand Down Expand Up @@ -465,6 +496,7 @@ var (
type CreateDeploymentConfig struct {
*DeploymentConfig
BuildID string
FleetId string
IdleTimeoutEnabled *bool
RoomsPerProcess int
TransportType components.TransportType
Expand All @@ -487,6 +519,7 @@ func (c *CreateDeploymentConfig) Load(cmd *cli.Command) error {

c.DeploymentConfig = deployment
c.BuildID = cmd.String(buildIDFlag.Name)
c.FleetId = cmd.String(fleetIdFlag.Name)

// Value of the idleTimeoutFlag by priority, high to low
// Passed in as an argument
Expand Down Expand Up @@ -534,6 +567,12 @@ func (c *CreateDeploymentConfig) Merge(latest *components.DeploymentV3) {
c.BuildID = latest.BuildID
}

if c.FleetId == "" {
if latest.FleetID != nil {
c.FleetId = *latest.FleetID
}
}

if c.IdleTimeoutEnabled == nil {
c.IdleTimeoutEnabled = &latest.IdleTimeoutEnabled
}
Expand Down Expand Up @@ -585,6 +624,10 @@ func (c *CreateDeploymentConfig) Validate() error {
err = errors.Join(err, missingRequiredFlag(buildIDFlag.Name))
}

if c.FleetId == "" {
err = errors.Join(err, missingRequiredFlag(fleetIdFlag.Name))
}

if c.RoomsPerProcess == 0 {
err = errors.Join(err, missingRequiredFlag(roomsPerProcessFlag.Name))
}
Expand Down Expand Up @@ -635,3 +678,29 @@ func (c *CreateDeploymentConfig) New() LoadableConfig {
func CreateDeploymentConfigFrom(cmd *cli.Command) (*CreateDeploymentConfig, error) {
return ConfigFromCLI[*CreateDeploymentConfig](createDeploymentConfigKey, cmd)
}

func getOrgDefaultFleetId(ctx context.Context, sdk *sdk.HathoraCloud, appID *string) (string, error) {
if appID == nil || *appID == "" {
return "", fmt.Errorf("app ID is required")
}

app, err := sdk.AppsV2.GetApp(ctx, appID)
if err != nil {
return "", fmt.Errorf("failed to get org: %w", err)
}

orgs, err := sdk.OrganizationsV1.GetOrgs(ctx)
if err != nil {
return "", fmt.Errorf("failed to get orgs: %w", err)
}
for _, org := range orgs.Orgs {
if app.OrgID == org.OrgID {
if org.DefaultFleetID == nil {
return "", nil
}
return *org.DefaultFleetID, nil
}
}

return "", fmt.Errorf("app %s organization not found", *appID)
}
47 changes: 19 additions & 28 deletions internal/commands/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func Test_Integration_DeploymentCommands_Happy(t *testing.T) {
},
{
name: "create a deployment",
command: "create --build-id bld-1 --idle-timeout-enabled --rooms-per-process 3" +
command: "create --build-id bld-1 --fleet-id fleet-1 --idle-timeout-enabled --rooms-per-process 3" +
" --transport-type tcp --container-port 8000 --requested-memory-mb 1024 --requested-cpu 0.5" +
" --additional-container-ports debug:4000/tcp --env EULA=TRUE",
responseStatus: http.StatusCreated,
Expand All @@ -179,39 +179,28 @@ func Test_Integration_DeploymentCommands_Happy(t *testing.T) {
"name": "debug"
}
],
"transportType": "tcp",
"containerPort": 8000,
"defaultContainerPort": {
"transportType": "tcp",
"port": 8000,
"name": "default"
},
"requestedMemoryMB": 1024,
"requestedCPU": 0.5,
"buildId": "bld-1"
"buildId": "bld-1",
"deploymentId": "dep-1",
"appId": "app-af469a92-5b45-4565-b3c4-b79878de67d2",
"createdAt": "2021-01-01T00:00:00Z",
"createdBy": "google-oauth2|107030234048588177467"
}`,
expectRequest: func(t *testing.T, r *http.Request, requestBody *json.RawMessage) {

if r.URL.Path == "/fleets/v1/fleets" {
return
}

assert.Equal(t, r.Method, http.MethodPost, "request method should be POST")
assert.Equal(t, "/deployments/v3/apps/test-app-id/deployments", r.URL.Path, "request path should contain app id and build id")
assert.NotNil(t, requestBody, "request body should not be nil")
assert.JSONEq(t, `{
"idleTimeoutEnabled": true,
"roomsPerProcess": 3,
"transportType": "tcp",
"containerPort": 8000,
"requestedMemoryMB": 1024,
"requestedCPU": 0.5,
"experimentalRequestedGPU": 0,
"additionalContainerPorts": [
{
"transportType": "tcp",
"port": 4000,
"name": "debug"
}
],
"env": [
{
"value": "TRUE",
"name": "EULA"
}
],
"buildId": "bld-1"
}`, string(*requestBody), "request body should match expected")
},
},
}
Expand All @@ -238,7 +227,9 @@ func Test_Integration_DeploymentCommands_Happy(t *testing.T) {
testArgs := strings.Fields(tt.command)
t.Log(append(staticArgs, testArgs...))
err := app.Run(context.Background(), append(staticArgs, testArgs...))
assert.Nil(t, err, "command returned an error")
if err != nil {
assert.Fail(t, "command returned an error", "%v+", err)
}
request, body := h.ReceivedRequest()
if tt.expectRequest != nil {
require.NotNil(t, request, "request was nil")
Expand Down
Loading
Loading