Skip to content

Commit

Permalink
Merge branch 'main' into i4k-remove-codecov
Browse files Browse the repository at this point in the history
  • Loading branch information
i4ki committed Oct 17, 2023
2 parents 3e3f843 + b0b6318 commit 9d45dcd
Show file tree
Hide file tree
Showing 8 changed files with 382 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Given a version number `MAJOR.MINOR.PATCH`, we increment the:

- Add `--cloud-sync-terraform-plan-file=<plan>` flag for synchronizing the plan
file in rendered ASCII and JSON (sensitive information removed).
- Add configuration attribute `terramate.config.cloud.organization` to select which cloud organization to use when syncing with Terramate Cloud.

## 0.4.2

Expand Down
20 changes: 18 additions & 2 deletions cmd/terramate/cli/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ func (c *cli) checkCloudSync() {
}
}

func (c *cli) cloudOrgName() string {
orgName := os.Getenv("TM_CLOUD_ORGANIZATION")
if orgName != "" {
return orgName
}

cfg := c.rootNode()
if cfg.Terramate != nil &&
cfg.Terramate.Config != nil &&
cfg.Terramate.Config.Cloud != nil {
return cfg.Terramate.Config.Cloud.Organization
}

return ""
}

func (c *cli) setupCloudConfig() error {
logger := log.With().
Str("action", "cli.setupCloudConfig").
Expand All @@ -123,7 +139,7 @@ func (c *cli) setupCloudConfig() error {
// at this point we know user is onboarded, ie has at least 1 organization.
orgs := c.cred().organizations()

useOrgName := os.Getenv("TM_CLOUD_ORGANIZATION")
useOrgName := c.cloudOrgName()
if useOrgName != "" {
var useOrgUUID string
for _, org := range orgs {
Expand Down Expand Up @@ -172,7 +188,7 @@ func (c *cli) setupCloudConfig() error {
}
if len(activeOrgs) > 1 {
logger.Error().
Msgf("Please set TM_CLOUD_ORGANIZATION environment variable to a specific available organization: %s", activeOrgs)
Msgf("Please set TM_CLOUD_ORGANIZATION environment variable or terramate.config.cloud.organization configuration attribute to a specific available organization: %s", activeOrgs)

return cloudError()
}
Expand Down
200 changes: 200 additions & 0 deletions cmd/terramate/e2etests/run_cloud_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright 2023 Terramate GmbH
// SPDX-License-Identifier: MPL-2.0

package e2etest

import (
"errors"
"fmt"
"net/http"
"os"
"testing"
"time"

"github.com/terramate-io/terramate/cloud"
"github.com/terramate-io/terramate/cloud/testserver"
"github.com/terramate-io/terramate/cmd/terramate/cli/clitest"
"github.com/terramate-io/terramate/test/sandbox"
)

func TestCloudConfig(t *testing.T) {
type testcase struct {
name string
layout []string
want runExpected
customEnv map[string]string
}

writeJSON := func(w http.ResponseWriter, str string) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(str))
}

const fatalErr = `FTL ` + string(clitest.ErrCloud)

for _, tc := range []testcase{
{
name: "empty cloud block == no organization set",
layout: []string{
"s:s1:id=s1",
`f:cfg.tm.hcl:terramate {
config {
cloud {
}
}
}`,
},
want: runExpected{
Status: 1,
StderrRegexes: []string{
`Please set TM_CLOUD_ORGANIZATION environment variable`,
fatalErr,
},
},
},
{
name: "not a member of selected organization",
layout: []string{
"s:s1:id=s1",
`f:cfg.tm.hcl:terramate {
config {
cloud {
organization = "world"
}
}
}`,
},
want: runExpected{
Status: 1,
StderrRegexes: []string{
`You are not a member of organization "world"`,
fatalErr,
},
},
},
{
name: "member of organization",
layout: []string{
"s:s1:id=s1",
`f:cfg.tm.hcl:terramate {
config {
cloud {
organization = "mineiros-io"
}
}
}`,
},
want: runExpected{
Status: 0,
},
},
{
name: "cloud organization env var overrides value from config",
layout: []string{
"s:s1:id=s1",
`f:cfg.tm.hcl:terramate {
config {
cloud {
organization = "mineiros-io"
}
}
}`,
},
customEnv: map[string]string{
"TM_CLOUD_ORGANIZATION": "override",
},
want: runExpected{
Status: 1,
StderrRegexes: []string{
`You are not a member of organization "override"`,
fatalErr,
},
},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
router := testserver.RouterWith(map[string]bool{
cloud.UsersPath: true,
cloud.MembershipsPath: false,
cloud.DeploymentsPath: true,
cloud.DriftsPath: true,
})

fakeserver := &http.Server{
Handler: router,
Addr: "localhost:3001",
}
testserver.RouterAddCustoms(router, testserver.Custom{
Routes: map[string]testserver.Route{
"GET": {
Path: cloud.MembershipsPath,
Handler: http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, `[
{
"org_name": "terramate-io",
"org_display_name": "Terramate",
"org_uuid": "c7d721ee-f455-4d3c-934b-b1d96bbaad17",
"status": "active"
},
{
"org_name": "mineiros-io",
"org_display_name": "Mineiros",
"org_uuid": "b2f153e8-ceb1-4f26-898e-eb7789869bee",
"status": "active"
}
]`)
},
),
},
},
})

const fakeserverShutdownTimeout = 3 * time.Second
errChan := make(chan error)
go func() {
errChan <- fakeserver.ListenAndServe()
}()

t.Cleanup(func() {
err := fakeserver.Close()
if err != nil {
t.Logf("fakeserver HTTP Close error: %v", err)
}
select {
case err := <-errChan:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
t.Error(err)
}
case <-time.After(fakeserverShutdownTimeout):
t.Error("time excedeed waiting for fakeserver shutdown")
}
})

s := sandbox.New(t)
layout := tc.layout
if len(layout) == 0 {
layout = []string{
"s:stack:id=test",
}
}
s.BuildTree(layout)
s.Git().CommitAll("created stacks")
env := removeEnv(os.Environ(), "CI")

for k, v := range tc.customEnv {
env = append(env, fmt.Sprintf("%v=%v", k, v))
}

tm := newCLI(t, s.RootDir(), env...)

cmd := []string{
"run",
"--cloud-sync-deployment",
"--", testHelperBin, "true",
}
assertRunResult(t, tm.run(cmd...), tc.want)
})
}
}
1 change: 0 additions & 1 deletion cmd/terramate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@ import (
)

