Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sourcing deployment ENVs from config file #63

Merged
merged 9 commits into from
Dec 17, 2024
11 changes: 6 additions & 5 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/hathora/ci/internal/archive"
"github.com/hathora/ci/internal/commands/altsrc"
"github.com/hathora/ci/internal/httputil"
"github.com/hathora/ci/internal/output"
"github.com/hathora/ci/internal/sdk"
"github.com/hathora/ci/internal/sdk/models/shared"
Expand All @@ -27,6 +28,8 @@ type etagPart struct {
etag string
}

const buildFlagEnvVarPrefix = globalFlagEnvVarPrefix + "BUILD_"

var Build = &cli.Command{
Name: "build",
Usage: "options for builds",
Expand Down Expand Up @@ -185,7 +188,7 @@ func doBuildCreate(ctx context.Context, hathora *sdk.SDK, buildTag, buildId, fil
return nil, fmt.Errorf("failed to upload parts: %w", err)
}

resp, err := http.Post(createRes.CreatedBuildV3WithMultipartUrls.CompleteUploadPostRequestURL, "application/xml", bytes.NewBufferString(createEtagXML(etagParts...)))
resp, err := http.Post(createRes.CreatedBuildV3WithMultipartUrls.CompleteUploadPostRequestURL, httputil.ValueApplicationXML, bytes.NewBufferString(createEtagXML(etagParts...)))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -248,8 +251,6 @@ func buildFlagEnvVar(name string) string {
}

var (
buildFlagEnvVarPrefix = globalFlagEnvVarPrefix + "BUILD_"

buildIDFlag = &cli.StringFlag{
Name: "build-id",
Aliases: []string{"b"},
Expand Down Expand Up @@ -432,12 +433,12 @@ func uploadFileToS3(preSignedURL string, byteBuffer []byte, globalUploadProgress
globalUploadProgress: globalUploadProgress,
hideUploadProgress: hideUploadProgress,
}
req, err := http.NewRequest("PUT", preSignedURL, progressReader)
req, err := http.NewRequest(http.MethodPut, preSignedURL, progressReader)
if err != nil {
os.Stderr.WriteString(fmt.Sprintf("failed to create request: %v", err))
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set(httputil.NameContentType, httputil.ValueApplicationOctetStream)
req.ContentLength = int64(requestBody.Len())

client := &http.Client{}
Expand Down
3 changes: 2 additions & 1 deletion internal/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/hathora/ci/internal/commands"
"github.com/hathora/ci/internal/httputil"
"github.com/hathora/ci/internal/mock"
)

Expand Down Expand Up @@ -125,7 +126,7 @@ func Test_Integration_BuildCommands_Happy(t *testing.T) {
expectRequest: func(t *testing.T, r *http.Request, requestBody *json.RawMessage) {
assert.Equal(t, r.Method, http.MethodPost, "request method should be POST")
assert.Equal(t, "/builds/v3/test-app-id/create", r.URL.Path, "request path should contain app id")
assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "request should have a JSON content type")
assert.Equal(t, httputil.ValueApplicationJSON, r.Header.Get(httputil.NameContentType), "request should have a JSON content type")
assert.NotNil(t, requestBody, "request body should not be nil")
assert.Equal(t, `{"buildTag":"test-build-tag"}`, string(*requestBody), "request body should have supplied build tag")
},
Expand Down
24 changes: 19 additions & 5 deletions internal/commands/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/hathora/ci/internal/workaround"
)

const deploymentFlagEnvVarPrefix = globalFlagEnvVarPrefix + "DEPLOYMENT_"

var (
allowedTransportTypes = []string{"tcp", "udp", "tls"}
minRoomsPerProcess = 1
Expand Down Expand Up @@ -192,8 +194,6 @@ func deploymentEnvVar(name string) string {
}

var (
deploymentFlagEnvVarPrefix = fmt.Sprintf("%s%s", globalFlagEnvVarPrefix, "DEPLOYMENT_")

deploymentIDFlag = &cli.StringFlag{
Name: "deployment-id",
Aliases: []string{"d"},
Expand Down Expand Up @@ -258,8 +258,11 @@ var (
}

envVarsFlag = &cli.StringSliceFlag{
Name: "env",
Sources: cli.EnvVars(deploymentEnvVar("ENV")),
Name: "env",
Sources: cli.NewValueSourceChain(
cli.EnvVar(deploymentEnvVar("ENV")),
altsrc.ConfigFile(configFlag.Name, "deployment.env"),
),
Usage: "`<KEY=VALUE>` formatted environment variables (use quotes for values with spaces or commas)",
Category: "Deployment:",
}
Expand Down Expand Up @@ -289,7 +292,7 @@ var (
fromLatestFlag = &cli.BoolFlag{
Name: "from-latest",
Sources: cli.EnvVars(deploymentEnvVar("FROM_LATEST")),
Usage: "whether to use settings from the latest deployment; if true other flags act as overrides",
Usage: "whether to use settings from the latest deployment; if true other flags and config file values act as overrides",
Category: "Deployment:",
}
)
Expand All @@ -307,6 +310,17 @@ func parseContainerPorts(ports []string) ([]shared.ContainerPort, error) {
}

func parseEnvVars(envVars []string) ([]shared.DeploymentConfigV3Env, error) {
// Envs from a Config File are parsed from urfave/cli as a single-element
// string slice of all the values like:
// []string{`[KEY1=VAL1 KEY2=VAL2 KEY3="QUOTED VAL3"]`}
// This stanza parses those into a proper slice of one Env per slice element
jranson marked this conversation as resolved.
Show resolved Hide resolved
if len(envVars) == 1 && strings.HasPrefix(envVars[0], "[") && strings.HasSuffix(envVars[0], "]") {
var err error
envVars, err = shorthand.ParseConfigFileVars(envVars[0])
if err != nil {
return nil, err
}
}
envVars = fixOverZealousCommaSplitting(envVars)
output := make([]shared.DeploymentConfigV3Env, 0, len(envVars))
for _, envVar := range envVars {
Expand Down
6 changes: 2 additions & 4 deletions internal/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"github.com/hathora/ci/internal/commands/altsrc"
)

const globalFlagEnvVarPrefix = "HATHORA_"

var (
outputFlag = &cli.StringFlag{
Name: "output",
Expand Down Expand Up @@ -103,10 +105,6 @@ var (
}
)

var (
globalFlagEnvVarPrefix = "HATHORA_"
)

func globalFlagEnvVar(name string) string {
return globalFlagEnvVarPrefix + name
}
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/hathora/ci/internal/workaround"
)

const logFlagEnvVarPrefix = globalFlagEnvVarPrefix + "LOG_"

var (
minTailLines = 1
maxTailLines = 5000
Expand Down Expand Up @@ -66,8 +68,6 @@ func logFlagEnvVar(name string) string {
}

var (
logFlagEnvVarPrefix = globalFlagEnvVarPrefix + "LOG_"

followFlag = &cli.BoolFlag{
Name: "follow",
Sources: cli.NewValueSourceChain(
Expand Down
11 changes: 9 additions & 2 deletions internal/httputil/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import (
"net/http"
)

const (
NameContentType = "Content-Type"
ValueApplicationJSON = "application/json"
ValueApplicationOctetStream = "application/octet-stream"
ValueApplicationXML = "application/xml"
)

type contentTypeRoundTripper struct {
underlying http.RoundTripper
}
Expand All @@ -19,8 +26,8 @@ func (c *contentTypeRoundTripper) RoundTrip(req *http.Request) (*http.Response,
return res, err
}

if res.Header.Get("content-type") == "" {
res.Header.Set("content-type", "application/octet-stream")
if res.Header.Get(NameContentType) == "" {
res.Header.Set(NameContentType, ValueApplicationOctetStream)
}

return res, err
Expand Down
3 changes: 2 additions & 1 deletion internal/mock/hathora.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http/httptest"
"testing"

"github.com/hathora/ci/internal/httputil"
"github.com/hathora/ci/internal/sdk"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -120,7 +121,7 @@ func (m *mockHathora) ServeHTTP(w http.ResponseWriter, r *http.Request) {
jsonBody := json.RawMessage(body)
m.capturedRequestBodies = append(m.capturedRequestBodies, &jsonBody)

w.Header().Set("Content-Type", "application/json")
w.Header().Set(httputil.NameContentType, httputil.ValueApplicationJSON)
w.WriteHeader(m.cannedStatuses[m.nextResponseIndex])

_, err = w.Write(m.cannedResponses[m.nextResponseIndex])
Expand Down
29 changes: 27 additions & 2 deletions internal/shorthand/env_var.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package shorthand

import (
"fmt"
"regexp"
"strings"

"github.com/hathora/ci/internal/sdk/models/shared"
Expand All @@ -14,15 +15,35 @@ func ParseDeploymentEnvVar(s string) (*shared.DeploymentConfigV3Env, error) {

parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid env var format: %s", s)
return nil, ErrInvalidEnvVarFormat(s)
}

parts[1] = strings.TrimSpace(parts[1])
// remove any enclosing quotes
if strings.HasPrefix(parts[1], `"`) && strings.HasSuffix(parts[1], `"`) {
parts[1] = strings.TrimPrefix(strings.TrimSuffix(parts[1], `"`), `"`)
}

return &shared.DeploymentConfigV3Env{
Name: strings.TrimSpace(parts[0]),
Value: strings.TrimSpace(parts[1]),
Value: parts[1],
}, nil
}

var reEnvs = regexp.MustCompile(`\b[A-Za-z0-9_]+="[^"]*"|\b[A-Za-z0-9_]+=[^" ]+`)

func ParseConfigFileVars(input string) ([]string, error) {
input = strings.TrimPrefix(strings.TrimSuffix(input, "]"), "[")
if len(input) == 0 {
return nil, nil
}
matches := reEnvs.FindAllString(input, -1)
if len(matches) == 0 {
return nil, ErrInvalidEnvVarFormat(input)
}
return matches, nil
}

func MapEnvToEnvConfig(input []shared.DeploymentV3Env) []shared.DeploymentConfigV3Env {
output := make([]shared.DeploymentConfigV3Env, 0)

Expand All @@ -32,3 +53,7 @@ func MapEnvToEnvConfig(input []shared.DeploymentV3Env) []shared.DeploymentConfig

return output
}

func ErrInvalidEnvVarFormat(env string) error {
return fmt.Errorf("invalid env var format: %s", env)
}
jranson marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 14 additions & 0 deletions internal/shorthand/env_var_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,17 @@ func Test_DeploymentEnvVarShorthand(t *testing.T) {
}

}

func TestParseConfigFileVars(t *testing.T) {
input := `[KEY1=VAL1 KEY3="VAL 3 WITH SPACES" KEY2=VAL2]`
out, err := shorthand.ParseConfigFileVars(input)
if err != nil {
t.Error(err)
}
assert.Equal(t, 3, len(out))
input = `[KEY1 =SHOULD_ERROR]`
_, err = shorthand.ParseConfigFileVars(input)
if err == nil {
t.Error("expected error for invalid env var format")
}
}