diff --git a/go.mod b/go.mod index c542b9d..786cec9 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 50730a7..727b717 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index a6825f6..b944024 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -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") @@ -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" } ] }`, @@ -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") @@ -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") @@ -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") diff --git a/internal/commands/deploy.go b/internal/commands/deploy.go index 2fd25b2..c8160b1 100644 --- a/internal/commands/deploy.go +++ b/internal/commands/deploy.go @@ -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 { @@ -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) @@ -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, @@ -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 } @@ -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)) } diff --git a/internal/commands/deployment.go b/internal/commands/deployment.go index 66b6ab7..168ddeb 100644 --- a/internal/commands/deployment.go +++ b/internal/commands/deployment.go @@ -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) } @@ -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...") @@ -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) @@ -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, @@ -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 `` of the fleet", + Persistent: true, + Category: "Deployment:", + } ) func parseContainerPorts(ports []string) ([]components.ContainerPort, error) { @@ -465,6 +496,7 @@ var ( type CreateDeploymentConfig struct { *DeploymentConfig BuildID string + FleetId string IdleTimeoutEnabled *bool RoomsPerProcess int TransportType components.TransportType @@ -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 @@ -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 } @@ -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)) } @@ -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) +} diff --git a/internal/commands/deployment_test.go b/internal/commands/deployment_test.go index c66515a..1f67fac 100644 --- a/internal/commands/deployment_test.go +++ b/internal/commands/deployment_test.go @@ -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, @@ -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") }, }, } @@ -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") diff --git a/internal/output/text_test.go b/internal/output/text_test.go index d608d51..b22ee78 100644 --- a/internal/output/text_test.go +++ b/internal/output/text_test.go @@ -52,8 +52,8 @@ func Test_DeploymentTextOutput(t *testing.T) { RequestedCPU: 0.5, }, expect: [][]string{ - {"DeploymentID", "BuildID", "CreatedAt", "IdleTimeoutEnabled", "RoomsPerProcess", "RequestedCPU", "RequestedMemory", "DefaultContainerPort", "AdditionalContainerPorts", "BuildTag", "DeploymentTag", "ExperimentalRequestedGPU"}, - {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null"}, + {"DeploymentID", "BuildID", "CreatedAt", "IdleTimeoutEnabled", "RoomsPerProcess", "RequestedCPU", "RequestedMemory", "DefaultContainerPort", "AdditionalContainerPorts", "BuildTag", "DeploymentTag", "ExperimentalRequestedGPU", "FleetID", "RequestedGPU"}, + {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null", "null", "null"}, }, }, { @@ -88,8 +88,8 @@ func Test_DeploymentTextOutput(t *testing.T) { RequestedCPU: 0.5, }, expect: [][]string{ - {"DeploymentID", "BuildID", "CreatedAt", "IdleTimeoutEnabled", "RoomsPerProcess", "RequestedCPU", "RequestedMemory", "DefaultContainerPort", "AdditionalContainerPorts", "BuildTag", "DeploymentTag", "ExperimentalRequestedGPU"}, - {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null"}, + {"DeploymentID", "BuildID", "CreatedAt", "IdleTimeoutEnabled", "RoomsPerProcess", "RequestedCPU", "RequestedMemory", "DefaultContainerPort", "AdditionalContainerPorts", "BuildTag", "DeploymentTag", "ExperimentalRequestedGPU", "FleetID", "RequestedGPU"}, + {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null", "null", "null"}, }, }, { @@ -155,9 +155,9 @@ func Test_DeploymentTextOutput(t *testing.T) { }, }, expect: [][]string{ - {"DeploymentID", "BuildID", "CreatedAt", "IdleTimeoutEnabled", "RoomsPerProcess", "RequestedCPU", "RequestedMemory", "DefaultContainerPort", "AdditionalContainerPorts", "BuildTag", "DeploymentTag", "ExperimentalRequestedGPU"}, - {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null"}, - {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null"}, + {"DeploymentID", "BuildID", "CreatedAt", "IdleTimeoutEnabled", "RoomsPerProcess", "RequestedCPU", "RequestedMemory", "DefaultContainerPort", "AdditionalContainerPorts", "BuildTag", "DeploymentTag", "ExperimentalRequestedGPU", "FleetID", "RequestedGPU"}, + {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null", "null", "null"}, + {"dep-2", "bld-1", "2021-01-01T00:00:00Z", "true", "3", "0.5", "1.0", "GiB", "default:3000/tcp", "debug:4000/tcp", "null", "null", "null", "null", "null"}, }, }, }