func main() {
// TO BE REMOVED - force triggering of benchmark
cli.Exec(terramate.Version(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr)
}
21 changes: 21 additions & 0 deletions docs/configuration/project-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,24 @@ on `terramate.config.run.env` blocks won't affect the `env` namespace.

You can have multiple `terramate.config.run.env` blocks defined on different
files, but variable names **cannot** be defined twice.

### The `terramate.config.cloud` block

Properties related to Terramate Cloud can be defined inside the `terramate.config.cloud` block.
Currently, this block is only used to set the default cloud organization name:
```hcl
terramate {
config {
cloud {
organization = "my-org-name"
}
}
}
```
Setting a cloud organization name is required when
* syncing with Terramate Cloud, i.e. by using `terramate run` with the `--cloud-sync-drift-status` or `--cloud-sync-deployment` options, and
* the user is member of more than one cloud organizations.

The specified name will be used to select which of the user's organizations to use in the scope of the project.

It's also possible to select a cloud organization by setting the environment variable `TM_CLOUD_ORGANIZATION` to the organization name. If set, the value from the environment variable will override the configuration setting.
74 changes: 71 additions & 3 deletions hcl/hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,17 @@ type GitConfig struct {
CheckRemote bool
}

// CloudConfig represents Terramate cloud configuration.
type CloudConfig struct {
// Organization is the name of the cloud organization
Organization string
}

// RootConfig represents the root config block of a Terramate configuration.
type RootConfig struct {
Git *GitConfig
Run *RunConfig
Git *GitConfig
Run *RunConfig
Cloud *CloudConfig
}

// ManifestDesc represents a parsed manifest description.
Expand Down Expand Up @@ -1416,7 +1423,7 @@ func parseRootConfig(cfg *RootConfig, block *ast.MergedBlock) error {
))
}

errs.AppendWrap(ErrTerramateSchema, block.ValidateSubBlocks("git", "run"))
errs.AppendWrap(ErrTerramateSchema, block.ValidateSubBlocks("git", "run", "cloud"))

gitBlock, ok := block.Blocks[ast.NewEmptyLabelBlockType("git")]
if ok {
Expand All @@ -1442,6 +1449,17 @@ func parseRootConfig(cfg *RootConfig, block *ast.MergedBlock) error {
errs.Append(parseRunConfig(cfg.Run, runBlock))
}

cloudBlock, ok := block.Blocks[ast.NewEmptyLabelBlockType("cloud")]
if ok {
logger.Trace().Msg("Type is 'cloud'")

cfg.Cloud = &CloudConfig{}

logger.Trace().Msg("Parse cloud config.")

errs.Append(parseCloudConfig(cfg.Cloud, cloudBlock))
}

return errs.AsError()
}

Expand Down Expand Up @@ -1601,6 +1619,56 @@ func parseGitConfig(git *GitConfig, gitBlock *ast.MergedBlock) error {
return errs.AsError()
}

func parseCloudConfig(cloud *CloudConfig, cloudBlock *ast.MergedBlock) error {
logger := log.With().
Str("action", "parseCloudConfig()").
Logger()

logger.Trace().Msg("Range over block attributes.")

errs := errors.L()

errs.AppendWrap(ErrTerramateSchema, cloudBlock.ValidateSubBlocks())

for _, attr := range cloudBlock.Attributes.SortedList() {
logger := logger.With().
Str("attribute", attr.Name).
Logger()

logger.Trace().Msg("setting attribute on config")

value, diags := attr.Expr.Value(nil)
if diags.HasErrors() {
errs.Append(errors.E(diags,
"failed to evaluate terramate.config.cloud.%s attribute", attr.Name,
))
continue
}

switch attr.Name {
case "organization":
if value.Type() != cty.String {
errs.Append(attrErr(attr,
"terramate.config.cloud.organization is not a string but %q",
value.Type().FriendlyName(),
))

continue
}

cloud.Organization = value.AsString()

default:
errs.Append(errors.E(
attr.NameRange,
"unrecognized attribute terramate.config.cloud.%s",
attr.Name,
))
}
}
return errs.AsError()
}

func (p *TerramateParser) parseTerramateSchema() (Config, error) {
logger := log.With().
Str("action", "parseTerramateSchema()").
Expand Down

0 comments on commit 9d45dcd

Please sign in to comment.