Skip to content

Commit

Permalink
chore(tmc): introduce uimode in the cli for enhanced cloud fail cases.
Browse files Browse the repository at this point in the history
The Human x Automation UI modes were introduced for different error
handling depending on where is the execution.

Signed-off-by: Tiago Natel <t.nateldemoura@gmail.com>
  • Loading branch information
i4ki committed Sep 21, 2023
1 parent 2d08318 commit 8fbe2f8
Show file tree
Hide file tree
Showing 15 changed files with 1,044 additions and 274 deletions.
96 changes: 65 additions & 31 deletions cloud/testserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ import (
// DefaultOrgUUID is the test organization UUID.
const DefaultOrgUUID = "0000-1111-2222-3333"

type (
// Route declares an HTTP route.
Route struct {
Path string
Handler http.Handler
}

// Custom declares a custom server config.
Custom struct {
Routes map[string]Route
}
)

func (orgHandler *membershipHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
w.Header().Add("Content-Type", "application/json")
writeString(w, fmt.Sprintf(`[
Expand Down Expand Up @@ -330,26 +343,6 @@ func (handler *stackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}

func newStackEndpoint() *stackHandler {
return &stackHandler{
stacks: make(map[string]map[int]cloud.StackResponse),
statuses: make(map[string]map[int]stack.Status),
}
}

func newDeploymentEndpoint() *deploymentHandler {
return &deploymentHandler{
deployments: make(map[string]map[string]map[int64]cloud.DeploymentStackRequest),
events: make(map[string]map[string]map[string][]string),
}
}

func newDriftEndpoint() *driftHandler {
return &driftHandler{
statuses: make(map[string]stack.Status),
}
}

// Router returns the default fake cloud router.
func Router() *httprouter.Router {
return RouterWith(EnableAllConfig())
Expand All @@ -359,9 +352,14 @@ func Router() *httprouter.Router {
// enabled endpoints.
func RouterWith(enabled map[string]bool) *httprouter.Router {
router := httprouter.New()
RouterAdd(router, enabled)
return router
}

// RouterAdd enables endpoints in an existing router.
func RouterAdd(router *httprouter.Router, enabled map[string]bool) {
if enabled[cloud.UsersPath] {
router.Handler("GET", cloud.UsersPath, &userHandler{})
router.Handler("GET", cloud.UsersPath, newUserEndpoint())
}

if enabled[cloud.StacksPath] {
Expand All @@ -373,7 +371,7 @@ func RouterWith(enabled map[string]bool) *httprouter.Router {
}

if enabled[cloud.MembershipsPath] {
router.Handler("GET", cloud.MembershipsPath, &membershipHandler{})
router.Handler("GET", cloud.MembershipsPath, newMembershipEndpoint())
}

deploymentEndpoint := newDeploymentEndpoint()
Expand All @@ -393,7 +391,26 @@ func RouterWith(enabled map[string]bool) *httprouter.Router {

// test endpoint always enabled
router.Handler("GET", fmt.Sprintf("%s/:orguuid/:deployuuid/events", cloud.DeploymentsPath), deploymentEndpoint)
return router
}

// RouterAddCustoms add custom routes to the fake server.
// This is used by very specific test cases which requires injection of custom
// errors in the server.
func RouterAddCustoms(router *httprouter.Router, custom Custom) {
for method, route := range custom.Routes {
router.Handler(method, route.Path, route.Handler)
}
}

// EnableAllConfig returns a map that enables all cloud endpoints.
func EnableAllConfig() map[string]bool {
return map[string]bool{
cloud.UsersPath: true,
cloud.MembershipsPath: true,
cloud.DeploymentsPath: true,
cloud.DriftsPath: true,
cloud.StacksPath: true,
}
}

type (
Expand All @@ -418,14 +435,31 @@ type (
}
)

// EnableAllConfig returns a map that enables all cloud endpoints.
func EnableAllConfig() map[string]bool {
return map[string]bool{
cloud.UsersPath: true,
cloud.MembershipsPath: true,
cloud.DeploymentsPath: true,
cloud.DriftsPath: true,
cloud.StacksPath: true,
func newMembershipEndpoint() *membershipHandler {
return &membershipHandler{}
}

func newUserEndpoint() *userHandler {
return &userHandler{}
}

func newStackEndpoint() *stackHandler {
return &stackHandler{
stacks: make(map[string]map[int]cloud.StackResponse),
statuses: make(map[string]map[int]stack.Status),
}
}

func newDeploymentEndpoint() *deploymentHandler {
return &deploymentHandler{
deployments: make(map[string]map[string]map[int64]cloud.DeploymentStackRequest),
events: make(map[string]map[string]map[string][]string),
}
}

func newDriftEndpoint() *driftHandler {
return &driftHandler{
statuses: make(map[string]stack.Status),
}
}

Expand Down
5 changes: 4 additions & 1 deletion cloud/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,11 @@ func (orgs MemberOrganizations) String() string {

// Validate if the user has the Terramate CLI required fields.
func (u User) Validate() error {
if u.Email == "" {
return errors.E(`missing "email" field.`)
}
if u.DisplayName == "" {
return errors.E(`missing "display_name" field.`)
return errors.E(`missing "display_name" field`)

Check warning on line 243 in cloud/types.go

View check run for this annotation

Codecov / codecov/patch

cloud/types.go#L243

Added line #L243 was not covered by tests
}
return nil
}
Expand Down
21 changes: 19 additions & 2 deletions cmd/terramate/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ const defaultVendorDir = "/modules"

const terramateUserConfigDir = ".terramate.d"

const (
// HumanMode is the default normal mode when Terramate is executed at the user's machine.
HumanMode UIMode = iota
// AutomationMode is the mode when Terramate executes in the CI/CD environment.
AutomationMode
)

// UIMode defines different modes of operation for the cli.
type UIMode int

type cliSpec struct {
Version struct{} `cmd:"" help:"Terramate version"`
VersionFlag bool `name:"version" help:"Terramate version"`
Expand Down Expand Up @@ -247,6 +257,7 @@ type cli struct {
prj project
httpClient http.Client
cloud cloudConfig
uimode UIMode

checkpointResults chan *checkpoint.CheckResponse

Expand Down Expand Up @@ -461,6 +472,11 @@ func newCLI(version string, args []string, stdin io.Reader, stdout, stderr io.Wr
log.Fatal().Msg("flag --changed provided but no git repository found")
}

uimode := HumanMode
if val := os.Getenv("CI"); envVarIsSet(val) {
uimode = AutomationMode
}

Check warning on line 478 in cmd/terramate/cli/cli.go

View check run for this annotation

Codecov / codecov/patch

cmd/terramate/cli/cli.go#L475-L478

Added lines #L475 - L478 were not covered by tests

return &cli{
version: version,
stdin: stdin,
Expand All @@ -471,6 +487,7 @@ func newCLI(version string, args []string, stdin io.Reader, stdout, stderr io.Wr
clicfg: clicfg,
ctx: ctx,
prj: prj,
uimode: uimode,

Check warning on line 490 in cmd/terramate/cli/cli.go

View check run for this annotation

Codecov / codecov/patch

cmd/terramate/cli/cli.go#L490

Added line #L490 was not covered by tests

// in order to reduce the number of TCP/SSL handshakes we reuse the same
// http.Client in all requests, for most hosts.
Expand Down Expand Up @@ -1882,7 +1899,7 @@ func (c *cli) setupEvalContext(st *config.Stack, overrideGlobals map[string]stri
}

func envVarIsSet(val string) bool {
return val != "0" && val != "false"
return val != "" && val != "0" && val != "false"

Check warning on line 1902 in cmd/terramate/cli/cli.go

View check run for this annotation

Codecov / codecov/patch

cmd/terramate/cli/cli.go#L1902

Added line #L1902 was not covered by tests
}

func (c *cli) checkOutdatedGeneratedCode() {
Expand Down Expand Up @@ -1940,7 +1957,7 @@ func (c *cli) wd() string { return c.prj.wd }
func (c *cli) rootdir() string { return c.prj.rootdir }
func (c *cli) cfg() *config.Root { return &c.prj.root }
func (c *cli) rootNode() hcl.Config { return c.prj.root.Tree().Node }
func (c *cli) cred() credential { return c.cloud.credential }
func (c *cli) cred() credential { return c.cloud.client.Credential.(credential) }

Check warning on line 1960 in cmd/terramate/cli/cli.go

View check run for this annotation

Codecov / codecov/patch

cmd/terramate/cli/cli.go#L1960

Added line #L1960 was not covered by tests

func (c *cli) friendlyFmtDir(dir string) (string, bool) {
return prj.FriendlyFmtDir(c.rootdir(), c.wd(), dir)
Expand Down
6 changes: 6 additions & 0 deletions cmd/terramate/cli/clitest/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright 2023 Terramate GmbH
// SPDX-License-Identifier: MPL-2.0

// Package clitest provides constants and errors kind reused by the cli implementation
// and the e2e test infrastructure.
package clitest
32 changes: 32 additions & 0 deletions cmd/terramate/cli/clitest/messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2023 Terramate GmbH
// SPDX-License-Identifier: MPL-2.0

package clitest

import "github.com/terramate-io/terramate/errors"

const (
// CloudDisablingMessage is the message displayed in the warning when disabling
// the cloud features.
CloudDisablingMessage = "disabling the cloud features"

// CloudNoMembershipMessage is the message displayed when the user is not member
// of any organization.
CloudNoMembershipMessage = "You are not member of any organization"

// CloudSyncDriftFailedMessage is the message displayed when a drift sync fails.
CloudSyncDriftFailedMessage = "failed to sync the drift status"
)

const (
// ErrCloud indicates the cloud feature is unprocessable for any reason.
// Depending on the uimode (human x automation) the cli can fatal or skip the
// cloud integration.
ErrCloud errors.Kind = "unprocessable cloud feature"

// ErrCloudOnboardingIncomplete indicates the onboarding process is incomplete.
ErrCloudOnboardingIncomplete errors.Kind = "cloud commands cannot be used until onboarding is complete"

// ErrCloudStacksWithoutID indicates that some stacks are missing the ID field.
ErrCloudStacksWithoutID errors.Kind = "all the cloud sync features requires that selected stacks contain an ID field"
)

0 comments on commit 8fbe2f8

Please sign in to comment.